iOS 網絡編程:socket


1 Socket基礎

      在IOS中,根據不同的語言環境可以使用不同的方法來創建socket連接。

1) 在Objective-C語言環境使用NSStream類API

     *如果知道遠程主機的 DNS或者是IP 地址,那么可以使用 CFStreamCreatePairWithSocketToHost 或者函數 CFStreamCreatePairWithSocketToCFHost創建 core foundation的連接,然后將CFStream對象toll-free bridged轉換為NSStream 對象。
     *也可以傳遞給 CFStreamCreatePairWithSocketToNetService函數 一個CFNetServiceRef對象,來創建一個到Bonjour 服務器上的連接。

2) 在C語言環境使用CFStream類API

      可以使用低級別的CFStream API來創建socket連接,這種方式與NSStream API的使用方式一樣,也是通過三個函數來創建與遠程主機的socket連接:CFStreamCreatePairWithSocketToHostCFStreamCreatePairWithSocketToCFHostCFStreamCreatePairWithSocketToNetService。只是不需要將其轉換為NSStream 對象,其使用方式與第1種類似。  

3) 在跨平台環境使用POSIX調用

      也可以使用POSIX類型的socket連接,但是如果在OS X 和iOS系統中,應避免使用這種方式,因為其使用方式非常繁瑣。特別是不要在GUI主線程中使用同步方式的POSIX連接,因為這樣會影響用戶體驗。

2 BSD Socket

2.1 簡介

      UNIX內核加入TCP/IP協議的時候,便在系統中引入了一種新的IO操作,只不過由於網絡連接的不可靠性,所以網絡IO比本地設備的IO復雜很多。這一系列的接口叫做BSD Socket API,當初由伯克利大學研發,最終成為網絡開發接口的標准。 網絡通信從本質上講也是進程間通信,只是這兩個進程一般在網絡中不同計算機上。

      由於本文重點是討論IOS的socket編程,並且Apple官網也不推薦使用BSD socket編程,所以這里只稍微紀錄,若需詳細研究可以參考《UNIX網絡編程卷1:套接字聯網API(第3版)》和另一篇筆記《Socket知識整理》。

2.2 基本程序

      socket連接由TCP和UDP兩種類型,而TCP的使用頻率較高,下面參考《UNIX網絡編程卷1:套接字聯網API(第3版)》的基本TCP連接圖,實現一個簡單的例子,其中這個例子是UNIX程序,即在MAC系統中也可正確執行。

圖 21 基本TCP客戶端/服務器socket連接圖

2.2.1 Client端程序

 1 #include <stdio.h> 
 2 #include <netinet/ in.h> 
 3 #include <sys/socket.h> 
 4 #include <arpa/inet.h> 
 5 #include < string.h> 
 6  
 7  int main ( int argc,  const  char * argv[]) 
 8 { 
 9      struct sockaddr_in server_addr; 
10     server_addr.sin_len =  sizeof( struct sockaddr_in); 
11     server_addr.sin_family = AF_INET; 
12     server_addr.sin_port = htons( 11332); 
13     server_addr.sin_addr.s_addr = inet_addr( " 127.0.0.1 "); 
14     bzero(&(server_addr.sin_zero), 8); 
15      
16      int server_socket = socket(AF_INET, SOCK_STREAM,  0); 
17      if (server_socket == - 1) { 
18         perror( " socket error "); 
19          return  1
20     } 
21      char recv_msg[ 1024]; 
22      char reply_msg[ 1024]; 
23      
24      if (connect(server_socket, ( struct sockaddr *)&server_addr,  sizeof( struct sockaddr_in))== 0)  { 
25          // connect 成功之后,其實系統將你創建的socket綁定到一個系統分配的端口上,且其為全相關,包含服務器端的信息,可以用來和服務器端進行通信。 
26           while ( 1) { 
27             bzero(recv_msg,  1024); 
28             bzero(reply_msg,  1024); 
29              long byte_num = recv(server_socket,recv_msg, 1024, 0); 
30             recv_msg[byte_num] =  ' \0 '
31             printf( " server said:%s\n ",recv_msg); 
32              
33             printf( " reply: "); 
34             scanf( " %s ",reply_msg); 
35              if (send(server_socket, reply_msg,  10240) == - 1) { 
36                 perror( " send error "); 
37             } 
38         }    
39     }      
40      return  0
41 }

 

