Java如何为聊天程序网络消息实现marshall与unmarshall功能
编组(Marshalling)与解组(Unmarshalling)实现方案
首先明确两个操作的核心定义:
- 编组:把内存中的消息对象转换成可在网络传输的字节序列,提前约定统一的字节格式保证服务端、客户端能互相识别
- 解组:把收到的字节序列还原成对应的消息对象,反向执行编组的操作流程即可
你当前的架构(统一消息类型枚举+通用消息接口)已经非常适配需求,可以先约定所有消息通用的字节结构,7种消息全部套用该结构即可:
通用字节结构:[1字节消息ID] + [2字节内容长度] + [N字节消息内容]
解析时优先读取前3个固定位置的字节,就能快速判断后续要读取的内容长度、对应的消息类型
1. 编组实现示例(以REGISTRATION_MESSAGE为例)
假设注册消息类包含用户名、密码两个字符串属性,在类中实现接口定义的marshall方法即可:
@Override public byte[] marshall() { // 先把消息内容属性转换为字节数组,统一指定UTF-8字符集避免乱码 byte[] usernameBytes = this.username.getBytes(StandardCharsets.UTF_8); byte[] passwordBytes = this.password.getBytes(StandardCharsets.UTF_8); // 内容段总长度 = 2字节存储用户名长度 + 用户名字节长度 + 2字节存储密码长度 + 密码字节长度 int contentLen = usernameBytes.length + passwordBytes.length + 4; // 最终返回的完整字节数组总长度 = 1字节ID + 2字节内容长度 + 内容段总长度 byte[] result = new byte[1 + 2 + contentLen]; // 写入消息ID:取枚举中REGISTRATION_MESSAGE对应的id值 result[0] = (byte) MessageType.REGISTRATION_MESSAGE.getId(); // 写入内容段总长度,用short类型存储(刚好占2字节) ByteBuffer.wrap(result, 1, 2).putShort((short) contentLen); // 写入内容段:先写字段长度再写字段值,解析时按相同顺序读取即可 ByteBuffer contentBuffer = ByteBuffer.wrap(result, 3, contentLen); contentBuffer.putShort((short) usernameBytes.length); contentBuffer.put(usernameBytes); contentBuffer.putShort((short) passwordBytes.length); contentBuffer.put(passwordBytes); return result; }
2. 解组实现示例
可以写一个统一的静态解组工具方法,先读取公共头判断消息类型,再调用对应消息类的解析逻辑:
public static MessageInterface unmarshall(byte[] fullBytes) { // 读取第一个字节拿到消息ID int msgId = fullBytes[0] & 0xFF; // 读取2-3字节拿到内容段总长度 short contentLen = ByteBuffer.wrap(fullBytes, 1, 2).getShort(); // 截取内容段字节数组 byte[] contentBytes = Arrays.copyOfRange(fullBytes, 3, 3 + contentLen); // 根据消息ID匹配对应枚举值,走对应解析逻辑 MessageType msgType = MessageType.valueOfId(msgId); switch (msgType) { case REGISTRATION_MESSAGE: return unmarshallRegistration(contentBytes); // 剩余6种消息直接新增case即可,逻辑完全一致,仅内容段解析部分不同 default: throw new IllegalArgumentException("未知消息类型ID:" + msgId); } } // 注册消息专属解析逻辑,和编组顺序严格对应即可 private static RegistrationMessage unmarshallRegistration(byte[] contentBytes) { ByteBuffer buffer = ByteBuffer.wrap(contentBytes); // 先读用户名长度,再读用户名字节转字符串 short usernameLen = buffer.getShort(); byte[] usernameBytes = new byte[usernameLen]; buffer.get(usernameBytes); String username = new String(usernameBytes, StandardCharsets.UTF_8); // 再读密码长度,再读密码字节转字符串 short passwordLen = buffer.getShort(); byte[] passwordBytes = new byte[passwordLen]; buffer.get(passwordBytes); String password = new String(passwordBytes, StandardCharsets.UTF_8); return new RegistrationMessage(username, password); }
你可以先给枚举类加一个静态方法valueOfId(int id),实现通过id值快速匹配对应枚举实例,避免每次写大量if判断。
落地注意事项
- 所有字符串和字节互转的操作必须指定相同字符集,统一用UTF-8即可
- 多字节数值(short/int等)统一使用大端序,Java的ByteBuffer默认就是大端序,符合网络传输通用规范
- 剩余6种消息的编组解组逻辑完全和注册消息一致,仅需要调整内容段的字段读写顺序即可
内容的提问来源于stack exchange,提问作者Jordan Taylor
相关产品推荐
相关产品推荐

