问题背景

NIO是面向缓冲区进行通信的,不是面向流的。我们都知道,既然是缓冲区,那它一定存在一个固定大小。这样一来通常会遇到两个问题:

  • 消息粘包:当缓冲区足够大,由于网络不稳定种种原因,可能会有多条消息从通道读入缓冲区,此时如果无法分清数据包之间的界限,就会导致粘包问题;
  • 消息不完整:若消息没有接收完,缓冲区就被填满了,会导致从缓冲区取出的消息不完整,即半包的现象。

介绍这个问题之前,务必要提一下我代码整体架构。

代码参见GitHub仓库

https://github.com/CuriousLei/smyl-im

在这个项目中,我的NIO核心库设计思路流程图如下所示

介绍:

  • 服务端为每一个连接上的客户端建立一个Connector对象,为其提供IO服务;
  • ioArgs对象内部实例域引用了缓冲区buffer,作为直接与channel进行数据交互的缓冲区;
  • 两个线程池,分别操控ioArgs进行读和写操作;
  • connector与ioArgs关系:(1)输入,线程池处理读事件,数据写入ioArgs,并回调给connector;(2)输出,connector将数据写入ioArgs,将ioArgs传入Runnable对象,供线程池处理;
  • 两个selector线程,分别监听channel的读和写事件。事件就绪,则触发线程池工作。

思路

光这样实现,必然会有粘包、半包问题。要重现这两个问题也很简单。

  • ioArgs中把缓冲区设置小一点,发送一条大于该长度的数据,服务端会当成两条消息读取,即消息不完整;
  • 在线程代码中,加一个Thread.sleep()延时等待,客户端连续发几条消息(总长度小于缓冲区大小),也可以重现粘包现象。

这个问题实质上是消息体与缓冲区数据不一一对应导致的。那么,如何解决呢?

固定头部方案

可以采用固定头部方案来解决,头部设置四个字节,存储一个int值,记录后面数据的长度。以此来标记一个消息体。

  • 读取数据时,根据头部的长度信息,按序读取ioArgs缓冲区中的数据,若没有达到长度要求,继续读下一个ioArgs。这样自然不会出现粘包、半包问题。
  • 输出数据时,也采用同样的机制封装数据,首部四个字节记录长度。

我的工程项目中,客户端和服务端共用一个nio核心包,即niohdl,可保证收发数据格式一致。

设计方案

要实现以上设想,必须在connector和ioArgs之间加一层Dispatcher类,用于处理消息体缓冲区之间的转化关系(消息体取个名字:Packet)。根据输入和输出的不同,分别叫ReceiveDispatcher和SendDispatcher。即通过它们来操作Packet与ioArgs之间的转化。

Packet

定义这个消息体,继承关系如下图所示:

Packet是基类,代码如下:

  1. package cn.buptleida.niohdl.core;
  2. import java.io.Closeable;
  3. import java.io.IOException;
  4. /**
  5. * 公共的数据封装
  6. * 提供了类型以及基本的长度的定义
  7. */
  8. public class Packet implements Closeable {
  9. protected byte type;
  10. protected int length;
  11. public byte type(){
  12. return type;
  13. }
  14. public int length(){
  15. return length;
  16. }
  17. @Override
  18. public void close() throws IOException {
  19. }
  20. }

SendPacket和ReceivePacket分别代表发送消息体和接收消息体。StringReceivePacket和StringSendPacket代表字符串类的消息,因为本次实践只限于字符串消息的收发,今后可能有文件之类的,有待扩展。

代码中必然会涉及到字节数组的操作,所以,以StringSendPacket为例,需要提供将String转化为byte[]的方法。代码如下所示:

  1. package cn.buptleida.niohdl.box;
  2. import cn.buptleida.niohdl.core.SendPacket;
  3. public class StringSendPacket extends SendPacket {
  4. private final byte[] bytes;
  5. public StringSendPacket(String msg) {
  6. this.bytes = msg.getBytes();
  7. this.length = bytes.length;//父类中的实例域
  8. }
  9. @Override
  10. public byte[] bytes() {
  11. return bytes;
  12. }
  13. }

SendDispatcher

在connector对象的实例域中会引用一个SendDispatcher对象。发送数据时,会通过SendDispatcher中的方法对数据进行封装和处理。其大致的关系图如下所示:

SendDispatcher中设置任务队列Queue queue,需要发送消息时,connector将消息写入sendPacket,并存入队列queue,执行出队。用packetTemp变量引用出队的元素,将四字节的长度信息和packetTemp写入ioArgs的缓冲区中,发送完毕之后,再判断packetTemp是否完整写出(使用position和total指针标记、判断),决定继续输出packetTemp的内容,还是开始下一轮出队。

这个过程的程序框图如下所示:

