一、Mina对编解码的支持

我们知道网络通讯过程实际是对二进制数据进行处理的过程,二进制数据是计算机认识的数据。对于接收到的二进制数据我们需要将其转换成我们所熟悉的数据格式,此过程称为解码(decode);对于所要发送的数据,我们需要转换为计算机所能处理的二进制数据,此过程称为编码(encode)。

Mina对数据的编解码提供了良好的支持,它提供了过滤器ProtocolCodecFilter支持编码和解码过程,可以查看包org.apache.mina.filter.codec下的代码。

看下此过滤器的调用,代码很简单:

  1. // 加入编解码过滤器
  2. chain.addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory()));

实现原理

ProtocolCodecFilter包含一个编码器和解码器工厂:

  1. //编码器和解码器工厂
  2. private final ProtocolCodecFactory factory;

此工厂可以通过构造方法传入,具体构造方法可以具体看源码,比较简单,此处不做详细介绍。

主要看下解码和编码过程,解码应该是消息接收到,我们程序对消息进行处理时进行的,此时我们想到ProtocolCodecFilter应该覆盖messageReceived方法。编码应该是发送消息时,需要将我们的业务数据结构转换为二进制数据,此时我们想到ProtocolCodecFilter应该覆盖filterWrite方法。

解码过程

前面已经说了,解码过程就是将二进制数据转换为我们可以识别的数据结构,所以messageReceived方法一开始就有个判断:

  1. //对于解码,消息类型必须是IoBuffer类型的,如果不是,转向下个filter
  2. if (!(message instanceof IoBuffer)) {
  3. nextFilter.messageReceived(session, message);
  4. return;
  5. }

解码的核心操作:

  1. //处理消息,如果buffer中还有数据,就处理数据
  2. while (in.hasRemaining()) {
  3. int oldPos = in.position();
  4. try {
  5. synchronized (decoderOut) {
  6. //进行解码操作。
  7. decoder.decode(session, in, decoderOut);
  8. }
  9. …………
  10. }

我们需要实现解码器ProtocolDecoder接口,主要实现解码方法decode。可以参考TextLineDecoder类的实现,下面代码是本人实际项目中的实现:

  1. public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
  2. final int packHeadLength = 2;
  3. // 先获取上次的处理上下文,其中可能有未处理完的数据
  4. Context ctx = getContext(session);
  5. // 先把当前buffer中的数据追加到Context的buffer当中
  6. ctx.append(in);
  7. // 把position指向0位置,把limit指向原来的position位置
  8. IoBuffer buf = ctx.getBuffer();
  9. buf.flip();
  10. // 当前剩余长度大于2
  11. while (buf.remaining() >= packHeadLength) {
  12. buf.mark();
  13. if (ByteConvertUtil.toHex(buf.get()).equalsIgnoreCase("eb")) {
  14. if (ByteConvertUtil.toHex(buf.get()).equalsIgnoreCase("93")) {
  15. buf.reset();
  16. if(buf.remaining()<11){
  17. break;
  18. }
  19. byte[] dataArray = new byte[11];
  20. buf.get(dataArray, 0, 11);
  21. if (SensorData.checkData(dataArray)) {
  22. SensorData data = new SensorData(dataArray);
  23. out.write(data);
  24. // 回应客户端
  25. byte[] b = new byte[2];
  26. b[0] = ByteConvertUtil.uniteBytes("eb");
  27. b[1] = ByteConvertUtil.uniteBytes("93");
  28. session.write(IoBuffer.wrap(b));
  29. }
  30. } else {
  31. continue;
  32. }
  33. } else {
  34. continue;
  35. }
  36. }
  37. //断包处理,将剩余数据放入CONTEXT中
  38. if (buf.hasRemaining()) {
  39. IoBuffer temp = IoBuffer.allocate(maxPackLength).setAutoExpand(true);
  40. temp.put(buf);
  41. temp.flip();
  42. buf.clear();
  43. buf.put(temp);
  44. } else {
  45. buf.clear();
  46. }
  47. }

