mina socket 通信时粘包问题
socket 通信时会经常遇到粘包问题!尼玛,数据多和频发时概率很高。
mina 已经考虑到了这个问题,CumulativeProtocolDecoder这个就是answer!
CumulativeProtocolDecoder 类,累积性的协议解码器,也就是说只要有数据发送过来,这个类就会去 读取数据,然后累积到内部的 IoBuffer 缓冲区,但是具体的拆包(把累积到缓冲区的数据 解码为 JAVA 对象)交由子类的 doDecode()方法完成,实际上 CumulativeProtocolDecoder 就是在 decode()反复的调用暴漏给子类实现的 doDecode()方法。
具体执行过程如下所示:
A. 你的 doDecode()方法返回 true 时,CumulativeProtocolDecoder 的 decode()方法会首先判断你是否在 doDecode()方法中从内部的 IoBuffer 缓冲区读取了数据,如果没有,ce); buffer.putString(smsContent, ce);buffer.flip();则会抛出非法的状态异常,也就是你的 doDecode()方法返回 true 就表示你已经消费了 本次数据(相当于聊天室中一个完整的消息已经读取完毕),进一步说,也就是此时你 必须已经消费过内部的 IoBuffer 缓冲区的数据(哪怕是消费了一个字节的数据)。如果 验证过通过,那么 CumulativeProtocolDecoder 会检查缓冲区内是否还有数据未读取, 如果有就继续调用 doDecode()方法,没有就停止对 doDecode()方法的调用,直到有新 的数据被缓冲。
B. 当你的 doDecode()方法返回 false 时,CumulativeProtocolDecoder 会停止对 doDecode() 方法的调用,但此时如果本次数据还有未读取完的,就将含有剩余数据的 IoBuffer 缓 冲区保存到 IoSession 中,以便下一次数据到来时可以从 IoSession 中提取合并。如果 发现本次数据全都读取完毕,则清空 IoBuffer 缓冲区。简而言之,当你认为读取到的数据已经够解码了,那么就返回 true,否则就返回 false。这 个 CumulativeProtocolDecoder 其实最重要的工作就是帮你完成了数据的累积,因为这个工 作是很烦琐的。
CumulativeProtocolDecoder:
public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception
Cumulates content of in into internal buffer and forwards decoding request todoDecode(IoSession, IoBuffer, ProtocolDecoderOutput)
. doDecode() is invoked repeatedly until it returns false and the cumulative buffer is compacted after decoding ends.
- Throws:
IllegalStateException
- if your doDecode() returned true not consuming the cumulative buffer.Exception
- if the read data violated protocol specification
说明:decode函数将IoBuffer in对象中的内容累积添加到 内部的buffer上,并调用doDecode方法。,如果doDecode方法返回false且内部累积buffer中的数据再次可用时,doDecode方法会被再次调用,直到doDecode方法返回true;
protected abstract boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception
- Implement this method to consume the specified cumulative buffer and decode its content into message(s).
- Parameters:
in
- the cumulative buffer- Returns:
- true if and only if there's more to decode in the buffer and you want to have doDecode method invoked again. Return false if remaining data is not enough to decode, then this method will be invoked again when more data is cumulated.
- Throws:
Exception
- if cannot decode in.- 说明:如果累积buffer中的数据长度大于解析协议需要的长度时,返回true;当该方法返回结果为true时,会再次调用该方法。如果累积buffer中的数据不够协议解析需要的长度时,返回false,直到累积buffer中的数据再次可用时。
例:
public class AsResponseDecoder extends CumulativeProtocolDecoder { private static Logger LOG = LoggerFactory.getLogger(AsResponseDecoder.class); private final Charset charset; public AsResponseDecoder(Charset charset){ this.charset = charset; } /** * 这个方法的返回值是重点: * 1、当内容刚好时,返回false,告知父类接收下一批内容 * 2、内容不够时需要下一批发过来的内容,此时返回false,这样父类 CumulativeProtocolDecoder * 会将内容放进IoSession中,等下次来数据后就自动拼装再交给本类的doDecode * 3、当内容多时,返回true,因为需要再将本批数据进行读取,父类会将剩余的数据再次推送本 类的doDecode */ public boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { CharsetDecoder cd = charset.newDecoder(); if(in.remaining() > 0){//有数据时,读取4字节判断消息长度 byte [] sizeBytes = new byte[4]; in.mark();//标记当前位置,以便reset in.get(sizeBytes);//读取前4字节 //NumberUtil是自己写的一个int转byte[]的一个工具类 int size = NumberUtil.byteArrayToInt(sizeBytes); //如果消息内容的长度不够则直接返回true if(size > in.remaining()){//如果消息内容不够,则重置,相当于不读取size in.reset(); return false;//接收新数据,以拼凑成完整数据 } else{ byte[] bytes = new byte[size]; in.get(bytes, 0, size); String xmlStr = new String(bytes,"UTF-8"); System.out.println("------------"+xmlStr); if(null != xmlStr && xmlStr.length() > 0){ AsResponse resCmd = new AsResponse(); AsXmlPacker.parse(resCmd, xmlStr); if(resCmd != null){ out.write(resCmd); } } if(in.remaining() > 0){//如果读取内容后还粘了包,就让父类再给俺 一次,进行下一次解析 return true; } } } return false;//处理成功,让父类进行接收下个包 } }
IoBuffer相关函数:
Capacity:开的内存的大小,一旦设定了,就不能更改了。注意,这里指的是原生的NIO。
Limit:可以分读写来统计。在写入buffer时,limit表示有多少空间可以写入。在从buffer写出时,limit表示有多少可以写出。
Position:下一个要被读或写的位置。
Mark:标记位,可以记住某个position,方便后续操作。
对于ByteBuffer有如下常用的操作:
flip()::读写模式的转换。
rewind() :将 position 重置为 0 ,一般用于重复读。
clear() :清空 buffer ,准备再次被写入 (position 变成 0 , limit 变成 capacity) 。
compact(): 将未读取的数据拷贝到 buffer 的头部位。
mark() 、 reset():mark 可以标记一个位置, reset 可以重置到该位置。
get()、getShort()等一系列get操作:获取ByteBuffer中的内容,当然这里get的内容都是从position开始的,所以要时刻注意position。每次get之后position都会改变。Position的变化是根据你get的类型,如果是short,那就是2个byte,如果是int,那就是增加4个byte,即32。
put()、putShort()等一系列put操作:向ByteBuffer添加内容,这里put的内容都是从position开始的。每次put之后position都会改变。
当然还有allocate、hasRemaining等常用的方法,不过这些用法一般都不会出错,使用起来和4个attributes也没有多大相关。特别注意:Buffers are not thread-safe. If you want to access a given buffer concurrently from multiple threads, you will need to do your own synchronization prior to accessing the buffer.至于Buffer或者ByteBuffer有什么用?那太多了,只要涉及到传输、涉及到通信,都可以用到。当然你也可以用它最原始的含义,缓冲。