概述

TCP传输协议是面向连接的,面向流提供高可靠的服务。收发两端(服务端和客户端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效地发给对方,使用了优化算法(Nagle算法),将多次间隔时间较小且数据较小的数据包,合成一个大的数据块,然后进行封包,这样做虽然提高了传输的效率,但是这样接收端就难以分辨出一个个完整的包的大小了,因为面向流的通信时无消息保护边界的。

由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是粘包、拆包问题。

示意图:

说明:

  1. 服务端给客户端发送D1、D2两个数据包。
  2. 第一种情况是服务端两次都读取到的是两个独立的数据包,无拆包粘包问题,可以正常解析。
  3. 第二种情况D1、D2包合并在一起一次发送,服务端一次性接收了两个数据包,分别是D1、D2,我们称之为粘包,因为不知道边界,所以服务端不知如何拆出来解析。
  4. 第三种情况,服务端第一次接收到了完整D1包和部分D2包,第二次接收到了剩余的D2包,D2包被拆开来分多次发送了,我们称之为拆包。
  5. 第四种情况,服务端第一次接收到了部分D1包,第二次接收到了剩余部分D1包和完整D2包,也称之为拆包。

总的来说拆包和粘包问题,因为没有边界,最终会在正常情况下导致接收方无法分辨出一个一个包。

粘包实例

以下实例证实了粘包的存在:
服务端及服务端的Handler:

public class NettyTcpServer {private  int port;public NettyTcpServer(int port){this.port = port;}public void start() throws InterruptedException {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new NettyTcpServerChannelHandler());}});ChannelFuture channelFuture = serverBootstrap.bind(port);channelFuture.channel().closeFuture().sync();}public static void main(String[] args) throws InterruptedException {NettyTcpServer nettyTcpServer = new NettyTcpServer(8989);nettyTcpServer.start();}
}public class NettyTcpServerChannelHandler extends ChannelInboundHandlerAdapter {private int count;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf byteBuf = (ByteBuf) msg;System.out.println("数据是:" + byteBuf.toString(CharsetUtil.UTF_8));System.out.println("count = " + (++count));super.channelRead(ctx, msg);}
}

客户端及客户端的Handler

public class NettyTcpClient {private  int serverPort;public NettyTcpClient(int serverPort){this.serverPort = serverPort;}public void start() throws InterruptedException {NioEventLoopGroup worker = new NioEventLoopGroup();Bootstrap bootstrap = new Bootstrap();bootstrap.group(worker).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new NettyTcpClientChannelHandler());}});ChannelFuture connect = bootstrap.connect("127.0.0.1", serverPort);connect.channel().closeFuture().sync();}public static void main(String[] args) throws InterruptedException {NettyTcpClient nettyTcpClient = new NettyTcpClient(8989);nettyTcpClient.start();}
}public class NettyTcpClientChannelHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//发生这个事件代表通道已经顺利连接到远端,可以收发数据。我们就在这里发送数据、//分十次发送for (int i = 0;i<10;i++){ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Hello I am John;", CharsetUtil.UTF_8));}ctx.fireChannelActive();}
}

执行结果:

这个是服务端的输出结果,如果是没有拆包粘包的话,客户端分十次发送了数据,那么服务端也应该分十次接收,count应该等于10,但是此时count=1,然后数据一次性显示出来了 ,说明客户端的数据时把十次合并成一个数据包发送的,数据粘包现象。

拆包演示

将一次发送的数据量弄大一点,比mss大,就会发生拆包现象。
上面粘包演示的客户端和服务端不变,把两个Handler修改一下。

@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//发生这个事件代表通道已经顺利连接到远端,可以收发数据。我们就在这里发送数据、//分十次发送//一次发送102400字节数据byte[] bytes = new byte[102400];Arrays.fill(bytes, (byte) 10);for (int i = 0;i<10;i++){ctx.channel().writeAndFlush(Unpooled.copiedBuffer(bytes));}ctx.fireChannelActive();}@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf byteBuf = (ByteBuf) msg;System.out.println("长度是:" + byteBuf.readableBytes());System.out.println("count = " + (++count));super.channelRead(ctx, msg);
}

执行结果:

每次发送的数据长度都小于102400,并且发送次数count>10次,所以发生了拆包。

解决方案

使用自定义协议加编码解码器来解决。
需要对Netty的解码编码机制有一定的理解。

关键就是定义每一个数据包的边界,让服务端知道如何对粘包进行拆分和对拆包进行拆分和组合,从而避免拆包粘包问题。

服务端代码