顺便说下,我们最好要把我们的的数据包的格式提前定义好,了解了数据包的格式我们才能更好的进行数据的编解码。定义好数据包格式一方面方便编解码,另一方面可以解决下面要说的粘包和断包的问题。

数据包的定义有很多种方式,这里说下我所用过的两种方式:

1.固定消息长度,消息头+消息体+校验码。此方式相对简单,表示的内容也比较少。

2.不定消息长度,消息头+消息长度+消息体。此方式可以无限消息长度,比较灵活。

解码出一个消息体后,需要将数据通过ProtocolDecoderOutput的write方法写入到队列(queue)里面去:

  1. public void write(Object message) {
  2. if (message == null) {
  3. throw new IllegalArgumentException("message");
  4. }
  5. //将消息写入队列
  6. messageQueue.add(message);
  7. }

真正执行消息向下传递是通过flush方法:

  1. public void flush(NextFilter nextFilter, IoSession session) {
  2. Queue<Object> messageQueue = getMessageQueue();
  3. // 取出队列里面的消息向下传递
  4. while (!messageQueue.isEmpty()) {
  5. nextFilter.messageReceived(session, messageQueue.poll());
  6. }
  7. }

编码过程

看了上面的解码过程,编码过程就不难理解了,编码过程只不过是解码过程的逆向过程,同样在filterWrite方法里有消息类型的判断:

  1. //消息如果已经是IoBuffer,就不需要再进行编码
  2. if ((message instanceof IoBuffer) || (message instanceof FileRegion)) {
  3. nextFilter.filterWrite(session, writeRequest);
  4. return;
  5. }

编码:

  1. // 进行数据编码
  2. encoder.encode(session, message, encoderOut);

此处编码实现可以参考TextLineEncoder的编码实现,比较简单,此处就不多做解释了。

同样编码也是通过write到一个队列中,然后通过flush写入到后面的过滤器中的。

二、Mina对粘包和断包的处理

上面说了mina对编解码的支持,在解码过程中,不得不面对的一个问题就是TCP的粘包和断包,先说下什么是粘包和断包。

TCP通讯是面向数据流的通讯,我们将数据流理解为一支竹竿,数据包就相当于竹竿中的每一节,那么我们的解码过程就相当于对竹竿进行分解的过程。竹竿就是多个数据包的“粘包”,断包就是指竹节中间断开,我们需要将它拼接成为一个完整的竹节,如果不能拼接起来就要废弃这部分。

粘包:

断包:

对粘包的处理相对比较简单,只需要依据数据包的格式进行数据流的分割即可;对于断包的处理我们需要将断包的数据保存起来,等待接收下次的数据进行拼接。

通常情况下我们要考虑粘包和断包同时出现的情况下的解码代码编写。有两种实现方式:

1.继承CumulativeProtocolDecoder类,实现doDecode方法。

2.实现ProtocolDecoder接口,自己解决粘包和断包的问题。

先看下CumulativeProtocolDecoder的实现。

它有一个成员变量BUFFER:

  1. //存放断包数据
  2. private final AttributeKey BUFFER = new AttributeKey(getClass(), "buffer");

doDecode方法一方面判断数据包是否符合解码要求(数据包可能过短,数据包格式不合要求都可能不能通过解码要求),不符合刚返回false;另一方面对于符合解码要求的数据进行数据解码,并返回true。可以参考ImageRequestDecoder类的实现。

