目录
一、TCP/IP 通信协议
IP协议在网络层,定义了一套地址称之为ip地址,目前通常使用的是IPV4,协议规定地址为32位表示,通常大家看到ip地址都是以十进制展示,每8位移 . 号分割,例如:192.168.1.1,IP数据包同样分为head和data两部分(IP头+IP数据),实现定位目标网路和锁定目标主机。
而TCP/UDP协议位于传输层,需要帮我们解决定位是哪个服务程序的数据包问题。其本质就是依靠端口来唯一定位一个服务,而发送端端口+接收端端口组成一组链接,传输层的作用总结为提供端口到端口的通信,传输层通常是指tcp(udp)协议,TCP协议提供可靠性传输能力、分包传输能力。tcp层数据包也分为head和data部分(TCP头+TCP数据)。
网络层(head) | 传输层(网络层data) | |
IP头 | TCP头 | TCP数据部分 |
在不同应用程序所要应用的场景不同,需要数据的数据结构也不尽相同,这是我们开发人员需要接在应用层定义各种协议来对应适配复杂场景所需的数据结构,故应用层主要就是定义为规范了应用层的数据结构,例如一些常用的应用层协议有http、ftp等。
二、如何简单设计一个自己应用协议案例
2.1 应用协议设计
应用协议的本质就是定义数据结构,一般我们从通信接口获取或写入的数据大多都是二进制数据,这些数据有明文、暗文(需要通过协议转换才知其义),也有格式约定要求,通常这些格式会包含数据头、数据段(包含真实数据)、数据尾(可以省略)三个部分。而对于这些二进制字段的数据处理就是对数据位逐位处理。
假设现在有一个服务端和多个客户端进行通信,服务需要广播传输带时间戳消息到各个客户端的应用场景来设计一个简单应用协议,约定如下:
【1】每帧报文不超过128字节,字符串采用ANSI格式,报文内容采用大端模式(高字节在前)。
【2】报文序列化要求:报文第一字节以 大于0XF0开头,表示报文头,末端以0XFF结尾,报文从第二个字节到倒数第二个字节中,如果有大于0XF0需要做转义处理,例如将0XF3转义为0XF0 0X03。
【3】报文反序列化要求:报文中如果发现某字节等于0XF0,与其后续字节合并转义,例如0XF0 0X0A转义0XFA。
【4】服务端在无数据推送时发送心跳报文,心跳报文无需反馈信息。
2.2 协议格式
下行通信(服务端->客户端)
字节序 | Content | Format | Range | Sample |
0 | Header | BYTE | 0XF1 | 0XF1 |
1 | MsgType | BYTE | 0X01 | 0X01 |
2 | Time | time_t32 | 0~0xffffffff | tt=time(NULL)[秒] 第2字节:tt&0xff 第3字节:(tt>>8)&0xff 第4字节:(tt>>16)&0xff 第5字节:(tt>>24)&0xff |
3 | ||||
4 | ||||
5 | ||||
6 | Len | Short | (0~128-9) | 其后后面字节数 |
7 | ||||
8~8+Len | INFO | String-ANSI | 传输内容 | (待扩展) |
9+Len | End | BYTE | 0XFF | 0XFF |
说明:所有short, time_t32采用小尾端,即低字节在前,高字节在后,传输内容采用大端模式,内容待定。
心跳通信(服务端->客户端),可以看做下行通信的特殊形式,判断客户端连接是否存在
字节序 | Content | Format | Range | Sample |
0 | Header | BYTE | 0XF1 | 0XF1 |
1 | MsgType | BYTE | 0X02 | 0X02 |
2 | Time | time_t32 | 0~0xffffffff | (见上表) |
3 | ||||
4 | ||||
5 | ||||
6 | End | BYTE | 0XFF | 0XFF |
上行通信(客户端->服务端),类似心跳通信,为客户端收到一条服务端广播消息后(包括心跳)的应答
字节序 | Content | Format | Range | Sample |
0 | Header | BYTE | 0XF1 | 0XF1 |
1 | MsgType | BYTE | 0X03 | 0X03 |
2 | Time | time_t32 | 0~0xffffffff | (见上表) |
3 | ||||
4 | ||||
5 | ||||
6 | End | BYTE | 0XFF | 0XFF |
2.3 协议序列化和反序列化代码设计
//序列化
int sye_code(const unsigned char *buff, int len, unsigned char *outbuf)
{
char ch = 0;
int nLen = 0;
unsigned char * buf = (unsigned char *)buff;
for(int i=0;i<len;i++, nLen++)
{
ch = buf[i];
if((buf[i]|0x0f) == 0xff&&i>0&&i<(len-1))
{
*outbuf++ = 0xf0 & buf[i];
*outbuf++ = 0x0f & buf[i];
nLen+=1;
}else{
*outbuf++ = ch;
}
}
return nLen;
}
//反序列化
int sye_uncode(const unsigned char *buff, int len, unsigned char *outbuf)
{
char ch = 0;
int nLen = 0;
unsigned char * buf = (unsigned char *)buff;
for(int i=0;i<len;i++, nLen++)
{
ch = buf[i];
if(buf[i] == 0xf0)
{
ch = 0xf0 | buf[++i];
}
*outbuf++ = ch;
}
return nLen;
}
2.4 关于数据分帧设计
通常发送数据都是逐帧发送的,接收方会给定读取缓存大小一次性读取约定字节数,在数据帧变化及高速发送数据情况下,接收方会涉及到数据读取时各个数据帧是连接在一起的,这是根据协议头及尾部进行数据分帧,本案例中由于通过数据序列化编码处理,只有帧结尾出现0xff字段,因此可以通过该标识进行数据帧分帧识别处理。
下面给出简单的伪代码:
unsigned char rdc_data[1024] = {0}; //读取指令集,从服务端
unsigned char rdata[256] = {0}; //缓存指令,进行协议解析
...
Read(rdata,256)
...
//追加新读取数据到缓存末尾,注意溢出判断处理
memcpy(rdc_data+ sizeof(rdc_data), rdata, sizeof(rdata));
//分帧处理
int start = 0;
unsigned char ctype = 0; //报文头标识,本文全部使用了0xf1
for (int i = 0; i < sizeof(rdc_data); ++i)
{
if (rdc_data[i] > 0xf0) //报文头判断,只有报文头和报文尾端>0xf0
{
if (rdc_data[i] == 0xff) //报文末端判断
{
if (ctype)
{
ctype = 0;
unsigned char rd[256] = {0}; //真正的数据帧缓存
//int rd_len = sye_uncode(rdc_data + start, i - start + 1, rd);
memcpy(rd, rdc_data + start, i - start + 1);
... //处理真正的数据帧,例如加入缓存,反序列处理,数据字段意思解析等
start = i + 1;
}
}
else{
ctype = rdc_data[i];
start = i;
}
}
}
if (start < sizeof(rdc_data))
{
unsigned char rdc[1024]={0}; //读取指令集,从服务端
memcpy(rdc, rdc_data+ start, sizeof(rdc_data) - start ); //剩余数据继续缓存
memset(rdc_data, 0, 1024);
memcpy(rdc_data, rdc, sizeof(rdc));
}else {
memset(rdc_data, 0, 1024);
}