CocoaAsyncSocket三方框架,其封装了TCP和UDP的socket,分别是GCDAsyncSocket和GCDAsyncUdpSocket,处理了iv4和ipv6,给开发者省去了不少麻烦,只需要按照规则使用即可
这里主要介绍基于TCP的GCDAsyncSocket,也会简单介绍GCDAsyncUdpSocket部分逻辑与应用
这是我写的可以发送文字和图片的demo(注意更改客户端连接的ip地址):服务端 --- 客户端
实际上平时我们接触的即时通信逻辑大体为:以服务器为中心,桥接来自客户端们发送的数据给另一个客户端,如下所示:
GCDAsyncSocket的基本使用
通过socket的交互就了解到,服务端和客户端的交互过程是有些不一样的,GCDAsyncSocket虽然简化了,但是改不一样的还是不一样,其为CocoaAsyncSocket中的tcp封装类
服务端的基本使用
初始化
通过直接设置代理,设置代理执行队列,且开启接收监听客户端的连接即可,ip是自动获取的,只需要设置端口号即可
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self
delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
NSError *error;
//开启接收服务器
[self.socket acceptOnPort:8040 error:&error];
if (error) {
NSLog(@"服务器开启失败:%@",error.localizedDescription);
}else {
NSLog(@"服务器socket开启成功");
}
遵循协议
初始化后,需要执行其遵循的GCDAsyncSocketDelegate代理协议,实现下面几个协议
客户端连接到当前服务器的协议回调,返回了客户端的socket,注意此时还还没开启数据监听回调功能,需要主动调用readDataWithTimeout方法,读取监听数据,且每次读取到新数据,监听会取消,需要重新开启调用
//客户端已经连接到当前服务器
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
[self.clientSockets addObject:newSocket];
[newSocket readDataWithTimeout:-1 tag:10010]; //读取客户端发送过来的消息
}
任何socket断开连接后的回调
//socket断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"socket断开连接: %@", err.localizedDescription);
}
通过调用readDataWithTimeout监听数据到来,当客户端发送数据时,会回调该方法,注意读取完毕数据后,需要再次调用readDataWithTimeout方法继续监听客户端数据
//接收到客户端的数据
//消息结构 数据长度 + 数据类型 + 数据,需要解决粘包和拆包的问题,后面单独介绍
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
[self reciveData:data]; //接收只有粘包的逻辑
//[self reciveMoreData:data]; //接收粘包拆包逻辑
//读取完毕数据之后,缓存区断开,需要重新监听
[sock readDataWithTimeout:-1 tag:10010];
}
消息发送成功的回调
//消息发送成功
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSLog(@"消息发送成功:%ld", tag);
}
客户端的基本使用
初始化
通过直接设置代理,设置代理执行队列,然后连接到指定ip的服务器
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self
delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
NSError *error;
//开启接收服务器
[self.socket connectToHost:@"192.168.1.1" onPort:8040 withTimeout:-1 error:&error];
if (error) {
NSLog(@"连接服务器失败:%@",error.localizedDescription);
}
遵循协议
初始化后,需要执行其遵循的GCDAsyncSocketDelegate代理协议,实现下面几个协议
客户端连接服务器成功后,会回调该方法,注意此时还还没开启数据监听回调功能,需要主动调用readDataWithTimeout方法,读取监听数据,且每次读取到新数据,监听会取消,需要重新开启调用
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(nonnull NSString *)host port:(uint16_t)port {
NSLog(@"连接服务器成功");
//需要开启读取数据监听
[sock readDataWithTimeout:-1 tag:10086];
}
任何socket断开连接后的回调
//socket断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"断开了与服务器的连接");
}
通过调用readDataWithTimeout监听数据到来,当服务端发送数据时,会回调该方法,注意读取完毕数据后,需要再次调用readDataWithTimeout方法继续监听服务端数据内容
//接收到客户端的数据
//消息结构 数据长度 + 数据类型 + 数据,需要解决粘包和拆包的问题,后面单独介绍
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
[self reciveData:data]; //接收只有粘包的逻辑
//[self reciveMoreData:data]; //接收粘包拆包逻辑
//读取完毕数据之后,缓存区断开,需要重新监听
[sock readDataWithTimeout:-1 tag:10086];
}
消息发送成功的回调
//消息发送成功
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSLog(@"发送消息成功");
}
粘包、拆包
注意:无论什么骚操作,一个socket发送时需要在一个线程内有序进行,可以避免额外的数据解析逻辑
传输数据时,如果一个信息数据过大,会分段传输,需要接收端根据其信息,拼接到一起封包处理,然而实际传输过程中,由于接收端可能延迟接收,导致会一次性从缓存区读取出好几段数据,因此会出现粘包拆包等现象,下面通过图文流程介绍:
情景1:包之间都是依次通过,读取读取过程及时,没有出现粘包,但是C数据被分为了C1、C2分段传输,因此出现了拆包情况,需要取出数据拼接,进行合并
情景2:B和C两个包被一次性从缓存区读取,出现粘包; D1和D2是被分段传输的数据,出现了拆包现象,同时D1、D2两段被同时从一个缓存区读取,因此也出现了粘包现象
情景3:B和C1被一次性从缓存区读取,出现粘包,同时B、C1,还有C2和D,同时出现了粘包现象,C1、C2为正常的拆包现象,需要合并
拆包:针对拆包现象,需要对数据取出拼接成一个完整的数据
粘包:针对粘包现象,需要根据指定数据长度进行分离出独立的包
方案一、传输数据结构与粘包处理
平时传输的数据包一般都有限制,加上现在的设备,数据大小一般都能一次性传输完毕,无需将一个数据包拆分成多个,因此不会出现拆包现象,平时传递大图片和视频时,可以走http的文件传输,最后用tcp传输的url文本,因此可以简化为只有粘包逻辑的出现(此过程如果服务端发送顺序出错,会出现接收顺序问题)
传输基本数据结构:数据长度(8) + 类型(4) + 数据(n)
其中数据长度为这段数据的总长度(为n,当前数据包长度:8+4+n),实际可能会加入其他数据,例如时间等,类型根据发送的内容来确定
下面是发送非拆包消息时的加工过程
- (void)sendData {
NSMutableData *mData = [NSMutableData data];
if (self.tfSendMessage.text.length > 0) {
//给没个客户端发送一段数据
const char *textStr = self.tfSendMessage.text.UTF8String;
NSData *data = [NSData dataWithBytes:textStr length:strlen(textStr)];
unsigned long dataLength = data.length;
NSData *lenData = [NSData dataWithBytes:&dataLength length:8];
[mData appendData:lenData];
//文字类型
unsigned int typeByte = 0x00000001;
NSData *typeData = [NSData dataWithBytes:&typeByte length:4];
[mData appendData:typeData];
[mData appendData:data];
NSLog(@"发送内容为:%@", self.tfSendMessage.text);
self.tfSendMessage.text = @"";
}else {
//发送图片,其实实际上不一定在非要传递图片的,有的走的是http上传到文件服务器,然后利用返回的url在发送给对方
NSData *imgData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"MMGG" ofType:@"jpeg"]];
//展示发送的图片4s
[self showImageInfo:imgData];
//发送图片
unsigned long dataLength = imgData.length;
NSData *lenData = [NSData dataWithBytes:&dataLength length:8];
[mData appendData:lenData];
//图片类型
unsigned int typeByte = 0x00000002;
NSData *typeData = [NSData dataWithBytes:&typeByte length:4];
[mData appendData:typeData];
[mData appendData:imgData];
}
//发送消息
[self.socket writeData:mData withTimeout:-1 tag:10086];
}
接收消息的过程要处理掉粘包的问题,因此需要一个循环解析掉几个连在一起的内容,将他们拆成独立的包
代码如下所示:
//处理粘包逻过程
- (void)reciveData:(NSData *)data {
if (data.length < 1) return;
//当前接收的数据包长度
unsigned long totolLength = data.length;
unsigned long currentLength = 0;
//do while解决粘包问题,在里面进行拆包
do {
unsigned long length;
unsigned int type;
[data getBytes:&length range:NSMakeRange(currentLength, 8)];
[data getBytes:&type range:NSMakeRange(currentLength + 8, 4)];
//获取实际数据
NSData *contentData = [data subdataWithRange:NSMakeRange(currentLength + 12, length)];
if (type == 1) {
//文字
NSString *content = [[NSString alloc] initWithData:contentData encoding:NSUTF8StringEncoding];
NSLog(@"接收的数据为:%@", content);
}else if (type == 2) {
//图片
[self showImageInfo:contentData];
}else {
NSLog(@"不支持的数据类型");
}
currentLength += length + 12;
} while (currentLength < totolLength);
}
方案二、传输数据结构与粘包拆包处理
实际工作中,可能有些人已经先入为主,已经提前完成了拆包的逻辑,内容不易更改,或者由于其他因素导致,因此会出现较大数据传输,因此会引发拆包现象,在解决粘包的过程,还需要解决拆包的问题(尤其是服务器端,还需要给每个客户端传来的额外在增加接收数据的字段)
注意:此方案一定要保证传输的数据顺序传输,否则不接收数据的顺序问题,可能会导致数据包整合混乱,数据接收失败,严重的会导致崩溃
传输数据结构:数据总长度(8) + 当前数据段长度(8) + 类型(4) + 数据(n)
其中数据总长度储存了完整数据长度(例如:一个完整视频大小),当前数据段长度储存的为拆包的大小(n),可以根据实际情况额外增加新字段
此种情况,在发送和接收到数据的时候,要额外处理一下拆包的过程,因此过程或稍复杂一些,不过其同时解决了只有粘包、只有拆包、粘包拆包的问题,因此实现逻辑叶建荣了上面的一种情况
发送分段处理代码逻辑如下:
//发送可以分段的数据格式
- (void)sendMoreData {
NSMutableData *mData = nil;
if (self.tfSendMessage.text.length > 0) {
mData = [NSMutableData data];
//给没个客户端发送一段数据
const char *textStr = self.tfSendMessage.text.UTF8String;
NSData *data = [NSData dataWithBytes:textStr length:strlen(textStr)];
unsigned long dataLength = data.length;
NSData *tolLenData = [NSData dataWithBytes:&dataLength length:8];
[mData appendData:tolLenData];
unsigned long length = data.length;
NSData *lenData = [NSData dataWithBytes:&length length:8];
[mData appendData:lenData];
//文字类型
unsigned int typeByte = 0x00000001;
NSData *typeData = [NSData dataWithBytes:&typeByte length:4];
[mData appendData:typeData];
[mData appendData:data];
NSLog(@"发送内容为:%@", self.tfSendMessage.text);
self.tfSendMessage.text = @"";
//发送消息
[self.socket writeData:mData withTimeout:-1 tag:10086];
}else {
//发送图片,其实实际上不一定在非要传递图片的,有的走的是http上传到文件服务器
、、然后利用返回的url在发送给对方
NSData *imgData = [NSData dataWithContentsOfFile:
[[NSBundle mainBundle] pathForResource:@"MMGG" ofType:@"jpeg"]];
//展示发送的图片4s
[self showImageInfo:imgData];
//分段发送图片
unsigned long dataLength = imgData.length;
NSData *tolLenData = [NSData dataWithBytes:&dataLength length:8];
unsigned currentIndex = 0;
do {
mData = [NSMutableData data];
//开头追加总长度
[mData appendData:tolLenData];
unsigned long length = dataLength > 1000 ? 1000 : dataLength;
dataLength -= length; //减少长度
//加入当前数据段长度
NSData *lenData = [NSData dataWithBytes:&length length:8];
[mData appendData:lenData];
//图片类型
unsigned int typeByte = 0x00000002;
NSData *typeData = [NSData dataWithBytes:&typeByte length:4];
[mData appendData:typeData];
[mData appendData:[imgData subdataWithRange:NSMakeRange(currentIndex, length)]];
//发送消息
[self.socket writeData:mData withTimeout:-1 tag:10086];
currentIndex += length; //设置下一个节点索引
} while (dataLength > 0);
}
}
接收粘包拆包逻辑代码如下所示:
//同时处理粘包拆包逻辑,只处理粘包逻辑的,这个也同样适用,这个总长度为数据的总长度,不计算前面的
- (void)reciveMoreData:(NSData *)data {
if (data.length < 1) return;
//当前接收的数据包长度
unsigned long totolLength = data.length;
unsigned long currentLength = 0;
//do while解决粘包问题,在里面进行拆包
do {
//处理粘包逻辑
unsigned long datalength; //数据总长度
unsigned long length; //当前数据包长度
unsigned int type; //数据类型
[data getBytes:&datalength range:NSMakeRange(currentLength, 8)];
[data getBytes:&length range:NSMakeRange(currentLength + 8, 8)];
[data getBytes:&type range:NSMakeRange(currentLength + 16, 4)];
//获取实际数据
NSData *contentData = [data subdataWithRange:NSMakeRange(currentLength + 20, length)];
currentLength += length + 20;
//处理拆包逻辑
if (self.reciveData.length < totolLength) {
[self.reciveData appendData: contentData];
}
unsigned long reciveLength = self.reciveData.length;
if (reciveLength == datalength) {
if (type == 1) {
//文字
NSString *content = [[NSString alloc] initWithData:self.reciveData
encoding:NSUTF8StringEncoding];
NSLog(@"接收的数据为:%@", content);
}else if (type == 2) {
//图片
[self showImageInfo:self.reciveData];
}else {
NSLog(@"不支持的数据类型");
}
self.reciveData = [NSMutableData data]; //重新初始化
}else if (reciveLength > datalength) {
NSLog(@"数据传输或解析出现错误");
return;
}
} while (currentLength < totolLength);
}
GCDAsyncUdpSocket简介
GCDAsyncUdpSocket为CocoaAsyncSocket中的Udp的代码封装,使用起来更简单
udp为非连接型的通信机制,即:没有客户端服务端的区别,开启upd可以直接向某个ip发送消息,也可以直接接收某个ip发来的消息,由于事先没有建立连接确认,因此可靠性没有了保证,平时使用较少
但是如果经过调整,那么在某些领域则会有一番用途,例如:部分游戏操作,人物位置、操作等,消息发送比较频繁,由于玩游戏一般网络较好,丢包率本身就比较低,如果在给udp加上心跳确认来保证连接,那么udp将在游戏中成为一个相对很可靠的连接,部分信息即使发送失败,也就最多相当于断网操作失败,也是比较正常的操作
下面简单介绍一下 GCDAsyncUdpSocket的代码设置
创建socket
创建socket过程需要遵循GCDAsyncUdpSocketDelegate协议,穿件完毕只需要准备接收信息即可
// 1 创建socket
if (!self.udpSocket) {
self.udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self
delegateQueue:dispatch_get_global_queue(0, 0)];
}
NSLog(@"创建socket 成功");
// 2: 绑定socket
NSError * error = nil;
[self.udpSocket bindToPort:8060 error:&error];
if (error) {
//监听错误打印错误信息
NSLog(@"error:%@",error);
}else {
// 3: 监听成功则开始接收信息
[self.udpSocket beginReceiving:&error];
}
常用的几个协议
// 连接成功
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address{
NSLog(@"连接成功 --- %@",address);
}
// 连接失败
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError *)error{
NSLog(@"连接失败 反馈: %@",error);
}
// 发送数据成功
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag{
NSLog(@"%ld tag 发送数据成功",tag);
}
// 发送数据失败
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag
dueToError:(NSError *)error{
NSLog(@"%ld tag 发送数据失败 : %@",tag,error);
}
// 接受数据的回调
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
fromAddress:(NSData *)address withFilterContext:(id)filterContext{
//在这里可以自行测试接收的数据
}
发送数据只需要直接发送到某个ip即可
[self..udpSocket sendData:data toHost:@"192.168.1.1" port:8070 withTimeout:-1 tag:10080];
最后
上面便是使用GCDAsyncSocket实现的服务端和客户端的交互过程,包括了粘包拆包,以及交互的过程
也简单介绍了GCDAsyncUdpSocket的简单使用和部分使用场景