iOS的Socket開發基礎
Socket簡介
首先讓我們通過一張圖知道socket在哪裡?
Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面。
tcp和udp的區別 在這裡就必須講一下udp和tcp的區別了
TCP:面向連線、傳輸可靠(保證資料正確性,保證資料順序)、用於傳輸大量資料(流模式)、速度慢,建立連線需要開銷較多(時間,系統資源)。
UDP:面向非連線、傳輸不可靠、用於傳輸少量資料(資料包模式)、速度快。
關於TCP是一種流模式的協議,UDP是一種資料包模式的協議,這裡要說明一下,TCP是面向連線的,也就是說,在連線持續的過程中,socket中收到的資料都是由同一臺主機發出的(劫持什麼的不考慮),因此,知道保證資料是有序的到達就行了,至於每次讀取多少資料自己看著辦。
而UDP是無連線的協議,也就是說,只要知道接收端的IP和埠,且網路是可達的,任何主機都可以向接收端傳送資料。這時候,如果一次能讀取超過一個報文的資料,則會亂套。比如,主機A向傳送了報文P1,主機B傳送了報文P2,如果能夠讀取超過一個報文的資料,那麼就會將P1和P2的資料合併在了一起,這樣的資料是沒有意義的。
TCP三次握手和四次揮手
相對於SOCKET開發者,TCP建立過程和連線拆除過程是由TCP/IP協議棧自動建立的。因此開發者並不需要控制這個過程。但是對於理解TCP底層運作機制,相當有幫助。
因此在這裡詳細解釋一下這兩個過程。
TCP三次握手
所謂三次握手(Three-way Handshake),是指建立一個TCP連線時,需要客戶端和伺服器總共傳送3個包。
三次握手的目的是連線伺服器指定埠,建立TCP連線,並同步連線雙方的序列號和確認號並交換 TCP 視窗大小資訊.在socket程式設計中,客戶端執行connect()時。將觸發三次握手。
首先了解一下幾個標誌,SYN(synchronous),同步標誌,ACK (Acknowledgement),即確認標誌,seq應該是Sequence Number,序列號的意思,另外還有四次握手的fin,應該是final,表示結束標誌。
第一次握手:客戶端傳送一個TCP的SYN標誌位置1的包指明客戶打算連線的伺服器的埠,以及初始序號X,儲存在包頭的序列號(Sequence Number)欄位裡。
第二次握手:伺服器發回確認包(ACK)應答。即SYN標誌位和ACK標誌位均為1同時,將確認序號(Acknowledgement Number)設定為客戶的序列號加1以,即X+1。
第三次握手:客戶端再次傳送確認包(ACK) SYN標誌位為0,ACK標誌位為1。並且把伺服器發來ACK的序號欄位+1,放在確定欄位中傳送給對方.並且在資料段放寫序列號的+1。 關於三次握手,知乎上有個段子我覺得挺好的。 「喂喂喂,能聽到我嗎?」 「沒問題。能聽到我說一聲。」 「沒問題。」 原連結:http://www.zhihu.com/question/20879359 tcp四次揮手
TCP的連線的拆除需要傳送四個包,因此稱為四次揮手(four-way handshake)。客戶端或伺服器均可主動發起揮手動作,在socket
程式設計中,任何一方執行close()操作即可產生揮手操作。
其實有個問題,為什麼連線的時候是三次握手,關閉的時候卻是四次揮手?
因為當Server端收到Client端的SYN連線請求報文後,可以直接傳送SYN+ACK報文。其中ACK報文是用來應答的,SYN報文是用來同步的。但是關閉連線時,當Server端收到FIN報文時,很可能並不會立即關閉SOCKET,所以只能先回復一個ACK報文,告訴Client端,”你發的FIN報文我收到了”。只有等到我Server端所有的報文都發送完了,我才能傳送FIN報文,因此不能一起傳送。故需要四步握手。
tcpsocket和udpsocket的具體實現
講了這麼久,終於要開始講socket的具體實現了,iOS提供了Socket網路程式設計的介面CFSocket,不過這裡使用BSD Socket。
tcp和udp的socket是有區別的,這裡給出這兩種的設計框架
基本TCP客戶—伺服器程式設計基本框架
基本UDP客戶—伺服器程式設計基本框架流程圖
常用的Socket型別有兩種:流式Socket(SOCK_STREAM)和資料報式Socket(SOCK_DGRAM)。流式是一種面向連線的Socket,針對於面向連線的TCP服務應用;資料報式Socket是一種無連線的Socket,對應於無連線的UDP服務應用。
1、socket呼叫庫函式主要有:
建立套接字
Socket(af,type,protocol)
建立地址和套接字的聯絡
bind(sockid, local addr, addrlen)
伺服器端偵聽客戶端的請求
listen( Sockid ,quenlen)
建立伺服器/客戶端的連線 (面向連線TCP)
客戶端請求連線
Connect(sockid, destaddr, addrlen)
伺服器端等待從編號為Sockid的Socket上接收客戶連線請求
newsockid=accept(Sockid,Clientaddr, paddrlen)
傳送/接收資料
面向連線:
send(sockid, buff, bufflen)
recv( )
面向無連線:
sendto(sockid,buff,…,addrlen)
recvfrom( )
釋放套接字
close(sockid)
tcpsocket的具體實現
伺服器的工作流程:首先呼叫socket函式建立一個Socket,然後呼叫bind函式將其與本機地址以及一個本地埠號繫結,然後呼叫listen在相應的socket上監聽,當accpet接收到一個連線服務請求時,將生成一個新的socket。伺服器顯示該客戶機的IP地址,並通過新的socket向客戶端傳送字串” hi,I am server!”。最後關閉該socket。
#import <Foundation/Foundation.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, const char * argv[])
{
@autoreleasepool {
// 1
int err;
int fd=socket(AF_INET, SOCK_STREAM , 0);
BOOL success=(fd!=-1);
// 1
// 2
if (success) {
NSLog(@"socket success");
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_len=sizeof(addr);
addr.sin_family=AF_INET;
// ==========================================================================
addr.sin_port=htons(1024);
// ==========================================================================
addr.sin_addr.s_addr=INADDR_ANY;
err=bind(fd, (const struct sockaddr *)&addr, sizeof(addr));
success=(err==0);
}
// 2
// ============================================================================
if (success) {
NSLog(@"bind(繫結) success");
err=listen(fd, 5);//開始監聽
success=(err==0);
}
// ============================================================================
//3
if (success) {
NSLog(@"listen success");
while (true) {
struct sockaddr_in peeraddr;
int peerfd;
socklen_t addrLen;
addrLen=sizeof(peeraddr);
NSLog(@"prepare accept");
peerfd=accept(fd, (struct sockaddr *)&peeraddr, &addrLen);
success=(peerfd!=-1);
// ============================================================================
if (success) {
NSLog(@"accept success,remote address:%s,port:%d",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
char buf[1024];
ssize_t count;
size_t len=sizeof(buf);
do {
count=recv(peerfd, buf, len, 0);
NSString* str = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding];
NSLog(@"%@",str);
} while (strcmp(buf, "exit")!=0);
}
// ============================================================================
close(peerfd);
}
}
//3
}
return 0;
}
客戶端的工作流程:首先呼叫socket函式建立一個Socket,然後呼叫bind函式將其與本機地址以及一個本地埠號繫結,請求連線伺服器,通過新的socket向客戶端傳送字串” hi,I am client!”。最後關閉該socket。
//
// main.m
// kewai_SocketClient
//
#import <Foundation/Foundation.h>
#include <sys/socket.h>
#include <netinet/in.h>
#import <arpa/inet.h>
int main(int argc, const char * argv[])
{
@autoreleasepool {
// 1
int err;
int fd=socket(AF_INET, SOCK_STREAM, 0);
BOOL success=(fd!=-1);
struct sockaddr_in addr;
// 1
// 2
if (success) {
NSLog(@"socket success");
memset(&addr, 0, sizeof(addr));
addr.sin_len=sizeof(addr);
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=INADDR_ANY;
err=bind(fd, (const struct sockaddr *)&addr, sizeof(addr));
success=(err==0);
}
// 2
//3
if (success) {
//============================================================================
struct sockaddr_in peeraddr;
memset(&peeraddr, 0, sizeof(peeraddr));
peeraddr.sin_len=sizeof(peeraddr);
peeraddr.sin_family=AF_INET;
peeraddr.sin_port=htons(1024);
// peeraddr.sin_addr.s_addr=INADDR_ANY;
peeraddr.sin_addr.s_addr=inet_addr("172.16.10.120");
// 這個地址是伺服器的地址,
socklen_t addrLen;
addrLen =sizeof(peeraddr);
NSLog(@"connecting");
err=connect(fd, (struct sockaddr *)&peeraddr, addrLen);
success=(err==0);
if (success) {
// struct sockaddr_in addr;
err =getsockname(fd, (struct sockaddr *)&addr, &addrLen);
success=(err==0);
//============================================================================
//============================================================================
if (success) {
NSLog(@"connect success,local address:%s,port:%d",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
char buf[1024];
do {
printf("input message:");
scanf("%s",buf);
send(fd, buf, 1024, 0);
} while (strcmp(buf, "exit")!=0);
}
}
else{
NSLog(@"connect failed");
}
}
// ============================================================================
//3
}
return 0;
}
udpsocket的具體實現
下面是udpsocket的具體實現
伺服器的工作流程:首先呼叫socket函式建立一個Socket,然後呼叫bind函式將其與本機地址以及一個本地埠號繫結,接收到一個客戶端時,伺服器顯示該客戶端的IP地址,並將字串返回給客戶端。
/*
*UDP/IP應用程式設計介面(API)
*伺服器的工作流程:首先呼叫socket函式建立一個Socket,然後呼叫bind函式將其與本機
*地址以及一個本地埠號繫結,接收到一個客戶端時,伺服器顯示該客戶端的IP地址,並將字串
*返回給客戶端。
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#import <arpa/inet.h>
int main(int argc,char **argv)
{
int ser_sockfd;
int len;
//int addrlen;
socklen_t addrlen;
char seraddr[100];
struct sockaddr_in ser_addr;
/*建立socket*/
ser_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(ser_sockfd<0)
{
printf("I cannot socket success\n");
return 1;
}
/*填寫sockaddr_in 結構*/
addrlen=sizeof(struct sockaddr_in);
bzero(&ser_addr,addrlen);
ser_addr.sin_family=AF_INET;
ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
ser_addr.sin_port=htons(1024);
/*繫結客戶端*/
if(bind(ser_sockfd,(struct sockaddr *)&ser_addr,addrlen)<0)
{
printf("connect");
return 1;
}
while(1)
{
bzero(seraddr,sizeof(seraddr));
len=recvfrom(ser_sockfd,seraddr,sizeof(seraddr),0,(struct sockaddr*)&ser_addr,&addrlen);
/*顯示client端的網路地址*/
printf("receive from %s\n",inet_ntoa(ser_addr.sin_addr));
/*顯示客戶端發來的字串*/
printf("recevce:%s",seraddr);
/*將字串返回給client端*/
sendto(ser_sockfd,seraddr,len,0,(struct sockaddr*)&ser_addr,addrlen);
}
}
客戶端的工作流程:首先呼叫socket函式建立一個Socket,填寫伺服器地址及埠號,從標準輸入裝置中取得字串,將字串傳送給伺服器端,並接收伺服器端返回的字串。最後關閉該socket。
/*
*UDP/IP應用程式設計介面(API)
*客戶端的工作流程:首先呼叫socket函式建立一個Socket,填寫伺服器地址及埠號,
*從標準輸入裝置中取得字串,將字串傳送給伺服器端,並接收伺服器端返回的字
*符串。最後關閉該socket。
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include <netinet/in.h>
#import <arpa/inet.h>
int GetServerAddr(char * addrname)
{
printf("please input server addr:");
scanf("%s",addrname);
return 1;
}
int main(int argc,char **argv)
{
int cli_sockfd;
int len;
socklen_t addrlen;
char seraddr[14];
struct sockaddr_in cli_addr;
char buffer[256];
GetServerAddr(seraddr);
/* 建立socket*/
cli_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(cli_sockfd<0)
{
printf("I cannot socket success\n");
return 1;
}
/* 填寫sockaddr_in*/
addrlen=sizeof(struct sockaddr_in);
bzero(&cli_addr,addrlen);
cli_addr.sin_family=AF_INET;
cli_addr.sin_addr.s_addr=inet_addr(seraddr);
//cli_addr.sin_addr.s_addr=htonl(INADDR_ANY);
cli_addr.sin_port=htons(1024);
bzero(buffer,sizeof(buffer));
/* 從標準輸入裝置取得字串*/
len=read(STDIN_FILENO,buffer,sizeof(buffer));
/* 將字串傳送給server端*/
sendto(cli_sockfd,buffer,len,0,(struct sockaddr*)&cli_addr,addrlen);
/* 接收server端返回的字串*/
len=recvfrom(cli_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&cli_addr,&addrlen);
//printf("receive from %s\n",inet_ntoa(cli_addr.sin_addr));
printf("receive: %s",buffer);
close(cli_sockfd);
}
最後,整篇文章只能用一句話形容,懶婆娘的裹腳布,又長又臭,不過本文的作用是讓我們瞭解socket的一些原理以及底層基本的結構,其實iOS的socket實現是特別簡單的,我一直都在用github的開源類庫cocoaasyncsocket,地址是CocoaAsyncSocket,cocoaasyncsocket是支援tcp和udp的,具體操作方法就不介紹了。