2.2.2 Server端程序

 1 #include <stdio.h>
 2 #include <netinet/ in.h>
 3 #include <sys/socket.h>
 4 #include <arpa/inet.h>
 5 #include < string.h>
 6 
 7  int main ( int argc,  const  char * argv[])
 8 {
 9      struct sockaddr_in server_addr;
10     server_addr.sin_len =  sizeof( struct sockaddr_in);
11     server_addr.sin_family = AF_INET; // Address families AF_INET互聯網地址簇
12      server_addr.sin_port = htons( 11332);
13     server_addr.sin_addr.s_addr = inet_addr( " 127.0.0.1 ");
14     bzero(&(server_addr.sin_zero), 8);
15 
16      // 創建socket
17       int server_socket = socket(AF_INET, SOCK_STREAM,  0); // SOCK_STREAM 有連接
18       if (server_socket == - 1) {
19         perror( " socket error ");
20          return  1;
21     }
22 
23      // 綁定socket:將創建的socket綁定到本地的IP地址和端口,此socket是半相關的,只是負責偵聽客戶端的連接請求,並不能用於和客戶端通信
24       int bind_result = bind(server_socket, ( struct sockaddr *)&server_addr,  sizeof(server_addr));
25      if (bind_result == - 1) {
26         perror( " bind error ");
27          return  1;
28     }
29 
30      // listen偵聽 第一個參數是套接字,第二個參數為等待接受的連接的隊列的大小,在connect請求過來的時候,完成三次握手后先將連接放到這個隊列中,直到被accept處理。如果這個隊列滿>了,且有新的連接的時候,對方可能會收到出錯信息。
31       if (listen(server_socket,  5) == - 1) {
32         perror( " listen error ");
33          return  1;
34     }
35 
36      struct sockaddr_in client_address;
37     socklen_t address_len;
38      int client_socket = accept(server_socket, ( struct sockaddr *)&client_address, &address_len);
39      // 返回的client_socket為一個全相關的socket,其中包含client的地址和端口信息,通過client_socket可以和客戶端進行通信。
40       if (client_socket == - 1) {
41         perror( " accept error ");
42          return - 1;
43     }
44 
45      char recv_msg[ 1024];
46      char reply_msg[ 1024];
47 
48      while ( 1) {
49         bzero(recv_msg,  1024);
50         bzero(reply_msg,  1024);
51 
52         printf( " reply: ");
53         scanf( " %s ",reply_msg);
54         send(client_socket, reply_msg,  10240);
55 
56          long byte_num = recv(client_socket,recv_msg, 1024, 0);
57         recv_msg[byte_num] =  ' \0 ';
58         printf( " client said:%s\n ",recv_msg);
59 
60     }
61 
62      return  0;
63 }

 

3 NSStream Socket

3.1 Cocoa Streams

3.1.1 NSStream相關類

         Cocoa Streams包含三個相關的類: NSStream、NSInputStream 和NSOutputStream。

  • NSStream:是個抽象類,定義了一些基本屬性和方法;
  • NSInputStream:是NSStream的子類,可通過它從NSData、File和socket中讀取數據流
  • NSOutputStream:也是NSStream的子類,可通過它將數據流寫入NSData、File和socket。

 

圖 31 NSInputStream和NSOutputStream數據轉換圖

3.1.2 NSStreamDelegate

      還可以給stream對象設置Delegate(NSStreamDelegate),如果沒有精確了給stream指定Delegate,那么默認將Delegate設置為其自己。

NSStreamDelegate只有一個方法:

