前言
何为自定义协议,其实是相对标准协议来说的,这里主要针对的是应用层协议;常见的标准的应用层协议如http、ftp、smtp等,如果我们在网络通信的过程中不去使用这些标准协议,那就需要自定义协议,比如我们常用的RPC框架(dubbo,thrift),分布式缓存(redis,memcached)等都是自定义协议;本文就来讲讲如何去自定义私有协议,在此之前我们先考虑一下为什么要自定义协议。
为什么要自定义协议
直接使用标准的协议好处是显而易见的,我个人理解的几点优点:
既然是标准协议说明已经成为了标准,这样很多系统就可以直接对接,无缝集成;
协议最重要的一点就是编码解码,标准协议往往有现成的编码解码包,直接拿来使用,减少开发时间;
有很多围绕标准协议的第三方测试工具,可以很方便的进行测试;
既然有这么多优点那我们为什么还要去自定义协议,大致出于以下几点考虑:
既然是标准协议,往往兼顾的东西比较多,导致协议数据相对来说比较大,这样可能在一些追求性能,流量的系统中不能容忍;
标准协议有很多,没有哪一种协议可以适用任何场景中,所以如果在某个场景中还没有既定的标准协议,这时候会有各种私有协议;
自定义协议只要双方约定好数据结构就行,不具有通用性,理论上来说会更加安全一点,当然现在很多标准协议都有安全版本,比如https,sftp等等;
以上只是个人的一点理解,欢迎大家补充;关于如何去自定义协议,其实可以去多参考一些主流的标准协议或者私有协议,其实有很多共同点可以去借鉴;下面先简单看看那些主流的协议;
主流协议
下面分别看看一些主流的标准协议或者私有协议都是如何去定义自己的数据结构的,对我们有非常好的借鉴意义;
http协议
http协议大家最熟悉不过了,全称叫超文本传输协议,整个请求报文可以分为三个部分分别是:请求行,请求报头,请求正文;
请求行
GET /test.html HTTP/1.1 (CRLF换行)
请求报头
Accept-Encoding: gzip, deflate
Content-Length: 38
Content-Encoding: gzip
...
请求包头有很多,每一个代表了各自的含义,这边就不一一列出,我们这里更加关注整个报文的结构;
请求正文
这个只有在POST请求的时候才有正文,里面存放业务数据,比如常见的json文本串;具体正文的长度可以根据消息头中的Content-Length来决定;
dubbo协议
dubbo协议格式可以直接参考官网提供的如下图片: 看上图其实整个协议数据包也大致分为两个部分:固定部分和可变部分,或者叫消息头和消息体;固定部分一共是4+8+4=16个字节,具体如下所示:
header{
Magic High = 8bit; //魔数高位
Magic Low = 8bit; //魔数低位
Req/Res = 1bit; //标识是请求或响应
2 Way = 1bit; //标记是否期望从服务器返回值
Event = 1bit; //标识是否是事件消息
Serialization ID = 5bit; //标识序列化类型
Status = 8bit; //标识响应的状态
Request ID = 64bit; //标识唯一请求
Data Length = 32bit; //序列化后的内容长度
}
可变部分根据固定部分中的Data Length来确定长度;
redis协议
Redis的客户端与服务端采用叫做 **RESP(Redis Serialization Protocol)**的网络通信协议交换数据,相对来说还是比较简单的,以下是这个协议的一般形式:
*< 参数数量 > CR LF
$< 参数 1 的字节数量 > CR LF
< 参数 1 的数据 > CR LF
...
$< 参数 N 的字节数量 > CR LF
< 参数 N 的数据 > CR LF
以上大致介绍了三种比较有代表性的协议,虽然说每种协议都有各自的使用场景,但是如果我们自己去定义协议,还是有一些相通的东西;
如何自定义协议
下面我们重点看看去自定义协议有哪些需要我们关注的点,以下是本人根据自己的理解整理了如下关注点:
完整的数据包
协议号
消息头标识
业务数据
预留字段
下面分别逐一详细介绍:
完整的数据包
我们平时经常讲数据包,但是TCP其实只有流的概念,并没有数据包的概念;那很重要的一点就是我们的程序怎么知道现在的业务数据已经接受全部接收完了,可以作为一个完整的数据包去处理了,如果不去做处理的话就会出现我们常说的半包和粘包问题;主流的的处理方式大致有这么两种:
在消息头部加上数据包长度描述,比如在http协议和dubbo协议中出现的dataLength字段;
用特殊的字符串作为数据包的结尾,这样我们在接受数据的时候接受到预定的特殊字符串就表示数据包完整了;
协议号
可能不同的协议有不同的叫法,我这里把它叫做协议号,个人理解就是根据这个协议号,服务器端知道去执行什么逻辑;比如http协议请求行中的/test.html,dubbo协议中的服务名+版本号,redis中的具体要执行什么key;
消息头标识
这个是否需要还是要看各自的场景,比如redis协议足够简单,无需任何标识,所有的东西都是双端约定好的;但是其他很多协议还是有一些需要的,除了上面说到的可以在消息头中指定dataLength,其实还有很多其他的东西可以指定比如:
业务数据格式:文本格式,json格式,html格式等等;
压缩格式:可能为了追求流量包大小对数据包进行压缩,gzip、deflater、snappy等;
加密算法:可能需要对我的业务数据进行加密处理,保证业务数据的安全性AES、DES等;
业务数据
业务数据往往在整个数据包中是最大的,同时也是大小可变的部分;我们上面所做的这些其实都是在为业务数据服务,业务数据需要在网络传输,最重要的一点就是序列化,一般就以下两种方式:
文本方式:序列化文本文档text,或者json串,xml格式等;
二进制方式:常见的比如protobuf,thrift,kyro等;
预留字段
是否需要预留字段这个得看情况,比如http协议整个消息头是可变的,每一行一个标识,知道读取到空行,表示消息头结束下面就是正文了,可以理解为http使用了两种方式来保证完整包,消息头使用特殊字符结尾,正文使用在消息头中指定dataLength;这种方式其实它的整个扩展性是非常好的;另外一种像dubbo这样,其实它的头部相当于已经固定好了16个字节,这种情况下是否可以预留几个字节防止后面的变更;
总结
自定义协议其实在我们真正的工作中还是很少能接触到的,更多的其实还是去实现业务,但是我们系统无时无刻不在和各种应用层协议打交道,如果我们了解了各种协议,在系统出现问题时可以做抓包分析;另外像我们常用的数据库中间件、缓存中间件等,都需要对协议都充分的了解,然后去实现代理。
感谢关注
- END -可以关注微信公众号「回滚吧代码」,第一时间阅读,文章持续更新;专注Java源码、架构、算法和面试。
往期推荐
RPC框架设计概要
《大厂面试》之线上故障排查
《生产事故》之反射引发的宕机问题