什么导致了断包、粘包:
mina是基于TCP/IP、UDP/IP协议栈的通信框架。Mina 可以帮助我们快速开发高性能、高扩展性的网络通信应用,Mina 提供了事件驱动、异步(Mina 的异步IO 默认使用的是JAVA NIO 作为底层支持)操作的编程模型。
断包、粘包的问题,是Mina基于TCP协议栈通信的问题。TCP是面向流的,而面向流传输的数据是无保护边界的,无保护边界代表着如果发送端连续传输数据,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。
什么是消息保护边界和无消息保护边界:
①消息保护边界:就是传输协议把数据当做独立的一条数据在网上进行传输,而且接收端也只能接收独立的消息,也就是因为存在着消息保护边界,接收端一次只能接收发送端传来的一个数据包,这一点有一点像UDP协议。
②无消息保护边界:面向流传输数据的是无消息保护边界的,也就是在发送端连续发送数据的情况下接受端可能会在一次中接收两个或多个数据包。
断包、粘包的体现实例:
① 先接收到数据包A,然后接收到数据包B;
② 先接收到数据包A的部分数据,然后接收到数据包A的剩余数据和数据包的全部数据。
③ 先接受到数据包A的全部数据和数据包B的部分数据,然后接收到数据包B的剩余全部数据。
④ 一次性接收完数据包A和数据包B的全部数据。
① 正常的情况
② 断包+粘包
③ 粘包+断包
④ 粘包
数据包(消息)的格式:
包头 + 消息长度(int)+消息内容(json字符串、普通字符串)+ 包尾
Mina处理 断包、粘包问题
在Mina框架中有个——CumulativeProtocolDecoder (累积性的协议解码器),专门用来处理粘包和断包问题。doDecode()的返回值有重要作用。
@ doDecode() 方法 ——》 返回 true ,CumulativeProtocolDecoder 的 decode() 方法会首先判断你是否在 doDecode() 方法中从内部的 IoBuffer 缓冲区中读取了数据,如果没有则会抛出非法状态的异常, 也就是因为 你的 doDecode()返回 true 的时候就表示你已经消费了本次的数据,(一个完整的消息已经被读取完毕)。
进一步说:也就是必须你已经消费过 内部 IoBuffer 缓冲区的数据(哪怕数据只有一个字节的大小)。如果 内部 验证通过,确实已经消费了数据,那么CumulativeProtocolDecoder(累积性的协议解码器)会检查缓冲区是否还有数据没有被读取,如果有那么 就继续调用 doDecode(),如果没有就停止对 doDecode() 方法的调用,直到有新的数据被缓冲。
@ doDecode() 方法 ——》 返回 false , CumulativeProtocolDecoder 会停止对 doDecode() 方法的调用,但此时如果本次数据还有未读取完的,就将含有数据的 IoBuffer 缓冲区保存到 IoSession 中,以便下一次数据到来时可以从 IoSession 中提取合并。如果发现本次数据全部读取完毕,则清空 IoBuffer 缓冲区 (开始进行接收下一个包)。
简单来说:当你认为读取的数据已经够解码了,那么就返回 true,否则就返回false。 CumulativeProtocolDecoder 就是帮你完成数据的累积,但是这个过程是很繁琐的。
也就是说:当返回 true 时,CumulativeProtocolDecoder 会调用 deDecoder() 把剩余的数据发下来(剩余数据就是在 remaining()中的数据),返回false 就是不处理剩余的数据(剩余数据不交给 doDecoder()处理),当有新的数据包传过来的时候再把剩余的数据和新的数据拼接在一起,然后调用 decoder。
需要的jar包
Mina 实例 Java代码
service类:
acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new ByteArrayCodecFactory(Charset.forName("UTF-8")))) ; // 自定义解编码器
编码器类:
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import java.nio.charset.Charset;
public class ByteArrayEncoder extends ProtocolEncoderAdapter {
private final Charset charset;
public ByteArrayEncoder(Charset charset) {
this.charset = charset;
}
/**
* 直接将数据发出去,数据格式,包头+消息长度(int)+消息内容(json字符串)+包尾 包头包尾是十六进 制字符串00 aa bb cc,转化成字节数组0, * -86, -69, -52四个字节
*
* @param session
* @param message
* @param out
* @throws Exception
*/
@Override
public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {
// 仿项目,解决断包,粘包问题
String value = (message == null ? "" : message.toString()); // 消息值
byte[] content = value.getBytes(charset);// 消息内容,字节数组
IoBuffer buf = IoBuffer.allocate(38 + content.length).setAutoExpand(true);
// 缓冲区容量大小38字节加上字符长度
buf.put(new byte[] { 0, -86, -69, -52 });
// 输入包开头固定值十六进制00 aa bb cc,转化成字节数组
buf.putUnsignedInt(content.length);
// int为4字节,一个字节等于2个16进制字符,所以有八位 00 00 00 0c,内容长度。 buf.put(content);// 消息内容
buf.put(new byte[] { 0, -86, -69, -52 });// 包尾
buf.flip();
out.write(buf); // 写入
}
}
解码器类 :解决Mina断包,丢包问题 (重点)
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import java.nio.charset.Charset;
/**
* 自定义解码器,确保能读到完整的包
*/
public class ByteArrayDecoder extends CumulativeProtocolDecoder {
private final Charset charset;
public ByteArrayDecoder(Charset charset) {
this.charset = charset;
}
@Override
protected boolean doDecode (IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput protocolDecoderOutput)throws Exception {
// 丢包,断包处理
if (ioBuffer.remaining() > 4)// 有包头,包头足够
{
ioBuffer.mark();
// 标记当前position的快照标记mark,以便后继的reset操作能恢复position位置,开始是0
byte[] l = new byte[4];
ioBuffer.get(l);
// 读取包头,占4个字节
if (ioBuffer.remaining() < 4)
// 内容长度的4个字节不够,断包
{
ioBuffer.reset();
return false;
} else {
// 内容长度的4个字节数组足够
byte[] bytesLegth = new byte[4];
// 内容长度
ioBuffer.get(bytesLegth);// 读取内容长度,int类型,占四个字节
int len = MinaUtil.byteArrayToInt(bytesLegth);
// 内容长度有多少
if (ioBuffer.remaining() < len) // 内容不够,断包
{
ioBuffer.reset();
return false;
} else {
// 消息内容足够
byte[] bytes = new byte[len];
ioBuffer.get(bytes, 0, len);
protocolDecoderOutput.write(new String(bytes, charset));
// 读取内容,并且发送
if (ioBuffer.remaining() < 4) { // 包尾不够
ioBuffer.reset();
return false;
} else {
// 包尾足够
byte[] tails = new byte[4];
ioBuffer.get(tails);
// 读取包尾
if (ioBuffer.remaining() > 0){
// 最后如果粘了包,会再次调用doDeocde()方法,把剩余数据给doDeocde()方法处理
return true;
}}}}}
return false; // 断包,或者执行完,
} }
编解码工厂
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolEncoder;
import java.nio.charset.Charset;
/** * 自定义解编码器工厂 * */
public class ByteArrayCodecFactory implements ProtocolCodecFactory {
private ByteArrayDecoder decoder;
private ByteArrayEncoder encoder;
public ByteArrayCodecFactory() {
this(Charset.defaultCharset());
}
public ByteArrayCodecFactory(Charset charSet) {
encoder = new ByteArrayEncoder(charSet);
decoder = new ByteArrayDecoder(charSet);
}
@Override
public ProtocolDecoder getDecoder(IoSession session) throws Exception {
return decoder;
}
@Override
public ProtocolEncoder getEncoder(IoSession session) throws Exception {
return encoder;
} }
工具类方法 (二进制转整型)
public static int byte2Int(byte[] l) {
return (l[0]&0xff)<<24
| (l[1]&0xff)<<16
| (l[2]&0xff)<<8
| (l[3]&0xff);
}
强调: 这个 编解码器 封装的是 String 类型数组,在业务逻辑处理层 收发信心的时候 要用 String 格式定义!