Netty学习之读netty权威指南(三)

前两个博客磕磕绊绊的写完了客户端和服务端的例子,今天就来研究一点更深入的东西。

TCP的粘包和拆包

这个是我在网上找到的解释粘包和拆包的博客,看着还可以:解释拆包粘包

下面咱们介绍一下书上咋说的,TCP是个流协议,什么是流,就是一大串没有边界的数据,大家可以想象一下河里的水。TCP底层不了解咱们上层业务数据的具体含义,它会根据TCP缓冲区的实际情况对数据进行划分,所以在业务上可以认为,一个完整的业务数据可能会被TCP拆分成多个包进行发送,也可能把多个小包合成一个包进行发送,这就是TCP的拆包和粘包。

图示TCP粘包拆包

书上是下面这样解释上图的

TCP拆包粘包发生的原因

  1. 应用程序写入的大小大于套接字缓冲区的大小
  2. 进行MSS大小的分片
  3. 以太网帧的payload大于MTU进行ip分片

图示:

如何解决粘包拆包

  1. 消息定长
  2. 消息末尾添加换行符或指定标识
  3. 将消息分为消息头和消息体消息头中含有消息体的长度
  4. 更复杂的应用层协议

下面咱们在代码里看看(还是写代码舒服)

TimeServerHandler:加了一个请求计数器

package NettyTestTCP;


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    private int counter = 0;
    /*** 服务端读取到网络数据后的处理*/
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        String body = new String(bytes,"UTF-8");
        System.out.println("服务端得到的请求次数:"+ ++counter);
        String currentTime = "获取当前时间".equals(body)?new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()):"指令有误";
        ByteBuf buf = Unpooled.copiedBuffer(currentTime.getBytes());

        //将消息写到缓冲数组中
        ctx.write(buf);

    }


    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //将消息推到SocketChannel中发送给客户端
        ctx.flush();
    }

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

TimeClientHandler:加了一个返回计数器

package NettyTestTCP;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class TimeClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private final ByteBuf sendMsg;
    private int counter = 0;
    public TimeClientHandler() {
        byte[] bytes = "获取当前时间".getBytes();
        sendMsg = Unpooled.buffer(bytes.length);
        sendMsg.writeBytes(bytes);
    }


    //读取到服务器端的数据后的操作
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        String body = new String(bytes,"UTF-8");

        System.out.println("当前时间是:"+body +",-------"+ ++counter);
        System.out.println();

    }
    /*客户端被通知channel活跃以后,做事*/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //往服务器写数据
        for (int i = 0;i<100;i++){

            byte[] bytes = "获取当前时间".getBytes();
            ByteBuf sendMsg = Unpooled.buffer(bytes.length);
            sendMsg.writeBytes(bytes);

            ctx.writeAndFlush(sendMsg);
        }

    }

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

结果如下所示

服务端应该接收到100次请求但是只有30多次

客户端应该得到100次响应但是只有三次

利用LineBasedFrameDecoder解决TCP粘包问题

为了解决TCP 的粘包和拆包问题Netty提供了许多种编解码器用来处理,只要咱们掌握了这些类库的使用,粘包拆包问题就贼简单,甚至咱们都不需要知道它是咋搞的这也是其他NIO框架无法比拟的。下面开始搞咱们上边的问题还是以时间服务器来说

TimeClient:

package NettyTestTCPNew;


import LineBase.LineBaseClientHandler;
import io.netty.bootstrap.Bootstrap;
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;

public class TimeClient {

    public void connect(int port, String host)  {
        //配置客户端的NIO线程
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
        //服务器端是ServerBootstrap
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup)
                .channel(NioSocketChannel.class)
                //.option(ChannelOption.TCP_NODELAY,true)
                .remoteAddress("127.0.0.1",9999)
                .handler(new ChannelInitializerImp());
        //发起异步连接
            ChannelFuture  channelFuture = bootstrap.connect().sync();
            //等待客户端的连接关闭
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            //释放IO线程组
            eventLoopGroup.shutdownGracefully();
        }
    }
    private static class ChannelInitializerImp extends ChannelInitializer<Channel> {

        @Override
        protected void initChannel(Channel ch) throws Exception {
            //跟服务端一样加了这个编解码器
            ch.pipeline().addLast(new LineBasedFrameDecoder(1024*100000));
            // socketChannel.pipeline().addLast(new StringDecoder());
            ch.pipeline().addLast(new TimeClientHandler());
        }
    }
    public static void main(String[] args) {
        new TimeClient().connect(8989,"127.0.0.1");
    }
}