看下它的decode方法实现:

  1. public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
  2. if (!session.getTransportMetadata().hasFragmentation()) {
  3. while (in.hasRemaining()) {
  4. // 判断是否符合解码要求,不符合则中断并返回
  5. if (!doDecode(session, in, out)) {
  6. break;
  7. }
  8. }
  9. return;
  10. }
  11. boolean usingSessionBuffer = true;
  12. // 取得上次断包数据
  13. IoBuffer buf = (IoBuffer) session.getAttribute(BUFFER);
  14. // If we have a session buffer, append data to that; otherwise
  15. // use the buffer read from the network directly.
  16. if (buf != null) { // 如果有断包数据
  17. boolean appended = false;
  18. // Make sure that the buffer is auto-expanded.
  19. if (buf.isAutoExpand()) {
  20. try {
  21. // 将断包数据和当前传入的数据进行拼接
  22. buf.put(in);
  23. appended = true;
  24. } catch (IllegalStateException e) {
  25. // A user called derivation method (e.g. slice()),
  26. // which disables auto-expansion of the parent buffer.
  27. } catch (IndexOutOfBoundsException e) {
  28. // A user disabled auto-expansion.
  29. }
  30. }
  31. if (appended) {
  32. buf.flip();// 如果是拼接的数据,将buf置为读模式
  33. } else {
  34. // Reallocate the buffer if append operation failed due to
  35. // derivation or disabled auto-expansion.
  36. //如果buf不是可自动扩展的buffer,刚通过数据拷贝的方式将断包数据和当前数据进行拼接
  37. buf.flip();
  38. IoBuffer newBuf = IoBuffer.allocate(buf.remaining() + in.remaining()).setAutoExpand(true);
  39. newBuf.order(buf.order());
  40. newBuf.put(buf);
  41. newBuf.put(in);
  42. newBuf.flip();
  43. buf = newBuf;
  44. // Update the session attribute.
  45. session.setAttribute(BUFFER, buf);
  46. }
  47. } else {
  48. buf = in;
  49. usingSessionBuffer = false;
  50. }
  51. for (;;) {
  52. int oldPos = buf.position();
  53. boolean decoded = doDecode(session, buf, out);// 进行数据的解码操作
  54. if (decoded) {
  55. // 如果符合解码要求并进行了解码操作,
  56. // 则当前position和解码前的position不可能一样
  57. if (buf.position() == oldPos) {
  58. throw new IllegalStateException("doDecode() can't return true when buffer is not consumed.");
  59. }
  60. // 如果已经没有数据,则退出循环
  61. if (!buf.hasRemaining()) {
  62. break;
  63. }
  64. } else {// 如果不符合解码要求,则退出循环
  65. break;
  66. }
  67. }
  68. // if there is any data left that cannot be decoded, we store
  69. // it in a buffer in the session and next time this decoder is
  70. // invoked the session buffer gets appended to
  71. if (buf.hasRemaining()) {
  72. if (usingSessionBuffer && buf.isAutoExpand()) {
  73. buf.compact();
  74. } else {
  75. //如果还有没处理完的数据(一般为断包),刚将此数据存入session中,以便和下次数据进行拼接。
  76. storeRemainingInSession(buf, session);
  77. }
  78. } else {
  79. if (usingSessionBuffer) {
  80. removeSessionBuffer(session);
  81. }
  82. }
  83. }

上面的处理过程可以这样理解:

1.取得断包数据,如果有断包数据,就和当前数据拼接。

2.进行数据解码操作。

3.将可以进行解码操作的数据解码完成后,如果还有数据,则将剩余数据存入session中,等待下次数据到来,从步骤1开始再次执行。

通过继承ProtocolDecoder,实现decode方法,自己处理粘包和断包的方式其实和CumulativeProtocolDecoder类的实现原理是类似的,此处实现可以参考类TextLineDecoder,内部类Context保存了上下文信息,同样是保存在了sesion中的,具体实现方式大家可以仔细阅读代码。

三、总结

基于TCP的通讯协议才有可能产生粘包和断包的情况,粘包和断包的产生有多种原因,处理好粘包和断包的问题是网络编程必然面对的情况,对于这块的处理,大家如果有什么好的想法可以一起讨论。

每天进步一点点,不做无为的码农。。。。。

