0%

自己实现一套应用层网络协议

本文介绍如何自己实现一套应用层网络协议

应用层协议要解决的问题

应用层协议有 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

传输层协议不关心数据是什么格式和含义,都按照字节流的形式交给应用层处理,应用层解析协议头和数据部分等

所以,应用层要解决的问题有:

  1. 解析传输层数据
  2. 将数据交给应用为使用者服务

应用层协议的实现思路

想自己实现一套应用层协议的思路如下:

  1. 需要定义应用层协议头格式
  2. 根据协议头格式,解析协议头
  3. 根据协议头,解析数据部分

下面设计一套简单的应用层协议(Timo)仅供参考

定义协议头

对数据的处理方式,可以分为:

  1. 固定长度协议

    每条消息长度固定,读写效率比较高,但消息过小存在浪费,超过限制则需拼接

  2. 特殊终止符协议

    每条消息都有一个特殊终止符表示消息结束,不会浪费空间,接收时需要搜索终止符来确定结束位置,如果内容中包含了终止符需要转移处理

  3. 可变长协议

    每条消息的长度可变,由头部标志消息长度,不会浪费空间,需要解析消息头来确定长度

注:消息指一个完整的应用层数据包,在传输层可能是多个TCP数据包

参考 Dubbo 协议头,定义 Timo 的协议头如下:

Bit offset 0-23 24 25 26-27 28-31 32-63 64 - ?
含义 Magic Value req success serialization code len data

其中:

  1. Magic Value是固定的 24 位二进制,用于判断是否是 Timo 协议
  2. req:0表示请求,1表示响应,长度1bit
  3. success:1表示成功,0表示失败,长度1bit
  4. serialization 表示序列化方法,长度2bit,最多支持4种序列化方法,0表示json,1表示Hessian,2表示XML,3表示Kryo
  5. code 表示状态码,长度4bit
  6. len 表示数据长度,长度32bit,最大支持传输2^32-1长度的数据
  7. 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, "魔法值");
// 请求 or 响应
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协议