- ( void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;

         NSStreamEvent有多種類型,主要的是NSStreamEventHasBytesAvailable,表示已經可以從輸入stream對象中讀取數據了,或是寫入的數據已經被接收了。

3.1.3 與CFStream比較

     NSStream是基於CFStream創建的,所以可以將NSInputStream 和NSOutputStream轉換為CFWriteStream 和CFReadStream。雖然NSStream和CFStream非常相似,但是它們仍有所不同,NSStream是Cocoa API,它是通過設置delegate類實現異步行為CFStream是Core Foundation API,它是通過設置回調函數來實現異步的行為

3.2 通過NSInputStream 讀數據

      在Cocoa中,通過NSInputStream對象讀數據,可以分為如下步驟完成:

       a) 從數據源創建初始化一個NSInputStream對象;

       b) 配置run loop打開stream對象;

       c) 響應NSInputStream事件NSStreamDelegate

       d) 關閉NSInputStream對象。

如下例子是打開一個Document目錄下的文件"theFile.txt",該文件預先創建好的。

 1 - ( void)viewDidLoad {
 2 [super viewDidLoad];
 3     NSArray *document = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,  true);
 4     NSString *myDocPath = [document objectAtIndex: 0];
 5     NSString* fileName = [myDocPath stringByAppendingPathComponent: @" theFile.txt "];
 6     
 7     [self setUpStreamForFile:fileName];
 8 }
 9 
10 - ( void)setUpStreamForFile:(NSString *)path {  // 自定義方法,初始化input Stream,並啟動讀文件
11      NSInputStream *iStream = [[NSInputStream alloc] initWithFileAtPath:path];
12     [iStream setDelegate:self];
13     [iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
14     [iStream open];
15 }
16 - ( void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {  // 實現協議方法,響應事件。
17      NSMutableData *_data;
18      switch(eventCode) {
19          case NSStreamEventHasBytesAvailable:
20         {
21             uint8_t buf[ 1024];
22             unsigned  int len =  0;
23 len = [(NSInputStream *)stream read:buf maxLength: 1024];     // 當有可讀數據時,才開始讀。
24  printf( " %s\n ",buf);
25               break;
26         }
27          case NSStreamEventEndEncountered:
28         {
29             [stream close];
30             [stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
31             stream = nil; 
32              break;
33         }
34     }
35 }

3.3 通過NSOutputStream寫數據

      在Cocoa中,通過NSOutputStream對象寫數據,可以分為如下步驟完成:

       a) 從數據源創建初始化一個NSOutputStream對象;

       b) 配置run loop打開stream對象

       c) 響應NSOutputStream事件NSStreamDelegate

       d) 關閉NSOutputStream對象。

 

如下例子是打開一個Document目錄下的文件"theFile.txt",並將數據寫入該文件中。

 1 - ( void)viewDidLoad {
 2     [super viewDidLoad];
 3     NSArray *document = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,  true);
 4     NSString *myDocPath = [document objectAtIndex: 0];
 5     NSString* fileName = [myDocPath stringByAppendingPathComponent: @" theFile.txt "];
 6     
 7     [self createOutputStream:fileName]; 
 8 }
 9 
10 - ( void)createOutputStream:(NSString *)path
11 {
12     NSOutputStream* oStream = [[NSOutputStream alloc] initToFileAtPath:path append: true];
13     [oStream setDelegate:self];
14     [oStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
15     [oStream open];
16 }
17 
18 - ( void)setUpStreamForFile:(NSString *)path {
19     NSInputStream *iStream = [[NSInputStream alloc] initWithFileAtPath:path];
20     [iStream setDelegate:self];
21     [iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
22     [iStream open];
23 }
24 - ( void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
25 {
26     NSOutputStream *oStream = stream;
27      switch(eventCode) {
28          case NSStreamEventHasSpaceAvailable:
29         {
30             uint8_t buf[]= " hello my lover ";
31             unsigned  int len = strlen(buf)+ 1;
32             [oStream write:( const uint8_t *)buf maxLength:len];
33             [oStream close];
34              break;
35         }
36          case NSStreamEventEndEncountered:
37         {
38             [stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
39             oStream = nil; 
40              break;
41         }
42     }
43 }

 

3.4 建立socket stream

3.4.1 實現原理

      由於NSStream類不支持在IOS平台上創建socket連接,而CFStream支持在IOS平台的socket行為。所以若知道遠程主機的DNS或者是IP地址,可以使用CFStreamCreatePairWithSocketToHost函數來創建socket連接,通過該函數創建了CFStream類型為全雙工的socket連接,接着可以利用toll-free bridge,將CFStream對象轉換為NSStream對象

      CFStreamCreatePairWithSocketToHost函數是基於TCP協議創建的socket連接,其函數原型是:

void CFStreamCreatePairWithSocketToHost(CFAllocatorRef alloc, CFStringRef host, UInt32 port, CFReadStreamRef *readStream, CFWriteStreamRef *writeStream);

 

      通過NSStream對象進行Socket通信,與通過NSStream進行IO操作的步驟基本一樣:

       a) 創建NSStream對象,通過CFStreamCreatePairWithSocketToHost函數創建CFReadStreamRef 和CFWriteStreamRef 對象;繼而將兩者轉換為NSInputStreamNSOutputStream 對象;

       b) 配置run loop打開NSInputStream NSOutputStream對象

       c) 響應事件,在Delegate中響應不同的信號;

       d) 關閉NSStream對象。