mina编解码(摘录)的更多相关文章

  1. 【MINA】用protobuf做编解码协议

    SOCKET协议 支持java serial 与 AMF3的混合协议,目前没有基于xml 与 json的实现. 协议说明: * 9个字节协议头+协议体. * * 协议头1-4字节表示协议长度 =协议体 ...

  2. Netty 编解码技术 数据通信和心跳监控案例

    Netty 编解码技术 数据通信和心跳监控案例 多台服务器之间在进行跨进程服务调用时,需要使用特定的编解码技术,对需要进行网络传输的对象做编码和解码操作,以便完成远程调用.Netty提供了完善,易扩展 ...

  3. 【转】Netty系列之Netty编解码框架分析

    http://www.infoq.com/cn/articles/netty-codec-framework-analyse/ 1. 背景 1.1. 编解码技术 通常我们也习惯将编码(Encode)称 ...

  4. iOS8系统H264视频硬件编解码说明

    公司项目原因,接触了一下视频流H264的编解码知识,之前项目使用的是FFMpeg多媒体库,利用CPU做视频的编码和解码,俗称为软编软解.该方法比较通用,但是占用CPU资源,编解码效率不高.一般系统都会 ...

  5. IOS和Android支持的音频编解码

    1.IOS编码 参考文档地址:https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/Multimedi ...

  6. java编解码技术,netty nio

    对于java提供的对象输入输出流ObjectInputStream与ObjectOutputStream,可以直接把java对象作为可存储 的字节数组写入文件,也可以传输到网络上去.对与java开放人 ...

  7. 编解码-marshalling

    JBoss的Marshalling序列化框架,它是JBoss内部使用的序列化框架,Netty提供了Marshalling编码和解码器,方便用户在Netty中使用Marshalling. JBoss M ...

  8. 编解码-protobuf

    Google的Protobuf在业界非常流行,很多商业项目选择Protobuf作为编解码框架,Protobuf的优点. (1)在谷歌内部长期使用,产品成熟度高: (2)跨语言,支持多种语言,包括C++ ...

  9. 编解码-java序列化

    大多数Java程序员接触到的第一种序列化或者编解码技术就是Java的默认序列化,只需要序列化的POJO对象实现java.io.Serializable接口,根据实际情况生成序列ID,这个类就能够通过j ...

随机推荐

  1. 【BZOJ 3473】 字符串 (后缀数组+RMQ+二分 | 广义SAM)

    3473: 字符串 Description 给定n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中至少k个字符串的子串? Input 第一行两个整数n,k. 接下来n行每行一个字符串 ...

  2. warning: push.default is unset;

    git push warning questions This warning was introduced in Git 1.7.11 along with the simple style of ...

  3. WCF的配置文件中的要素

    Windows Communication Foundation Configuration Schema

  4. Chrome浏览器插件VisualEvent,可以方便的查看页面绑定的事件

    http://files.cnblogs.com/files/jiqing9006/VisualEvent.zip

  5. bzoj3238

    都LCP了很显然是要用到后缀数组的 显然前面的那个东西是可以直接算出来的 关键在于LCP的和怎么快速的计算 不难想到穷举height[i],然后判断这个height[i]可能成为多少对后缀的LCP 考 ...

  6. 页面与母版页面的asp:ContentPlaceHolder不匹配

    问题描述:The page has one or more <asp:Content> controls that do not correspond with <asp:Conte ...

  7. selenium webdriver(3)---操作页面对象

    页面对象的相关操作可以通过接口文件org.openqa.selenium.WebElement查看,本文只是对象接口的使用方式,具体的实现方式在org.openqa.selenium.remote.R ...

  8. AppStore IAP 客户端校验代码

    -(BOOL)putStringToItunes:(SKPaymentTransaction*)transaction { NSData * iapData = transaction.transac ...

  9. sublime中文乱码解决办法

    欲解决问题,关键在于让Sublime Text 2支持GB2312和GBK.步骤如下: 1.安装Sublime Package Control. 在Sublime Text 2上用Ctrl+-打开控制 ...

  10. Bzoj 3171: [Tjoi2013]循环格 费用流

    3171: [Tjoi2013]循环格 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 741  Solved: 463[Submit][Status][ ...