使用CFNetwork實現TCP協議的通信
TCP/IP通信協議是一種可靠的網絡協議,它在通信的兩端各建立一個通信接口,從而在通信的兩端之間形成網絡虛擬鏈路.一旦建立了虛擬的網絡鏈路,兩端的程序就可以通過虛擬鏈路進行通信.CFNetwork對基於TCP協議的網絡通信提供了良好的封裝,CFNetwork使用CFSocket來代表兩端的通信接口,還可以通過CFStream讀/寫數據.
IP地址與端口號
IP地址用於唯一地標識網絡中的一個通信實體,這個通信實體既可以是一台主機,也可以是一台打印機,或者是路由器的某一個端口.在基於IP協議的網絡中傳輸的數據包,都必須使用IP地址來進行標識.
IP地址是數字型的,是一個32位(32Bit)整數,通常把它分成4個8位的二進制數,每8為之間用圓點隔開,每個8位整數可以轉換成一個0-255的十進制整數,因此看到的IP地址常常是這樣的形式:198.162.8.10.
NIC(Internet Network Information Center)統一負責全球Internet IP地址的規划、管理,而Inter NIC、APNIC、RIPE三大網絡信息中心具體負責美國及其他地區的IP地址分配。其中APNIC負責亞太地區的IP管理,我國申請IP地址也要通過APNIC,APNIC的總部設在日本東京大學。
IP地址被分成了A、B、C、D、E五類,每個類別的網絡標識和主機標識各有規則。
- A類:10.0.0.0~10.255.255.255
- B類:172.16.0.0~172.31.255.255
- C類:192.168.0.0~192.168.255.255
IP地址用於唯一地標識網絡上的一個通信實體,但一個通信實體可以有多個通信程序同時提供網絡服務,此時還需要使用端口.
端口是一個16位的整數,用於標識數據交給那個通信程序處理.因此,端口就是應用程序與外界交流的出入口,它是一種抽象的軟件結構,包括一些數據結構和I/O(基本輸入/輸出)緩沖區.
不同的應用程序處理不同端口上的數據,同一台機器上不能有兩個程序使用同一個端口,端口號可以從0到65535,通常將它分為3類.
- 公認端口(Well Know Ports):從0到1023,它們緊密綁定(Binding)一些特定的服務.
- 注冊端口(Registered Ports):從1024到49151,它們松散地綁定一些服務。應用程序通常使用這個范圍內的端口。
- 動態和/或私有端口(Dynamic and/or Private Ports):從49152到65535,這些端口是應用程序使用的動態端口,應用程序一般不會主動使用這些端口。
TCP協議基礎
IP協議是 Internet 上使用的一個關鍵協議,它是全稱是 Internet Protocol, 即Internet協議,通常簡稱 IP 協議.通過使用 IP協議,使 Internet 成為一個允許連接不同類型的計算機和不同操作系統的網絡.
要使兩台計算機之間彼此進行通信,必須使兩台計算機使用同一種”語言”,IP 協議只保證計算機能發送和接收分組數據. IP協議負責將消息從一個主機傳送到另一個主機,消息在傳送的過程中被分割成一個個小包.
安裝 IP 協議之后,可保證計算機之間發送和接收數據,但 IP協議還不能解決數據分組在傳輸過程中可能出現的問題.因此,若要解決可能出現的問題,連接上 Internet 的計算機還需要安裝 TCP 協議來提供可靠並且無差錯的通信服務.
TCP協議被稱作一種端對端協議.這是因為它為兩台計算機之間的連接起到了重要作用-----當一台計算機需要與另一台遠程計算機連接時, TCP 協議會讓它們建立一個連接:用於發送和接收數據的虛擬鏈路.
TCP 協議負責收集這些信息包,並將其按適當的次序放好傳送,在接收端收到后再將其正確地還原. TCP協議保證了數據包在傳送中准備無誤. TCP 協議使用重發機制:當一個通信實體發送一個消息給另一個通信實體后,需要收到另一個通信實體的確認信息,如果沒有收到另一個通信實體的確認信息,則會再次重發剛才發送的信息.
通過這種重發機制, TCP協議向應用程序提供可靠的通信連接,使它能夠自動適應網上的各種變化.即使在 Internet 暫時出現堵塞的情況下, TCP也能夠保證通信的可靠性.
使用 CFSocket 實現 TCP服務器端
使用CFSocket建立服務器的步驟如下. |
|||||||||
1 |
創建一個監聽Socket Accept(Socket連接)的CFSocket,並為kCFSocketAcceptCallBack事件綁定回調函數. |
||||||||
2 |
調用CFSocketSetAddress()函數,將服務器端的CFSocket綁定到本地IP地址和端口 |
||||||||
3 |
將CFSocket作為source添加到指定線程的CFRunLoop上,並運行該線程的CFRunLoop,從而保證該CFSocket能持續不斷地接受來自客戶端的連接. |
||||||||
代碼片段 |
1 #import <sys/socket.h> 2 3 #import <arpa/inet.h> 4 5 #import<Foundation/Foundation.h> 6 7 // 讀取數據的回調函數 8 9 void readStream(CFReadStreamRef iStream, CFStreamEventType eventType, void *clientCallBackInfo) 10 11 { 12 13 UInt 8 buff[2048]; 14 15 CFIndex hasRead = CFReadStreamRead(iStream, buff, 2048); 16 17 if(hasRead > 0) 18 19 { 20 21 // 強制只處理hasRead前面的數據 22 23 buff[hasRead]=’\0’; 24 25 printf(“接收到數據: %s\n”, buff); 26 27 } 28 29 } 30 31 // 有客戶端連接進來的回調函數 32 33 void TCPServerAcceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info ) 34 35 { 36 37 // 如果有客戶端Socket連接進來 38 39 if(kCFSocketAcceptCallBack == type) 40 41 { 42 43 // 獲取本地Socket的Handle 44 45 CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle*)data; 46 47 uint8_t name[SOCk_MAXADDRLEN]; 48 49 socklen_t nameLen = sizeof(name); 50 51 // 獲取對方Socket信息,還有getsocketname()函數則用於獲取本程序所在Socket信息 52 53 if(getpeername(nativeSocketHandle, (struct sockaddr *)name, &nameLen) != 0) 54 55 { 56 57 NSLog(@”error”); 58 59 exit(1); 60 61 } 62 63 // 獲取連接信息 64 65 struct sockeaddr_in * addr_in = (struct socketaddr_in*) name; 66 67 NSLog(@”%s: %d 連接進來了”, inet_ntoa(addr_in->sin_addr) 68 69 , addr_in->sin_port); 70 71 CFReadStreamRef iStream; 72 73 CFWriteStreamRef oStream; 74 75 // 創建一組可讀/寫的CFStream 76 77 CFStreamCreatePairWithSocket(kCFAllocatorDefault , nativeSocketHandle, &iStream, &oStream); 78 79 if(iStream && oStream) 80 81 { 82 83 // 打開輸入流和輸入流 84 85 CFReadStreamOpen(iStream); 86 87 CFWriteStreamOpen(oStream); 88 89 CFStreamClientContext streamContext = {0, NULL, NULL, NULL}; 90 91 if(!CFReadStreamSetClient(iStream, kCFStreamEventHasBytesAvailable, 92 93 readStream/*回調函數,當有可讀的數據時調用*/, &streamContext)) 94 95 { 96 97 exit(1); 98 99 } 100 101 CFReadStreamScheduleWithRunLoop(iStream, CFRunLoopGetCurrent(), 102 103 kC FRunLoopCommonModes); 104 105 const char* str = “您好,您收到Mac服務器的新年祝福!\n”; 106 107 // 向客戶端輸出數據 108 109 CFWriteStreamWrite(oStream, (UInt8 *)str, strlen(str) + 1); 110 111 } 112 113 } 114 115 } 116 117 int main(int argc, char * argv[]) 118 119 { 120 121 @autoreleasepool{ 122 123 // 創建Socket,指定TCPServerAcceptCallBack 124 125 // 作為kCFSocketAcceptCallBack事件的監聽函數 126 127 CFSocketRef _socket = CFSocketCreate(kCFAllocatorDefault 128 129 , PF_INEF // 指定協議族,如果該參數為0或者負數,則默認為PF_INEF 130 131 // 指定Socket類型,如果協議族為PF_INEF,且該參數為0或負數 132 133 // 則它會默認為SOCK_STREAM,如果要使用UDP協議,則該參數指定為SOCK_DGRAM 134 135 , SOCK_STREAM 136 137 // 指定通信協議。如果前一個參數為SOCK_STREAM,則默認使用TCP協議 138 139 // 如果前一個參數為SOCK_DGRAM,則默認使用UDP協議 140 141 ,IPPROTO_TCP 142 143 // 該參數指定下一個回調函數所監聽的事件類型 144 145 ,kCFSocketAcceptCallBack 146 147 ,TCPServerAcceptCallBack // 回調函數 148 149 ,NULL); 150 151 if(_socket == NULL) 152 153 { 154 155 NSLog(@”創建Socket失敗!”); 156 157 return 0; 158 159 } 160 161 int optval = 1; 162 163 // 設置允許重用本地地址和端口 164 165 setsockopt(CFSocketGetNative(_socket), SOL_SOCKET, SO_REUSEADDR, 166 167 (void *)&optval, sizeof(optval)); 168 169 // 定義sockaddr_in類型的變量,該變量將作為CFSocket的地址 170 171 struct sockaddr_in addr4; 172 173 memset(&addr4, 0 , sizeof(addr4)); 174 175 addr4.sin_len = sizeof(addr4); 176 177 addr4.sin_family = AF_INEF; 178 179 // 設置該服務器監聽本機任意可用的IP地址 180 181 // addr4.sin_addr.s_addr =htonl(INADDR_ANY); 182 183 // 設置服務器監聽地址 184 185 addr4.sin_addr.s_addr = inet_addr(“192.168.1.100”); 186 187 // 設置服務器監聽端口 188 189 addr4.sin_port = htons(30000); 190 191 // 將IPv4的地址轉換成CFDataRef 192 193 CFDataRef address = CFDataCreate(kCFAllocatorDefault 194 195 , (UInt8 *)&addr4, sizeof(addr4)); 196 197 // 將CFSocket綁定到指定IP地址 198 199 if(CFSocketSetAddress(_socket, address) != kCFSocketSuccess) 200 201 { 202 203 NSLog(@”地址綁定失敗!”); 204 205 // 如果_socket不為NULL,則釋放_socket 206 207 if(_socket) 208 209 { 210 211 CFRelease(_socket); 212 213 exit(1); 214 215 } 216 217 _socket = NULL; 218 219 } 220 221 NSLog(@”---啟動循環監聽客戶端連接-----”); 222 223 // 獲取當前線程的CFRunLoop 224 225 CFRunLoopRef cfRunLoop = CFRunLoopGetCurrent(); 226 227 // 將_socket包裝成CFRunLoopSource 228 229 CFRunLoopSourceRef source = CFSocketCreateRunLoopSource( 230 231 kCFAllocatorDefault , _socket, 0); 232 233 // 為CFRunLoop對象添加source 234 235 CFRunLoopAddSource(cfRunLoop, source, kCFRunLoopCommonModes); 236 237 CFRelease(source); 238 239 // 運行當前線程的CFRunLoop 240 241 CFRunLoopRun(); 242 243 } 244 245 return 0; 246 247 }
|
||||||||
說明 |
上面程序中的main()函數作為程序的入口,程序從該函數開始執行.main()函數的第1段紅色字代碼創建了一個CFSocket對象,並指定了該CFSocket使用TCP協議,基於流經行輸入/輸出. sockaddr_in類型的結構體變量,該結構體變量將會作為CFSocket綁定的監聽地址,因此程序為socketaddr_in類型的結構體變量指定了IP地址和端口,然后程序中的紅色字代碼調用了CFSocketSetAddress()函數將指定CFSocket綁定到指定的IP地址和端口. main()函數的最后一段紅色體代碼將該CFSocket作為source添加到主線程的CFRunLoop上,並運行主線程的CFRunLoop,從而保證該CFSocket能持續不斷地接受來自客戶端的連接. |
||||||||
|
該程序的另一個重點是TCPServerAcceptCallBack回調函數---當CFSocket接受來自客戶端的連接后,該函數將會被調用.該函數主要做了如下4件事情.
|
使用CFSocket實現TCP客戶端
創建TCP客戶端同樣通過CFSocket完成。使用CFSocket創建Socket客戶端的步驟如下。
1 |
創建一個不監聽任何事件或監聽Connection的CFSocket。如果要監聽Connection,則需要為kCFSocketConnectCallBack事件綁定回調函數. |
2 |
調用CFSocketConnectToAddress()函數,將客戶端的CFSocket連接到指定IP地址和端口的服務器上. |
3 |
得到客戶端CFSocket之后,既可直接使用CFSocket對應的CFSocketNativeHandle進行讀/寫,也可通過CFSocket獲取CFReadStreamRef、CFWriteStreamRef后進行讀/寫。 |
代碼片段 |
ViewController.m #import <sys/socket.h> #import <netinet/in.h> #import <arpa/inet.h> #import <ViewController.h> @implementation ViewController CFSocketRef _socket; BOOL isOnline; - (void)viewDidLoad { [super viewDidLoad]; // 創建Socket,無須回調函數 _socket = CFSocketCreate(kCFAllocatorDefault, PF_INEF // 指定協議族,如果該參數為0或者負數,則默認為PF_INET // 指定Socket類型,如果協議族為PF_INEF,且該參數為0或負數 // 則它會默認為SOCK_STREAM,如果要使用UDP協議,則該參數指定為SOCK_DGRAM , SOCK_STREAM // 指定通信協議.如果前一個參數為SOCK_STREAM,則默認使用TCP協議 // 如果前一個參數SOCK_DGRAM,則默認使用UDP協議 , IPPROTO_TCP // 該參數指定下一個回調函數所監聽的事件類型 , kCFSocketNoCallBack , nil , NULL); if(_socket != nil) { // 定義sockaddr_in類型的變量,該變量將作為CFSocket的地址 struct sockaddr_in addr4; memset(&addr4, 0, sizeof(addr4)); addr4.sin_len = sizeof(addr4); addr4.sin_family = AF_INET; // 設置連接遠程服務器的地址 addr4.sin_addr.s_addr = inet_addr(“192.168.1.88”); // 設置連接遠程服務器的監聽端口 addr4.sin_port = htons(30000); // 將IPv4的地址轉換為CFDataRef CFDateRef addres = CFDataCreate(kCFAllocatorDefault , (UInt8 *)&addr4, sizeof(addr4)); // 連接遠程服務器的Socket,並返回連接結果 CFSocketError result = CFSocketConnectionToAddress(_socket , address // 指定遠程服務器的IP地址和端口 , 5 // 指定連接超時時長, 如果該參數為負數, 則把連接操作放在后台進行 // 當_socket消息類型為kCFSocketConnectCallBack時 // 將會在連接成功或失敗的時候在后台觸發回調函數 ); // 如果連接遠程服務器成功 if(result == kCFSocketSuccess) { isOnline = YES; // 啟動新線程來讀取服務器響應的數據 [NSThread detachNewThreadSelector:@selector(readStream) toTarget:self withObject:nil]; } } } // 讀取接收的數據 - (void)readStream { char buffer[2048]; int hasRead; // 與本機關聯的Socket如果已經失效,則返回-1:INVALID_SOCKET while ((hasRead = recv(CFSocketGetNative(_socket) , buffer, sizeof(buffer), 0))) { NSLog(@”%@”,[[NSString alloc] initWithBytes:buffer length:hasRead encoding:NSUTF8StringEncoding]); } } - (IBAction)clicked:(id)sender { if(isOnline) { NSString* stringTosend = @”來自iOS客戶端的問候”; const char* data = [stringTosend UTF8String]; send(CFSocketGetNative(_socket), data, strlen(data) +1, 1); } else { NSLog(@”暫未連接服務器”); } } @end
|
說明 |
上面程序中的第1段紅色字代碼創建了一個CFSocket, 該CFSocket同樣適用了TCP協議,並且是基於SOCK_STREAM流的Socket,然后程序創建了一個struct sockaddr_in 結構體變量,該結構體變量代表遠程服務器的地址. 上面程序中的第2段紅色字代碼調用了CFSocketConnectToAddress()函數將CFSocket連接到遠程服務器地址---如果連城成功,就可得到一個進行網絡讀/寫的CFSocket.剩下的事情是程序以readStream作為新線程的執行體,啟動了一個新線程,其中readStream方法中的紅色字代碼調用了recv()函數從指定CFSocket讀取數據.而clicked:方法則用於向服務器發送數據,該方法中的紅色字代碼調用了send()函數向CFSocket發送數據. |
使用CocoaAsyncSocket實現TCP客戶端
CocoaAsyncSocket封裝了CFNetwork底層的CFSocket和CFStream,並提供了異步操作,從而可簡化Socket網絡編程.CocoaAsyncSocket不僅支持TCP協議的網絡編程,也支持UDP協議的網絡編程.CocoaAsyncSocket是CFSocket的絕佳替代者.
CocoaAsyncSocket主要有以下特性 |
|
1 |
非阻塞方式的讀和寫,而且可設置超時時長. |
2 |
自動的Socket接收。如果調用它接受連接,它將為每個連接啟動新的實例,當然也可以立即關閉這些連接。 |
3 |
委托(delegate)支持。錯誤、連接、接收、完整的讀取、完整的寫入、進度以及斷開連接,都可通過委托模式調用。 |
4 |
所有操作都封裝在一個類中,無須操作Socket或流,該類封裝了所有操作。 |
下載和安裝CocoaAsyncSocket的步驟如下 |
|
1.CocoaAsyncSocket的官方網站是https://github.com/robbiehanson/CocoaAsyncSocket,登錄該站點,單擊頁面中間的release鏈接. |
|
2.瀏覽器將會打開一個新的列表頁面,該列表頁面中列出了CocoaAsyncSocket的所有發布版本,建議下載最新版的CocoaAsyncSocket. |
|
3.下載完成將可以得到一個CocoaAsyncSocket.zip文件,解壓該壓縮包,將可以看到如下文件結構. RunLoop:該目錄下包含了AsyncSocket\AsyncUDPSocket兩個類的源文件和Xcode目錄.其中AsyncSocket就是基於TCP協議的CocoaAsyncSocket實現,AsyncUDPSocket就是基於UDP協議的AsyncUDPSocket實現。而Xcode目錄下則包含了使用CocoaAsyncSocket開發服務器端與客戶端的示例項目。 GCD:該目錄下的內容與RunLoop目錄下的內容基本相似,只是類名變成了GCDAsyncSocket、GCDAsyncUDPSocket,這是因為該目錄下的CocoaAsyncSocket是基於GCD的實現。 Vendor:其他相關類。 其他雜項文件。 需要為項目增加CFNetwork.framework框架。 |
|
添加CocoaAsyncSocket支持之后,使用AsyncSocket開發TCP客戶端的步驟如下 |
|
|
|
代 碼 片 段 |
|
1 ViewController.h 2 3 #import <UIKit/UIKit.h> 4 5 #import “AsyncSocket.h” 6 7 @interface ViewController : UIViewController<AsyncSocketDelegate> 8 9 @property (strong, nonatomic) IBOutlet UITextView *showView; 10 11 @property (strong, nonatomic) IBOutlet UITextFiled *inputField; 12 13 - (IBAction)finishEdit:(id)sender; 14 15 - (IBAction)send:(id)sender; 16 17 @end 18 19 ViewController.m 20 21 @implementation ViewController 22 23 NSString* myName; 24 25 AsyncSocket* socket; 26 27 BOOL iSOnline; 28 29 - (void)viewDidLoad 30 31 { 32 33 [super viewDidLoad]; 34 35 // 創建一個UIAlertView提醒用戶輸入名字 36 37 UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@”名字” 38 39 message:@”請輸入您的名字” delegate:self 40 41 cancelButtonTitle:@”確定” otherButtonTitles: nil ]; 42 43 // 設置該UIAlertView為UIAlertViewStylePlainTextInput風格 44 45 alert.alertViewStyle = UIAlertViewStylePlainTextInput; 46 47 [alert show]; 48 49 } 50 51 - (IBAction)finishEdit:(id)sender 52 53 { 54 55 [sender resignFirstResponder]; 56 57 } 58 59 - (IBAction)send:(id)sender 60 61 { 62 63 if(isOnline) 64 65 { 66 67 // 定義要發送的字符串內容 68 69 NSString* stringTosend = [NSString stringWithFormat:@”%@說: %@”, myName, self.inputField.text]; 70 71 self.inputField.text = nil; 72 73 NSData *data = [stringTosend dataUsingEncoding:NSUTF8StringEncoding]; 74 75 // 調用writeData:withTimeout:tag:方法發送數據 76 77 [socket writeData:data withTimeout: - 1 tag:0]; 78 79 } 80 81 else 82 83 { 84 85 NSLog(@”暫未連接服務器”); 86 87 } 88 89 } 90 91 // AsyncSocketDelegate中定義的方法,當成功連接到服務器時激發該方法 92 93 - (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port 94 95 { 96 97 isOnline = YES; 98 99 // 調用readDataWithTimeout: tag: 方法讀取數據 100 101 [sock readDataWithTimeout:-1 tag:0]; 102 103 } 104 105 // AsyncSocketDelegate中定義的方法,當讀取數據完成時激發該方法 106 107 - (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag 108 109 { 110 111 // 獲取讀到的內容 112 113 NSData* strData = [data subDataWithRange:NSMakeRange(0, [data length])]; 114 115 NSString* content = [[NSString alloc] initWithData:strData encoding:NSUTF8StringEncoding]; 116 117 if(content) 118 119 { 120 121 // 使用showView控件顯示從網絡讀取的內容 122 123 self.showView.text = [NSString stringWithFormat:@”%@\n%@”, 124 125 content , self.showView.text]; 126 127 } 128 129 // 再次調用readDataWithTimeout:tag:方法讀取數據 130 131 [sock readDataWithTimeout:-1 tag:0]; 132 133 } 134 135 - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex 136 137 { 138 139 // 獲取UIAlertView控件上 文本框內的字符串,並將字符串賦值給myName變量 140 141 myName = [alertView textFieldAtIndex:0].text; 142 143 socket = [[AsyncSocket alloc] initWithDelegate:self]; 144 145 NSError* error = nil; 146 147 int port = 30000; 148 149 NSString* host = @”192.168.1.88”; 150 151 // 調用connectToHost:onPort:error:方法連接指定IP地址、指定端口的服務器 152 153 [socket connectToHost:host onPort:port withTimeout:2 error:&error]; 154 155 if(error) 156 157 { 158 159 NSLog(@”連接出現錯誤:%@”, error); 160 161 } 162 163 } 164 165 @end
|
|
上面程序中的第1行紅色字代碼創建了一個AsyncSocket,並指定該視圖控制器本身作為它的delegate對象,這意味着該視圖控制器本身需要實現AsyncSocketDelegate協議,並實現該協議中特定的方法.程序中的第2行紅色字代碼調用了AsyncSocket的connectToHost:onPort:error: 方法來連接指定IP地址、指定端口的服務器程序。 處理AsyncSocket網絡連接及網絡通信過程中的事件,該視圖控制器(作為AsyncSocket的delegate)實現了onSocket:didConnectToHost:port:方法------當AsyncSocket成功連接指定服務器時激發該方法,該方法中的程序調用了AsyncSocket的readDataWithTimeout: tag:方法讀取網絡數據。當AsyncSocket成功讀取網絡數據之后,系統會自動調用視圖控制器(作為AsyncSocket的delegate)的onSocket:didReadData:方法------這就實現了通過AsyncSocket讀取網絡數據。 |