创建自定义可扩展协议架构的最佳实践?

Best Practice to creating an custom expandable protocol architecture?

免责声明:我在创建自定义的、更大规模的协议方面没有经验。

我打算开始一个新的项目(最好在java),由一个主服务器(MS),同一网络上的几个较小的服务器(SS)和几个客户端组成。所有这三方都应该相互传达信息。

示例:

我对更大规模的自定义协议和数据包的最大经验来自 Minecraft 服务器(Spigot 等)。看Servers包系统的时候还是有点懵

虽然我大部分时间都在研究这个,但我只找到了关于如何用各种编程语言创建 TCP/UDP 服务器-客户端模型的基本教程,我对此并不感兴趣。

我想知道的:

一个简单的回答或一个link的建议就可以帮到我很多!我知道这是一个非常广泛的问题,但我需要从某个时候开始。

基本上你描述的是代理服务器。

目前,这就是我的想法。有任何疑问请告诉我,以便我通过扩大回复来解决它们。

什么是代理服务器?

代理服务器是将传入流量路由到其他服务器(内部或外部)并充当客户端和最终服务器之间的中介的服务器。

有多种方法可以解决您的问题。


方法一:Nginx + JSON

在这种情况下,我建议您使用一些使用 HTTP 协议的代理服务器,例如 Nginx。然后信息将作为 JSON 字符串传输,而不是使用原始二进制数据包,这会大大简化问题。

有关 NGINX 的更多信息:

有关 JSON 的更多信息:


方法二:制作自己的代理服务器并使用二进制数据包

对于代理部分,您可以使用 Java Sockets 和一个 class,它通过读取和打开客户端指定所需目的地的数据包来分配连接。那么你将有两个选择:

  1. 将 (Client-Proxy) 套接字流重定向到 (Proxy-WantedDestination) 套接字。
  2. 告诉 WantedDestination 打开到客户端的连接。 (客户端上的 ServerSocket 和 WantedDestination 上的 Socket)因此,WantedDestination 将打开与客户端的套接字连接,而不是客户端打开与 Wanted 目的地的连接。

第一种方法允许您记录所有传入和传出数据。第二种方法允许您保证 WantedDestination 的安全。

第一种方法:

Client  <-->  Proxy  <-->  WantedDestination          (2 Sockets)

第二种方法:

Step 1: Client  <-->  Proxy

Step 2:               Proxy  <-->  WantedDestination  

Step 3: Client  <--------------->  WantedDestination  (1 socket)

如何构造数据包

我通常按以下方式构造数据包:

  1. 数据包header
  2. 数据包长度
  3. 数据包负载
  4. 数据包校验和

数据包 header 可用于识别数据包是否来自您的软件,以及您是否开始从正确的位置读取数据。

数据包长度将指示在尝试将数据包反序列化到其包装器之前流必须读取多少字节 class。假设 header 的长度为 2 个字节,而 length 的长度为 3 个字节。那么如果length表示数据包是30字节长,就知道数据包的结尾是(30 - 3 - 2) = 25 bytes away.

数据包有效负载的大小可变,并在开头包含一些固定大小的字节,指示数据包类型。数据包类型可以任意选择。例如,您可以确定 (byte) 12 类型的数据包必须被解释为包含乒乓球比赛数据的数据包。

最后,数据包校验和表示您可以验证数据包完整性的数据包字节总和。 Java已经提供了一些校验和算法,比如CRC32。如果 Packet Checksum = CRC32(Packet header, Packet length, and Packet Payload),则数据未损坏。

最后,数据包是一个字节数组,可以使用 Java 输入和输出流进行传输。尽管如此,直接使用字节数组通常是困难且令人沮丧的,因此我建议您使用包装器 class 来表示数据包,然后扩展该 class 以创建其他数据包。例如:

package me.PauMAVA.DBAR.common.protocol;

import java.util.Arrays;
import java.util.zip.CRC32;
import java.util.zip.Checksum;

import static me.PauMAVA.DBAR.common.util.ConversionUtils.*;

public abstract class Packet implements Serializable {

    public static final byte[] DEFAULT_HEADER = new byte[]{(byte) 0xAB, (byte) 0xBA};

    private final byte[] header;

    private final byte packetType;

    private byte[] packetParameter;

    private byte[] packetData;

    private byte[] packetCheckSum;

    Packet(PacketType type, PacketParameter parameter) {
        this(type, parameter, new byte[0]);
    }

    Packet(PacketType type, PacketParameter parameter, byte[] data) {
        this.header = DEFAULT_HEADER;
        this.packetType = type.getCode();
        this.packetParameter = parameter.getData();
        this.packetData = data;
        recalculateChecksum();
    }

    public byte[] getParameterBytes() {
        return packetParameter;
    }

    public PacketParameter getPacketParameter() {
        return PacketParameter.getByData(packetParameter);
    }

    public byte[] getPacketData() {
        return packetData;
    }

    public void setParameter(PacketParameter parameter) {
        this.packetParameter = parameter.getData();
        recalculateChecksum();
    }

    public void setPacketData(byte[] packetData) {
        this.packetData = packetData;
        recalculateChecksum();
    }

    public void recalculateChecksum() {
        Checksum checksum = new CRC32();
        checksum.update(header);
        checksum.update(packetParameter);
        checksum.update(packetType);
        if (packetData.length > 0) {
            checksum.update(packetData);
        }
        this.packetCheckSum = longToBytes(checksum.getValue());
    }

    public byte[] toByteArray() {
        return concatArrays(header, new byte[]{packetType}, packetParameter, packetData, packetCheckSum);
    }

然后自定义数据包可以是:

package me.PauMAVA.DBAR.common.protocol;

import java.nio.charset.StandardCharsets;

import static me.PauMAVA.DBAR.common.util.ConversionUtils.subArray;

public class PacketSendPassword extends Packet {

    private String passwordHash;

    public PacketSendPassword() {
        super(PacketType.SEND_PASSWORD, PacketParameter.NO_PARAM);
    }

    public PacketSendPassword(String passwordHash) {
        super(PacketType.SEND_PASSWORD, PacketParameter.NO_PARAM);
        super.setPacketData(passwordHash.getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public byte[] serialize() {
        return toByteArray();
    }

    @Override
    public void deserialize(byte[] data) throws ProtocolException {
        validate(data, PacketType.SEND_PASSWORD, PacketParameter.NO_PARAM);
        PacketParameter packetParameter = PacketParameter.getByData(subArray(data, 3, 6));
        if (packetParameter != null) {
            super.setParameter(packetParameter);
        }
        byte[] passwordHash = subArray(data, 7, data.length - 9);
        super.setPacketData(passwordHash);
        this.passwordHash = new String(passwordHash, StandardCharsets.UTF_8);
    }

    public String getPasswordHash() {
        return passwordHash;
    }
}

通过流发送数据包就像:

byte[] buffer = packet.serialize();
dout.write(buffer);

你可以看看我为 Bukkit 服务器自动重载器开发的一个小协议 here

请注意,此方法需要您在不同数据类型和字节数组之间进行转换,因此您需要对二进制的数字和字符表示有很好的理解。