[深入淺出Cocoa]iOS網絡程式設計之Socket
羅朝輝 ( http://blog.csdn.net/kesalin) CC 許可,轉載請注明出處
更多 Cocoa 開發文章,敬請通路《深入淺出Cocoa》 CSDN專欄: http://blog.csdn.net/column/details/cocoa.html
一,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 程式設計并無多大差別。
本文源碼:https://github.com/kesalin/iOSSnippet/tree/master/KSNetworkDemo
運作效果如下:
二,BSD socket API 簡介
BSD socket API 和 winsock API 接口大體差不多,下面将列出比較常用的 API:
API接口 | 講解 |
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。 | |
| 将 socket 與特定主機位址與端口号綁定,成功綁定傳回0,失敗傳回 -1。 成功綁定之後,根據協定(TCP/UDP)的不同,我們可以對 socket 進行不同的操作: UDP:因為 UDP 是無連接配接的,綁定之後就可以利用 UDP socket 傳送資料了。 TCP:而 TCP 是需要建立端到端連接配接的,為了建立 TCP 連接配接伺服器必須調用 listen(int socketFileDescriptor, int backlogSize) 來設定伺服器的緩沖區隊列以接收用戶端的連接配接請求,backlogSize 表示用戶端連接配接請求緩沖區隊列的大小。當調用 listen 設定之後,伺服器等待用戶端請求,然後調用下面的 accept 來接受用戶端的連接配接請求。 |
| 接受用戶端連接配接請求并将用戶端的網絡位址資訊儲存到 clientAddress 中。 當用戶端連接配接請求被伺服器接受之後,用戶端和伺服器之間的鍊路就建立好了,兩者就可以通信了。 |
| 用戶端向特定網絡位址的伺服器發送連接配接請求,連接配接成功傳回0,失敗傳回 -1。 當伺服器建立好之後,用戶端通過調用該接口向伺服器發起建立連接配接請求。對于 UDP 來說,該接口是可選的,如果調用了該接口,表明設定了該 UDP socket 預設的網絡位址。對 TCP socket來說這就是傳說中三次握手建立連接配接發生的地方。 注意:該接口調用會阻塞目前線程,直到伺服器傳回。 |
| 使用 DNS 查找特定主機名字對應的 IP 位址。如果找不到對應的 IP 位址則傳回 NULL。 |
| 通過 socket 發送資料,發送成功傳回成功發送的位元組數,否則傳回 -1。 一旦連接配接建立好之後,就可以通過 send/receive 接口發送或接收資料了。注意調用 connect 設定了預設網絡位址的 UDP socket 也可以調用該接口來接收資料。 |
| 從 socket 中讀取資料,讀取成功傳回成功讀取的位元組數,否則傳回 -1。 一旦連接配接建立好之後,就可以通過 send/receive 接口發送或接收資料了。注意調用 connect 設定了預設網絡位址的 UDP socket 也可以調用該接口來發送資料。 |
| 通過UDP socket 發送資料到特定的網絡位址,發送成功傳回成功發送的位元組數,否則傳回 -1。 由于 UDP 可以向多個網絡位址發送資料,是以可以指定特定網絡位址,以向其發送資料。 |
| 從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];
}];
}