在代码中,SendDispatcher实际上是一个接口,我用AsyncSendDispatcher实现此接口,代码如下:

  1. package cn.buptleida.niohdl.impl.async;
  2. import cn.buptleida.niohdl.core.*;
  3. import cn.buptleida.utils.CloseUtil;
  4. import java.io.IOException;
  5. import java.util.Queue;
  6. import java.util.concurrent.ConcurrentLinkedDeque;
  7. import java.util.concurrent.atomic.AtomicBoolean;
  8. public class AsyncSendDispatcher implements SendDispatcher {
  9. private final AtomicBoolean isClosed = new AtomicBoolean(false);
  10. private Sender sender;
  11. private Queue<SendPacket> queue = new ConcurrentLinkedDeque<>();
  12. private AtomicBoolean isSending = new AtomicBoolean();
  13. private ioArgs ioArgs = new ioArgs();
  14. private SendPacket packetTemp;
  15. //当前发送的packet大小以及进度
  16. private int total;
  17. private int position;
  18. public AsyncSendDispatcher(Sender sender) {
  19. this.sender = sender;
  20. }
  21. /**
  22. * connector将数据封装进packet后,调用这个方法
  23. * @param packet
  24. */
  25. @Override
  26. public void send(SendPacket packet) {
  27. queue.offer(packet);//将数据放进队列中
  28. if (isSending.compareAndSet(false, true)) {
  29. sendNextPacket();
  30. }
  31. }
  32. @Override
  33. public void cancel(SendPacket packet) {
  34. }
  35. /**
  36. * 从队列中取数据
  37. * @return
  38. */
  39. private SendPacket takePacket() {
  40. SendPacket packet = queue.poll();
  41. if (packet != null && packet.isCanceled()) {
  42. //已经取消不用发送
  43. return takePacket();
  44. }
  45. return packet;
  46. }
  47. private void sendNextPacket() {
  48. SendPacket temp = packetTemp;
  49. if (temp != null) {
  50. CloseUtil.close(temp);
  51. }
  52. SendPacket packet = packetTemp = takePacket();
  53. if (packet == null) {
  54. //队列为空,取消发送状态
  55. isSending.set(false);
  56. return;
  57. }
  58. total = packet.length();
  59. position = 0;
  60. sendCurrentPacket();
  61. }
  62. private void sendCurrentPacket() {
  63. ioArgs args = ioArgs;
  64. args.startWriting();//将ioArgs缓冲区中的指针设置好
  65. if (position >= total) {
  66. sendNextPacket();
  67. return;
  68. } else if (position == 0) {
  69. //首包,需要携带长度信息
  70. args.writeLength(total);
  71. }
  72. byte[] bytes = packetTemp.bytes();
  73. //把bytes的数据写入到IoArgs中
  74. int count = args.readFrom(bytes, position);
  75. position += count;
  76. //完成封装
  77. args.finishWriting();//flip()操作
  78. //向通道注册OP_write,将Args附加到runnable中;selector线程监听到就绪即可触发线程池进行消息发送
  79. try {
  80. sender.sendAsync(args, ioArgsEventListener);
  81. } catch (IOException e) {
  82. closeAndNotify();
  83. }
  84. }
  85. private void closeAndNotify() {
  86. CloseUtil.close(this);
  87. }
  88. @Override
  89. public void close(){
  90. if (isClosed.compareAndSet(false, true)) {
  91. isSending.set(false);
  92. SendPacket packet = packetTemp;
  93. if (packet != null) {
  94. packetTemp = null;
  95. CloseUtil.close(packet);
  96. }
  97. }
  98. }
  99. /**
  100. * 接收回调,来自writeHandler输出线程
  101. */
  102. private ioArgs.IoArgsEventListener ioArgsEventListener = new ioArgs.IoArgsEventListener() {
  103. @Override
  104. public void onStarted(ioArgs args) {
  105. }
  106. @Override
  107. public void onCompleted(ioArgs args) {
  108. //继续发送当前包packetTemp,因为可能一个包没发完
  109. sendCurrentPacket();
  110. }
  111. };
  112. }

ReceiveDispatcher

同样,ReceiveDispatcher也是一个接口,代码中用AsyncReceiveDispatcher实现。在connector对象的实例域中会引用一个AsyncReceiveDispatcher对象。接收数据时,会通过ReceiveDispatcher中的方法对接收到的数据进行拆包处理。其大致的关系图如下所示:

每一个消息体的首部会有一个四字节的int字段,代表消息的长度值,按照这个长度来进行读取。如若一个ioArgs未满足这个长度,就读取下一个ioArgs,保证数据包的完整性。这个流程就不画程序框图了,偷个懒hhhh。其实看下面代码注释已经很清晰了,容易理解。

