基于Socket之CocoaAsyncSocket使用

本文介绍了CocoaAsyncSocket库中GCDAsyncSocket类的使用、粘包处理以及时间延迟测试.

一.CocoaAsyncSocket介绍

CocoaAsyncSocket中主要包含两个类: GCDAsyncSocket、GCDAsyncUdpSocket

1)GCDAsyncSocket

用GCD搭建的基于TCP/IP协议的socket网络库
GCDAsyncSocket is a TCP/IP socket networking library built atop Grand Central Dispatch. -- 引自CocoaAsyncSocket.

2)GCDAsyncUdpSocket

用GCD搭建的基于UDP/IP协议的socket网络库.
GCDAsyncUdpSocket is a UDP/IP socket networking library built atop Grand Central Dispatch..-- 引自CocoaAsyncSocket.

二.下载CocoaAsyncSocket

点击CocoaAsyncSocket下载

Jietu20200407-133555.png

拷贝下面两个文件到项目
GCDAsyncSocket.h
GCDAsyncSocket.m

三.客户端介绍

先讲解客户端创建过程,大部分项目已经有服务端socket。

步骤:

1).继承GCDAsyncSocketDelegate协议.
2).声明属性

// 客户端socket
@property (strong, nonatomic) GCDAsyncSocket *clientSocket;

3).创建socket并指定代理对象为self,代理队列必须为主队列.

self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

4).连接指定主机的对应端口.

NSError *error = nil;
self.connected = [self.clientSocket connectToHost:self.addressTF.text onPort:[self.portTF.text integerValue] viaInterface:nil withTimeout:-1 error:&error];

5).成功连接主机对应端口号.

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port 
{
//    NSLog(@"连接主机对应端口%@", sock);
    [self showMessageWithStr:@"链接成功"];
    [self showMessageWithStr:[NSString stringWithFormat:@"服务器IP: %@-------端口: %d", host,port]];

    // 连接成功开启定时器
    [self addTimer];
    // 连接后,可读取服务端的数据
    [self.clientSocket readDataWithTimeout:- 1 tag:0];
    self.connected = YES;
}

注意:
The host parameter will be an IP address, not a DNS name.(连接的主机为IP地址,并非DNS名称.) -- 引自GCDAsyncSocket

6).发送数据给服务端

// 发送数据
- (IBAction)sendMessageAction:(id)sender
{
    NSData *data = [self.messageTextF.text dataUsingEncoding:NSUTF8StringEncoding];
    // withTimeout -1 : 无穷大,一直等
    // tag : 消息标记
    [self.clientSocket writeData:data withTimeout:- 1 tag:0];
}

注意:
*发送数据主要通过- (void)writeData:(NSData )data withTimeout:(NSTimeInterval)timeout tag:(long)tag写入数据的.

7).读取服务端数据

/**
 读取数据
 @param sock 客户端socket
 @param data 读取到的数据
 @param tag 本次读取的标记
 */
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag 
{
    NSString *text = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    [self showMessageWithStr:text];
    // 读取到服务端数据值后,能再次读取
    [self.clientSocket readDataWithTimeout:- 1 tag:0];
}

注意:
有的人写好代码,而且第一次能够读取到数据,之后,再也接收不到数据.那是因为,在读取到数据的代理方法中,需要再次调用[self.clientSocket readDataWithTimeout:- 1 tag:0];方法,框架本身就是这么设计的.

8).客户端socket断开连接.

/**
 客户端socket断开
 @param sock 客户端socket
 @param err 错误描述
 */
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
    [self showMessageWithStr:@"断开连接"];
    self.clientSocket.delegate = nil;
    self.clientSocket = nil;
    self.connected = NO;
    [self.connectTimer invalidate];
}

注意:
sokect断开连接时,需要清空代理和客户端本身的socket.
self.clientSocket.delegate = nil;
self.clientSocket = nil;

9).建立心跳连接.

 // 计时器
@property (nonatomic, strong) NSTimer *connectTimer;

