Mina提供的TextLineCodecFactory
编码器,默认读取字节是2048,在一般场景下是可以的。当数据包比较大时,可能会分多次读取,导致断包,造成后面的IoHandler处理异常。
当接收到的数据包大小不是很固定,TextLineCodecFactory就不适合了。Mina框架提供了一个CumulativeProtocolDecoder
。只要有数据包发送过来,这个decoder就会读取,然后累积到IoSession内部的IoBuffer缓冲区中,而具体的拆包动作,则交由子类的doDecode
方法完成。
1 - 源码看处理过程
先来看一下*CumulativeProtocolDecoder#decode()*方法:
for (;;) {
int oldPos = buf.position();
boolean decoded = doDecode(session, buf, out);
if (decoded) {
if (buf.position() == oldPos) {
throw new IllegalStateException("doDecode() can't return true when buffer is not consumed.");
}
if (!buf.hasRemaining()) {
break;
}
} else {
break;
}
}
// if there is any data left that cannot be decoded, we store
// it in a buffer in the session and next time this decoder is
// invoked the session buffer gets appended to
if (buf.hasRemaining()) {
if (usingSessionBuffer && buf.isAutoExpand()) {
buf.compact();
} else {
storeRemainingInSession(buf, session);
}
} else {
if (usingSessionBuffer) {
removeSessionBuffer(session);
}
}
- oldPos:内部的IoBuffer缓冲区的当前位置
- 然后调用子类的doDecode()方法
- doDecode()方法返回true, 并且调用之后buf的position和oldPos一样,也就是说子类并没有消费buf,这时候会抛出异常
- 缓冲区如果消费完毕了,则跳出循环,不在调用子类的doDecode()方法
- doDecode()方法返回false,直接跳出循环,不在调用子类的doDecode()方法
- 跳出循环后,缓冲区还是有没被消费的字节,就会将剩余的字节放到缓冲区
- 跳出循环且全部被消费,就会清空字节缓冲区
再看一下for循环前面的一段代码:
boolean usingSessionBuffer = true;
IoBuffer buf = (IoBuffer) session.getAttribute(BUFFER);
// If we have a session buffer, append data to that; otherwise
// use the buffer read from the network directly.
if (buf != null) {
boolean appended = false;
// Make sure that the buffer is auto-expanded.
if (buf.isAutoExpand()) {
try {
buf.put(in);
appended = true;
} catch (IllegalStateException e) {
// A user called derivation method (e.g. slice()),
// which disables auto-expansion of the parent buffer.
} catch (IndexOutOfBoundsException e) {
// A user disabled auto-expansion.
}
}
if (appended) {
buf.flip();
} else {
// Reallocate the buffer if append operation failed due to
// derivation or disabled auto-expansion.
buf.flip();
IoBuffer newBuf = IoBuffer.allocate(buf.remaining() + in.remaining()).setAutoExpand(true);
newBuf.order(buf.order());
newBuf.put(buf);
newBuf.put(in);
newBuf.flip();
buf = newBuf;
// Update the session attribute.
session.setAttribute(BUFFER, buf);
}
} else {
buf = in;
usingSessionBuffer = false;
}
- 数据包到来时,首先看一下IoSession内部的缓冲区buf中是否有数据
- 没有数据,则读取本次收到的数据。就是执行上面for循环的逻辑
- 有数据,把buf中的和本次接受到的数据合并,然后就是for循环了,去调用子类doDecode()方法
稍微总结一下 CumulativeProtocolDecoder子类的doDecode()方法返回值:
- true:本次数据已经全部消费完毕,可以解包
- false: 本次数据接收到的数据还不够解包,返回false,等待下次数据到来时继续解包
2 - 编写自己的解码器
public class IMessageCodecFactory implements ProtocolCodecFactory {
private final IMessageEncoder encoder;
private final IMessageDecoder decoder;
public IMessageCodecFactory() {
this.encoder = new IMessageEncoder();
this.decoder = new IMessageDecoder();
}
public ProtocolEncoder getEncoder(IoSession session) throws Exception {
return encoder;
}
public ProtocolDecoder getDecoder(IoSession session) throws Exception {
return decoder;
}
}
3 - 解包
public class IMessageDecoder extends CumulativeProtocolDecoder {
private static final Logger LOGGER = LoggerFactory.getLogger(IMessageDecoder.class);
private static final Charset CHARSET = Charset.forName("UTF-8");
@Override
public boolean doDecode(IoSession session, IoBuffer ioBuf, ProtocolDecoderOutput out) throws Exception {
boolean complete = false;
IoBuffer buff = IoBuffer.allocate(320).setAutoExpand(true);
while (ioBuf.hasRemaining()) {
byte b = ioBuf.get();
if (b == CIMConstant.MESSAGE_SEPARATE) {
complete = true;
break;
}
buff.put(b);
}
buff.flip();
byte[] bytes = new byte[buff.limit()];
buff.get(bytes);
String message = new String(bytes, CHARSET);
if (!complete) {
int position = ioBuf.position();
int limit = ioBuf.limit();
LOGGER.warn("消息不够消费, {} => :: 当前位置 => {} :: 长度 => {} ", message, position, limit);
ioBuf.flip();
return false;
}
buff.clear();
// 消息读取完毕, 解包为高级对象
// out.write(obj);
return true;
}
}