一、应用层通信协议概述
TCP/UDP是基于字节流的传输层通信协议,对于其的编程是基于IO流编程,所谓"流",就是没有界限的一长串二进制数据。TCP/UDP作为传输层协议,并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分。所以在业务上一个完整的数据包在进行传输时,可能会被拆分成多个包进行发送,也可能将很多小的数据包封装成一个大的数据包发送,也就是TCP/UDP的拆包和粘包问题。如果直接序列化发出数据包,接收方无法知道一个完整的报文从哪里开始,到哪里结束,这个问题需要通过上层的应用协议设计来解决。
二、业界主流协议调研
目前业界主流的协议的解决方案如下:
1.消息定长:报文长度固定,例如每个报文的长度固定为 200 字节,如果不够空位补空格,接受方每次拿 200 字节。
2.使用特殊分隔符分隔:例如每条报文结束都添加回车换行符作为报文分隔符,接收方读到回车换行符则分割出报文。
3.分为消息头和消息体:消息头包含消息的长度,接收方从消息头拿到消息长度,就知道剩下的报文是多少字节了。
4.更复杂的自定义应用层协议。
三、自定义应用层通信协议结构
综合以上内容,本项目工程中定义了一个应用层通信协议,结构如下:
如上图所示,发送的数据由消息头(header)和主体(body)组成,消息头包括:
- 报文类型:不同类型的报文在服务器中执行不同的指令和功能。
- 数据长度:数据长度由消息头长度加上数据主体内容的长度组成。
消息主体为定长的数据缓冲区(但传输的消息长度是不定的)。
为了实现上述的结构,定义了两个结构体,分别是消息头结构体和消息结构体,消息头结构体嵌套在消息结构体中,同时定义了一个枚举类型,用于定义通信所需的各种消息类型。
不同类型的消息的编码方式如下表所示:
可以看到,报文整体由$分隔的单个字符串组成,消息头的组成基本相同,以logIn为例,报文的第一位是消息类型LogIn,第二位是不包括消息头的消息长度,之后依次是用户名和密码。而在Getfrdlist中,除了消息头,好友人数之外,循环编码了由uid,uname,usex,role组成的字符串,各项之间用#分隔。
协议由服务端和客户端双方遵守,共用一套相同的编码和解码函数,编码即是按照前一页的协议具体内容拼接成字符串。解码需要读取消息头,先判断消息类型,用于区分不同消息以确定不同的解码方式,然后获取消息长度,用于对报文进行长度校验,以确保数据发送的安全性和完整性。
四、总结
在我看来,通信协议的设计主要需要满足三个点:
- 统一:协议由服务端和客户端双方遵守,共用一套相同的编码和解码函数。
- 区分:用消息头来区分不同的消息类型,用于判断是什么类型的消息,选择对应的解码。
- 安全:要对数据包的完整性进行校验,进行长度的校验是最基本的校验,常见的检验方式由checksum校验和、CRC校验等类型。