// 添加定时器
- (void)addTimer
{
 // 长连接定时器
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self  selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];
     // 把定时器添加到当前运行循环,并且调为通用模式
    [[NSRunLoop currentRunLoop] addTimer:self.connectTimer forMode:NSRunLoopCommonModes];
}
// 心跳连接
- (void)longConnectToSocket
{
    // 发送固定格式的数据,指令@"longConnect"
    float version = [[UIDevice currentDevice] systemVersion].floatValue;
    NSString *longConnect = [NSString stringWithFormat:@"123%f",version];

    NSData  *data = [longConnect dataUsingEncoding:NSUTF8StringEncoding];

    [self.clientSocket writeData:data withTimeout:- 1 tag:0];
}

注意:
心跳连接中发送给服务端的数据只是作为测试代码,根据你们公司需求,或者和后台商定好心跳包的数据以及发送心跳的时间间隔.因为这个项目的服务端socket也是我个人写的,所以,我自定义心跳包协议.客户端发送心跳包,服务端也需要有对应的心跳检测,以此检测客户端是否在线.

代理方法如下:
/socket成功连接到才会服务器调用
  -(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port;
 //接受到新的socket连接才会调用
  - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket;
 //读取数据,有数据就会调用
  - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
 //直到读到这个长度的数据,才会触发代理
  - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; 
 //直到读到data这个边界,才会触发代理 
  - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
 //有socket断开连接调用
  - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err;

四.模拟服务端

1.继承GCDAsyncSocketDelegate协议.
2.声明属性
3.创建socket并指定代理对象为self,代理队列必须为主队列.
4.开放服务端的指定端口.
5.连接上新的客户端socket
6.发送数据给客户端
7.读取客户端的数据
8.建立检测心跳连接.

心跳检测方法只提供部分思路:

1.懒加载一个可变字典,字典的键作为客户端的标识.如:客户端标识为123456.

*2.在- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData )data withTag:(long)tag方法中,将读取到的数据或者数据中的部分字符串作为键.字典的值为系统当前时间.服务端第一次读取数据时,字典中没有数据,所以,直接添加到可变字典中,之后每次读取数据时,都用字典的setObject: forKey:方法添加字典,若存储的键相同,即客户端标识相同,键会被覆盖,再使用系统的当前时间作为值.

3.在- (void)checkLongConnect中,获取此时的当前时间,遍历字典,将每个键的值和当前时间进行比较即可.判断的延迟时间可以写8秒.时间自定.之后,再根据自己的需求进行后续处理.

五.数据粘包处理

粘包解决思路:

思路1)
发送方将数据包加上包头和包尾,包头、包体以及包尾用字典形式包装成json字符串,接收方,通过解析获取json字符串中的包体,便可进行进一步处理.

思路2)
添加前缀.和包内容拼接成同一个字符串.

思路3)
如果最终要得到的数据的长度是个固定长度,用一个字符串作为缓冲池,每次收到数据,都用字符串拼接对应数据,每当字符串的长度和固定长度相同时,便得到一个完整数据,处理完这个数据并清空字符串,再进行下一轮的字符拼接.

六.检验测试.

1)测试配置

测试时,两端需要处于同一WiFi下.客户端中的IP地址为服务端的IP地址,具体信息进入Wifi设置中查看.

2)测试所需环境.

将客户端程序安装在每个客户端,让一台服务端测试机和一台客户端测试机连接mac并运行,这两台测试机可以看到打印结果,所有由服务端发送到客户端的数据,通过客户端再回传给服务端,在服务端看打印结果.

3)进行延迟差测试.

延迟差即服务端发送数据到第一台客户端和服务端发送数据到最后一台客户端的时间差.根据服务端发送数据给不同数量的客户端进行测试.而且,发送数据时,是随机发送.

4)单次信息收发测试.

让服务端给每个客户端随机发送200次数据.并计算服务端发送数据到某一客户端,完整的一次收发时间情况.

学习资料查询如下:

https://www.cnblogs.com/letougaozao/p/3842113.html

http://www.cocoachina.com/ios/20170615/19529.html

https://www.cnblogs.com/XYQ-208910/p/5169209.html

IOS 详解socket编程[oc]粘包、半包处理

iOS开发项目- 基于WebSocket的聊天通讯(2)

iOS开发项目- 基于WebSocket的聊天通讯(1)

IOS开发网络篇—Socket编程详解

iOS App通信之local socket示例

iOS App之间的通信 local socket

IOS中使用 CocoaAsyncSocket​

iOS 使用 socket 实现即时通信示例(非第三方库)

你可能感兴趣的:(基于Socket之CocoaAsyncSocket使用)