public class NettyTcpServer {private  int port;public NettyTcpServer(int port){this.port = port;}public void start() throws InterruptedException {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();//这里添加一个解码器,顺序必须在业务处理的Handler前面。//因为要对数据先解码了才能进行业务处理。pipeline.addLast(new MessageProtocolDecoder());pipeline.addLast(new NettyTcpServerChannelHandler());}});ChannelFuture channelFuture = serverBootstrap.bind(port);channelFuture.channel().closeFuture().sync();}public static void main(String[] args) throws InterruptedException {NettyTcpServer nettyTcpServer = new NettyTcpServer(8989);nettyTcpServer.start();}
}

客户端:

public class NettyTcpClient {private  int serverPort;public NettyTcpClient(int serverPort){this.serverPort = serverPort;}public void start() throws InterruptedException {NioEventLoopGroup worker = new NioEventLoopGroup();Bootstrap bootstrap = new Bootstrap();bootstrap.group(worker).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new NettyTcpClientChannelHandler());//这里添加一个编码器,对发送的数据进行编码,因为编码器属于出站的Handler。//而上面的业务逻辑Handler:NettyTcpClientChannelHandler属于入站Handler,所以添加顺序在这里暂时还没所谓。pipeline.addLast(new MessageProtocolEncode());}});ChannelFuture connect = bootstrap.connect("127.0.0.1", serverPort);connect.channel().closeFuture().sync();}public static void main(String[] args) throws InterruptedException {NettyTcpClient nettyTcpClient = new NettyTcpClient(8989);nettyTcpClient.start();}
}

协议类:

@Data
public class MessageProtocol {//内容private byte[] content;//长度private int len;public MessageProtocol(byte[] content){this.content = content;//计算出长度并赋值this.len = content.length;}
}

服务端的业务处理Handler:

public class NettyTcpServerChannelHandler extends ChannelInboundHandlerAdapter {private int count;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {MessageProtocol messageProtocol = (MessageProtocol) msg;System.out.println("数据是:" + new String(messageProtocol.getContent(),CharsetUtil.UTF_8) + ",长度是:" + messageProtocol.getLen());System.out.println("count = " + (++count));super.channelRead(ctx, msg);}
}

客户端的业务处理类:


//由原来的直接发送字符串,修改成现在使用协议对象来发送
public class NettyTcpClientChannelHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//发生这个事件代表通道已经顺利连接到远端,可以收发数据。我们就在这里发送数据、//分十次发送for (int i = 0;i<10;i++){byte[] content = "Hello I am John;".getBytes(CharsetUtil.UTF_8);//创建协议对象MessageProtocol message = new MessageProtocol(content);//发送ctx.channel().writeAndFlush(message);}super.channelActive(ctx);}
}

服务端的解码器:

public class MessageProtocolDecoder extends ByteToMessageDecoder {@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {//记录初始的读偏移量,用于拆包情况下的偏移量回滚int beginReadIndex = in.readerIndex();//首先读数据的长度出来先int len = in.readInt();if (in.readableBytes() < len){//这种情况下就是发生了拆包问题,因读取的数据长度没有len长,这个包还有数据没有到达。//重置读偏移量,等待下次重新读写in.readerIndex(beginReadIndex);//直接返回,等待下次return;}//能到这里,就证明包是完整的//读数据byte[] data = new byte[len];in.readBytes(data);MessageProtocol messageProtocol = new MessageProtocol(data);//写出给下一个Handler处理。out.add(messageProtocol);}
}

客户端的编码器:

public class MessageProtocolEncode  extends MessageToByteEncoder<MessageProtocol> {@Overrideprotected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {//对协议的编码时,把数据的前4个字节固定为数据的长度,跟着就是数据的内容int len = msg.getLen();//先写数据的长度out.writeInt(len);//再写数据的内容out.writeBytes(msg.getContent());}
}

结果:

由上图可见,解决了粘包问题。

下面来测试是否解决了拆包问题。发送的数据使用上面的拆包演示代码,修改一下成用协议即可。
修改后的业务逻辑处理Handler

//客户端@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//发生这个事件代表通道已经顺利连接到远端,可以收发数据。我们就在这里发送数据、//分十次发送//一次发送102400字节数据byte[] bytes = new byte[102400];Arrays.fill(bytes, (byte) 10);for (int i = 0;i<10;i++){MessageProtocol messageProtocol = new MessageProtocol(bytes);ctx.channel().writeAndFlush(messageProtocol);}ctx.fireChannelActive();}
//服务端
@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {MessageProtocol messageProtocol = (MessageProtocol) msg;System.out.println("长度是:" + messageProtocol.getLen());System.out.println("count = " + (++count));super.channelRead(ctx, msg);}

运行结果:


由上图可知拆包问题已经解决,每次读取的数据都为102400.这里就只打印长度了,因为数据太大,就不打印了。

至此,完毕。

Netty粘包拆包问题说明、演示拆包粘包情况代码以及解决相关推荐

