自己动手写一个通信协议

什么是通信协议

我们常用的聊天软件比如:微信,都是基于一组通信协议进行服务端与客户端数据交互。协议指的就是客户端与服务端事先约定好的,每个二进制数据包中,每一段字节分别代表什么含义的规则。如下图所示一个简单的登录指令:
在这里插入图片描述
在这个数据包中,第一个字节为 1 表示这是一个登录指令,接下来是用户名和密码,这两个值以 \0 分割,客户端发送这段二进制数据包到服务端,服务端就能根据这个协议来取出用户名密码,进行登录逻辑。实际的通信协议设计中,我们会考虑更多细节,要比这个稍微复杂一些。
那么,协议设计好之后,客户端与服务端的通信过程又是怎样的呢?
在这里插入图片描述

通信协议的设计

在这里插入图片描述

  1. 第一个字段为魔数(magic_number),通常情况下都是几个字节(这里我们定义 4 个字节),魔数的作用类似于协议内的标识,通过客户端与服务端魔数对比,我们就知道这组二进制数据是否属于当前通信协议。这和 Java 字节码 中的魔数 0xCAFEBABE 用来标识这个文件,有着异曲同工之妙。
  2. 第二个字段为版本号,占用 1 个字节,通常情况下是预留字段。
  3. 第三部分是序列化算法,占用 1 个字节,表示如何把 Java 对象转换成二进制,二进制转换成 java 对象,比如 Java 自带的序列化,json,hessian 等序列化方式。
  4. 第四部分是指令,占用 1 个字节,服务端或者客户端每收到一种指令都会有相应的处理逻辑,最高支持256种指令,对于我们这个通信系统来说已经完全足够了。
  5. 第五部分是数据长度,占用 4 个字节。
  6. 最后一部分则是数据内容,每一个指令对应的数据是不一样的,比如登录的时候需要用户名密码,收消息的时候需要用户标识和具体消息内容等等。

通常情况下,这样一套标准的协议能够适配大多数情况下的服务端与客户端的通信场景,接下来我们就来看一下我们如何使用 Netty 来实现这套协议。

通信协议的实现

我们把Java 对象封装成二进制的过程叫编码, 从二进制数据包解析成 Java 对象的过程叫解码,在学习如何使用 Netty 进行通信协议的编解码之前,我们先来定义一下客户端与服务端通信的 Java 对象。

Java 对象
public abstract class Packet {
    /**
     * 协议版本
     */
    private Byte version = 1;

    /**
    * 指令
    * /
    public abstract Byte getCommand();
}

接下来,我们拿客户端登录请求为例,定义登录请求数据包

public interface Command {
    Byte LOGIN_REQUEST = 1;
}
public class LoginRequestPacket extends Packet {
    private Integer userId;
    private String username;
    private String password;

    @Override
    public Byte getCommand() { 
        return LOGIN_REQUEST;
    }
}

Command 定义一些指令,不同的 command 指令分别对应不同的数据内容。
这里 Command 定义了 LOGIN_REQUEST 指令表示登录请求,相应的就会有与之对应的 Java 对象 LoginRequestPacket。

Java 对象定义完成之后,接下来我们就需要定义一种规则,如何把一个 Java 对象转换成二进制数据,这个规则叫做 Java 对象的序列化。

序列化

定义序列化接口:

public interface Serializer {

    /**
     * 序列化算法
     */
    byte getSerializerAlgorithm(); // (1)
    
    /**
     * java 对象转换成二进制
     */
    byte[] serialize(Object object); // (2)

    /**
     * 二进制转换成 java 对象
     */
    <T> T deserialize(Class<T> clazz, byte[] bytes); // (3)
}
  1. getSerializerAlgorithm() 获取具体的序列化算法标识。
  2. serialize() 将 Java 对象转换成字节数组。
  3. deserialize() 将字节数组转换成某种类型的 Java 对象。

序列化接口的实现,这里我们使用最简单的 json 序列化方式,使用阿里巴巴的 fastjson 作为序列化框架:

public interface SerializerAlgorithm {
    /**
     * json 序列化标识
     */
    byte JSON = 1;
}


public class JSONSerializer implements Serializer {
    @Override
    public byte getSerializerAlgorithm() {
        return SerializerAlgorithm.JSON;
    } 

    @Override
    public byte[] serialize(Object object) {
        return JSON.toJSONBytes(object);
    }

    @Override
    public <T> T deserialize(Class<T> clazz, byte[] bytes) {
        return JSON.parseObject(bytes, clazz);
    }
}

这样我们就实现了序列化的相关逻辑。

序列化定义了 Java 对象与二进制数据的互转过程,接下来,我们就来学习一下,如何把这部分的数据编码到通信协议的二进制数据包中去。

编码
private static final int MAGIC_NUMBER = 0x12345678;

public ByteBuf encode(ByteBufAllocator byteBufAllocator, Packet packet){
    // 创建ByteBuf对象
    ByteBuf byteBuf = byteBufAllocator.ioBuffer(); // (1)

    // 序列化java对象
    byte[] bytes = Serializer.DEFAULT.serialize(packet); // (2)

    // 实际编码过程 (3)
    // 魔数
    byteBuf.writeInt(MAGIC_NUMBER);
    // 版本号
    byteBuf.writeByte(packet.getVersion());
    // 序列化算法
    byteBuf.writeByte(Serializer.DEFAULT.getSerializerAlgorithm());
    // 指令操作
    byteBuf.writeByte(packet.getCommand());
    // 数据长度
    byteBuf.writeInt(bytes.length);
    // 数据内容
    byteBuf.writeBytes(bytes);

    return byteBuf;
}

