iOS网络编程之Socket
[深入淺出Cocoa]iOS網(wǎng)絡(luò)編程之Socket
羅朝輝 (http://blog.csdn.net/kesalin) CC 許可,轉(zhuǎn)載請(qǐng)注明出處
更多 Cocoa 開發(fā)文章,敬請(qǐng)?jiān)L問《深入淺出Cocoa》 CSDN專欄:http://blog.csdn.net/column/details/cocoa.html
一,iOS網(wǎng)絡(luò)編程層次模型
在前文《深入淺出Cocoa之Bonjour網(wǎng)絡(luò)編程》中我介紹了如何在Mac系統(tǒng)下進(jìn)行 Bonjour 編程,在那篇文章中也介紹過 Cocoa 中網(wǎng)絡(luò)編程層次結(jié)構(gòu)分為三層,雖然那篇演示的是 Mac 系統(tǒng)的例子,其實(shí)對(duì)iOS系統(tǒng)來說也是一樣的。iOS網(wǎng)絡(luò)編程層次結(jié)構(gòu)也分為三層:
- Cocoa層:NSURL,Bonjour,Game Kit,WebKit
- Core Foundation層:基于 C 的?CFNetwork 和 CFNetServices
- OS層:基于 C 的 BSD socket
Cocoa層是最上層的基于 Objective-C 的 API,比如 URL訪問,NSStream,Bonjour,GameKit等,這是大多數(shù)情況下我們常用的 API。Cocoa 層是基于 Core Foundation 實(shí)現(xiàn)的。
Core Foundation層:因?yàn)橹苯邮褂?socket 需要更多的編程工作,所以蘋果對(duì) OS 層的 socket 進(jìn)行簡單的封裝以簡化編程任務(wù)。該層提供了 CFNetwork 和 CFNetServices,其中 CFNetwork 又是基于 CFStream 和 CFSocket。
OS層:最底層的 BSD socket 提供了對(duì)網(wǎng)絡(luò)編程最大程度的控制,但是編程工作也是最多的。因此,蘋果建議我們使用 Core Foundation 及以上層的 API 進(jìn)行編程。
本文將介紹如何在 iOS 系統(tǒng)下使用最底層的 socket 進(jìn)行編程,這和在 window 系統(tǒng)下使用 C/C++ 進(jìn)行 socket 編程并無多大區(qū)別。
本文源碼:https://github.com/kesalin/iOSSnippet/tree/master/KSNetworkDemo
運(yùn)行效果如下:
二,BSD socket API 簡介
BSD socket API 和 winsock API 接口大體差不多,下面將列出比較常用的 API:
| API接口 | 講解 |
| int socket(int addressFamily, int type, int protocol) int close(int socketFileDescriptor) | socket?創(chuàng)建并初始化 socket,返回該 socket 的文件描述符,如果描述符為 -1 表示創(chuàng)建失敗。 通常參數(shù) addressFamily 是 IPv4(AF_INET) 或 IPv6(AF_INET6)。type 表示 socket 的類型,通常是流stream(SOCK_STREAM) 或數(shù)據(jù)報(bào)文datagram(SOCK_DGRAM)。protocol 參數(shù)通常設(shè)置為0,以便讓系統(tǒng)自動(dòng)為選擇我們合適的協(xié)議,對(duì)于 stream socket 來說會(huì)是 TCP 協(xié)議(IPPROTO_TCP),而對(duì)于 datagram來說會(huì)是 UDP 協(xié)議(IPPROTO_UDP)。 close?關(guān)閉 socket。 |
| int bind(int socketFileDescriptor,sockaddr *addressToBind,int addressStructLength) | 將 socket 與特定主機(jī)地址與端口號(hào)綁定,成功綁定返回0,失敗返回 -1。 成功綁定之后,根據(jù)協(xié)議(TCP/UDP)的不同,我們可以對(duì) socket 進(jìn)行不同的操作: UDP:因?yàn)?UDP 是無連接的,綁定之后就可以利用 UDP socket 傳送數(shù)據(jù)了。 TCP:而 TCP 是需要建立端到端連接的,為了建立 TCP 連接服務(wù)器必須調(diào)用 listen(int socketFileDescriptor, int backlogSize) 來設(shè)置服務(wù)器的緩沖區(qū)隊(duì)列以接收客戶端的連接請(qǐng)求,backlogSize 表示客戶端連接請(qǐng)求緩沖區(qū)隊(duì)列的大小。當(dāng)調(diào)用 listen 設(shè)置之后,服務(wù)器等待客戶端請(qǐng)求,然后調(diào)用下面的 accept 來接受客戶端的連接請(qǐng)求。 |
| int accept(int socketFileDescriptor,sockaddr *clientAddress, intclientAddressStructLength) | 接受客戶端連接請(qǐng)求并將客戶端的網(wǎng)絡(luò)地址信息保存到 clientAddress 中。 當(dāng)客戶端連接請(qǐng)求被服務(wù)器接受之后,客戶端和服務(wù)器之間的鏈路就建立好了,兩者就可以通信了。 |
| int connect(int socketFileDescriptor,sockaddr *serverAddress, intserverAddressLength) | 客戶端向特定網(wǎng)絡(luò)地址的服務(wù)器發(fā)送連接請(qǐng)求,連接成功返回0,失敗返回 -1。 當(dāng)服務(wù)器建立好之后,客戶端通過調(diào)用該接口向服務(wù)器發(fā)起建立連接請(qǐng)求。對(duì)于 UDP 來說,該接口是可選的,如果調(diào)用了該接口,表明設(shè)置了該 UDP socket 默認(rèn)的網(wǎng)絡(luò)地址。對(duì)?TCP socket來說這就是傳說中三次握手建立連接發(fā)生的地方。 注意:該接口調(diào)用會(huì)阻塞當(dāng)前線程,直到服務(wù)器返回。 |
| hostent* gethostbyname(char *hostname) | 使用 DNS 查找特定主機(jī)名字對(duì)應(yīng)的 IP 地址。如果找不到對(duì)應(yīng)的 IP 地址則返回 NULL。 |
| int send(int socketFileDescriptor, char*buffer, int bufferLength, int flags) | 通過 socket 發(fā)送數(shù)據(jù),發(fā)送成功返回成功發(fā)送的字節(jié)數(shù),否則返回 -1。 一旦連接建立好之后,就可以通過 send/receive 接口發(fā)送或接收數(shù)據(jù)了。注意調(diào)用 connect 設(shè)置了默認(rèn)網(wǎng)絡(luò)地址的 UDP socket 也可以調(diào)用該接口來接收數(shù)據(jù)。 |
| int receive(int socketFileDescriptor,char *buffer, int bufferLength, int flags) | 從 socket 中讀取數(shù)據(jù),讀取成功返回成功讀取的字節(jié)數(shù),否則返回 -1。 一旦連接建立好之后,就可以通過 send/receive 接口發(fā)送或接收數(shù)據(jù)了。注意調(diào)用 connect 設(shè)置了默認(rèn)網(wǎng)絡(luò)地址的 UDP socket 也可以調(diào)用該接口來發(fā)送數(shù)據(jù)。 |
| int sendto(int socketFileDescriptor,char *buffer, int bufferLength, intflags, sockaddr *destinationAddress, intdestinationAddressLength) | 通過UDP socket 發(fā)送數(shù)據(jù)到特定的網(wǎng)絡(luò)地址,發(fā)送成功返回成功發(fā)送的字節(jié)數(shù),否則返回 -1。 由于 UDP 可以向多個(gè)網(wǎng)絡(luò)地址發(fā)送數(shù)據(jù),所以可以指定特定網(wǎng)絡(luò)地址,以向其發(fā)送數(shù)據(jù)。 |
| int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, intflags, sockaddr *fromAddress, int*fromAddressLength) | 從UDP socket 中讀取數(shù)據(jù),并保存發(fā)送者的網(wǎng)絡(luò)地址信息,讀取成功返回成功讀取的字節(jié)數(shù),否則返回 -1?。 由于 UDP 可以接收來自多個(gè)網(wǎng)絡(luò)地址的數(shù)據(jù),所以需要提供額外的參數(shù),以保存該數(shù)據(jù)的發(fā)送者身份。 |
三,服務(wù)器工作流程
有了上面的 socket API 講解,下面來總結(jié)一下服務(wù)器的工作流程。
由于 iOS 設(shè)備通常是作為客戶端,因此在本文中不會(huì)用代碼來演示如何建立一個(gè)iOS服務(wù)器,但可以參考前文:《深入淺出Cocoa之Bonjour網(wǎng)絡(luò)編程》看看如何在 Mac 系統(tǒng)下建立桌面服務(wù)器。
?
四,客戶端工作流程
由于 iOS 設(shè)備通常是作為客戶端,下文將演示如何編寫客戶端代碼。先來總結(jié)一下客戶端工作流程。
?
五,客戶端代碼示例
下面的代碼就實(shí)現(xiàn)了上面客戶端的工作流程:
- (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 線程中進(jìn)行。如下所示:
NSThread * backgroundThread = [[NSThread alloc] initWithTarget:selfselector:@selector(loadDataFromServerWithURL:)object:url];[backgroundThread start];同樣,在獲取到數(shù)據(jù)或者網(wǎng)絡(luò)異常導(dǎo)致任務(wù)失敗,我們需要更新 UI,這也要回到 UI 線程中去做這個(gè)事情。如下所示:
- (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];}]; }總結(jié)
以上是生活随笔為你收集整理的iOS网络编程之Socket的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Cocoa]深入浅出 Cocoa 之
- 下一篇: 详解Framework