  1. TCP解决粘包问题(结构数据封包拆包)

    TCP封包拆包 前言 封包 一.包结构 二.封包方法 拆包 总结 前言 TCP协议(Transmission Control Protocol)是一种面向连接的.可靠的.基于字节流的通信协议,即TCP ...

  2. netty 数据接收端 收到消息 被截取。zan包 拆包问题处理

    问题描述 在进行数据推送服务上线后,客户反馈收到的数据 存在被截取的情况. 背景 推送服务推送方式为:每10条数据进行一次writeAndFlush.每条数据结尾用换行符[\n分隔]. 推测可能原因: ...

  3. 什么是粘包?socket 中造成粘包的原因是什么? 粘包的处理方式

    1.粘包的概念 粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到 ...

  4. LaTeX 包wwwhy76888com18669144449 Beamer 创建演示文稿

    Beamer 是用于生成幻灯片的 LaTeX 包.它最棒的功能之一是它可以利用 LaTeX 强大的排版系统和其生态系统中的所有其他软件包.例如,我经常在包含代码的 Beamer 演示文稿中使用 LaT ...

  5. 网络协议和Netty(7):常用的网络抓包工具 Wireshark

    目录 Wireshark的使用 1.下载 2.安装 数据包的捕获和基本用法 过滤器 捕获过滤器 捕获过滤器的 BPF 语法 显示过滤器 比较操作符和逻辑操作符 预定义过滤器 前言:前面三次握手和四次挥 ...

  6. java 捕获数据包,缓存从pcap捕获的数据包

    我想要完成的事情: functionA:使用pcap捕获数据包 . 修改源/目标地址 . 重新计算校验和 . 注入pcap . functionB:创建两个线程 . 线程1发送一个魔术包以唤醒睡眠客户 ...

  7. python哪里下载import包-【Python实战】模块和包导入详解(import)

    1.模块(module) 1.1 模块定义 通常模块为一个.py文件,其他可作为module的文件类型还有".pyo".".pyc".".pyd&qu ...

  8. ssis包部署到数据库_使用SSIS包将行标题和数据添加到平面文件中

    ssis包部署到数据库 In this article, we will configure an SSIS package to generate a composite output in the ...

  9. 四、小程序|App抓包(四)-Tcpdump抓取手机数据包分析

    小程序|App抓包(四) Tcpdump抓取手机数据包分析 一.环境需求: 1.手机需要root 2.电脑上安装SDK(建议安装android studio)也可单独安装SDk也行 下载地址 : ht ...

最新文章

  1. 学界 | 进化算法可以不再需要计算集群,开普敦大学的新方法用一块GPU也能刷新MNIST记录
  2. 大数据小项目之电视收视率企业项目04--完全分布式搭建
  3. Access界面基础操作
  4. Sitecore® 8.2 Professional Developer考试心得
  5. ASP.NET Core2基于RabbitMQ对Web前端实现推送功能
  6. sql 存储过程中top 后面跟参数的问题
  7. c语言logo,真好玩 C语言输出Yahoo动态logo
  8. MySQL 索引分析除了 EXPLAIN 还有什么方法?
  9. 如何卸载 Internet Explorer 7
  10. linux整人指令,六个愚人节Linux恶作剧
  11. ASP.NET4.0尚未在Web服务器上注册
  12. Windows Server 2008 R2 WSUS服务器的详细配置和部署
  13. 你真的了解传统IT的集中式三层架构及主流开发技术选型?!
  14. 原生js生成渐变色数组集合
  15. Xilinx Bit文件格式详解
  16. 你觉得什么叫做幸福?
  17. Oracle查询连续几年,几月,几天的数据
  18. 成都计算机职高学校排名,成都计算机职高排名
  19. iOS:制作简易的 AAC 播放器 —— 了解音频的播放流程
  20. 电影天堂React Native客户端V2.0

热门文章

  1. 大数据学习笔记49:Flume Sinks(Flume接收器)
  2. 【codevs1040】【01NOIPTG】统计单词个数,字符串的划分DP
  3. win7怎么修改服务器端口,win7服务器端口设置方法
  4. linux下配置iscsi存储,linux 下iscsi网络存储配置
  5. 织梦charset.func.php,织梦程序百度php主动推送代码,亲测可用!
  6. bzoj1041 [HAOI2008]圆上的整点 gcd
  7. 2017.10.11 network 网络扩容 思考记录
  8. mysql中timestamp,datetime,int类型的区别与优劣
  9. 实时体积云渲染(地平线):一.云的生成
  10. php新闻列表排序,javascript 新闻列表排序简单封装