分为三个步骤:

  1. 首先,我们需要创建一个 ByteBuf,这里我们调用 Netty 的 ByteBuf 分配器来创建,ioBuffer() 方法会返回适配 io 读写相关的内存,它会尽可能创建一个直接内存,直接内存可以理解为不受 jvm 堆管理的内存空间,写到 IO 缓冲区的效果更高。
  2. 将 Java 对象序列化成二进制数据包
  3. 编码过程

编码完成之后会发送给另一端,另一端就需要进行解码操作。

解码
public Packet decode(ByteBuf byteBuf) {
    // 跳过 magic number
    byteBuf.skipBytes(4);

    // 跳过版本号
    byteBuf.skipBytes(1);

    // 序列化算法标识
    byte serializeAlgorithm = byteBuf.readByte();

    // 指令
    byte command = byteBuf.readByte();

    // 数据包长度
    int length = byteBuf.readInt();

    byte[] bytes = new byte[length];
    byteBuf.readBytes(bytes);

	// 根据指令获取数据内容
    Class<? extends Packet> requestType = getRequestType(command);
    // 根据序列化算法标识,找到对应的序列化方式
    Serializer serializer = getSerializer(serializeAlgorithm);

    if (requestType != null && serializer != null) {
    	// 解码
        return serializer.deserialize(requestType, bytes);
    }

    return null;
}

解码过程刚刚好和编码过程相反。

总结

我们了解了什么是通信协议,并且自己动手实现一个简单的通信协议,还了解到了编码和解码的过程。
文章参考:https://juejin.im/book/5b4bc28bf265da0f60130116
代码下载:https://github.com/jeansTuo/-

### 回答1: 我可以给你一些建议,但最终还是要你自己动手实现。首先,你需要使用Python编程语言,并了解基本的网络编程,例如TCP/IP等。其次,你需要熟悉游戏引擎,比如Unity或者Unreal Engine等。最后,你需要学习如何设计游戏的架构,确定游戏的玩法,并开发出游戏的具体内容。 ### 回答2: 用Python编网络游戏是完全可行的。Python提供了丰富的库和框架,使得开发网络游戏变得简单而且高效。 首先,我们可以使用Python的网络编程库(如socket或Twisted)来建立游戏服务器。服务器负责接收客户端的请求、处理游戏逻辑和更新游戏状态。我们可以定义游戏逻辑和规则,例如角色的移动、攻击和互动等。 其次,我们还可以使用Python的GUI库(如Pygame或Pyglet)来创建游戏的图形界面。这些库可以方便地渲染游戏界面、处理用户输入,并与服务器进行通信以保持游戏状态的同步。 在网络游戏中,通信协议和数据格式也非常重要。我们可以使用Python的序列化库(如pickle或JSON)来将游戏状态数据转换为网络可传输的格式。客户端和服务器之间可以使用HTTP、TCP或UDP等协议进行通信。 最后,为了提高游戏的性能和安全性,我们可以使用Python的并发编程库(如多线程或异步IO)来处理多个玩家的并发连接、游戏逻辑和事件处理等。 总而言之,使用Python编网络游戏可以充分利用其丰富的库和框架,快速开发和部署游戏,并且具有较高的可扩展性和灵活性。 ### 回答3: 用Python编一个网络游戏可以通过多种方式实现,以下是一种简单的实现方法: 首先,我们需要一个服务器来处理游戏逻辑和与客户端之间的通信。可以使用Python的Socket模块来创建一个服务器,并监听特定的端口。当客户端连接到服务器时,服务器接受连接并与客户端建立通信。 接下来,我们需要定义游戏的规则和逻辑。例如,可以创建一个迷宫游戏,在迷宫中玩家需要寻找出口,避免遇到陷阱和怪物。可以使用Python的图形库如Pygame来创建游戏界面和角色。 在服务器端,需要处理游戏中的各种动作,如移动、攻击等。当玩家在客户端进行操作时,客户端将请求发送到服务器,服务器收到请求后,根据游戏规则进行处理,并将结果发送回客户端,更新游戏状态。 在客户端,可以使用Python的Socket模块来与服务器建立通信。客户端接收服务器发送的游戏状态,并根据返回的信息更新游戏界面。同时,客户端也需要监听玩家的输入,将操作请求发送给服务器。 为了提高游戏的可玩性,还可以添加其他功能,如多人在线游戏、排行榜、聊天等。多人在线游戏可以使多个玩家同时在同一游戏世界中进行游戏,他们可以互相交互和竞争。排行榜可以记录每个玩家的成绩,并展示给其他玩家。聊天功能可以使玩家在游戏中实时交流。 总结起来,编一个网络游戏需要有一个服务器和多个客户端,服务器处理游戏逻辑和与客户端之间的通信,客户端负责接收游戏状态和将操作请求发送给服务器。通过这种方式,可以使用Python编出基本的网络游戏。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值