欢迎转载!转载时请注明。
iOS中Socket的实现方式有很多种,你可以选择使用CFNetwork库API自己敲,当然,如果你想绕过那些繁琐的逻辑控制和异常处理,那么最好的解决方案就是寻找一个稳定、安全、可靠的开源库。
曾经有一个朋友问过我这样一个问题:
你能自己写为什么还要去用别人的?
我的回答是:
全世界几乎所有iOS开发者都能写HTTP请求为什么还在用AF...和ASI...?
其实这个问题很简单,就是愿意装x还是愿意选择效率的区别。就啰嗦到这里,下面进入正题。
开源库:AsyncSocket, 可以在Git中直接搜索下载。
还是先说一下基础:
初始化和连接:
@property(nonatomic,strong)AsyncSocket * mainSocket;
- (AsyncSocket *)mainSocket { if (!_mainSocket) { _mainSocket = [[AsyncSocket alloc] init]; } return _mainSocket; }
[self.mainSocket setDelegate:self];
连接到服务器:BOOL successToConnect = [self.mainSocket connectToHost:host onPort:port error:&error];
当我们请求数据发送后,服务器响应的数据会在代理方法 - (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag 中,那么问题来了:
前提:Socket读取的数据(当前通道中可读取的数据)并不一定是发送方所发送的完整数据包,比如发送方发送了一个结构为0xabc的数据,那么读取方在读取的时候受多种因素的限制,有可能读取出来的可用数据会是0xa + 0xbc 或者 0xab + 0xc ...那么这个时候从Socket的回调函数中获得的数据可能将会是被拆分后的部分数据。(采用TCP协议的话数据是不会出现丢失情况,也就是说无论回调函数中一次返回的数据长度是多少,无论一个完整地数据包是经过多少次才读取完成,这个数据最终将会是完整地被读取方读取。)
问题一:
假设服务器是会返回一个长度为150KB的二进制数据,那么我们怎么却确定我们在代理方法中所得到的NSData对象是完整地呢?
首先想到的肯定是这样:
创建一个全局的 NSMutableData 数据缓存对象,收到代理方法返回数据后
那么这个时候问题又来了,我们怎么确定这个数据包得长度和这个数据包起始偏移量?{ [self.buffer appendData:data]; NSLog(@"<< Len : %ld",_buffer.length); }
此时最好的解决方法就是在所有数据传输中为每一个数据包加上一个"包头"。这个包头可以是JSON字符串也可试试结构体(Struct)或者任一能后识别的数据类型。下面详细的说一下步骤。
1.定义一个包头结构体,发送和接受双方需要一致,这里可能有的朋友会遇到sizeof()函数在不同平台读取出来长度不一致的问题,我们需要先设置一下字节对齐方式:如果是一个字节对齐 #pragma pack(1)
typedef struct main_data_struct { long long dSize; }HEADERPKG;
代理方法中第一个if分支是为了保证buffer长度不会小于结构体大小,如果长度小于了结构体大小,那么在方法 - (void)getBytes:(void *)buffer length:(NSUInteger)length 中会直接导致程序崩溃,原因很简单,数据读取越界。- (void)onSocket:(NSData *)sock didReadData:(NSData *)data withTag:(long)tag { [self.buffer appendData:data]; struct main_data_struct header_pkg; if (_buffer.length >= sizeof(header_pkg)) { // Read header. [_buffer getBytes:&header_pkg length:sizeof(header_pkg)]; if (_buffer.length == header_pkg.dSize) { // Finished. // Do something... } } }
第二个if分支是是为了解决我们之前的假设,此时本地的buffer拼接后得到的最终数据如果等于包头中发送方所填的 dSize 字段的值,那么这个时候所拼接的数据就是一个完整地数据。
到此为止,我们的第一个假设已经得到解决,那么问题又来了:
问题二:
假设服务器会连续返回10个长度不均的数据包,我们怎么去确定哪一个包是第一个,第二个从什么地方开始?
没错,首先想到的还是拼接,按照 问题一 的步骤进行拼包,然后将所有得到的数据包放到一个全局缓冲队列中,需要使用的时候再有序地从队列中读取。那么另一个衍生问题又来了:
这样的做法直接就会导致发送与接收方数据交互会出现不可预计的延迟时间,原因也很简单,因为及时是网络传输延迟确定的情况下,我们读取缓冲队列的速度未必能后和Socket读取到可用字节的速度相匹配,一旦这个缓冲队列数据逐渐增大,很有可能就会出现内存溢出或者影响设备其他进程的数据处理响应速度。
考虑到上面这个子问题,我们不得不另寻方法。思路大体上是这样:
1.Socket连接成功后,立即开始已包头长度读取数据,至于怎么实现读取指定大小的数据,你可以选择继续拼接缓冲,但是这里 AsyncSocket 为我们提供了一个封装后的快捷方法,在下面的代码片段会看到。
2.读取到第一个包头后,获得当前数据包长度,继续按照当前数据包长度读取当前包剩余的数据。
3.读取完成第一个包后,开始下一次的包头读取。
...无限循环
实现代码:
- (void)startReadSrteam { struct main_data_struct header; [self.mainSocket readDataToLength:sizeof(header) withTimeout:- 1 tag:TAG_SOCKET_READ_HEADER]; }
- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { if (self.shouldReadBuffer) { // Buffer appending. struct main_data_struct header_received; if (data.length == sizeof(header_received)) { [self.appendBuffer appendData:data]; [data getBytes:&header_received range:NSMakeRange(0, data.length)]; if (header_received.wHeader == MAIN_HEADER) { // Is package. if (header_received.unPacketSize > 0) { // Has bytes. [self.mainSocket readDataToLength:header_received.unPacketSize withTimeout:- 1 tag:TAG_SOCKET_APPEND_DATA]; }else { // Finished single buffer append. (Only header) if (_receivedDataBlock) { _receivedDataBlock(YES, data, TAG_SOCKET_READ_HEADER); } [self clearAppendBuffer]; [self.mainSocket readDataToLength:sizeof(header_received) withTimeout:- 1 tag:TAG_SOCKET_READ_HEADER]; } }else { // Is not package (when data length = sizeOf(header)). [self isNotCompletePackage:data tag:tag]; } }else { // Is not package (when data length != sizeOf(header)). [self isNotCompletePackage:data tag:tag]; } } // Lock. } /** * Package process. */ - (void)isNotCompletePackage:(NSData *)data tag:(long)tag { // Is not package (when data length != sizeOf(header)). [self.appendBuffer appendData:data]; struct main_data_struct header_appended; [self.appendBuffer getBytes:&header_appended range:NSMakeRange(0, sizeof(header_appended))]; if (header_appended.header == MAIN_HEADER) { if (header_appended.unPacketSize == (self.appendBuffer.length - sizeof(header_appended))) { // Finished single buffer append. if (_receivedDataBlock) { _receivedDataBlock(YES, self.appendBuffer, TAG_SOCKET_FINISHED_DATA); } [self clearAppendBuffer]; [self.mainSocket readDataToLength:sizeof(header_appended) withTimeout:- 1 tag:TAG_SOCKET_READ_HEADER]; }else { // Unfinished. NSInteger shouldReadLen = header_appended.unPacketSize - (self.appendBuffer.length - sizeof(header_appended)); if (shouldReadLen > 0) { // Go on. [self.mainSocket readDataToLength:shouldReadLen withTimeout:- 1 tag:TAG_SOCKET_APPEND_DATA]; }else { // The buffer did overflow. should rebuild pkg. [self clearAppendBuffer]; [self.mainSocket readDataToLength:sizeof(header_appended) withTimeout:- 1 tag:TAG_SOCKET_READ_HEADER]; } } }else { // Wrong pkg. should rebuild pkg. [self clearAppendBuffer]; [self.mainSocket readDataToLength:sizeof(header_appended) withTimeout:- 1 tag:TAG_SOCKET_READ_HEADER]; } }
1、- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag 方法中第一个if反之大家可以忽略,第二个if反之则是判断是否读取到包头,如果读取到,获取当前包数据大小。
2、获取到当前包大小后,继续按照此大小读取数据。
3、else则是放数据长度不到包头长度,表明数据并不是我们想要读取的包头数据,此时扔掉这一段,开始下一次包头读取。
4、如果读取到的长度不等于dSize大小,此时表明数据包尚未读完,跳转到方法 - (void)isNotCompletePackage:(NSData *)data tag:(long)tag 中处理拼包。
5、在 - (void)isNotCompletePackage:(NSData *)data tag:(long)tag 方法中主要做的判断主要是当如果当前buffer长度小于 dSize 则继续读取 dSize - currentSize 长度的数据;大于的话则丢弃当前数据并重新开始拼包。
总结:
单线程的套接字通讯并不适用于普通JSON数据的请求交互,大多数时候我们会选择HTTP方式。当然你也可以根据项目需求创建多个套接字以提高效率,这种方案一般适用于远程演示或者一些应用部署在云服务器中,而客户端只需要负责发送控制命令和接收结果数据的情况。