一.iOS網(wǎng)絡(luò)編程層次模型
在前文《深入淺出的Cocoa之Bonjour網(wǎng)絡(luò)編程》中我介紹了如何在Mac系統(tǒng)下進(jìn)行Bonjour編程,在那篇文章中也介紹過Cocoa中網(wǎng)絡(luò)編程層次結(jié)構(gòu)分為三層,雖然那篇演示的是Mac系統(tǒng)的例子,其實(shí)對(duì)iOS系統(tǒng)來說也是一樣的。iOS網(wǎng)絡(luò)編程層次結(jié)構(gòu)也分為三層:
Cocoa層:NSURL,Bonjour,Game Kit,WebKit
Core Foundation層:基于C的CFNetwork和CFNetServices
OS層:基于C的BSD socket
cocoa層是最上層的基于Objective-C的API,比如URL訪問,NSStream,Bonjour,GameKit等,這是大多數(shù)情況下我們常用的API。Cocoa層是基于Core Foundation實(shí)現(xiàn)的。
Core Foundation層:因?yàn)橹苯邮褂胹ocket需要更多的編程工作,所以蘋果對(duì)OS層的socket進(jìn)行簡單的封裝以簡化編程任務(wù)。該層提供了CFNetwork和CFNetServices,其中CFNetwork又是基于CFStream和CFSocket。
OS層:最底層的BSD socket提供了對(duì)網(wǎng)絡(luò)編程最大程度的控制,但是編程工作也是最多的。因此,蘋果建議我們使用Core Foundation及以上的API進(jìn)行編程。
本文將介紹如何在iOS系統(tǒng)下使用最底層的socket進(jìn)行編程,這和在window系統(tǒng)下使用C/C++進(jìn)行socket編程并無多大區(qū)別。
本文源碼:https://github.com/kesalin/iOSSnippet/tree/master/KSNetworkDemo
二.BSD socket API簡介
BSD socket API和winsock API接口大體差不多,下面將列出比較常用的API:
API接口
int socket(int addressFamily, int type, int protocol)
socket創(chuàng)建并初始化socket,返回該socket的文件描述符,如果描述符為-1表示創(chuàng)建失敗。通常參數(shù)addressFamily是IPv4(AF_INET)或IPv6(AF_INET6)。type表示socket的類型,通常是流stream(SOCK_STREAM)或數(shù)據(jù)報(bào)文datagram(SOCK_DGRAM)。protocol參數(shù)通常設(shè)置為0,以便讓系統(tǒng)自動(dòng)為選擇我們合適的協(xié)議,對(duì)于stream socket來說會(huì)是TCP協(xié)議(IPPROTO_TCP),而對(duì)于datagram來說會(huì)是UDP協(xié)議(IPPROTO_UDP)。
int close(int socketFileDescriptor)
close關(guān)閉socket
int bind(int socketFileDescriptor, sockaddr *addressToBind, int addressStructLength)
將socket與特定主機(jī)地址與端口號(hào)綁定,成功綁定返回0,失敗返回-1。成功綁定之后,根據(jù)協(xié)議(TCP/UDP)的不同,我們可以對(duì)socket進(jìn)行不同的操作:
UDP:因?yàn)閁DP是無連接的,綁定之后就可以利用UDP socket傳輸數(shù)據(jù)了。
TCP:而TCP是需要建立端到端連接的,為了建立TCP連接服務(wù)器必須調(diào)用listen(int socketFileDescriptor, int backlogSize)來設(shè)置服務(wù)器的緩沖區(qū)隊(duì)列以接收客戶端的連接請(qǐng)求,backlogSize表示客戶端連接請(qǐng)求緩沖區(qū)隊(duì)列的大小。當(dāng)調(diào)用listen設(shè)置之后,服務(wù)器等待客戶端請(qǐng)求,然后調(diào)用下面的accept來接受客戶端的連接請(qǐng)求。
int accept(int socketFileDescriptor, sockaddr* clientAddress, int clientAddressStructLength)
接受客戶端連接請(qǐng)求并將客戶端的網(wǎng)絡(luò)地址信息保存到clientAddress中。當(dāng)客戶端連接請(qǐng)求被服務(wù)端接受之后,客戶端和服務(wù)端之間的鏈路就建立好了,兩者就可以通信了。
int connect(int socketFileDescriptor, sockaddr* serverAddress, int serverAddressLength)
客戶端向特定網(wǎng)絡(luò)地址的服務(wù)器發(fā)送連接請(qǐng)求,連接成功返回0,失敗返回-1。當(dāng)服務(wù)器建立好之后,客戶端通過調(diào)用該接口向服務(wù)器發(fā)起建立連接的請(qǐng)求。對(duì)于UDP來說,該接口是可選的,如果調(diào)用了該接口,表明設(shè)置了該UDP socket默認(rèn)的網(wǎng)絡(luò)地址。對(duì)TCP socket來說這就是傳說中三次握手建立連接發(fā)生的地方。注意:該接口調(diào)用會(huì)阻塞當(dāng)前線程,直到服務(wù)器返回。
hostent* gethostbyname(char *hostname)
使用DNS查找特定主機(jī)名字對(duì)應(yīng)的IP地址。如果找不到對(duì)應(yīng)的IP地址則返回NULL。
int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags)
通過socket發(fā)送數(shù)據(jù),發(fā)送成功返回成功發(fā)送的字節(jié)數(shù),否則返回-1。一旦連接建立之后,就可以通過send/receive接口發(fā)送或接收數(shù)據(jù)了。注意調(diào)用connect設(shè)置了默認(rèn)網(wǎng)絡(luò)地址的UDP socket也可以調(diào)用該接口來接收數(shù)據(jù)。
int receive(int socketFileDescriptor, char *buffer, int bufferLength, int flags)
從socket中讀取數(shù)據(jù),讀取成功返回成功讀取的字節(jié)數(shù),否則返回-1。一旦連接建立好之后,就可以通過send/receive接口發(fā)送或接收數(shù)據(jù)了。注意調(diào)用connect設(shè)置了默認(rèn)網(wǎng)絡(luò)地址的UDP socket也可以調(diào)用該接口來發(fā)送數(shù)據(jù)。
int sendto(int socketFileDescriptor, char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength)
通過UDP socket 發(fā)送數(shù)據(jù)到特定的網(wǎng)絡(luò)地址,發(fā)送成功返回成功發(fā)送的字節(jié)數(shù),否則返回-1. 由于UDP可以向多個(gè)網(wǎng)絡(luò)地址發(fā)送數(shù)據(jù),所以可以指定特定網(wǎng)絡(luò)地址,以向其發(fā)送數(shù)據(jù)。
int recvfrom(int socketFileDescriptor, char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int fromAddressLength)
從UDP socket中讀取數(shù)據(jù),并保存發(fā)送者的網(wǎng)絡(luò)地址信息,讀取成功返回成功讀取的字節(jié)數(shù),否則返回-1;由于UDP可以接收啦自多個(gè)網(wǎng)絡(luò)地址的數(shù)據(jù),所以需要提供額外的參數(shù),以保存該數(shù)據(jù)的發(fā)送者身份。
三.服務(wù)器工作流程
有了上面的socket API講解,下面來總結(jié)一下服務(wù)器的工作流程。
1.服務(wù)器調(diào)用socket(...)創(chuàng)建socket
2.服務(wù)器調(diào)用listen(...)設(shè)置緩沖區(qū)
3.服務(wù)器通過accept(...)接受客戶端請(qǐng)求建立連接
4.服務(wù)器與客戶端建立連接之后,就可以通過send(...)/receive(...)向客戶端發(fā)送或從客戶端接收數(shù)據(jù);
5.服務(wù)器調(diào)用close關(guān)閉socket
由于iOS設(shè)備通常是作為客戶端,因此在本文中不會(huì)用代碼來演示如何建立一個(gè)iOS服務(wù)器,但可以參考前文:《深入淺出Cocoa之Bonjour網(wǎng)絡(luò)編程》看看如何在Mac系統(tǒng)下建立桌面服務(wù)器。
四. 客戶端工作流程
由于iOS設(shè)備通常是作為客戶端,下文將演示如何編寫客戶端代碼。先來總結(jié)一下客戶端工作流程。
1.客戶端調(diào)用socket(...)創(chuàng)建socket;
2.客戶端調(diào)用connect(...)向服務(wù)器發(fā)起連接請(qǐng)求以建立連接;
3.客戶端與服務(wù)器建立連接之后,就可以通過send(...)/receive(...)向客戶端發(fā)送或從客戶端接收數(shù)據(jù);
4.客戶端調(diào)用close關(guān)閉socket;
-(void)loadDataFromServerWithURL:(NSURL*)url
{
NSString *host=[url host];
NSNumber *port=[url port];
int socketFileDescriptor=socket(AF_INET, SOCK_STREAM, 0);
if(socketFileDescriptor==-1){
NSLog(@"Failed to create socket.");
return;
}
struct hostent *remoteHostEnt=gethostbyname([host UTF8String]);
if(NULL==remoteHostEnt){
close(socketFileDescriptor);
[self networkFailedWithErrorMessage:@"Unable to resolve the hostname of the ware house server."];
return;
}
struct in_addr *remoteInAddr=(struct in_addr*)remoteHostEnt->h_addr_list[0];
struct sockaddr_in socketParameters;
socketParameters.sin_family=AF_INET;
socketParameters.sin_addr=remoteInAddr;
socketParameters.sin_port=htons([port intValue]);
int ret=connect(socketFileDescripter, (struct sockaddr*)&socketParameters, sizeof(socketParameters));
if(ret==-1){
close(socketFileDescriptor);
NSString *errorInfo=[NSString stringWithFormat:@">>failed to connect to %@:%@", host, port];
[self networkFailedWithErrorMessage:errorInfo];
return;
}
NSLog(@">>Successfully connected to %@:%@, host, port");
NSMutableData *data=[[NSMutableData alloc] init];
BOOL waitingForData=YES;
int maxCount = 5;
int i=0;
while(waitingForData && I<maxCount){
const char *buffer[1024];
int length = sizeof(buffer);
int result=recv(socketFileDescriptor, &buffer, length, 0);
if(result>0){
[data appendBytes:buffer length:result];
}else{
waitingForData=NO;
}
++i;
}
close(socketFileDescriptor);
[self networkSucceedWithData:data];
}
前面說過,connect/recv/send 等接口都是阻塞式的,因此我們需要將這些操作放在非UI線程中進(jìn)行。如下所示:
NSThread *backgroundThread=[[NSThread alloc] initWithTarget:self selector:@selector(loadDataFromServerWithURL:) object:url];?
[backgroundThread start];
同樣,在獲取到數(shù)據(jù)或者網(wǎng)絡(luò)異常導(dǎo)致任務(wù)失敗,我們需要更新UI,這也要回到UI 線程中去做這個(gè)事情。如下所示:
-(void)networkFailedWithErrorMessage:(NSString*)message{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"%@",message);
self.receiveTextView.text=message;
self.connectButton.enabled=YES;
[self.networkActivityView stopAnimating];
}];
}
-(void)networkSucceedWithData:(NSData*)data{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSString *resultsString=[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@">> Received string:'%@'",resultsString);
self.receiveTextView.text=resultsString;
self.connectButton.enabled=YES;
[self.networkActivityView stopAnimating];
}];
}