在“编码”和“解码”中的一个重要问题是如何在字节流中判断消息的边界。通常来说,有三种办法解决这个问题:
- 使用固定长度的消息。这种方式实现起来比较简单,只需要每次读取特定数量的字节即可。
- 使用固定长度的消息头来指明消息主体的长度。比如每个消息开始的 4 个字节的值表示了后面紧跟的消息主体的长度。只需要首先读取该长度,再读取指定数量的字节即可。
- 使用分隔符。消息之间通过特定模式的分隔符来分隔。每次只要遇到该模式的字节,就表示到了一个消息的末尾。
具体到示例应用来说,客户端和服务器之间的通信协议比较复杂,有不同种类的消息。每种消息的格式都不相同,同类消息的内容也不尽相同。因此,使用固定长度的消息头来指明消息主体的长度就成了最好的选择。
示例应用中的每种消息主体由两部分组成,第一部分是固定长度的消息类别名称,第二部分是每种消息的主体内容。图1 中给出了示例应用中一条完整的消息的结构。
图 4. 示例应用中消息的结构
解码实例代码:
public class CommandDecoder extends CumulativeProtocolDecoder { protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { if (in.prefixedDataAvailable(4, Constants.MAX_COMMAND_LENGTH)) { int length = in.getInt(); byte[] bytes = new byte[length]; in.get(bytes); int commandNameLength = Constants.COMMAND_NAME_LENGTH; byte[] cmdNameBytes = new byte[commandNameLength]; System.arraycopy(bytes, 0, cmdNameBytes, 0, commandNameLength); String cmdName = StringUtils.trim(new String(cmdNameBytes)); AbstractTetrisCommand command = TetrisCommandFactory .newCommand(cmdName); if (command != null) { byte[] cmdBodyBytes = new byte[length - commandNameLength]; System.arraycopy(bytes, commandNameLength, cmdBodyBytes, 0, length - commandNameLength); command.bodyFromBytes(cmdBodyBytes); out.write(command); } return true; } else { return false; } } } |
以上 中可以看到,解码器 CommandDecoder
继承自 CumulativeProtocolDecoder
。这是 Apache MINA 提供的一个帮助类,它会自动缓存所有已经接收到的数据,直到编码器认为可以开始进行编码。这样在实现自己的编码器的时候,就只需要考虑如何判断消息的边界即可。如果一条消息的后续数据还没有接收到,CumulativeProtocolDecoder
会自动进行缓存。在之前提到过,解码过程的一个重要问题是判断消息的边界。对于固定长度的消息来说,只需要使用 Apache MINA 的 IoBuffer
的 remaining
方法来判断当前缓存中的字节数目,如果大于消息长度的话,就进行解码;对于使用固定长度消息头来指明消息主体的长度的情况,IoBuffer
提供了prefixedDataAvailable
方法来满足这一需求。prefixedDataAvailable
会检查当前缓存中是否有固定长度的消息头,并且由此消息头指定长度的消息主体是否已经全部在缓存中。如果这两个条件都满足的话,说明一条完整的消息已经接收到,可以进行解码了。解码的过程本身并不复杂,首先读取消息的类别名称,然后通过 TetrisCommandFactory.newCommand
方法来生成一个该类消息的实例,接着通过该实例的 bodyFromBytes
方法就可以从字节数组中恢复消息的内容,得到一个完整的消息对象。每次成功解码一个消息对象,需要调用 ProtocolDecoderOutput
的 write
把此消息对象往后传递。消息对象会通过过滤器链,最终达到 I/O 处理器,在IoHandler.messageReceived
中接收到此消息对象。如果当前缓存的数据不足以用来解码一条消息的话,doDecode
只需要返回 false
即可。接收到新的数据之后,doDecode
会被再次调用。