Netty之TCP/IP的粘包.拆包(三)

上一章节,介绍了netty的服务端和客户端初始化过程并且最后还使用了一个代码实现了netty的入门编程,那么从本章开始要陆续介绍和netty编程中使用到的相关知识,今天要介绍的知识点如下

1 Netty的tcp的粘包

2 Netty的tcp的拆包

3 Netty的tcp的粘包,拆包结局方案

OK 接下来开始一个一个来说,首先对于一个正常的TCP发送数据和接受数据会产生以下几种情况...

第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象

第二中情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包

第三种情况,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包


粘包原因:

>要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包:发送数据小于缓冲区

>接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包:读取数据超时

拆包原因:

>要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包:发送数据大于缓冲区大小

>待发送数据大于最大报文长度也会发生拆包:发送数据大于报文长度:数据大于报文长度

总结:不管是拆包,还是粘包都是在发送数据,数据缓冲区,以及报文长度这三者之间满足某种关系而发生的一种现象

解决方案:

固定数据包长度:发送端把数据包封装成固定长度,连接端每次从接收缓冲区读取固定长度数据包

添加长度首部:给每个数据包添加包首部,首部中应该至少包含数据包的长度,接收端在接收到数据后,通过读取包首部的长度字段

设置边界:在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开


Netty中的实现方案:

其实在netty已经提供了几种默认实现好的方案,开发者只需要了解每一种是什么作用,然后根据业务需求选择对应的实现即可,下面列举出默认的各种实现类和对应的功能

DelimiterBasedFrameDecoder:分隔符解码器

LineBasedFrameDecoder:回车换行解码器

FixedLengthFrameDecoder:固定长度解码器

LengthFieldBasedFrameDecoder:用于标识消息体或者整包消息的长度>解决'读半包'

上面说的基本上都是理论,接下来进入实践环节,接下来通过代码演示一下什么叫拆包和粘包,首先演示'粘包'例子很简单,就是客户端连接上服务端之后发送一句话'烧烤小分队',客户端循环重复发送,服务端接受并累加次数.看代码,为了方便演示,把和客户端相关的类都放在客户端,服务端都放在服务端,具体请看代码,首先看客户端的代码

package client;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.logging.Logger;

/**
 * 模拟发送数据的客户端
 */
public class CounterClient {

    public void connect(String host,int port)throws Exception{
        // 配置服务端的NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            // Bootstrap 类,是启动NIO服务器的辅助启动类
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception{
                            ch.pipeline().addLast(new StringEncoder());
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new CounterClientHandler());
                        }
                    });

            // 发起异步连接操作
            ChannelFuture f= b.connect(host,port).sync();

            // 等待客服端链路关闭
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[]args)throws Exception{
        int port = 8080;
        if(args!=null && args.length>0){
            try {
                port = Integer.valueOf(args[0]);
            }
            catch (NumberFormatException ex){}
        }
        new CounterClient().connect("127.0.0.1",port);
    }

    /**
     * 客户端发送数据的handler
     */
    class CounterClientHandler extends ChannelInboundHandlerAdapter {

        //统计频率
        private int counter;

        public void channelRead(ChannelHandlerContext ctx, Object msg)
                throws Exception{
            String body = (String)msg;
            System.out.println("当前时间是 : " + body+";当前频率是 : "+ ++counter);
        }

        /**
         * 客户端连接上服务端之后会调用该方法
         * @param ctx
         */
        public void channelActive(ChannelHandlerContext ctx){
            ByteBuf firstMessage=null;
            String value = "烧烤小分队";
            String data = value;
            for (int i=0;i<3;i++){
                firstMessage = Unpooled.buffer(data.getBytes().length);
                firstMessage.writeBytes(data.getBytes());
                ctx.writeAndFlush(firstMessage);
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
            System.out.println(("message from:"+cause.getMessage()));
            ctx.close();
        }
    }

}


 

服务端的代码:

package server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Date;

/**
 * 模拟词频统计的服务端
 */
public class CounterServer {
    public void bind(int port)throws Exception{
        // 网络读写
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup WorkerGroup = new NioEventLoopGroup();

        try {
            // ServerBootstrap 类,是启动NIO服务器的辅助启动类
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup,WorkerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,1024)
                    .childHandler(new ChildChannelHandler());

            // 绑定端口,同步等待成功
            ChannelFuture f= b.bind(port).sync();

            // 等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        }finally {
            // 释放线程池资源
            bossGroup.shutdownGracefully();
            WorkerGroup.shutdownGracefully();
        }
    }

    /**
     * 初始化channel的handler
     */
    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
        @Override
        protected  void initChannel(SocketChannel ch)throws Exception{
            ch.pipeline().addLast(new StringEncoder());
            ch.pipeline().addLast(new StringDecoder());
            ch.pipeline().addLast(new CounterServerHandler());
        }
    }

    /**
     * 法务端业务处理的handler
     */
    public class CounterServerHandler extends ChannelInboundHandlerAdapter {

        private int counter;

        // 用于网络的读写操作

        public void channelRead(ChannelHandlerContext ctx, Object msg)
                throws Exception{
            String body = (String)msg;
            System.out.println("法务端接受到的数据 : " + body+";累加频率是:"+ (++counter));
            String currentTime = "烧烤小分队".equalsIgnoreCase(body)?new Date(System.currentTimeMillis()).toString():"数据接收不正确";
            ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
            ctx.writeAndFlush(resp);

        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
            ctx.close();
        }
    }


    public static void main(String[]args)throws Exception{
        int port = 8080;
        if(args!=null && args.length>0){
            try {
                port = Integer.valueOf(args[0]);
            }
            catch (NumberFormatException ex){}
        }
        new CounterServer().bind(port);
    }
}
 

代码很简单,主要看一下效果,先启动服务端,再启动客户端,最终效果如下



OK 问题出现了就想解决办法,刚刚说了netty已经提供了默认了解决实现方案,接下来就修改一下代码,解决思路有很多种,这里使用换行符来搞定,就是每发送一句话,会在后面加上一个换行符,这样服务端就会根据换行符来解析数据,获取的数据结果自然就是正确的。

看修改之后的客户端代码,其实很多和上一个代码是一致的,在这里为了更好的区分,又重新建立一个类,方便看对比效果

package client;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * 模拟发送数据的客户端
 */
public class CounterResovlerPacketClient {

    public void connect(String host,int port)throws Exception{
        // 配置服务端的NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            // Bootstrap 类,是启动NIO服务器的辅助启动类
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception{
                            //增加换行解码器
                            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new CounterClientHandler());
                        }
                    });

            // 发起异步连接操作
            ChannelFuture f= b.connect(host,port).sync();

            // 等待客服端链路关闭
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[]args)throws Exception{
        int port = 8080;
        if(args!=null && args.length>0){
            try {
                port = Integer.valueOf(args[0]);
            }
            catch (NumberFormatException ex){}
        }
        new CounterResovlerPacketClient().connect("127.0.0.1",port);
    }

    /**
     * 客户端发送数据的handler
     */
    class CounterClientHandler extends ChannelInboundHandlerAdapter {

        //统计频率
        private int counter;

        public void channelRead(ChannelHandlerContext ctx, Object msg)
                throws Exception{
            String body = (String)msg;
            System.out.println("客户端接受服务度返回的数据,当前时间是 : " + body+";当前频率是 : "+ ++counter);
        }

        /**
         * 客户端连接上服务端之后会调用该方法
         * @param ctx
         */
        public void channelActive(ChannelHandlerContext ctx){
            ByteBuf firstMessage=null;
            String value = "烧烤小分队带你走上IT巅峰";
            //这里加了一个换行符,为了让服务端进行按行读取数据的依据
            String data = value + "\n";
            for (int i=0;i<10;i++){
                firstMessage = Unpooled.buffer(data.getBytes().length);
                firstMessage.writeBytes(data.getBytes());
                ctx.writeAndFlush(firstMessage);
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
            System.out.println(("message from:"+cause.getMessage()));
            ctx.close();
        }
    }

}


 

再看一下服务端的代码:

package server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Date;

/**
 * 模拟词频统计的服务端
 */
public class CounterResovlerPacketServer {
    public void bind(int port)throws Exception{
        // 网络读写
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup WorkerGroup = new NioEventLoopGroup();

        try {
            // ServerBootstrap 类,是启动NIO服务器的辅助启动类
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup,WorkerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,1024)
                    .childHandler(new ChildChannelHandler());

            // 绑定端口,同步等待成功
            ChannelFuture f= b.bind(port).sync();

            // 等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        }finally {
            // 释放线程池资源
            bossGroup.shutdownGracefully();
            WorkerGroup.shutdownGracefully();
        }
    }

    /**
     * 初始化channel的handler
     */
    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
        @Override
        protected  void initChannel(SocketChannel ch)throws Exception{
            //增加换行符解码器
            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
            ch.pipeline().addLast(new StringDecoder());
            ch.pipeline().addLast(new CounterServerHandler());
        }
    }

    /**
     * 法务端业务处理的handler
     */
    public class CounterServerHandler extends ChannelInboundHandlerAdapter {

        private int counter;

        // 用于网络的读写操作

        public void channelRead(ChannelHandlerContext ctx, Object msg)
                throws Exception{
            String body = (String)msg;
            System.out.println("服务端接受到的数据 : " + body+";累加频率是:"+ (++counter));
            String currentTime = "烧烤小分队带你走上IT巅峰".equalsIgnoreCase(body)?new Date(System.currentTimeMillis()).toString():"错误数据";
            currentTime +=System.getProperty("line.separator");//按换行符进行数据的读取
            ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
            ctx.writeAndFlush(resp);

        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
            ctx.close();
        }
    }


    public static void main(String[]args)throws Exception{
        int port = 8080;
        if(args!=null && args.length>0){
            try {
                port = Integer.valueOf(args[0]);
            }
            catch (NumberFormatException ex){}
        }
        new CounterResovlerPacketServer().bind(port);
    }
}

这时候重复模拟发送10篇数据,再看一下效果:


客户端的效果:


注意:LineBaseFrameDecoder+StringDecoder组合,设置的顺序一定要LineBaseFrameDecoder在StringDecoder前面,否则这个解码器没有效果,一定要留心

这时候发现频率统计是正确的,且数据也不会再粘在一起,这样就解决了'粘包'的问题,当然这这是一种方式,还可以选择其他实现方式,因为原理都差不多,这里就不多说了,接下来再看另一种问题,就是'拆包',其实要实现这个效果很简单就是把LineBaseFrameDecoder(1024)可以该小点,然后增加要发送字符串的长度,就会自然而然出现拆包现象,然而解决方式可以通过发送固定长度的字符串,然后使用FixedLengthFrameDecoder来解析即可,看个简单的例子 比如客户端发送'this is hello word please come to me',这个时候如果使用FixedLengthFrameDecoder且设置长度为10,并发生了'拆包',效果如下:


很显然把一句完整的话给拆分了,那么如果不想拆分,只需要把长度设置和发送字符串的长度一样即可:简单通过代码演示一下:

客户端代码:

package client;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;

import java.net.InetSocketAddress;

/**
 * Created jhp
 */
public class SplitPacketClient {
    private final String host;
    private final int port;//定义服务器端监听的端口
    /** 构造函数中传入参数 **/
    public SplitPacketClient(String host, int port){
        this.host = host;
        this.port = port;
    }

    /** 启动服务器 **/
    public void start() throws Exception{
        EventLoopGroup group = new NioEventLoopGroup();
        //创建一个client 的bootstrap实例
        Bootstrap clientBootstrap = new Bootstrap();

        try {
            clientBootstrap.group(group)
                    .channel(NioSocketChannel.class)//指定使用一个NIO传输Channel
                    .remoteAddress(new InetSocketAddress(host, port))//设置远端服务器的host和端口
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        //在channel的ChannelPipeline中加入EchoClientHandler到最后
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            channel.pipeline().addLast(new FixedLengthFrameDecoder(40));//只要发送的字符串等于这个长度就不会发送拆包


                            channel.pipeline().addLast(new StringDecoder());
                            channel.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            ChannelFuture f = clientBootstrap.connect().sync();//连接到远端,一直等到连接完成
            f.channel().closeFuture().sync();//一直阻塞到channel关闭
        } finally {
            group.shutdownGracefully().sync();//关闭group,释放所有的资源
        }
    }


    /**
     * main
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        new SplitPacketClient("127.0.0.1", 8000).start();
    }


    class EchoClientHandler extends SimpleChannelInboundHandler<String> {

        private int counter=0;

        private static final String REQ = "this is hello word please come to me";

        /**
         * 当收到连接成功的通知,发送一条消息.
         * @param ctx
         * @throws Exception
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {

            for(int i=0; i<10; i++){
                ctx.writeAndFlush( Unpooled.copiedBuffer(REQ.getBytes()) );
            }
        }

        /**
         * 每当收到数据时这个方法会被调用.打印收到的消息日志
         * @param channelHandlerContext
         * @param msg
         * @throws Exception
         */
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
            System.out.println("client received: " + "counter:" + (++counter) + "  msg:"+msg);
        }

        /**
         * 异常发生时,记录错误日志,关闭channel
         * @param ctx
         * @param cause
         * @throws Exception
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();//打印堆栈的错误日志
            ctx.close();
        }

    }

}

服务端代码:

package server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;

/**
 * Create jhp
 */
public class SplitPacketServer {
    private final int port;//定义服务器端监听的端口
    /** 构造函数中传入参数 **/
    public SplitPacketServer(int port){
        this.port = port;
    }

    /** 启动服务器 **/
    public void start() throws Exception{
        //县城组
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();

        //创建一个serverbootstrap实例
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        try {
            serverBootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)//指定使用一个NIO传输Channel
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        //在channel的ChannelPipeline中加入EchoServerHandler到最后
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            channel.pipeline().addLast(new FixedLengthFrameDecoder(40));

                            channel.pipeline().addLast(new StringDecoder());

                            channel.pipeline().addLast(new EchoServerHandler());
                        }
                    });
            //异步的绑定服务器,sync()一直等到绑定完成.
            ChannelFuture future = serverBootstrap.bind(this.port).sync();
            System.out.println(SplitPacketServer.class.getName()+" started and listen on '"+ future.channel().localAddress());
            future.channel().closeFuture().sync();//获得这个channel的CloseFuture,阻塞当前线程直到关闭操作完成
        } finally {
            boss.shutdownGracefully().sync();//关闭group,释放所有的资源
            worker.shutdownGracefully().sync();//关闭group,释放所有的资源
        }
    }


    /**
     * main
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        new SplitPacketServer(8000).start();
    }

    class EchoServerHandler extends ChannelInboundHandlerAdapter {

        private int counter=0;
        /**
         * 每次收到消息的时候被调用;
         * @param ctx
         * @param msg
         * @throws Exception
         */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String)msg;

            System.out.println("this is:"+ (++counter) +" time." + " Server received: " + body);

            ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());

            ctx.writeAndFlush(echo);
        }


        /**
         * 在读操作异常被抛出时被调用
         * @param ctx
         * @param cause
         * @throws Exception
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();//打印异常的堆栈跟踪信息
            ctx.close();//关闭这个channel
        }
    }


}

看一下最终的效果:


完全正确了,OK 到此为止粘包和拆包有关知识就讲完了。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值