AsyncReceiveDispatcher的代码如下所示:

  1. package cn.buptleida.niohdl.impl.async;
  2. import cn.buptleida.niohdl.box.StringReceivePacket;
  3. import cn.buptleida.niohdl.core.ReceiveDispatcher;
  4. import cn.buptleida.niohdl.core.ReceivePacket;
  5. import cn.buptleida.niohdl.core.Receiver;
  6. import cn.buptleida.niohdl.core.ioArgs;
  7. import cn.buptleida.utils.CloseUtil;
  8. import java.io.IOException;
  9. import java.util.concurrent.atomic.AtomicBoolean;
  10. public class AsyncReceiveDispatcher implements ReceiveDispatcher {
  11. private final AtomicBoolean isClosed = new AtomicBoolean(false);
  12. private final Receiver receiver;
  13. private final ReceivePacketCallback callback;
  14. private ioArgs args = new ioArgs();
  15. private ReceivePacket packetTemp;
  16. private byte[] buffer;
  17. private int total;
  18. private int position;
  19. public AsyncReceiveDispatcher(Receiver receiver, ReceivePacketCallback callback) {
  20. this.receiver = receiver;
  21. this.receiver.setReceiveListener(ioArgsEventListener);
  22. this.callback = callback;
  23. }
  24. /**
  25. * connector中调用该方法进行
  26. */
  27. @Override
  28. public void start() {
  29. registerReceive();
  30. }
  31. private void registerReceive() {
  32. try {
  33. receiver.receiveAsync(args);
  34. } catch (IOException e) {
  35. closeAndNotify();
  36. }
  37. }
  38. private void closeAndNotify() {
  39. CloseUtil.close(this);
  40. }
  41. @Override
  42. public void stop() {
  43. }
  44. @Override
  45. public void close() throws IOException {
  46. if(isClosed.compareAndSet(false,true)){
  47. ReceivePacket packet = packetTemp;
  48. if(packet!=null){
  49. packetTemp = null;
  50. CloseUtil.close(packet);
  51. }
  52. }
  53. }
  54. /**
  55. * 回调方法,从readHandler输入线程中回调
  56. */
  57. private ioArgs.IoArgsEventListener ioArgsEventListener = new ioArgs.IoArgsEventListener() {
  58. @Override
  59. public void onStarted(ioArgs args) {
  60. int receiveSize;
  61. if (packetTemp == null) {
  62. receiveSize = 4;
  63. } else {
  64. receiveSize = Math.min(total - position, args.capacity());
  65. }
  66. //设置接受数据大小
  67. args.setLimit(receiveSize);
  68. }
  69. @Override
  70. public void onCompleted(ioArgs args) {
  71. assemblePacket(args);
  72. //继续接受下一条数据,因为可能同一个消息可能分隔在两份IoArgs中
  73. registerReceive();
  74. }
  75. };
  76. /**
  77. * 解析数据到packet
  78. * @param args
  79. */
  80. private void assemblePacket(ioArgs args) {
  81. if (packetTemp == null) {
  82. int length = args.readLength();
  83. packetTemp = new StringReceivePacket(length);
  84. buffer = new byte[length];
  85. total = length;
  86. position = 0;
  87. }
  88. //将args中的数据写进外面buffer中
  89. int count = args.writeTo(buffer,0);
  90. if(count>0){
  91. //将数据存进StringReceivePacket的buffer当中
  92. packetTemp.save(buffer,count);
  93. position+=count;
  94. if(position == total){
  95. completePacket();
  96. packetTemp = null;
  97. }
  98. }
  99. }
  100. private void completePacket() {
  101. ReceivePacket packet = this.packetTemp;
  102. CloseUtil.close(packet);
  103. callback.onReceivePacketCompleted(packet);
  104. }
  105. }

总结

其实粘包、半包的解决方案并没有什么奥秘,单纯地复杂而已。方法核心就是自定义一个消息体Packet,完成Packet中的byte数组与缓冲区数组之间的复制转化即可。当然,position、limit等等指针的辅助很重要。

总结这个博客,也是将目前为止的工作进行梳理和记录。我将通过smyl-im这个项目来持续学习+实践。因为之前学习过程中有很多零碎的知识点,都躺在我的有道云笔记里,感觉没必要总结成博客。本次博客讲的内容刚好是一个成体系的东西,正好可以将这个项目背景带出来,后续的博客就可以在这基础上衍生拓展了。

