本文主要介绍
CocoaAsyncSocket
的 读/写 操作,以及如何处理TCP粘包的问题,CocoaAsyncSocket
基本使用的请查看上一篇文章Socket & CocoaAsyncSocket介绍与使用
排队 读/写 操作
CocoaAsyncSocket
库的最佳功能之一是“排队 读/写 操作”。
-
写操作
: 套接字未连接,但我还是可以开始写它,库将排队我的所有写操作,在套接字连接后,它将自动开始执行我的写操作!
NSError * err = nil ;
NSError *err = nil;
if (![self.clientSocket connectToHost:@"ip地址" onPort: "端口号" error:&err]) //异步!
{
NSLog(@"I goofed: %@", err);
}
//此时套接字未连接。
//但我还是可以开始写它!
//库将排队我的所有写操作,
//在套接字连接后,它将自动开始执行我的写操作!
[socket writeData: data1 withTimeout: - 1 tag: 1 ];
[socket writeData: data2 withTimeout: - 1 tag: 2 ];
-
排队读
: 我们可以通过长度获取到相应长度的数据,可以很好解决粘包问题
[socket readDataToLength: datalength withTimeout: -1 tag: 0];
Tag参数了解
CocoaAsyncSocket中的tag参数
是不通过套接字发送的,也不是从套接字读取的。tag参数只需通过各种委托方法回显给您。它旨在帮助简化委托方法中的代码。
[socket writeData: data1 withTimeout: - 1 tag: 1 ];
[socket writeData: data2 withTimeout: - 1 tag: 2 ];
// 当我们发送数据时候使用 tag 标记后,发送后可以在委托方法中根据 tag 看到那条数据已经发送出去了。
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag :( long)tag{
if(tag == 1)
NSLog(@"First request sent ");
else if(tag == 2)
NSLog(@"Second request sent ");
}
在读取时数据时,tag 也很有帮助:
[socket readDataWithTimeout:-1 tag:0];
// 读取 tag 与上面方法的 tag 值是一一对应的。
#define TAG_WELCOME 10
#define TAG_CAPABILITIES 11
#define TAG_MSG 12
- (void)socket:(GCDAsyncSocket *)sender didReadData :( NSData *)data withTag :( long)tag{
if(tag == TAG_WELCOME)
{
// 忽略欢迎信息
}
else if(tag == TAG_CAPABILITIES)
{
[self processCapabilities:data];
}
else if (tag == TAG_MSG)
{
[self processMessage: data];
}
}
Tcp 粘包
1. 什么是tcp粘包?
TCP
是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法)
,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,就会出现粘包现象
2. TCP粘包解决方案
目前应用最广泛的是在消息的头部添加数据包长度,接收方根据消息长度进行接收;在一条TCP连接上,数据的流式传输在接收缓冲区里是有序的,其主要的问题就是第一个包的包尾与第二个包的包头共存接收缓冲区,所以根据长度读取是十分合适的;
2.1 解决发送方粘包
方案一: 发送产生是因为Nagle算法合并小数据包,那么可以禁用掉该算法;
方案二: TCP提供了强制数据立即传送的操作指令push,当填入数据后调用操作指令就可以立即将数据发送,而不必等待发送缓冲区填充自动发送;
方案三: 数据包中加头,头部信息为整个数据的长度(最广泛最常用)
;
// `方案三`发送方解决粘包的代码部分:
- (void)sendData:(NSData *)data{
NSMutableData *sendData = [NSMutableData data];
// 获取数据长度
NSInteger datalength = data.length;
// NSInteger长度转 NSData
NSData *lengthData = [NSData dataWithBytes:&datalength length:sizeof(datalength)];
// 长度几个字节和服务器协商好。这里我们用的是4个字节存储长度信息
NSData *newLengthData = [lengthData subdataWithRange:NSMakeRange(0, 4)];
// 拼接长度信息
[sendData appendData:newLengthData];
//拼接数据
[sendData appendData:data];
// 发送加了长度信息的包
[self.clientSocket writeData:[sendData copy] withTimeout:-1 tag:0];
}
2.2解决接收方粘包
- 解析数据包头部信息,根据长度来接收;
(最广泛最常用)
/**
数据缓冲区
*/
@property (nonatomic, strong) NSMutableData *dataBuffer;;
// 读取客户端发送的数据,通过包头长度进行拆包
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 数据存入缓冲区
[self.dataBuffer appendData:data];
// 如果长度大于4个字节,表示有数据包。4字节为包头,存储包内数据长度
while (self.dataBuffer.length >= 4) {
NSInteger datalength = 0;
// 获取包头,并获取长度
[[self.dataBuffer subdataWithRange:NSMakeRange(0, 4)] getBytes:&datalength length:sizeof(datalength)];
// 判断缓存区内是否有包
if (self.dataBuffer.length >= (datalength+4)) {
// 获取去掉包头的数据
NSData *realData = [[self.dataBuffer subdataWithRange:NSMakeRange(4, datalength)] copy];
// 解析处理
[self handleData:realData socket:sock];
// 移除已经拆过的包
self.dataBuffer = [NSMutableData dataWithData:[self.dataBuffer subdataWithRange:NSMakeRange(datalength+4, self.dataBuffer.length - (datalength+4))]];
}else{
break;
}
}
[sock readDataWithTimeout:-1 tag:0];
}
- 自定义数据格式:在数据中放入开始、结束标识;解析时根据格式抓取数据,缺点是数据内不能含有开始或结束标识;
- 短连接传输,建立一次连接只传输一次数据就关闭;(不推荐)
注:以上代码仅提供粘包的解决思路,具体如何解包以及包头数据结构可以和服务器进行商定