iOS網路程式設計之BSD Socket
[深入淺出Cocoa]iOS網路程式設計之Socket
一,iOS網路程式設計層次模型
在前文《深入淺出Cocoa之Bonjour網路程式設計》中我介紹瞭如何在Mac系統下進行 Bonjour 程式設計,在那篇文章中也介紹過 Cocoa 中網路程式設計層次結構分為三層,雖然那篇演示的是 Mac 系統的例子,其實對iOS系統來說也是一樣的。iOS網路程式設計層次結構也分為三層:
- Cocoa層:NSURL,Bonjour,Game Kit,WebKit
- Core Foundation層:基於 C 的 CFNetwork 和 CFNetServices
- OS層:基於 C 的 BSD socket
Cocoa層是最上層的基於 Objective-C 的 API,比如 URL訪問,NSStream,Bonjour,GameKit等,這是大多數情況下我們常用的 API。Cocoa 層是基於 Core Foundation 實現的。
Core Foundation層:因為直接使用 socket 需要更多的程式設計工作,所以蘋果對 OS 層的 socket 進行簡單的封裝以簡化程式設計任務。該層提供了 CFNetwork 和 CFNetServices,其中 CFNetwork 又是基於 CFStream 和 CFSocket。
OS層:最底層的 BSD socket 提供了對網路程式設計最大程度的控制,但是程式設計工作也是最多的。因此,蘋果建議我們使用 Core Foundation 及以上層的 API 進行程式設計。
本文將介紹如何在 iOS 系統下使用最底層的 socket 進行程式設計,這和在 window 系統下使用 C/C++ 進行 socket 程式設計並無多大區別。
執行效果如下:
二,BSD socket API 簡介
BSD socket API 和 winsock API 介面大體差不多,下面將列出比較常用的 API:
API介面 | 講解 |
int socket(int addressFamily, int type, int protocol) int close(int socketFileDescriptor) |
socket 建立並初始化 socket,返回該 socket 的檔案描述符,如果描述符為 -1 表示建立失敗。 |
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, intclientAddressStructLength) |
接受客戶端連線請求並將客戶端的網路地址資訊儲存到 clientAddress 中。 當客戶端連線請求被伺服器接受之後,客戶端和伺服器之間的鏈路就建立好了,兩者就可以通訊了。 |
int connect(int socketFileDescriptor,sockaddr *serverAddress, intserverAddressLength) |
客戶端向特定網路地址的伺服器傳送連線請求,連線成功返回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, intflags, sockaddr *destinationAddress, intdestinationAddressLength) |
通過UDP socket 傳送資料到特定的網路地址,傳送成功返回成功傳送的位元組數,否則返回 -1。 由於 UDP 可以向多個網路地址傳送資料,所以可以指定特定網路地址,以向其傳送資料。 |
int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, intflags, sockaddr *fromAddress, int *fromAddressLength) |
從UDP socket 中讀取資料,並儲存傳送者的網路地址資訊,讀取成功返回成功讀取的位元組數,否則返回 -1 。 由於 UDP 可以接收來自多個網路地址的資料,所以需要提供額外的引數,以儲存該資料的傳送者身份。 |
三,伺服器工作流程
有了上面的 socket API 講解,下面來總結一下伺服器的工作流程。
- 伺服器呼叫 socket(...) 建立socket;
- 伺服器呼叫 listen(...) 設定緩衝區;
- 伺服器通過 accept(...)接受客戶端請求建立連線;
- 伺服器與客戶端建立連線之後,就可以通過 send(...)/receive(...)向客戶端傳送或從客戶端接收資料;
- 伺服器呼叫 close 關閉 socket;
由於 iOS 裝置通常是作為客戶端,因此在本文中不會用程式碼來演示如何建立一個iOS伺服器,但可以參考前文:《深入淺出Cocoa之Bonjour網路程式設計》看看如何在 Mac 系統下建立桌面伺服器。
四,客戶端工作流程
由於 iOS 裝置通常是作為客戶端,下文將演示如何編寫客戶端程式碼。先來總結一下客戶端工作流程。
- 客戶端呼叫 socket(...) 建立socket;
- 客戶端呼叫 connect(...) 向伺服器發起連線請求以建立連線;
- 客戶端與伺服器建立連線之後,就可以通過 send(...)/receive(...)向客戶端傳送或從客戶端接收資料;
- 客戶端呼叫 close 關閉 socket;
五,客戶端程式碼示例
下面的程式碼就實現了上面客戶端的工作流程:
- (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]; }]; }