[深刻浅出Cocoa]iOS网络编程之Socketios
在前文《深刻浅出Cocoa之Bonjour网络编程》中我介绍了如何在Mac系统下进行 Bonjour 编程,在那篇文章中也介绍过 Cocoa 中网络编程层次结构分为三层,虽然那篇演示的是 Mac 系统的例子,其实对iOS系统来讲也是同样的。iOS网络编程层次结构也分为三层:git
Cocoa层是最上层的基于 Objective-C 的 API,好比 URL访问,NSStream,Bonjour,GameKit等,这是大多数状况下咱们经常使用的 API。Cocoa 层是基于 Core Foundation 实现的。github
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/KSNetworkDemoapp
运行效果以下:socket
BSD socket API 和 winsock API 接口大致差很少,下面将列出比较经常使用的 API:oop
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 讲解,下面来总结一下服务器的工做流程。
因为 iOS 设备一般是做为客户端,所以在本文中不会用代码来演示如何创建一个iOS服务器,但能够参考前文:《深刻浅出Cocoa之Bonjour网络编程》看看如何在 Mac 系统下创建桌面服务器。
因为 iOS 设备一般是做为客户端,下文将演示如何编写客户端代码。先来总结一下客户端工做流程。
下面的代码就实现了上面客户端的工做流程:
- (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]; }]; }