3.4.2 示例

      如下是由NSStream實現的socket client,其中socket server可以使用2章節的例子配合測試。實現的功能是進行client和server消息的收發。

1 - ( void)viewDidLoad {  // 該方法是IOS的入口方法。
2      [super viewDidLoad];
3     NSString *urlStr = [NSString stringWithFormat: @" 127.0.0.1 "];
4     [self searchForSite:urlStr]; 
5 }

 

 1 - (IBAction)searchForSite:(NSString *)urlStr  // 該方法實現的功能是創建socket連接,並啟動對socket描述符進行監聽。
 2  {
 3         CFReadStreamRef readStream;
 4         CFWriteStreamRef writeStream;
 5          // 該方法就是通過CFStream創建的socket連接
 6          CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)urlStr,  11332, &readStream, &writeStream);
 7         
 8         NSInputStream *inputStream = (__bridge_transfer NSInputStream *)readStream;      // 實現轉換
 9          NSOutputStream *outputStream = (__bridge_transfer NSOutputStream *)writeStream; // 實現轉換
10         
11         [inputStream setDelegate:self];   // 設置代理
12          [outputStream setDelegate:self];
13 
14         [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
15         [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
16 
17         [inputStream open];
18         [outputStream open];
19         
20          /*  Store a reference to the input and output streams so that they don't go away....  */
21     }
22 }

 

 1 - ( void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode  // 該方法是NSStreamDelegate協議方法,對事件的響應方法
 2 
 3      switch(eventCode) {
 4          case NSStreamEventHasSpaceAvailable:    // 可寫的事件響應處理
 5          {
 6             NSOutputStream *oStream = stream; // 因NSStream不能調用write方法,故需強制轉換為NSOutputStream。
 7              uint8_t buf[]= " hello socket ";
 8             unsigned  int len = strlen(buf)+ 1;
 9             [oStream write:( const uint8_t *)buf maxLength:len];
10              break;
11         }
12          case NSStreamEventEndEncountered:   // 結束事件
13          {
14             [stream close];
15             [stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
16             stream = nil;  //  stream is ivar, so reinit it
17               break
18         }
19          case NSStreamEventNone:
20         {
21              break;
22         }
23          case NSStreamEventOpenCompleted:   // 打開完成事件
24          {
25             NSLog( @" NSStreamEventOpenCompleted ");
26              break;
27         }
28          case NSStreamEventErrorOccurred:   // 錯誤發生事件
29          {
30             NSError *theError = [stream streamError];
31             NSLog( @" Error %i: %@ ", [theError code], [theError localizedDescription]);
32             [stream close];
33              break;
34         }
35          case NSStreamEventHasBytesAvailable:   // 可讀的事件響應處理
36          {
37             NSMutableData *_data;
38             uint8_t buf[ 1024];
39             unsigned  int len =  0;
40             len = [(NSInputStream *)stream read:buf maxLength: 1024]; 
41              if(len) {
42                 [_data appendBytes:( const  void *)buf length:len];
43                 printf( " %s\n ",buf);
44             }  else {
45                 NSLog( @" no buffer! ");
46             }
47              break;
48         }     
49     }
50 }

 

4 參考文獻

      [1] Stream Programming Guide.

      [2] Networking Programming Topics

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM