虽然TCP协议是可靠性传输协议,可是对于TCP长链接而言,对于消息发送仍然可能会发生粘贴的情形。主要是由于TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分。有可能将一个大数据包拆分红多个小的数据包,也有可能将多个小的数据包合并成一个数据包。并发
本篇文章将对TCP粘包和拆包进行介绍:框架
假设Client端发送两个数据包给Server端,以下图:tcp
可是Server端实际接收到的数据包形式可能存在以上三种形式:ide
不管是以上哪一种状况,从应用层的角度而言,Server端都将处理错误。首先以没有考虑TCP拆包和粘包的场景为例,分析下TCP拆包粘包将形成什么样的现象:oop
public static class EchoClientHandler extends ChannelHandlerAdapter { static final String ECHO_REQ = "Hi, huaijin.Welcome to Netty."; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 发送100次消息至server端 for (int i = 0; i < 100; i++) { System.out.println("This is " + (i + 1) + " times send server: [" + ECHO_REQ + "]"); ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes())); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
这里关于Client的启动代码省略,重点关注业务Handler。其中Echo总共发送了100次消息给Server,若是按照正确的情形,Server端应该接受到100次,而后分别进行处理。可是实际的情形并非这样。大数据
public static class EchoServerHandler extends ChannelHandlerAdapter { /** * 原子计数器,统计接受到的次数 */ private AtomicInteger counter = new AtomicInteger(0); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 接受到消息打印 String body = (String) msg; System.out.println("This is " + counter.incrementAndGet() + " times receive client: [" + body + "]"); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } }
服务端中使用原子计数器统计接收到的包的次数并打印接受到的消息。下面运行下实例,客户端输出以下:编码
This is 1 times send server: [Hi, huaijin.Welcome to Netty.] This is 2 times send server: [Hi, huaijin.Welcome to Netty.] This is 3 times send server: [Hi, huaijin.Welcome to Netty.] This is 4 times send server: [Hi, huaijin.Welcome to Netty.] .... 中间部分省略 This is 97 times send server: [Hi, huaijin.Welcome to Netty.] This is 98 times send server: [Hi, huaijin.Welcome to Netty.] This is 99 times send server: [Hi, huaijin.Welcome to Netty.] This is 100 times send server: [Hi, huaijin.Welcome to Netty.]
从中能够看出,Client端总共发送了100条消息至Server,可是Server端接收状况以下:设计
This is 1 times receive client: [Hi, huaijin.Welcome to Netty.] This is 2 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.] This is 3 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.] This is 4 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.] This is 5 times receive client: [Hi, huaijin.Welcome to Netty.] ... 省略 This is 69 times receive client: [Hi, huaijin.Welcome to Netty.] This is 70 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.Hi, hu] This is 71 times receive client: [aijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.] This is 72 times receive client: [Hi, huaijin.Welcome to Netty.] ... 省略 This is 84 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.] This is 93 times receive client: [Hi, huaijin.Welcome to Netty.] This is 94 times receive client: [Hi, huaijin.Welcome to Netty.]
因为发生了粘包致使Server端只接收到94次,其中有两条消息粘合在一块儿。netty
有以上的情形能够看出,当应用使用长链接并发发送请求时,会形成Server端接收到的请求数据发生混乱,从而处理错误。code
关于TCP拆包粘包的解决方式有不少,目前的主流解决方式有如下几种:
固然netty做为成熟框架,提供了多种方式解决TCP的拆包粘包问题,一般称做为半包解码器。
netty中提供了基于分隔符实现的半包解码器和定长的半包解码器:
这里仍然以上例为主,使用DelimiterBasedFrameDecoder做为半包解码器。
public static class EchoClientHandler extends ChannelHandlerAdapter { /** * 消息使用"$_"分割 */ static final String ECHO_REQ = "Hi, huaijin.Welcome to Netty.$_"; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for (int i = 0; i < 100; i++) { System.out.println("This is " + (i + 1) + " times send server: [" + ECHO_REQ + "]"); ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes())); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
客户端代码改动较小,只是每条消息后使用分割符"$_"分割,而后发送消息。
服务端须要使用分割符解码器,利用其对粘包消息进行拆分:
/** * netty实现echo server * * @author huaijin */ public class EchoServer { public void bind(int port) throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { // 使用分隔符"$_"的半包解码器 ByteBuf byteBuf = Unpooled.copiedBuffer("$_".getBytes()); ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, byteBuf)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new EchoServerHandler()); } }); ChannelFuture f = b.bind(port).sync(); f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } public static void main(String[] args) throws InterruptedException { new EchoServer().bind(8080); } public static class EchoServerHandler extends ChannelHandlerAdapter { /** * 原子计数器,统计接受到的次数 */ private AtomicInteger counter = new AtomicInteger(0); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 接受到消息打印 String body = (String) msg; System.out.println("This is " + counter.incrementAndGet() + " times receive client: [" + body + "]"); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
当再次运行客户端和服务端代码时,服务端表现正常,接收到了100次:
This is 1 times receive client: [Hi, huaijin.Welcome to Netty.] This is 2 times receive client: [Hi, huaijin.Welcome to Netty.] This is 3 times receive client: [Hi, huaijin.Welcome to Netty.] ... 省略 This is 98 times receive client: [Hi, huaijin.Welcome to Netty.] This is 99 times receive client: [Hi, huaijin.Welcome to Netty.] This is 100 times receive client: [Hi, huaijin.Welcome to Netty.]
本篇文章主要介绍了什么是TCP的拆包和粘包,并展现了拆包和粘包带来的现象。并经过netty提供的方案,是如何解决TCP拆包和粘包问题。