创建自定义可扩展协议架构的最佳实践?
Best Practice to creating an custom expandable protocol architecture?
免责声明:我在创建自定义的、更大规模的协议方面没有经验。
我打算开始一个新的项目(最好在java),由一个主服务器(MS),同一网络上的几个较小的服务器(SS)和几个客户端组成。所有这三方都应该相互传达信息。
示例:
- 客户端 'logs in' 到 MS。
- MS向SS发送客户端。 (SS要启动,MS发送IP/PORT的SS给Client告诉他连接,SS等待Client连接,...)
- SS和Client互相传递信息(比如游戏服务器和客户端)
我对更大规模的自定义协议和数据包的最大经验来自 Minecraft 服务器(Spigot 等)。看Servers包系统的时候还是有点懵
虽然我大部分时间都在研究这个,但我只找到了关于如何用各种编程语言创建 TCP/UDP 服务器-客户端模型的基本教程,我对此并不感兴趣。
我想知道的:
- 我想创建自己的协议 'architecture',但我不知道从哪里开始。我希望它具有很强的可扩展性,但不要太复杂。
- 是否有任何创建良好数据包的常见做法
->
“数据包消息应该是什么样的?”
一个简单的回答或一个link的建议就可以帮到我很多!我知道这是一个非常广泛的问题,但我需要从某个时候开始。
基本上你描述的是代理服务器。
目前,这就是我的想法。有任何疑问请告诉我,以便我通过扩大回复来解决它们。
什么是代理服务器?
代理服务器是将传入流量路由到其他服务器(内部或外部)并充当客户端和最终服务器之间的中介的服务器。
有多种方法可以解决您的问题。
方法一:Nginx + JSON
在这种情况下,我建议您使用一些使用 HTTP 协议的代理服务器,例如 Nginx。然后信息将作为 JSON 字符串传输,而不是使用原始二进制数据包,这会大大简化问题。
有关 NGINX 的更多信息:
有关 JSON 的更多信息:
方法二:制作自己的代理服务器并使用二进制数据包
对于代理部分,您可以使用 Java Sockets 和一个 class,它通过读取和打开客户端指定所需目的地的数据包来分配连接。那么你将有两个选择:
- 将 (Client-Proxy) 套接字流重定向到 (Proxy-WantedDestination) 套接字。
- 告诉 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)
如何构造数据包
我通常按以下方式构造数据包:
- 数据包header
- 数据包长度
- 数据包负载
- 数据包校验和
数据包 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。
请注意,此方法需要您在不同数据类型和字节数组之间进行转换,因此您需要对二进制的数字和字符表示有很好的理解。
免责声明:我在创建自定义的、更大规模的协议方面没有经验。
我打算开始一个新的项目(最好在java),由一个主服务器(MS),同一网络上的几个较小的服务器(SS)和几个客户端组成。所有这三方都应该相互传达信息。
示例:
- 客户端 'logs in' 到 MS。
- MS向SS发送客户端。 (SS要启动,MS发送IP/PORT的SS给Client告诉他连接,SS等待Client连接,...)
- SS和Client互相传递信息(比如游戏服务器和客户端)
我对更大规模的自定义协议和数据包的最大经验来自 Minecraft 服务器(Spigot 等)。看Servers包系统的时候还是有点懵
虽然我大部分时间都在研究这个,但我只找到了关于如何用各种编程语言创建 TCP/UDP 服务器-客户端模型的基本教程,我对此并不感兴趣。
我想知道的:
- 我想创建自己的协议 'architecture',但我不知道从哪里开始。我希望它具有很强的可扩展性,但不要太复杂。
- 是否有任何创建良好数据包的常见做法
->
“数据包消息应该是什么样的?”
一个简单的回答或一个link的建议就可以帮到我很多!我知道这是一个非常广泛的问题,但我需要从某个时候开始。
基本上你描述的是代理服务器。
目前,这就是我的想法。有任何疑问请告诉我,以便我通过扩大回复来解决它们。
什么是代理服务器?
代理服务器是将传入流量路由到其他服务器(内部或外部)并充当客户端和最终服务器之间的中介的服务器。
有多种方法可以解决您的问题。
方法一:Nginx + JSON
在这种情况下,我建议您使用一些使用 HTTP 协议的代理服务器,例如 Nginx。然后信息将作为 JSON 字符串传输,而不是使用原始二进制数据包,这会大大简化问题。
有关 NGINX 的更多信息:
有关 JSON 的更多信息:
方法二:制作自己的代理服务器并使用二进制数据包
对于代理部分,您可以使用 Java Sockets 和一个 class,它通过读取和打开客户端指定所需目的地的数据包来分配连接。那么你将有两个选择:
- 将 (Client-Proxy) 套接字流重定向到 (Proxy-WantedDestination) 套接字。
- 告诉 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)
如何构造数据包
我通常按以下方式构造数据包:
- 数据包header
- 数据包长度
- 数据包负载
- 数据包校验和
数据包 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。
请注意,此方法需要您在不同数据类型和字节数组之间进行转换,因此您需要对二进制的数字和字符表示有很好的理解。