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連接:CFStreamCreatePairWithSocketToHost 、CFStreamCreatePairWithSocketToCFHost或CFStreamCreatePairWithSocketToNetService。只是不需要將其轉換為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端程序
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, 1024, 0) == - 1) {
36 perror( " send error ");
37 }
38 }
39 }
40 return 0;
41 }
2.2.2 Server端程序
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, 1024, 0);
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只有一個方法:
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",該文件預先創建好的。
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",並將數據寫入該文件中。
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連接,其函數原型是:
通過NSStream對象進行Socket通信,與通過NSStream進行IO操作的步驟基本一樣:
a) 創建NSStream對象,通過CFStreamCreatePairWithSocketToHost函數創建CFReadStreamRef 和CFWriteStreamRef 對象;繼而將兩者轉換為NSInputStream 和NSOutputStream 對象;
b) 配置run loop,並打開NSInputStream 和NSOutputStream對象;
c) 響應事件,在Delegate中響應不同的信號;
d) 關閉NSStream對象。
3.4.2 示例
如下是由NSStream實現的socket client,其中socket server可以使用2章節的例子配合測試。實現的功能是進行client和server消息的收發。
2 [super viewDidLoad];
3 NSString *urlStr = [NSString stringWithFormat: @" 127.0.0.1 "];
4 [self searchForSite:urlStr];
5 }
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 }
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
