一. CocoaAsyncSocket API 基本使用介绍

提供支持 TCP 和 UDP 接口,乍一看以为很简单分别对应 GCDAsyncSocket 和 GCDAsyncUdpSocket 两个类,但是一个类中几千行代码。。。。

1. GCDAsyncSocket - TCP 主要 API

public - method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 创建 GCDAsyncSocket
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];

// 2. 连接 socket
NSError *error;
[self.socket connectToHost:@"127.0.0.1" onPort:8029 error:&error];

NSData *data = [@"zerocc发送消息" dataUsingEncoding:NSUTF8StringEncoding];
// 3. 发送消息
// -1 永不超时, tag: 标识码用于区分业务 ,
[self.socket writeData:data withTimeout:-1 tag:10000];

// 4. 关闭连接
[self.socket disconnect];
self.socket = nil;

GCDAsyncSocketDelegate - method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 连接服务器成功回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {

NSLog(@"连接成功 : %@---%d",host,port);
[self.socket readDataWithTimeout:-1 tag:10000];
}

// 连接断开回调方法
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {

NSLog(@"断开 socket 连接原因:%@",err);
}

// 持续接收服务器返回来的数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {

NSLog(@"接收到tag = %ld : %lu 长度的数据",tag,(unsigned long)data.length);
[self.socket readDataWithTimeout:-1 tag:10000];
}

// 向服务器消息发送成功代理方法
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {

NSLog(@"%ld 发送数据成功",tag);
}

二. 自定义报文通讯规约

通讯数据封装格式:

报文头 数据域长度 数据域 校验和 报文尾
0x86 长度低字节长度高字节 JSON 数据域 1字节CheckSum 00A8

1. 粘包拆包
粘包问题示例图

TCP 传输过程中不会流失字节,但是由于其是流式传输,传输的数据之间是没有边界的,不同于 UDP 提供传输服务,其传输的数据是有边界的,因而不能保证每次接收的包都是一个完整的包。应用层数据被分割成多次发送产生粘包问题的原因有以下几种情况:

  • 应用层调用 write 方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个 SO_SNDBUF 的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。
  • TCP 所传输的报文段有 MSS 的限制,如果套接字缓冲区的大小大于 MSS,也会导致消息的分割发送。
  • 由于链路层最大发送单元 MTU,在 IP 层会进行数据的分片。

处理方法:将 JSON 字符串转16进制表示然后转为 data 流,用0x86 和 0xA8 特殊字符分别作为报文头和报文尾 ASCII码与16进制转换对照表; 报文数据编码处理示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NSString *str = @"zerocc我";
// 普通字符串转 16进制字符串
NSData *strData = [str dataUsingEncoding:NSUTF8StringEncoding];
const Byte *bytes = (Byte *)[strData bytes];
NSString *hexStr = @"";
for (int i = 0; i < [strData length]; i++) {
NSString *newHexStr = [NSString stringWithFormat:@"%x",bytes[i]&0xff];
if([newHexStr length]==1) {
hexStr = [NSString stringWithFormat:@"%@0%@",hexStr,newHexStr];
}else {
hexStr = [NSString stringWithFormat:@"%@%@",hexStr,newHexStr];
}
}
NSLog(@"%@",hexStr);

// 16 进制字符串转data
unsigned long lens = [hexStr length] / 2;
unsigned char *buf = malloc(lens);
unsigned char *whole_byte = buf;
char byte_chars[3] = {'\0','\0','\0'};
for (int i=0; i < [hexStr length] / 2; i++) {
byte_chars[0] = [hexStr characterAtIndex:i*2];
byte_chars[1] = [hexStr characterAtIndex:i*2+1];
*whole_byte = strtol(byte_chars, NULL, 16);
whole_byte++;
}
NSData *messageData = [NSData dataWithBytes:buf length:lens];
free(buf);

// 拼接报文头和尾
NSMutableData *sourceData = [[NSMutableData alloc] init];
Byte head = 0x86;
NSData *headData = [NSData dataWithBytes:&head length:sizeof(head)];
Byte end = 0xA8;
NSData *endData = [NSData dataWithBytes:&end length:sizeof(end)];
[sourceData appendData:headData];
[sourceData appendData:messageData];
[sourceData appendData:endData];
NSLog(@"%@",sourceData);
1
2
3
4
5
6
7
8
9
10
运行结果对比几部如下:
...字符串:zerocc我
...16进制字符串:7a65726f6363e68891
...16进制字符串转data:<7a6572 6f6363e6 8891>
...拼接头尾:<867a6572 6f6363e6 8891a8>

分析:
字母 z 的 ASCII 码是 122
122 的十六进制表示就是 0x7a
依次类推但是0x860xA8 是特殊的,一般字符编码不会出现86 和 A8, 所以在进行 data 接收时可以通过头和尾去判断截取

2. 校验和CheckSum

主要作用用于校验数据传输的合法性;

1