TimeClientHandler:
package NettyTestTCPNew;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

import java.util.concurrent.atomic.AtomicInteger;

public class TimeClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private AtomicInteger counter = new AtomicInteger(0);


    //读取到服务器端的数据后的操作
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
//        byte[] bytes = new byte[byteBuf.readableBytes()];
//        byteBuf.readBytes(bytes);
//        String body = new String(bytes,"UTF-8");

        System.out.println("当前时间是:"+byteBuf.toString(CharsetUtil.UTF_8) +",-------"+ counter.incrementAndGet());

    }
    /*客户端被通知channel活跃以后,做事*/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //往服务器写数据
        ByteBuf sendMsg = null;
        String msg = "获取当前时间";
        for (int i = 0;i<100;i++){
            sendMsg = Unpooled.buffer(msg.length());
            sendMsg.writeBytes(msg.getBytes());
            ctx.writeAndFlush(sendMsg);
        }

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
TimeServer:
package NettyTestTCPNew;

import LineBase.LineBaseEchoServer;
import LineBase.LineBaseServerHandler;
import io.netty.bootstrap.ServerBootstrap;
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 java.net.InetSocketAddress;

public class TimeServer {
    public void bind() throws Exception {
        //两个线程组,各包含一组NIO线程
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            //ServerBootstrap是Netty的启动NIO的辅助类,目的降低服务端开发的难度
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)/*指定使用NIO进行网络传输*/
                    .localAddress(new InetSocketAddress(9999))/*指定服务器监听端口*/
                    /*服务端每接收到一个连接请求,就会新启一个socket通信,也就是channel,
                    所以下面这段代码的作用就是为这个子channel增加handle*/
                    //绑定一个处理请求的类  我这里用一个匿名内部类写了
                    .childHandler(new ChannelInitializerImp());
            //绑定端口     ChannelFuture异步回调 一直阻塞直到连接完成
            ChannelFuture future = bootstrap.bind().sync();
            //等待服务器监听端口关闭
            System.out.println("服务启动了");
            future.channel().closeFuture().sync();
        }finally {
            //关系线程池
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

    private static class ChannelInitializerImp extends ChannelInitializer<Channel> {

        @Override
        protected void initChannel(Channel ch) throws Exception {
            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
            ch.pipeline().addLast(new TimeServerHandler());
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8989;

        new TimeServer().bind();
    }
}
TimeServerHandler:
package NettyTestTCPNew;


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    private int counter = 0;
    /*** 服务端读取到网络数据后的处理*/
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        String body = new String(bytes,"UTF-8");
        System.out.println("服务端得到的请求:"+ body+" ,===== "+ ++counter);
        String currentTime = "获取当前时间".equals(body)?new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()):"指令有误";
        ByteBuf buf = Unpooled.copiedBuffer(currentTime.getBytes());

        //将消息发给客户端
        ctx.writeAndFlush(buf);

    }

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

具体的实现方法如下所示:

 LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断看是否有 "\n” 或者 "\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以回车换行符为结束标记的解码器,支持配置单行的最大长度,如果连续读取到最大长度后仍然没有发现换行符,会抛出异常,同时忽略掉之前读取到的异常码流。

      StringDecoder 解码器工作原理将接收到的对象转换成字符串,然后继续调用后面的 Hander。

      LineBasedFrameDecoder + StringDecoder 组合就是按行切换的文本解码器,它被设计用来支持 TCP 的粘包与拆包。

1)LineBasedFrameDecoder(final int maxLength)

2)LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast)
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值