java nio消息半包、粘包解决方案的更多相关文章

  1. [转]java nio解决半包 粘包问题

    java nio解决半包 粘包问题 NIO socket是非阻塞的通讯模式,与IO阻塞式的通讯不同点在于NIO的数据要通过channel放到一个缓存池ByteBuffer中,然后再从这个缓存池中读出数 ...

  2. 网络编程3 网络编程之缓冲区&subprocess&粘包&粘包解决方案

    1.sub简单使用 2.粘包现象(1) 3.粘包现象(2) 4.粘包现象解决方案 5.struct学习 6.粘包现象升级版解决方案 7.打印进度条

  3. netty10---分包粘包

    客户端:根据 长度+数据 方式发送 package com.server; import java.net.Socket; import java.nio.ByteBuffer; public cla ...

  4. mina框架tcpt通讯接收数据断包粘包处理

    用mina做基于tcp,udp有通讯有段时间了,一直对编码解码不是很熟悉,这次做项目的时候碰到了断包情况,贴一下解决过程, 我接受数据格式如下图所示: unit32为c++中数据类型,代表4个字节,由 ...

  5. goim socket丢包粘包问题解决。

    -(NSInteger)bytesToInt:(unsigned char*) data { return (data[3]&255)|(data[2]&255)<<8|( ...

  6. socket编程 TCP 粘包和半包 的问题及解决办法

    一般在socket处理大数据量传输的时候会产生粘包和半包问题,有的时候tcp为了提高效率会缓冲N个包后再一起发出去,这个与缓存和网络有关系. 粘包 为x.5个包 半包 为0.5个包 由于网络原因 一次 ...

  7. TCP的粘包、半包和Netty的处理

    参考文献:极客时间傅健老师的<Netty源码剖析与实战>Talk is cheap.show me the code! 什么是粘包和半包 在客户端发送数据时,实际是把数据写入到了TCP发送 ...

  8. tcp的粘包现象与解决方案

    粘包现象: 粘包1:连续的小包,会被优化机制给合并 粘包2:服务端一次性无法完全就收完客户端发送的数据,第二再次接收的时候,会接收到第一次遗留的内容 模拟一个粘包现象 服务端 import socke ...

  9. NIO框架之MINA源码解析(四):粘包与断包处理及编码与解码

    1.粘包与段包 粘包:指TCP协议中,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾.造成的可能原因: 发送端需要等缓冲区满才发送出去,造成粘包 接收 ...

随机推荐

  1. Java基础 - 数据类型和运算符

    Java 语言支持的类型分为两类:基本数据类型(Primitive Type)和引用类型(Reference Type). 目录 基本数据类型 数值类型 整数类型 byte short int lon ...

  2. Gogs

    Deploy Gogs(node2) 1 create gogs account sudo adduser git su git cd /home/git mkdir /home/git/.ssh 2 ...

  3. hdu2203kmp匹配

    拼接字符串即可解决移位的问题: 代码如下: #include<bits/stdc++.h> using namespace std; typedef unsigned int ui; ty ...

  4. 洛谷1378 油滴扩展 dfs进行回溯搜索

    题目链接:https://www.luogu.com.cn/problem/P1378 题目中给出矩形的长宽和一些点,可以在每个点放油滴,油滴会扩展,直到触碰到矩形的周边或者其他油滴的边缘,求出剩余面 ...

  5. 多线程之旅(Thread)

    在上篇文章中我们已经知道了多线程是什么了,那么它到底可以干嘛呢?这里特别声明一个前面的委托没看的同学可以到上上上篇博文查看,因为多线程要经常使用到委托.源码 一.异步.同步 1.同步(在计算的理解总是 ...

  6. [源码分析] 从实例和源码入手看 Flink 之广播 Broadcast

    [源码分析] 从实例和源码入手看 Flink 之广播 Broadcast 0x00 摘要 本文将通过源码分析和实例讲解,带领大家熟悉Flink的广播变量机制. 0x01 业务需求 1. 场景需求 对黑 ...

  7. 【Unity游戏开发】跟着马三一起魔改LitJson

    一.引子 在游戏开发中,我们少不了和数据打交道,数据的存储格式可谓是百花齐放,xml.json.csv.bin等等应有尽有.在这其中Json以其小巧轻便.可读性强.兼容性好等优点受到广大程序员的喜爱. ...

  8. 硬货 | 手把手带你构建视频分类模型(附Python演练))

    译者 | VK 来源 | Analytics Vidhya 概述 了解如何使用计算机视觉和深度学习技术处理视频数据 我们将在Python中构建自己的视频分类模型 这是一个非常实用的视频分类教程,所以准 ...

  9. jdbc连接方法

    jdbc(Java Database Connectivity)的5个步骤: 一.加载驱动. 反射中的主动加载,Driver.class右键copy qualified Name 二.创建连接 dat ...

  10. Python学习笔记:函数和变量详解

    一.面向对象:将客观世界的事物抽象成计算机中的数据结构 类:用class定义,这是当前编程的重点范式,以后会单独介绍. 二.函数编程:逻辑结构化和过程化的一种编程方法 1.函数-->用def定义 ...