[深刻淺出Cocoa]iOS網絡編程之Socketios
在前文《深刻淺出Cocoa之Bonjour網絡編程》中我介紹瞭如何在Mac系統下進行 Bonjour 編程,在那篇文章中也介紹過 Cocoa 中網絡編程層次結構分爲三層,雖然那篇演示的是 Mac 系統的例子,其實對iOS系統來講也是同樣的。iOS網絡編程層次結構也分爲三層:git
Cocoa層是最上層的基於 Objective-C 的 API,好比 URL訪問,NSStream,Bonjour,GameKit等,這是大多數狀況下咱們經常使用的 API。Cocoa 層是基於 Core Foundation 實現的。github
Core Foundation層:由於直接使用 socket 須要更多的編程工做,因此蘋果對 OS 層的 socket 進行簡單的封裝以簡化編程任務。該層提供了 CFNetwork 和 CFNetServices,其中 CFNetwork 又是基於 CFStream 和 CFSocket。編程
OS層:最底層的 BSD socket 提供了對網絡編程最大程度的控制,可是編程工做也是最多的。所以,蘋果建議咱們使用 Core Foundation 及以上層的 API 進行編程。服務器
本文將介紹如何在 iOS 系統下使用最底層的 socket 進行編程,這和在 window 系統下使用 C/C++ 進行 socket 編程並沒有多大區別。網絡
本文源碼:https://github.com/kesalin/iOSSnippet/tree/master/KSNetworkDemoapp
運行效果以下:socket
BSD socket API 和 winsock API 接口大致差很少,下面將列出比較經常使用的 API:oop
API接口 | 講解 |
int socket(int addressFamily, int type, int protocol) int close(int socketFileDescriptor) |
socket 建立並初始化 socket,返回該 socket 的文件描述符,若是描述符爲 -1 表示建立失敗。
一般參數 addressFamily 是 IPv4(AF_INET) 或 IPv6(AF_INET6)。type 表示 socket 的類型,一般是流stream(SOCK_STREAM) 或數據報文datagram(SOCK_DGRAM)。protocol 參數一般設置爲0,以便讓系統自動爲選擇咱們合適的協議,對於 stream socket 來講會是 TCP 協議(IPPROTO_TCP),而對於 datagram來講會是 UDP 協議(IPPROTO_UDP)。
close 關閉 socket。
|
int
bind(int socketFileDescriptor,
sockaddr *addressToBind, int addressStructLength) |
將 socket 與特定主機地址與端口號綁定,成功綁定返回0,失敗返回 -1。
成功綁定以後,根據協議(TCP/UDP)的不一樣,咱們能夠對 socket 進行不一樣的操做:
UDP:由於 UDP 是無鏈接的,綁定以後就能夠利用 UDP socket 傳送數據了。
TCP:而 TCP 是須要創建端到端鏈接的,爲了創建 TCP 鏈接服務器必須調用 listen(int socketFileDescriptor, int backlogSize) 來設置服務器的緩衝區隊列以接收客戶端的鏈接請求,backlogSize 表示客戶端鏈接請求緩衝區隊列的大小。當調用 listen 設置以後,服務器等待客戶端請求,而後調用下面的 accept 來接受客戶端的鏈接請求。
|
int
accept(int socketFileDescriptor,
sockaddr *clientAddress, int
clientAddressStructLength)
|
接受客戶端鏈接請求並將客戶端的網絡地址信息保存到 clientAddress 中。 當客戶端鏈接請求被服務器接受以後,客戶端和服務器之間的鏈路就創建好了,二者就能夠通訊了。 |
int
connect(int socketFileDescriptor,
sockaddr *serverAddress, int
serverAddressLength)
|
客戶端向特定網絡地址的服務器發送鏈接請求,鏈接成功返回0,失敗返回 -1。 當服務器創建好以後,客戶端經過調用該接口向服務器發起創建鏈接請求。對於 UDP 來講,該接口是可選的,若是調用了該接口,代表設置了該 UDP socket 默認的網絡地址。對 TCP socket來講這就是傳說中三次握手創建鏈接發生的地方。 注意:該接口調用會阻塞當前線程,直到服務器返回。 |
hostent* gethostbyname(char *hostname) |
使用 DNS 查找特定主機名字對應的 IP 地址。若是找不到對應的 IP 地址則返回 NULL。 |
int
send(int socketFileDescriptor, char
*buffer, int bufferLength, int flags)
|
經過 socket 發送數據,發送成功返回成功發送的字節數,不然返回 -1。 一旦鏈接創建好以後,就能夠經過 send/receive 接口發送或接收數據了。注意調用 connect 設置了默認網絡地址的 UDP socket 也能夠調用該接口來接收數據。 |
int
receive(int socketFileDescriptor,
char *buffer, int bufferLength, int flags)
|
從 socket 中讀取數據,讀取成功返回成功讀取的字節數,不然返回 -1。 一旦鏈接創建好以後,就能夠經過 send/receive 接口發送或接收數據了。注意調用 connect 設置了默認網絡地址的 UDP socket 也能夠調用該接口來發送數據。 |
int
sendto(int socketFileDescriptor,
char *buffer, int bufferLength, int
flags, sockaddr *destinationAddress, int
destinationAddressLength)
|
經過UDP socket 發送數據到特定的網絡地址,發送成功返回成功發送的字節數,不然返回 -1。 因爲 UDP 能夠向多個網絡地址發送數據,因此能夠指定特定網絡地址,以向其發送數據。 |
int
recvfrom(int socketFileDescriptor,
char *buffer, int bufferLength, int
flags, sockaddr *fromAddress, int *fromAddressLength) |
從UDP socket 中讀取數據,並保存發送者的網絡地址信息,讀取成功返回成功讀取的字節數,不然返回 -1 。 因爲 UDP 能夠接收來自多個網絡地址的數據,因此須要提供額外的參數,以保存該數據的發送者身份。 |
有了上面的 socket API 講解,下面來總結一下服務器的工做流程。
因爲 iOS 設備一般是做爲客戶端,所以在本文中不會用代碼來演示如何創建一個iOS服務器,但能夠參考前文:《深刻淺出Cocoa之Bonjour網絡編程》看看如何在 Mac 系統下創建桌面服務器。
因爲 iOS 設備一般是做爲客戶端,下文將演示如何編寫客戶端代碼。先來總結一下客戶端工做流程。
下面的代碼就實現了上面客戶端的工做流程:
- (void)loadDataFromServerWithURL:(NSURL *)url { NSString * host = [url host]; NSNumber * port = [url port]; // Create socket // int socketFileDescriptor = socket(AF_INET, SOCK_STREAM, 0); if (-1 == socketFileDescriptor) { NSLog(@"Failed to create socket."); return; } // Get IP address from host // struct hostent * remoteHostEnt = gethostbyname([host UTF8String]); if (NULL == remoteHostEnt) { close(socketFileDescriptor); [self networkFailedWithErrorMessage:@"Unable to resolve the hostname of the warehouse server."]; return; } struct in_addr * remoteInAddr = (struct in_addr *)remoteHostEnt->h_addr_list[0]; // Set the socket parameters // struct sockaddr_in socketParameters; socketParameters.sin_family = AF_INET; socketParameters.sin_addr = *remoteInAddr; socketParameters.sin_port = htons([port intValue]); // Connect the socket // int ret = connect(socketFileDescriptor, (struct sockaddr *) &socketParameters, sizeof(socketParameters)); if (-1 == ret) { 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; // Continually receive data until we reach the end of the data // int maxCount = 5; // just for test. int i = 0; while (waitingForData && i < maxCount) { const char * buffer[1024]; int length = sizeof(buffer); // Read a buffer's amount of data from the socket; the number of bytes read is returned // int result = recv(socketFileDescriptor, &buffer, length, 0); if (result > 0) { [data appendBytes:buffer length:result]; } else { // if we didn't get any data, stop the receive loop // waitingForData = NO; } ++i; } // Close the socket // close(socketFileDescriptor); [self networkSucceedWithData:data]; }
前面說過,connect/recv/send 等接口都是阻塞式的,所以咱們須要將這些操做放在非 UI 線程中進行。以下所示:
NSThread * backgroundThread = [[NSThread alloc] initWithTarget:self selector:@selector(loadDataFromServerWithURL:) object:url]; [backgroundThread start];
一樣,在獲取到數據或者網絡異常致使任務失敗,咱們須要更新 UI,這也要回到 UI 線程中去作這個事情。以下所示:
- (void)networkFailedWithErrorMessage:(NSString *)message { // Update UI // [[NSOperationQueue mainQueue] addOperationWithBlock:^{ NSLog(@"%@", message); self.receiveTextView.text = message; self.connectButton.enabled = YES; [self.networkActivityView stopAnimating]; }]; } - (void)networkSucceedWithData:(NSData *)data { // Update UI // [[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]; }]; }