目录
一、socket是什么,socket和HTTP的区别
二、如何建立一个socket连接
三、使用CocoaAsyncSocket实现socket编程
1、基本实现
2、数据粘包
3、心跳保活
4、断线重连
一、socket是什么,socket和HTTP的区别
- HTTP是应用层的一个协议,而socket不是协议、它只是操作系统提供给我们程序员用来操作传输层TCP协议和UDP协议的一套接口,使用socket其实就是在面向传输层的TCP协议和UDP协议编程,说白了使用socket编程我们就是在直接发送一个TCP请求或UDP请求;(如果直接使用TCP协议和UDP协议编程的话,我们程序员就需要按照这两个协议的标准去严格组织数据格式,并且还有很多其它的事情要做,这会非常复杂,而socket这套接口就方便了我们程序员开发,我们只需要调用简单的接口即可,它内部帮我们做了组织数据格式等复杂的事情。这就好比我们发送一个HTTP请求,如果我们直接使用HTTP协议编程,那它的数据格式也是很复杂的,包括请求行、请求头、请求体等,但是好在我们各个系统都有系统级别的网络框架,我们只需要使用它们的接口,它们内部会做好数据格式转化等复杂的事情,这样看起来socket跟网络框架倒更像是同一级别的概念)
- HTTP协议在传输层对应的是TCP协议,所以我们常说的HTTP连接其实就是一个TCP连接,HTTP连接可以是短连接也可以是长连接,HTTP/1.0采用的是短连接,HTTP/1.1采用的是长连接;而socket是TCP协议和UDP协议的一套接口,所以我们常说socket连接其实也是一个TCP连接,同样socket连接可以是短连接也可以是长连接,这取决于我们怎么使用,当然socket基于UDP时甚至都不存在连接,所以不要把socket连接和长连接划等号;(顺便说一下长连接和短连接,长连接和短连接是应用层或者传输层应用的一个概念,而不是传输层的概念,传输层只有连接这个概念,它本身没有长短之分,只是因为应用层或者传输层应用才导致了这个连接有长短之分。比如HTTP/1.0采用的是短连接,也就是说TCP三次握手之后,建立了一个连接,客户端发送一个请求,服务器给一个响应,此时TCP就要四次挥手了,连接就断开了,想再传数据的话就得再三次握手建立连接四次挥手断开连接,也就是说短连接一次只处理一个输入流和输出流;而HTTP/1.1采用的是长连接,TCP三次握手之后,建立了一个连接,客户端发送一个请求,服务器给一个响应,TCP并不挥手,连接也没断开,双方还可以继续传递下一次数据、下一次数据数据......,直到双方等了某个时间段发现双方都不需要传递数据了,才会四次挥手断开连接,也就是说长连接一次可以处理若干个输入流和输出流。又比如socket可以建立一个长连接,这就意味着TCP的那个连接要处理若干个输入流和输出流,socket也可以建立短连接,这就意味着TCP的那个连接只处理一个输入流和输出流。长连接和短连接不是靠时间长短来界定的,而是靠一次处理的输入流和输出流个数来界定的)
- HTTP主要用来做客户端发一个请求、服务端就给一个响应这样的场景,而socket主要用来做客户端和服务端都能主动给对方发数据的场景,通常情况下,单台服务器可以支持十万个socket连接的并发。
二、如何建立一个socket连接

