公司项目弃用了第三方的IM,使用socket来实现。我在网上看了一些资料,决定使用CocoaAsyncSocket来实现。总体来说,这个框架很轻量级,而且非常的好用、简单易懂。
cocoaAsyncSocket github 地址
下面说一下它的用法:
初始化socket
//创建一个socket对象
if(!_socket) {
dispatch_queue_t queue = dispatch_queue_create("SocketQueue", NULL);
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:queue];
}else{
[_socket setDelegate:self];
}
//连接
NSError *error = nil;
[self.socket connectToHost:addr onPort:port withTimeout:TIME_OUT error:&error]
这里创建socket的时传入的队列我自己创建了一个串行,你也可以加入主队列,但是不能是并发队列。TIME_OUT 为超时时间,-1表示不设置超时,你也可以根据实际情况设置超时限制。
发送socket消息
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
关闭socket
[self.socket disconnect];
[self.socket setDelegate:nil];
代理方法
#pragma mark -socket的代理
#pragma mark 连接成功
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"Socket连接成功");
}
#pragma mark 断开连接
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"Socket连接断开");
}
#pragma mark 数据发送成功
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSLog(@"Socket发送成功");
[socket readDataWithTimeout:WRITE_TIME_OUT tag:tag];//手动调用读取刚刚发送的数据
}
#pragma mark 读取数据
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
//在这里处理接收到的socket数据
[self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
}
以上就是CocoaAsyncSocket的基本用法,是不是很简单明了。实际开发中,我们会在这个的基础上添加一些其他的逻辑比如:连接成功后与服务器做权限校验,发送数据加密,接收数据解码、心跳等,这些和服务端协商好就可以了。本以为到此结束的时候,发现了一个问题:粘包。
为什么会出现粘包 ?
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
拆包
思路:预先设置一个全局的变量NSMutableData *cacheData,每收到一个data数据先不马上去解析它,而是把它拼接到cacheData中去。这样的话,如果包不完整,就可以保留数据不做处理等下一次拼接,然后处理;如果多个包粘一起,可以循环处理。
区分每个包:每个包分为 head 和 body 两部分,当多个包粘在一起时,我们从包的head 中取出这个包的长度。然后从cacheData中截取对应长度,然后再用同样的方法取下一个,就这样一个一个的拆分,如果不够读取数据等下一次拼接。
head 中的信息是和服务端协议的,按照具体情况定
代码:
#pragma mark 读取数据
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
//由于获取data数据可能为多个数据的拼接或是不完整 先加入cachedata中再解析
[self analyticalData:data];
[self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
}
#pragma mark 解析数据
- (void)analyticalData:(NSData *)data {
if (data.length > 0) {
[_cacheData appendData:data];
}
//我们这边定义的head的长度为16个字节,按具体情况。如果大于16个字节就循环取包
while (_cacheData.length > 16) {
NSData *head = [_cacheData subdataWithRange:NSMakeRange(0, 16)];//取得头部数据
//head中定义前4个字节为包的长度
NSData *lengthData = [head subdataWithRange:NSMakeRange(0, 4)];
NSInteger packageLength = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];
//从head中取出和服务器协商的消息协议
NSData *operation = [head subdataWithRange:NSMakeRange(8, 4)];;
//如果包的长度没有实际包的长度,读取数据等下次拼接
if (packageLength > _cacheData.length) {
[self.socket readDataWithTimeout:-1 tag:0];
return;
}
取出boday
NSData *bodyData = [_cacheData subdataWithRange:NSMakeRange(16, packageLength)];
//从cacheData中去掉已读取的数据
_cacheData = [NSMutableData dataWithData:[_cacheData subdataWithRange:NSMakeRange(packageLength, _cacheData.length - packageLength)]];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//异步处理数据,根据不同数据类型处理不同的事件
[weakSelf functionWithOperation:operation jsonData:bodyData];
});
}
}
一些问题:
读取数据
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
在这个方法中要手动调用readDataWithTimeout方法,不然会出现只有第一次读到数据。读取消息的TIME_OUT 要设置为-1,不然会频繁的断开。
因为我们的项目中解析数据要先将NSData 转成byte数组,然后进去解码。会用到Byte *byte = malloc() 方法,malloc 方法会在堆中分配内存,每次收到消息都要调用,内存就会一直涨需要使用完byte后要调用free(byte)释放。