[深入淺出Cocoa]iOS網絡編程之Socket

[深入淺出Cocoa]iOS網絡編程之Socket

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接口 講解
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 講解,下面來總結一下服務器的工作流程。

  1. 服務器調用 socket(...) 創建socket;
  2. 服務器調用 listen(...) 設置緩衝區;
  3. 服務器通過 accept(...)接受客戶端請求建立連接;
  4. 服務器與客戶端建立連接之後,就可以通過 send(...)/receive(...)向客戶端發送或從客戶端接收數據;
  5. 服務器調用 close 關閉 socket;

由於 iOS 設備通常是作爲客戶端,因此在本文中不會用代碼來演示如何建立一個iOS服務器,但可以參考前文:《深入淺出Cocoa之Bonjour網絡編程》看看如何在 Mac 系統下建立桌面服務器。

 

四,客戶端工作流程

由於 iOS 設備通常是作爲客戶端,下文將演示如何編寫客戶端代碼。先來總結一下客戶端工作流程。

  1. 客戶端調用 socket(...) 創建socket;
  2. 客戶端調用 connect(...) 向服務器發起連接請求以建立連接;
  3. 客戶端與服務器建立連接之後,就可以通過 send(...)/receive(...)向客戶端發送或從客戶端接收數據;
  4. 客戶端調用 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];
    }];
}

   

發佈了192 篇原創文章 · 獲贊 524 · 訪問量 246萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章