服务端socket要做五件事,客户端socket要做三件事:
- 服务端socket初始化、客户端socket初始化
- 服务端socket调用
bind()函数,绑定IP地址和端口 -
服务端socket调用
listen()函数,设置监听队列有多长(比如说我们设置监听队列的长度为10,这就意味着最多可以有10个客户端可以尝试连接服务器,而第11个客户端会被告知服务器太忙了) - 服务端socket调用
accept()函数,等待来自客户端socket的连接请求 - 客户端socket调用
connect()函数,向指定IP地址和端口的服务器发起连接请求 - 服务端socket的
accept()函数返回用于传输的socket的文件描述符,连接建立成功
接下来双方就可以通过read()和write()函数通信了,双方也都可以通过close()函数主动断开连接。
三、使用CocoaAsyncSocket实现socket编程
1、基本实现
- 服务端socket
#import "ViewController.h"
#import "GCDAsyncSocket.h"
#define kServerIPAddress @"192.168.148.63"
#define kServerPort 8080
@interface ViewController () <GCDAsyncSocketDelegate>
/// 服务端socket
@property (strong, nonatomic) GCDAsyncSocket *serverSocket;
/// 所有的客户端socket
@property (strong, nonatomic) NSMutableArray *clientSockets;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
#pragma mark - GCDAsyncSocketDelegate
/// 连接客户端成功的回调
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
NSLog(@"===========>连接客户端成功");
// 保存客户端socket
[self.clientSockets addObject:newSocket];
// 连接成功后,立马开始读取客户端的数据,调用这个读取方法后,当客户端有数据发过来时就会触发“读取客户端数据成功的回调”
[newSocket readDataWithTimeout:-1 tag:0];
}
/// 读取客户端数据成功的回调
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 此处我们不对数据做过多的处理,只是把它转换成JSON字符串打印出来看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>读取客户端数据成功:%@", jsonString]);
// 注意:读取客户端数据成功后,在这里需要再调用一次读取客户端数据的方法,框架本身就是这么设计的,否则我们就只能接收一次数据,之后再也接收不到数据
[sock readDataWithTimeout:-1 tag:0];
}
/// 与客户端断开连接的回调
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>与客户端断开连接:%@", err]);
if (err == nil) { // 代表是服务端主动断开连接,我们不做处理,客户端那边收到回调后会走断线重连的逻辑
NSLog(@"===========>服务端主动断开连接");
} else { // 代表是客户端断开连接,移除断开的客户端
[self.clientSockets removeObject:sock];
}
}
#pragma mark - action
/// 服务端开始监听来自客户端的连接请求,收到请求后就建立连接,连接成功后会触发“连接客户端成功的回调”
- (IBAction)listen:(id)sender {
NSError *error = nil;
BOOL success = [self.serverSocket acceptOnPort:kServerPort error:&error];
if (error == nil && success) {
NSLog(@"===========>服务端开始监听成功");
} else {
NSLog(@"%@", [NSString stringWithFormat:@"==========>服务端开始监听失败:%@", error]);
}
}
/// 向客户端发送数据(以JSON字符串的格式传输)
- (IBAction)send:(id)sender {
if (self.clientSockets == nil) {
return;
}
// 数据1
NSDictionary *dictionary1 = @{
@"data": @"江南忆,最忆是杭州。山寺月中寻桂子,郡亭枕上看潮头。何日更重游?",
};
NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 数据2
NSDictionary *dictionary2 = @{
@"data": @"江南忆,其次忆吴宫。吴酒一杯春竹叶,吴娃双舞醉芙蓉。早晚复相逢?",
};
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
[self.clientSockets enumerateObjectsUsingBlock:^(id _Nonnull clientSocket, NSUInteger idx, BOOL * _Nonnull stop) {
// 给客户端发送数据1(tag:消息标记)
[clientSocket writeData:data1 withTimeout:-1 tag:0];
// 给客户端发送数据2(tag:消息标记)
[clientSocket writeData:data2 withTimeout:-1 tag:0];
}];
}
/// 断开与客户端的连接
- (IBAction)disconnect:(id)sender {
[self.serverSocket disconnect];
self.serverSocket.delegate = nil;
self.serverSocket = nil;
}
#pragma mark - setter, getter
- (GCDAsyncSocket *)serverSocket {
if (_serverSocket == nil) {
_serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return _serverSocket;
}
- (NSMutableArray *)clientSockets {
if (_clientSockets == nil) {
_clientSockets = [NSMutableArray array];
}
return _clientSockets;
}
@end
- 客户端socket
#import "ViewController.h"
#import "GCDAsyncSocket.h"
#define kServerIPAddress @"192.168.148.63"
#define kServerPort 8080
@interface ViewController () <GCDAsyncSocketDelegate>
/// 客户端socket
@property (strong, nonatomic) GCDAsyncSocket *clientSocket;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
#pragma mark - GCDAsyncSocketDelegate
/// 连接服务端成功的回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>连接服务端成功");
// 连接成功后,立马开始读取服务端的数据,调用这个读取方法后,当服务端有数据发过来时就会触发“读取服务端数据成功的回调”
[sock readDataWithTimeout:-1 tag:0];
}
/// 读取服务端数据成功的回调
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 此处我们不对数据做过多的处理,只是把它转换成JSON字符串打印出来看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>读取服务端数据成功:%@", jsonString]);
// 注意:读取服务端数据成功后,在这里需要再调用一次读取服务端数据的方法,框架本身就是这么设计的,否则我们就只能接收一次数据,之后再也接收不到数据
[sock readDataWithTimeout:-1 tag:0];
}
/// 与服务端断开连接的回调
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>与服务端断开连接:%@", err]);
if (err == nil) { // 代表是客户端主动断开连接,我们不做处理,服务端那边收到回调后,会移除跟指定客户端的连接
NSLog(@"===========>客户端主动断开连接");
} else { // 其它情况下断开连接,暂时不做处理
}
}
#pragma mark - action
/// 向服务端发起连接请求
- (IBAction)connect:(id)sender {
NSError *error = nil;
BOOL success = [self.clientSocket connectToHost:kServerIPAddress onPort:kServerPort viaInterface:nil withTimeout:-1 error:&error];
if (error == nil && success) {
// 连接服务端成功后会触发“连接服务端成功的回调”
} else {
NSLog(@"%@", [NSString stringWithFormat:@"===========>连接服务端失败:%@", error]);
}
}
/// 向服务端发送数据(以JSON字符串的格式传输)
- (IBAction)send:(id)sender {
if (self.clientSocket == nil) {
return;
}
// 数据1
NSDictionary *dictionary1 = @{
@"data": @"水光潋滟晴方好,山色空蒙雨亦奇。 ",
};
NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 数据2
NSDictionary *dictionary2 = @{
@"data": @"欲把西湖比西子,淡妆浓抹总相宜。",
};
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 给服务端发送数据1(tag:消息标记)
[self.clientSocket writeData:data1 withTimeout:-1 tag:0];
// 给服务端发送数据2(tag:消息标记)
[self.clientSocket writeData:data2 withTimeout:-1 tag:0];
}
/// 断开与服务端的连接
- (IBAction)disconnect:(id)sender {
[self.clientSocket disconnect];
self.clientSocket.delegate = nil;
self.clientSocket = nil;
}
#pragma mark - setter, getter
- (GCDAsyncSocket *)clientSocket {
if (_clientSocket == nil) {
_clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return _clientSocket;
}
@end
以上我们就基本实现了socket编程,客户端和服务端之间就可以相互通信了。但是在实际开发中,我们还需要考虑三个问题:数据粘包、心跳保活和断线重连。
2、数据粘包
2.1 数据粘包是什么
上面的例子中,我们预期的效果是客户端点击一次发送,给服务端发送两条数据,服务端触发两次“收到客户端数据的回调”,然后分别打印:
===========>读取客户端数据成功:{
"data" : "水光潋滟晴方好,山色空蒙雨亦奇。 "
}
===========>读取客户端数据成功:{
"data" : "欲把西湖比西子,淡妆浓抹总相宜。"
}
但实际上两条数据被合并成一条数据发送给服务端了,服务端只触发了一次“收到客户端数据的回调”,也只打印了一次:
===========>读取客户端数据成功:{
"data" : "水光潋滟晴方好,山色空蒙雨亦奇。 "
}{
"data" : "欲把西湖比西子,淡妆浓抹总相宜。"
}
这就是数据粘包——多条数据被合并成了一条数据传输。
2.2 为什么会出现数据粘包
我们知道TCP有个发送缓存,有些情况下TCP并不是有一条数据就发一条数据,而是等发送缓存满了,再把发送缓存里的多条数据一起发送出去,这就会导致数据粘包。
此外TCP还采用了Nagle优化算法来打包数据,它会将多次间隔较小且数据量较小的数据自动合并成一个比较大的数据一块儿传输,这也会导致数据粘包。
2.3 怎么处理数据粘包
处理数据粘包也很简单,核心思路就是:发送方在发送数据的时候先给每条数据都添加一个包头,包头里存放的关键信息就是真实数据的长度,当然也可以存放更多的业务信息,此外包头的尾部还需要拼接一个包头结束标识——回车换行符,以便将来接收方读取数据时可以根据这个包头结束标识优先读取到包头数据。接收方调用指定的读取方法优先读取到包头数据,然后根据包头里的长度信息再去精准读取指定长度的真实数据,这样就可以读取到一条完整的数据了,然后再读取下一条数据就不会粘包了。
- 服务端socket(相对于上面的例子,变化的代码)
// 读取处理
/// 包头
@property (strong, nonatomic) NSDictionary *headerDictionary;
#pragma mark - GCDAsyncSocketDelegate
/// 连接客户端成功的回调
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
NSLog(@"===========>连接客户端成功");
// 保存客户端socket
[self.clientSockets addObject:newSocket];
// 连接成功后,立马开始读取客户端的数据,调用这个读取方法后,当客户端有数据发过来时就会触发“读取客户端数据成功的回调”
// [newSocket readDataWithTimeout:-1 tag:0];
// 连接成功后,立马开始读取客户端的数据,调用这个读取方法后,当客户端有数据发过来时就会触发“读取客户端数据成功的回调”,但跟上面方法不同的是这个方法只会从数据的开头起读取到包头结束标识为止,也就是说“读取客户端数据成功的回调”的data只会读取到包头数据
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
/// 读取客户端数据成功的回调
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包头为空,就代表之前没读取到过包头,那本次回调的触发肯定就是读取到包头了,因为上面采用的是readDataToData读取方法读取到包头结束标识为止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>数据格式出错了:包头数据为空");
return;
}
// 获取包头的内容1——真实数据的长度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此时再调用这个读取方法去读取指定长度的真实数据,读取到真实数据后还是会触发当前这个回调
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到这里,代表是读取到了真实数据
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 说明数据有问题
if (size <= 0 || data.length != size) {
NSLog(@"===========>数据格式出错了:真实数据大小不正确");
return;
}
// 此处我们不对数据做过多的处理,只是把它转换成JSON字符串打印出来看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>读取客户端数据成功:%@", jsonString]);
// 置位包头
self.headerDictionary = nil;
// 注意:读取客户端数据成功后,在这里需要再调用一次读取客户端数据的方法,框架本身就是这么设计的,否则我们就只能接收一次数据,之后再也接收不到数据
// [sock readDataWithTimeout:-1 tag:0];
// 继续读取下一条数据
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
// 发送处理
#pragma mark - action
/// 向客户端发送数据(以JSON字符串的格式传输)
- (IBAction)send:(id)sender {
if (self.clientSockets == nil) {
return;
}
// 数据1
NSDictionary *dictionary1 = @{
@"data": @"江南忆,最忆是杭州。山寺月中寻桂子,郡亭枕上看潮头。何日更重游?",
};
NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData1 = [self dataWithHeader:data1];
// 数据2
NSDictionary *dictionary2 = @{
@"data": @"江南忆,其次忆吴宫。吴酒一杯春竹叶,吴娃双舞醉芙蓉。早晚复相逢?",
};
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData2 = [self dataWithHeader:data2];
[self.clientSockets enumerateObjectsUsingBlock:^(id _Nonnull clientSocket, NSUInteger idx, BOOL * _Nonnull stop) {
// 给客户端发送数据1(tag:消息标记)
[clientSocket writeData:finalData1 withTimeout:-1 tag:0];
// 给客户端发送数据2(tag:消息标记)
[clientSocket writeData:finalData2 withTimeout:-1 tag:0];
}];
}
#pragma mark - private method
/// 获取拼接了包头后的数据
- (NSData *)dataWithHeader:(NSData *)data {
// 包头
NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary];
// 包头里的内容1——真实数据的长度
[headerDictionary setObject:[NSString stringWithFormat:@"%ld", data.length] forKey:@"size"];
// 把字典格式的包头转化成JSON字符串格式的包头
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:headerDictionary options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
// 得到JSON字符串格式的包头数据
NSMutableData *headerData = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 包头里的内容2——包头结束标识,回车换行符
[headerData appendData:[GCDAsyncSocket CRLFData]];
NSMutableData *finalData = headerData;
// 在包头数据后面拼接上真实数据
[finalData appendData:data];
return finalData;
}
- 客户端socket(相对于上面的例子,变化的代码)
// 读取处理
/// 包头
@property (strong, nonatomic) NSDictionary *headerDictionary;
#pragma mark - GCDAsyncSocketDelegate
/// 连接服务端成功的回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>连接服务端成功");
// 连接成功后,立马开始读取服务端的数据,调用这个读取方法后,当服务端有数据发过来时就会触发“读取服务端数据成功的回调”
// [sock readDataWithTimeout:-1 tag:0];
// 连接成功后,立马开始读取服务端的数据,调用这个读取方法后,当服务端有数据发过来时就会触发“读取服务端数据成功的回调”,但跟上面方法不同的是这个方法只会从数据的开头起读取到包头结束标识为止,也就是说“读取服务端数据成功的回调”的data只会读取到包头数据
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
/// 读取服务端数据成功的回调
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包头为空,就代表之前没读取到过包头,那本次回调的触发肯定就是读取到包头了,因为上面采用的是readDataToData读取方法读取到包头结束标识为止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>数据格式出错了:包头数据为空");
return;
}
// 获取包头的内容1——真实数据的长度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此时再调用这个读取方法去读取指定长度的真实数据,读取到真实数据后还是会触发当前这个回调
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到这里,代表是读取到了真实数据
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 说明数据有问题
if (size <= 0 || data.length != size) {
NSLog(@"===========>数据格式出错了:真实数据大小不正确");
return;
}
// 此处我们不对数据做过多的处理,只是把它转换成JSON字符串打印出来看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>读取服务端数据成功:%@", jsonString]);
// 置位包头
self.headerDictionary = nil;
// 注意:读取服务端数据成功后,在这里需要再调用一次读取服务端数据的方法,框架本身就是这么设计的,否则我们就只能接收一次数据,之后再也接收不到数据
// [sock readDataWithTimeout:-1 tag:0];
// 继续读取下一条数据
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
// 发送处理
#pragma mark - action
/// 向服务端发送数据(以JSON字符串的格式传输)
- (IBAction)send:(id)sender {
if (self.clientSocket == nil) {
return;
}
// 数据1
NSDictionary *dictionary1 = @{
@"data": @"水光潋滟晴方好,山色空蒙雨亦奇。 ",
};
NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData1 = [self dataWithHeader:data1];
// 数据2
NSDictionary *dictionary2 = @{
@"data": @"欲把西湖比西子,淡妆浓抹总相宜。",
};
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData2 = [self dataWithHeader:data2];
// 给服务端发送数据1(tag:消息标记)
[self.clientSocket writeData:finalData1 withTimeout:-1 tag:0];
// 给服务端发送数据2(tag:消息标记)
[self.clientSocket writeData:finalData2 withTimeout:-1 tag:0];
}
#pragma mark - private method
/// 获取拼接了包头后的数据
- (NSData *)dataWithHeader:(NSData *)data {
// 包头
NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary];
// 包头里的内容1——真实数据的长度
[headerDictionary setObject:[NSString stringWithFormat:@"%ld", data.length] forKey:@"size"];
// 把字典格式的包头转化成JSON字符串格式的包头
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:headerDictionary options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
// 得到JSON字符串格式的包头数据
NSMutableData *headerData = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 包头里的内容2——包头结束标识,回车换行符
[headerData appendData:[GCDAsyncSocket CRLFData]];
NSMutableData *finalData = headerData;
// 在包头数据后面拼接上真实数据
[finalData appendData:data];
return finalData;
}
3、心跳保活
正常来说,socket连接一旦建立之后就会一直挂在那里,直到某一端主动断开连接。但实际上,运营商在检测到链路上有一段时间无数据传输时,就会自动断开这种处于非活跃状态的连接,这就是所谓的运营商NAT超时,超时时间为5分钟。因此我们就需要做心跳保活——即客户端每隔一定的时间间隔就向服务端发送一个心跳数据包,用来保证当前socket连接处于活跃状态,避免运营商把我们的连接中断,这个时间间隔我们取的是3分钟,服务器在收到心跳包时不当做真实数据处理即可。
- 服务端socket(相对于上面的例子,变化的代码)
#pragma mark - GCDAsyncSocketDelegate
/// 读取客户端数据成功的回调
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包头为空,就代表之前没读取到过包头,那本次回调的触发肯定就是读取到包头了,因为上面采用的是readDataToData读取方法读取到包头结束标识为止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>数据格式出错了:包头数据为空");
return;
}
// 获取包头的内容1——真实数据的长度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此时再调用这个读取方法去读取指定长度的真实数据,读取到真实数据后还是会触发当前这个回调
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到这里,代表是读取到了真实数据
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 说明数据有问题
if (size <= 0 || data.length != size) {
NSLog(@"===========>数据格式出错了:真实数据大小不正确");
return;
}
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if ([dictionary[@"data"] isEqual:@"com.zhangshuo.alive"]) { // 如果是心跳包,我们不做处理
NSLog(@"===========>我是心跳包");
} else { // 如果不是心跳包,我们再做处理
// 此处我们不对数据做过多的处理,只是把它转换成JSON字符串打印出来看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>读取客户端数据成功:%@", jsonString]);
}
// 置位包头
self.headerDictionary = nil;
// 注意:读取客户端数据成功后,在这里需要再调用一次读取客户端数据的方法,框架本身就是这么设计的,否则我们就只能接收一次数据,之后再也接收不到数据
// [sock readDataWithTimeout:-1 tag:0];
// 继续读取下一条数据
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
- 客户端socket(相对于上面的例子,变化的代码)
#define kHeartBeatTimeInterval 60 * 3 // 心跳保活时间间隔:3分钟
/// 心跳保活定时器
@property (nonatomic, strong) NSTimer *heartBeatTimer;
#pragma mark - GCDAsyncSocketDelegate
/// 连接服务端成功的回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>连接服务端成功");
// 连接成功后,立马开始读取服务端的数据,调用这个读取方法后,当服务端有数据发过来时就会触发“读取服务端数据成功的回调”
// [sock readDataWithTimeout:-1 tag:0];
// 连接成功后,立马开始读取服务端的数据,调用这个读取方法后,当服务端有数据发过来时就会触发“读取服务端数据成功的回调”,但跟上面方法不同的是这个方法只会从数据的开头起读取到包头结束标识为止,也就是说“读取服务端数据成功的回调”的data只会读取到包头数据
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
// 添加心跳保活
[self addHeartBeat];
}
/// 与服务端断开连接的回调
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>与服务端断开连接:%@", err]);
// 移除心跳保活
[self removeHeartBeat];
}
#pragma mark - 心跳保活
// 添加心跳保活
- (void)addHeartBeat {
[self removeHeartBeat];
// 心跳时间设置为3分钟,NAT超时一般为3~5分钟
self.heartBeatTimer = [NSTimer scheduledTimerWithTimeInterval:kHeartBeatTimeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
// 和服务端约定好心跳保活数据包的内容,以便它们读取,尽可能减小心跳保活数据包的大小
NSDictionary *dictionary = @{
@"data": @"com.zhangshuo.alive",
};
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSData *data = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData = [self dataWithHeader:data];
// 给服务端发送数据1(tag:消息标记)
[self.clientSocket writeData:finalData withTimeout:-1 tag:0];
}];
[[NSRunLoop currentRunLoop]addTimer:self.heartBeatTimer forMode:NSRunLoopCommonModes];
}
// 移除心跳保活
- (void)removeHeartBeat {
[self.heartBeatTimer invalidate];
self.heartBeatTimer = nil;
}
4、断线重连
客户端主动断开连接时(如App退出登录或者App进入后台等场景),我们不需要做断线重连;其它情况下如果连接断开了(如服务器出了问题或者网断了等场景),我们就需要做断线重连,来尽量使连接处于正常连接的状态,这样才能保证业务的正常运行。具体做法就是,当客户端检测到跟服务端断开连接时就启动第一次断线重连,2秒后启动第二次断线重连,再隔4秒后启动第三次断线重连,如果三次断线重连还没成功,就认为是服务器出了问题,不再重连。
/// 断线重连总时长
@property (assign, nonatomic) NSTimeInterval reconnectDuration;
#pragma mark - GCDAsyncSocketDelegate
/// 连接服务端成功的回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>连接服务端成功");
// 连接成功后,立马开始读取服务端的数据,调用这个读取方法后,当服务端有数据发过来时就会触发“读取服务端数据成功的回调”
// [sock readDataWithTimeout:-1 tag:0];
// 连接成功后,立马开始读取服务端的数据,调用这个读取方法后,当服务端有数据发过来时就会触发“读取服务端数据成功的回调”,但跟上面方法不同的是这个方法只会从数据的开头起读取到包头结束标识为止,也就是说“读取服务端数据成功的回调”的data只会读取到包头数据
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
// 添加心跳保活
[self addHeartBeat];
// 断线重连成功后,置位断线重连总时长
self.reconnectDuration = 0;
}
/// 与服务端断开连接的回调
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>与服务端断开连接:%@", err]);
// 移除心跳保活
[self removeHeartBeat];
if (err == nil) { // 代表是客户端主动断开连接,我们不做处理,服务端那边收到回调后,会移除跟指定客户端的连接
NSLog(@"===========>客户端主动断开连接");
} else { // 其它情况下断开连接,启动断线重连
[self reconnect];
}
}
#pragma mark - 断线重连
// 断线重连
- (void)reconnect {
if (self.clientSocket.isConnected) {
[self.clientSocket disconnect];
}
// 第一次断线重连:0
// 第二次断线重连:2
// 第三次断线重连:4
if (self.reconnectDuration > 6) { // 已经三次断线重连了,还没成功
NSLog(@"===========>服务器出小差了,请稍后重试");
return;
}
NSLog(@"===========>断线重连总时长:%f", self.reconnectDuration);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.reconnectDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.clientSocket.isDisconnected) {
NSError *error = nil;
// 如果没重连成,还会触发“与服务端断开连接的回调”
BOOL success = [self.clientSocket connectToHost:kServerIPAddress onPort:kServerPort viaInterface:nil withTimeout:-1 error:&error];
if (error == nil && success) {
// 连接服务端成功后会触发“连接服务端成功的回调”
} else {
NSLog(@"%@", [NSString stringWithFormat:@"===========>连接服务端失败:%@", error]);
}
}
});
// 断线重连时间以2指数级增长
if (self.reconnectDuration == 0) {
self.reconnectDuration += 2;
} else if (self.reconnectDuration == 2) {
self.reconnectDuration += 4;
} else if (self.reconnectDuration == 6) {
self.reconnectDuration += 8;
}
}
参考
1、iOS即时通讯,从入门到“放弃”?
2、即时通讯下数据粘包、断包处理实例(基于CocoaAsyncSocket)