本文介绍如何自己实现一套应用层网络协议
应用层协议要解决的问题
应用层协议有
http,FTP,SMTP,dubbo等,以http协议为例,数据包的传输过程如下
image-20231007221130080
Ref https://zhuanlan.zhihu.com/p/648405247
Ref:
https://blog.csdn.net/why_still_confused/article/details/86599613
传输层协议不关心数据是什么格式和含义,都按照字节流的形式交给应用层处理,应用层解析协议头和数据部分等
所以,应用层要解决的问题有:
解析传输层数据
将数据交给应用为使用者服务
应用层协议的实现思路
想自己实现一套应用层协议的思路如下:
需要定义应用层协议头格式
根据协议头格式,解析协议头
根据协议头,解析数据部分
下面设计一套简单的应用层协议(Timo)仅供参考
定义协议头
对数据的处理方式,可以分为:
固定长度协议
每条消息长度固定,读写效率比较高,但消息过小存在浪费,超过限制则需拼接
特殊终止符协议
每条消息都有一个特殊终止符表示消息结束,不会浪费空间,接收时需要搜索终止符来确定结束位置,如果内容中包含了终止符需要转移处理
可变长协议
每条消息的长度可变,由头部标志消息长度,不会浪费空间,需要解析消息头来确定长度
注:消息指一个完整的应用层数据包,在传输层可能是多个TCP数据包
参考 Dubbo 协议头,定义 Timo 的协议头如下:
含义
Magic Value
req
success
serialization
code
len
data
其中:
Magic Value是固定的 24 位二进制,用于判断是否是 Timo 协议
req:0表示请求,1表示响应,长度1bit
success:1表示成功,0表示失败,长度1bit
serialization
表示序列化方法,长度2bit,最多支持4种序列化方法,0表示json,1表示Hessian,2表示XML,3表示Kryo
code 表示状态码,长度4bit
len 表示数据长度,长度32bit,最大支持传输2^32-1长度的数据
data 表示数据部分,变长
解析协议头
以 TCP 协议为例,TCP
协议会解析出数据包的数据部分(是一个字节数组),Timo
协议拿到这个字节数组,取前32位,则拿到了 Timo
协议头(如果长度不足32位等待下一个TCP数据包组成一起),然后根据上面定义的格式,解析出各部分含义
解析数据
由于 TCP 存在粘包和拆包,所以一个 TCP 数据包不一定包含了完整的 Timo
data,需要根据 len 判断,如果当前数据包末位大于 head size +
len,则截取到 head size + len 位置,如果当前数据包末位小于 head size +
len,则等待下一个 TCP 包到后再判断,直到找到完整的 data,再根据
serialization 解析
实例
服务端
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 public class Server { public static void start (int port) throws IOException { ServerSocket serverSocket = new ServerSocket (port); System.out.println("服务端已启动,等待客户端连接..." ); Socket client = serverSocket.accept(); System.out.println("客户端已连接: " + client.getRemoteSocketAddress()); BufferedInputStream bis = new BufferedInputStream (client.getInputStream()); PrintWriter out = new PrintWriter (client.getOutputStream(), true ); while (true ) { byte [] bytes = readFromBufferForLen(bis); if (bytes == null ) { out.println(Timo.encode(GsonUtils.toJson(ResponseFactory.fail()))); continue ; } Dog dog = Timo.decode(bytes, Dog.class); if (dog == null ) { System.out.println("exit" ); break ; } System.out.println("dog: " + dog); out.println(Timo.encode(GsonUtils.toJson(ResponseFactory.success()))); } bis.close(); out.close(); client.close(); serverSocket.close(); } public static void main (String[] args) throws IOException { start(8080 ); } }
客户端
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 public class Client { public static void start (int port) throws IOException { Socket socket = new Socket ("0.0.0.0" , port); socket.setSendBufferSize(30 ); System.out.println("已连接到服务器 " + socket.getRemoteSocketAddress()); BufferedReader reader = new BufferedReader (new InputStreamReader (System.in)); BufferedInputStream bis = new BufferedInputStream (socket.getInputStream()); PrintWriter out = new PrintWriter (socket.getOutputStream(), true ); while (true ) { String input = reader.readLine(); Dog dog = DogFactory.makeDog(); String requestBody = GsonUtils.toJson(dog); requestBody = Timo.encode(requestBody); out.println(requestBody); if (input.equals("bye" )) { break ; } byte [] bytes = readFromBuffer(bis); Response response = Timo.decode(bytes, Response.class); System.out.println("服务端响应: " + response); } reader.close(); out.close(); socket.close(); } public static void main (String[] args) throws IOException { start(8080 ); } }
协议解析
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 public static Header decodeHeader (byte [] head) { System.out.println("解析协议头" ); byte [] signal = new byte [3 ]; for (int i = 0 ; i < 3 ; i++) { signal[i] = head[i]; } String signalStr = ByteUtils.printBytesToStr(signal, "魔法值" ); int req = head[4 ] & 1 ; int success = (head[4 ] >> 1 ) & 1 ; int serial = (head[4 ] >> 2 ) & 1 ; byte [] lenBytes = new byte [5 ]; for (int i = 0 ; i < 4 ; i++) { lenBytes[i] = head[3 + i]; } int len = ByteUtils.bytesToInt(lenBytes); System.out.println("数据长度:" + len); Header header = new Header (); header.setSignal(signalStr); header.setLen(len); header.setSerialization(serial); header.setRequest(req == 1 ); header.setSuccess(success == 1 ); return header; }
协议构造同理,解析数据比较困难,需要根据解析出来的 len
截取字节流,或者等待下一个包,没写出来
Reference
我们一起来聊聊Dubbo协议