netty网络编程

121 阅读26分钟

Docs

Netty

一、概述

1、什么是Netty

Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.

Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端

注意netty的异步还是基于多路复用的,并没有实现真正意义上的异步IO

2、Netty的优势

如果使用传统NIO,其工作量大,bug 多

  • 需要自己构建协议
  • 解决 TCP 传输问题,如粘包、半包
  • 因为bug的存在,epoll 空轮询导致 CPU 100%

Netty 对 API 进行增强,使之更易用,如

  • FastThreadLocal => ThreadLocal
  • ByteBuf => ByteBuffer

二、入门案例

1、服务器端代码

public class HelloServer {
    public static void main(String[] args) {
        // 1、启动器,负责装配netty组件,启动服务器
        new ServerBootstrap()
                // 2、创建 NioEventLoopGroup,可以简单理解为 线程池 + Selector
                .group(new NioEventLoopGroup())
                // 3、选择服务器的 ServerSocketChannel 实现
                .channel(NioServerSocketChannel.class)
                // 4、child 负责处理读写,该方法决定了 child 执行哪些操作
            	// ChannelInitializer 处理器(仅执行一次)
            	// 它的作用是待客户端SocketChannel建立连接后,执行initChannel以便添加更多的处理器
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        // 5、SocketChannel的处理器,使用StringDecoder解码,ByteBuf=>String
                        nioSocketChannel.pipeline().addLast(new StringDecoder());
                        // 6、SocketChannel的业务处理,使用上一个处理器的处理结果
                        nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
                                System.out.println(s);
                            }
                        });
                    }
                    // 7、ServerSocketChannel绑定8080端口
                }).bind(8080);
    }
}

2、客户端代码

public class HelloClient {
    public static void main(String[] args) throws InterruptedException {
        new Bootstrap()
                .group(new NioEventLoopGroup())
                // 选择客户 Socket 实现类,NioSocketChannel 表示基于 NIO 的客户端实现
                .channel(NioSocketChannel.class)
                // ChannelInitializer 处理器(仅执行一次)
                // 它的作用是待客户端SocketChannel建立连接后,执行initChannel以便添加更多的处理器
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel channel) throws Exception {
                        // 消息会经过通道 handler 处理,这里是将 String => ByteBuf 编码发出
                        channel.pipeline().addLast(new StringEncoder());
                    }
                })
                // 指定要连接的服务器和端口
                .connect(new InetSocketAddress("localhost", 8080))
                // Netty 中很多方法都是异步的,如 connect
                // 这时需要使用 sync 方法等待 connect 建立连接完毕
                .sync()
                // 获取 channel 对象,它即为通道抽象,可以进行数据读写操作
                .channel()
                // 写入消息并清空缓冲区
                .writeAndFlush("hello world");
    }
}

3、运行流程

左:客户端 右:服务器端

Client发送数据,经过client自身的handler,将数据转为ByteBuf,server接收到数据后,通过server的handler将数据从byteBuf转为所需要的数据,并进行处理

组件解释

  • channel 可以理解为数据的通道

  • msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 中的各个 handler 加工,会变成其它类型对象,最后输出又变成 ByteBuf

  • handler 可以理解为数据的处理工序

    • 工序有多道,合在一起就是 pipeline(传递途径) ,pipeline 负责发布事件(读、读取完成…)传播给每个 handler, handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法)

      • pipeline 中有多个 handler,处理时会依次调用其中的 handler
    • handler 分 Inbound 和 Outbound 两类

      • Inbound 入站
      • Outbound 出站
  • eventLoop 可以理解为处理数据的工人

    • eventLoop 可以管理多个 channel 的 io 操作,并且一旦 eventLoop 负责了某个 channel,就会将其与channel进行绑定,以后该 channel 中的 io 操作都由该 eventLoop 负责
    • eventLoop 既可以执行 io 操作,也可以进行任务处理,每个 eventLoop 有自己的任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务
    • eventLoop 按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据,可以为每个 handler 指定不同的 eventLoop

三、组件

1、EventLoop

事件循环对象 EventLoop

EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理一个或多个 Channel 上源源不断的 io 事件

它的继承关系如下

  • 继承自 j.u.c.ScheduledExecutorService 因此包含了线程池中所有的方法

  • 继承自 netty 自己的 OrderedEventExecutor

    • 提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
    • 提供了 EventLoopGroup parent() 方法来看看自己属于哪个 EventLoopGroup

事件循环组 EventLoopGroup

EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)

  • 继承自 netty 自己的 EventExecutorGroup

    • 实现了 Iterable 接口提供遍历 EventLoop 的能力
    • 另有 next 方法获取集合中下一个 EventLoop

处理普通与定时任务

public class TestEventLoop {
    public static void main(String[] args) {
        //1 创建事件循环组

        // io事件,普通任务,定时任务
        NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(2);
        //普通任务,定时任务
        DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();

        //2 获取事件循环对象
        System.out.println(nioEventLoopGroup.next());
        System.out.println(nioEventLoopGroup.next());
        System.out.println(nioEventLoopGroup.next());

        //3 执行普通任务
        nioEventLoopGroup.next().execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        });

        //4 定时任务
        nioEventLoopGroup.next().scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(new Date());
            }
        },0,2, TimeUnit.SECONDS);   
        
        //5 关闭
        nioEventLoopGroup.shutdownGracefully();
    }
}

关闭 EventLoopGroup
优雅关闭 shutdownGracefully 方法。该方法会首先切换 EventLoopGroup 到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行。从而确保整体应用是在正常有序的状态下退出的

处理IO任务

服务器代码

public class EventLoopServer {
    public static void main(String[] args) {

        // 只负责accept事件
        NioEventLoopGroup boss = new NioEventLoopGroup();

        //负责read,write事件
        NioEventLoopGroup worker = new NioEventLoopGroup(2);

        // 对eventloop进行指责划分 分为boss和worker
        // 此处划分为accept事件和read事件
        new ServerBootstrap().
                group(boss,worker).
                //NioServerSocketChannel只会和NioEventLoopGroup中的一个EventLoop绑定
                channel(NioServerSocketChannel.class).
                childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        // 为handler设置指定的NioEventGroup
                        ch.pipeline().addLast(worker,"handleRead",new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                System.out.println(worker.next());
                                ByteBuf buffer = (ByteBuf) msg;
                                System.out.println(buffer.toString(Charset.defaultCharset()));
                            }
                        });
                    }
                }).
                bind(8080);
    }
}

客户端代码

public class EventLoopClient {
    public static void main(String[] args) throws InterruptedException {
        Channel client = new Bootstrap()
                .group(new NioEventLoopGroup())
                // 选择客户 Socket 实现类,NioSocketChannel 表示基于 NIO 的客户端实现
                .channel(NioSocketChannel.class)
                // ChannelInitializer 处理器(仅执行一次)
                // 它的作用是待客户端SocketChannel建立连接后,执行initChannel以便添加更多的处理器
                .handler(new ChannelInitializer<>() {
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        // 消息会经过通道 handler 处理,这里是将 String => ByteBuf 编码发出
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
                // 指定要连接的服务器和端口
                .connect(new InetSocketAddress("localhost", 8080))
                // Netty 中很多方法都是异步的,如 connect
                // 这时需要使用 sync 方法等待 connect 建立连接完毕
                .sync()
                // 获取 channel 对象,它即为通道抽象,可以进行数据读写操作
                .channel();
        client.writeAndFlush("hello\n");
        client.writeAndFlush("world\n");
    }
}

分工

Bootstrap的group()方法可以传入两个EventLoopGroup参数,分别负责处理不同的事件

public class MyServer {
    public static void main(String[] args) {
        new ServerBootstrap()
            	// 两个Group,分别为Boss 负责Accept事件,Worker 负责读写事件
                .group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
    }
}

一个EventLoop可以负责多个Channel,且EventLoop一旦与Channel绑定,则一直负责处理该Channel中的事件

增加自定义EventLoopGroup

当有的任务需要较长的时间处理时,可以使用非NioEventLoopGroup,避免同一个NioEventLoop中的其他Channel在较长的时间内都无法得到处理

public class MyServer {
    public static void main(String[] args) {
        // 增加自定义的非NioEventLoopGroup
        EventLoopGroup group = new DefaultEventLoopGroup();
        
        new ServerBootstrap()
                .group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        // 增加两个handler,第一个使用NioEventLoopGroup处理,第二个使用自定义EventLoopGroup处理
                        socketChannel.pipeline().addLast("nioHandler",new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                System.out.println(Thread.currentThread().getName() + " " + buf.toString(StandardCharsets.UTF_8));
                                // 调用下一个handler
                                ctx.fireChannelRead(msg);
                            }
                        })
                        // 该handler绑定自定义的Group
                        .addLast(group, "myHandler", new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                System.out.println(Thread.currentThread().getName() + " " + buf.toString(StandardCharsets.UTF_8));
                            }
                        });
                    }
                })
                .bind(8080);
    }
}

切换的实现

不同的EventLoopGroup切换的实现原理如下

由上面的图可以看出,当handler中绑定的Group不同时,需要切换Group来执行不同的任务

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    // 获得下一个EventLoop, excutor 即为 EventLoopGroup
    EventExecutor executor = next.executor();
    
    // 如果下一个EventLoop 在当前的 EventLoopGroup中
    if (executor.inEventLoop()) {
        // 使用当前 EventLoopGroup 中的 EventLoop 来处理任务
        next.invokeChannelRead(m);
    } else {
        // 否则让另一个 EventLoopGroup 中的 EventLoop 来创建任务并执行
        executor.execute(new Runnable() {
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}
  • 如果两个 handler 绑定的是同一个EventLoopGroup,那么就直接调用
  • 否则,把要调用的代码封装为一个任务对象,由下一个 handler 的 EventLoopGroup 来调用

2.Channel

主要方法

  • close() 可以用来关闭Channel

  • closeFuture() 用来处理 Channel 的关闭

    • sync 方法作用是同步等待 Channel 关闭
    • 而 addListener 方法是异步等待 Channel 关闭
  • pipeline() 方法用于添加处理器

  • write() 方法将数据写入

    • 因为缓冲机制,数据被写入到 Channel 中以后,不会立即被发送
    • 只有当缓冲满了或者调用了flush()方法后,才会将数据通过 Channel 发送出去
  • writeAndFlush() 方法将数据写入并立即发送(刷出)

ChannelFuture

带Future和Promise的类都是和异步方法配套使用的

public class ChannelClient {
    public static void main(String[] args) throws InterruptedException {
        ChannelFuture channelFuture = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<>() {
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
                //connect是异步非阻塞方法,在main函数中只是发起了调用,真正执行的是另一个nio线程,建立连接往往是需要消耗时间的,而如果不执行sync方法,就可能产生连接还没有建立成功,而主线程直接获取了channel
                //,并进行了消息的发送
                .connect(new InetSocketAddress("localhost", 8080));

        channelFuture.sync();
        Channel channel = channelFuture.channel();

        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while (true){
                String input = scanner.nextLine();
                if (input.equals("quit")){
                    channel.close();
                    break;
                }
                channel.writeAndFlush(input);
            }
        }).start();
        //异步等待直到线程关闭
        ChannelFuture closeFuture = channel.closeFuture();
        closeFuture.sync();
        
        //线程已经关闭
        System.out.println("thread is closed");
    }
}

ChannelFuture连接问题

connect是异步非阻塞方法,在main函数中只是发起了调用,真正执行的是另一个nio线程,建立连接往往是需要消耗时间的,而如果不执行sync方法,就可能产生连接还没有建立成功,而主线程直接获取了channel,这个channel是没有建立连接的channel,所以消息并不能真正发送出去

ChannelFuture关闭问题

当我们要关闭channel时,可以调用channel.close()方法进行关闭。但是该方法也是一个异步方法。真正的关闭操作并不是在调用该方法的线程中执行的,而是在NIO线程中执行真正的关闭操作

如果我们想在channel真正关闭以后,执行一些额外的操作,可以选择以下两种方法来实现

  • 通过channel.closeFuture()方法获得对应的ChannelFuture对象,然后调用sync()方法阻塞执行操作的线程,等待channel真正关闭后,再执行其他操作

    // 获得closeFuture对象
    ChannelFuture closeFuture = channel.closeFuture();
    
    // 同步等待NIO线程执行完close操作
    closeFuture.sync();
    
  • 调用closeFuture.addListener方法,添加close的后续操作

    closeFuture.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture channelFuture) throws Exception {
            // 等待channel关闭后才执行的操作
            System.out.println("关闭之后执行一些额外操作...");
            // 关闭EventLoopGroup
            group.shutdownGracefully();
        }
    });
    

Handler -> Pipeline

ChannelHandler用来处理Channel上的各种事件,分为入栈,出栈两种,所有的ChannelHandler连成一串就组成了Pipeline流水线

  • 入栈处理器通常是ChannelInBoundHandlerAdapter的子类,用来读取客户端数据,写回结果
  • 出栈处理器通常是ChannelOutBoundHandlerAdapter的子类,主要对写回的结果进行加工
public class PipeLineServer {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        // 在socketChannel的pipeline中添加handler
                        // pipeline中handler是带有head与tail节点的双向链表,的实际结构为
                        // head <-> handler1 <-> ... <-> handler4 <->tail
                        // Inbound主要处理入栈操作,一般为读操作,发生入栈操作时会触发Inbound方法
                        // 入栈时,handler是从head向后调用的
                        socketChannel.pipeline().addLast("handler1" ,new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf byteBuf = (ByteBuf) msg;
                                String message = byteBuf.toString(Charset.defaultCharset())+" 1 ";
                                System.out.println("handler1 msg:"+message);
                                // 父类该方法内部会调用fireChannelRead
                                // 将数据传递给下一个handler
                                super.channelRead(ctx, message);
                            }
                        });
                        socketChannel.pipeline().addLast("handler2", new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                String handle2 = msg.toString()+" 2 ";
                                System.out.println("handler2 msg:"+ handle2);
                                // 执行write操作,使得Outbound的方法能够得到调用
                                socketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("Server...".getBytes(StandardCharsets.UTF_8)));
                                super.channelRead(ctx, handle2);
                            }
                        });
                        // Outbound主要处理出栈操作,一般为写操作,发生出栈操作时会触发Outbound方法
                        // 出栈时,handler的调用是从tail向前调用的
                        socketChannel.pipeline().addLast("handler3" ,new ChannelOutboundHandlerAdapter(){
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                System.out.println(Thread.currentThread().getName() + " Outbound handler 1");
                                super.write(ctx, msg, promise);
                            }
                        });
                        socketChannel.pipeline().addLast("handler4" ,new ChannelOutboundHandlerAdapter(){
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                System.out.println(Thread.currentThread().getName() + " Outbound handler 2");
                                super.write(ctx, msg, promise);
                            }
                        });
                    }
                })
                .bind(8080);
    }
}

通过channel.pipeline().addLast(name, handler)添加handler时,可以handler取名字。这样可以调用pipeline的addAfter、addBefore等方法更灵活地向pipeline中添加handler

handler需要放入通道的pipeline中,才能根据放入顺序来使用handler

  • pipeline是结构是一个带有head与tail指针的双向链表,其中的节点为handler

    • 要通过ctx.fireChannelRead(msg)等方法,将当前handler的处理结果传递给下一个handler
  • 当有入栈(Inbound)操作时,会从head开始向后调用handler,直到handler不是处理Inbound操作为止

  • 当有出栈(Outbound)操作时,会从tail开始向前调用handler,直到handler不是处理Outbound操作为止

具体结构如下

调用顺序如下

OutboundHandler

socketChannel.writeAndFlush()

当handler中调用该方法进行写操作时,会触发Outbound操作,此时是从tail向前寻找OutboundHandler

ctx.writeAndFlush()

当handler中调用该方法进行写操作时,会触发Outbound操作,此时是从当前handler向前寻找OutboundHandler

总结

EventLoop定义了Netty的核心对象,用于处理IO事件,多线程模型、并发,EventLoop, channel, Thread 以及 EventLoopGroup 之间的关系如下图:

    1、一个EventLoopGroup包含一个或者多个EventLoop;

    2、一个EventLoop在它的生命周期内只和一个Thread绑定;

    3、所有有EventLoop处理的I/O事件都将在它专有的Thread上被处理;

    4、一个Channel在它的生命周期内只注册于一个EventLoop;

    5、一个EventLoop可能会被分配给一个或多个Channel;
其实我们可以简单的把EventLoop及其相关的实现NioEventLoop、NioEventLoopGroup等理解为netty针对我们网络编程时创建的多线程进行了封装和优化,构建了自己的线程模型。

练习

练习一 双向通信

编写一个服务端和一个客户端,如果客户端给服务端发送ping,那么服务端会回复pong,同时客户端也要接收pong并打印

服务端代码

//实现一个双向通信,客户端发送ping,服务端回复pong
public class Server {
    public static void main(String[] args) {
        // acceptWorker用于处理accept事件
        NioEventLoopGroup acceptWorker = new NioEventLoopGroup();
        // readWriteWorker用于处理read和write事件
        NioEventLoopGroup readWriteWorker = new NioEventLoopGroup();
        ChannelFuture channelFuture = new ServerBootstrap().
                //设置为NioEventLoopGroup
                group(acceptWorker, readWriteWorker).
                channel(NioServerSocketChannel.class).
                childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel socketChannel) throws Exception {
                        //readHandler 处理来自客户端的信息
                        socketChannel.pipeline().addLast(acceptWorker, "readHandler", new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf byteBuf = (ByteBuf) msg;
                                String message = byteBuf.toString(Charset.defaultCharset());
                                System.out.println(message);
                                //如果客户端信息为ping
                                if ("ping".equals(message)){
                                    //触发writeHandler
                                    socketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("pong".getBytes()));
                                }
                            }
                        });

                        //writeHandler 向客户端返回信息
                        socketChannel.pipeline().addLast(readWriteWorker, "writeHandler", new ChannelOutboundHandlerAdapter() {
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                ByteBuf byteBuf = (ByteBuf) msg;
                                super.write(ctx, msg, promise);
                            }
                        });
                    }
                }).
                bind(8080);
    }
}

客户端代码

public class Client {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        ChannelFuture channelFuture = new Bootstrap().
                group(worker).
                channel(NioSocketChannel.class).
                handler(new ChannelInitializer<>() {
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        //发送消息的处理器,对信息编码
                        ch.pipeline().addLast(new StringEncoder());
                        
                        //处理服务端返回的数据
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = (ByteBuf) msg;
                                System.out.println(buf.toString(Charset.defaultCharset()));
                                super.channelRead(ctx, msg);
                            }
                        });
                    }
                }).connect(new InetSocketAddress("localhost", 8080));


        channelFuture.sync();
        Channel channel = channelFuture.channel();


        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while (true){
                String input = scanner.nextLine();
                if (input.equals("quit")){
                    channel.close();
                    break;
                }
                channel.writeAndFlush(input);
            }
        }).start();
        //异步等待直到线程关闭
        ChannelFuture closeFuture = channel.closeFuture();
        closeFuture.sync();

        //线程已经关闭
        System.out.println("thread is closed");
        worker.shutdownGracefully();
    }
}

练习二 粘包半包

通过netty的方式解决粘包和半包的问题

服务端代码

@Slf4j
public class Server {
    void start() {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup(1);
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            //设置系统接收缓冲区大小,复显半包的问题
            //serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
            //serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR,new FixedRecvByteBufAllocator(10));

            //设置netty的缓冲区大小
            //serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
            
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ch.pipeline().addLast(worker,new LoggingHandler(LogLevel.INFO));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {

                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 连接建立时会执行该方法
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                            // 连接断开时会执行该方法
                            super.channelInactive(ctx);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            channelFuture.sync();
            // 关闭channel
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error");
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new Server().start();
    }
}

客户端代码

public class Client {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            /*
                             分十次每次发送十个字节
                             期望服务端每次收到16个字节,一共收到十次
                             但是实际情况是服务端一次收到了160个字节
                             */
                            for (int i = 0; i < 10; i++) {
                                ByteBuf byteBuf = ctx.alloc().buffer(16);
                                byteBuf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
                                ctx.writeAndFlush(byteBuf);

                            }
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("client error");
        } finally {
            worker.shutdownGracefully();
        }
    }
}

粘包现象

客户端向服务端发送十次数据,一次16个字节,原本期望的情况是,服务端接收到十次,每次16字节,但是实际情况是服务端一次就接收到了160个字节,这就是粘包

Sep 21, 2022 10:21:51 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0xd5093bd0, L:/127.0.0.1:8080 - R:/127.0.0.1:57243] REGISTERED
Sep 21, 2022 10:21:51 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0xd5093bd0, L:/127.0.0.1:8080 - R:/127.0.0.1:57243] ACTIVE
Sep 21, 2022 10:21:51 AM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0xd5093bd0, L:/127.0.0.1:8080 - R:/127.0.0.1:57243] READ: 160B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000010| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000020| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000030| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000040| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000050| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000060| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000070| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000080| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000090| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 10:21:51 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0xd5093bd0, L:/127.0.0.1:8080 - R:/127.0.0.1:57243] READ COMPLETE

半包现象

通过设置接收缓冲区的大小,从而限制接收方一次接收到的最大数据量,从而会对发送方的数据产生截断,这就是半包现象,只要使用TCP传输协议就一定会产生半包问题,而UDP不会

//设置接收缓冲区大小,复显半包的问题
//serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR,new FixedRecvByteBufAllocator(3));

现象分析

粘包

  • 现象

    • 发送 abc def,接收 abcdef
  • 原因

    • 应用层

      • 接收方 ByteBuf 设置太大(Netty 默认 1024)
    • 传输层-网络层

      • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大(大于256 bytes),这 256 bytes 字节就会缓冲在接收方的滑动窗口中, 当滑动窗口中缓冲了多个报文就会粘包
      • Nagle 算法:会造成粘包

半包

  • 现象

    • 发送 abcdef,接收 abc def
  • 原因

    • 应用层

      • 接收方 ByteBuf 小于实际发送数据量
    • 传输层-网络层

      • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时接收方窗口中无法容纳发送方的全部报文,发送方只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
    • 数据链路层

      • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

解决方案一 短连接

客户端每次向服务器发送数据以后,就与服务器断开连接,此时的消息边界为连接建立到连接断开。这时便无需使用滑动窗口等技术来缓冲数据,则不会发生粘包现象。但如果一次性数据发送过多,接收方无法一次性容纳所有数据,还是会发生半包现象,所以短链接无法解决半包现象

短连接客户端代码
//通过短连接的方式解决粘包问题
public class ShortConnectClient {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10 ; i++) {
            send();
            //使发送变得有序
            Thread.sleep(1000);
        }
    }

    private static void send() {
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            ByteBuf byteBuf = ctx.alloc().buffer(16);
                            byteBuf.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
                            ctx.writeAndFlush(byteBuf);
                            ctx.channel().close();
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("client error");
        } finally {
            worker.shutdownGracefully();
        }
    }
}
运行结果
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] REGISTERED
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] ACTIVE
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] READ: 16B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] READ COMPLETE
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] READ COMPLETE
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelInactive
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 ! R:/127.0.0.1:57699] INACTIVE
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelUnregistered
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 ! R:/127.0.0.1:57699] UNREGISTERED
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] REGISTERED
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] ACTIVE
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] READ: 16B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] READ COMPLETE
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] READ COMPLETE
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelInactive
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 ! R:/127.0.0.1:57700] INACTIVE
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelUnregistered
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 ! R:/127.0.0.1:57700] UNREGISTERED
...

解决方案二 定长解码器

客户端与服务器约定一个最大长度,保证客户端每次发送的数据长度都不会大于该长度。若发送数据长度不足则需要补齐至该长度
服务器接收数据时,将接收到的数据按照约定的最大长度进行拆分,即使发送过程中产生了粘包,也可以通过定长解码器将数据正确地进行拆分。服务端需要用到FixedLengthFrameDecoder对数据进行定长解码

定长解码服务端代码
@Slf4j
public class FixedLengthServer {
    void start() {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup(1);
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);;
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    //通过定长解码器规定最大消息长度为10
                    ch.pipeline().addLast(new FixedLengthFrameDecoder(16));
                    ch.pipeline().addLast(worker,new LoggingHandler(LogLevel.INFO));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {

                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 连接建立时会执行该方法
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                            // 连接断开时会执行该方法
                            super.channelInactive(ctx);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            channelFuture.sync();
            // 关闭channel
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error");
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new FixedLengthServer().start();
    }
}
定长解码器客户端代码
public class FixedLengthClient {
    public static void main(String[] args) throws InterruptedException {
        send();
    }

    private static void send() {
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 约定最大长度为16
                            final int maxLength = 16;
                            // 被发送的数据
                            char c = 'a';
                            // 向服务器发送10个报文
                            for (int i = 0; i < 10; i++) {
                                ByteBuf buffer = ctx.alloc().buffer(maxLength);
                                // 定长byte数组,未使用部分会以0进行填充
                                byte[] bytes = new byte[maxLength];
                                // 生成长度为0~15的数据
                                for (int j = 0; j < (int) (Math.random() * (maxLength - 1)); j++) {
                                    bytes[j] = (byte) c;
                                }
                                buffer.writeBytes(bytes);
                                c++;
                                // 将数据发送给服务器
                                ctx.writeAndFlush(buffer);
                            }
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("client error");
        } finally {
            worker.shutdownGracefully();
        }
    }
}
运行结果
INFO: [id: 0x856d9368, L:/127.0.0.1:8080 - R:/127.0.0.1:57793] READ: 16B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |a...............|
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 11:11:21 AM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0x856d9368, L:/127.0.0.1:8080 - R:/127.0.0.1:57793] READ: 16B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 62 62 62 00 00 00 00 00 00 00 00 00 00 00 00 |bbbb............|
+--------+-------------------------------------------------+----------------+
...

解决方案三 LTC长度字段解码器

在传送数据时可以在数据中添加一个用于表示有用数据长度的字段,在解码时读取出这个用于表明长度的字段,同时读取其他相关参数,即可知道最终需要的数据是什么样子的

LengthFieldBasedFrameDecoder解码器可以提供更为丰富的拆分方法,其构造方法有五个参数

public LengthFieldBasedFrameDecoder(
    int maxFrameLength,
    int lengthFieldOffset, int lengthFieldLength,
    int lengthAdjustment, int initialBytesToStrip)Copy

参数解析

  • maxFrameLength 数据最大长度

    • 表示数据的最大长度(包括附加信息、长度标识等内容)
  • lengthFieldOffset 数据长度标识的起始偏移量

    • 用于指明数据第几个字节开始是用于标识有用字节长度的,因为前面可能还有其他附加信息
  • lengthFieldLength 数据长度标识所占字节数(用于指明有用数据的长度)

    • 数据中用于表示有用数据长度的标识所占的字节数
  • lengthAdjustment 长度表示与有用数据的偏移量

    • 用于指明数据长度标识和有用数据之间的距离,因为两者之间还可能有附加信息
  • initialBytesToStrip 数据读取起点

    • 读取起点,不读取 0 ~ initialBytesToStrip 之间的数据

参数图解

lengthFieldOffset   = 0
lengthFieldLength   = 2
lengthAdjustment    = 0
initialBytesToStrip = 0 (= do not strip header)
  
BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
+--------+----------------+      +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
+--------+----------------+      +--------+----------------+Copy

从0开始即为长度标识,长度标识长度为2个字节

0x000C 即为后面 HELLO, WORLD的长度


lengthFieldOffset   = 0
lengthFieldLength   = 2
lengthAdjustment    = 0
initialBytesToStrip = 2 (= the length of the Length field)
  
BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
+--------+----------------+      +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
+--------+----------------+      +----------------+Copy

从0开始即为长度标识,长度标识长度为2个字节,读取时从第二个字节开始读取(此处即跳过长度标识)

因为跳过了用于表示长度的2个字节,所以此处直接读取HELLO, WORLD


lengthFieldOffset   = 2 (= the length of Header 1)
lengthFieldLength   = 3
lengthAdjustment    = 0
initialBytesToStrip = 0
  
BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
+----------+----------+----------------+      +----------+----------+----------------+
| Header 1 |  Length  | Actual Content |----->| Header 1 |  Length  | Actual Content |
|  0xCAFE  | 0x00000C | "HELLO, WORLD" |      |  0xCAFE  | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+      +----------+----------+----------------+Copy

长度标识前面还有2个字节的其他内容(0xCAFE),第三个字节开始才是长度标识,长度表示长度为3个字节(0x00000C)

Header1中有附加信息,读取长度标识时需要跳过这些附加信息来获取长度


lengthFieldOffset   = 0
lengthFieldLength   = 3
lengthAdjustment    = 2 (= the length of Header 1)
initialBytesToStrip = 0
  
BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
+----------+----------+----------------+      +----------+----------+----------------+
|  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
| 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
+----------+----------+----------------+      +----------+----------+----------------+Copy

从0开始即为长度标识,长度标识长度为3个字节,长度标识之后还有2个字节的其他内容(0xCAFE)

长度标识(0x00000C)表示的是从其后lengthAdjustment(2个字节)开始的数据的长度,即HELLO, WORLD,不包括0xCAFE


lengthFieldOffset   = 1 (= the length of HDR1)
lengthFieldLength   = 2
lengthAdjustment    = 1 (= the length of HDR2)
initialBytesToStrip = 3 (= the length of HDR1 + LEN)
  
BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+      +------+----------------+Copy

长度标识前面有1个字节的其他内容,后面也有1个字节的其他内容,读取时从长度标识之后3个字节处开始读取,即读取 0xFE HELLO, WORLD

客户端代码
@Slf4j
public class LengthFieldDecoder {
    public static void main(String[] args) {
        // 模拟服务器
        // 使用EmbeddedChannel测试handler
        EmbeddedChannel channel = new EmbeddedChannel(
                /*
                   数据最大长度为1KB,长度标识前后各有1个字节的附加信息,长度标识长度为4个字节(int)
                   只获取其中的message信息 其他不需要
                   数据实际为
                            +-------------------------------------------------+
                                     |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
                            +--------+-------------------------------------------------+----------------+
                            |00000000| ca 00 00 00 05 fe 57 6f 72 6c 64                |......World     |
                            +--------+-------------------------------------------------+----------------+
                            0的位置是长度前的信息,占位一个字节,所以lengthFieldOffset需要设置为1,从1的位置开始读取信息长度
                            1-4是长度信息,int类型占位四个字节,所以lengthFieldLength需要设置为4
                            5的位置为实际信息前的额外数据,所以lengthAdjustment需要设置为1,表明其后1位开始才是实际信息
                            6-a的位置是实际信息,如果想解码取出实际信息,initialBytesToStrip设置为6,因为前面的多余信息所占字节数为1+4+1=6
                 */
                new LengthFieldBasedFrameDecoder(1024, 1, 4, 1, 6),
                new LoggingHandler(LogLevel.INFO)
        );

        // 模拟客户端,写入数据
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        send(buffer, "Helloooooooo");
        channel.writeInbound(buffer);
        send(buffer, "World");
        channel.writeInbound(buffer);
    }

    private static void send(ByteBuf buf, String msg) {
        // 得到数据的长度
        int length = msg.length();
        byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
        // 将数据信息写入buf
        // 写入长度标识前的其他信息 占一个字节
        buf.writeByte(0xCA);
        // 写入数据长度标识 一个int占四个字节
        buf.writeInt(length);
        // 写入长度标识后的其他信息 占一个字节
        buf.writeByte(0xFE);
        // 写入具体的数据
        buf.writeBytes(bytes);
    }
}
运行结果
INFO: [id: 0xembedded, L:embedded - R:embedded] READ: 12B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 6f 6f 6f 6f 6f 6f 6f             |Helloooooooo    |
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 2:00:15 PM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
Sep 21, 2022 2:00:15 PM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0xembedded, L:embedded - R:embedded] READ: 5B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 57 6f 72 6c 64                                  |World           |
+--------+-------------------------------------------------+----------------+

问题1.系统缓冲区大小设置无效

原想通过以下代码,强行设置系统接收缓冲区的大小为10,从而复现半包的问题,但是实际发现并不可行,其原因是issue,由于不同os的差异,实际上这个参数未必会和设置的一样,最终缓冲区大小还是由os决定的,netty的默认大小是1024B。

serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR,new FixedRecvByteBufAllocator(8));

练习三 协议设计与解析

Redis协议

如果我们要向Redis服务器发送一条set name TianLe Zhou的指令,需要遵守如下协议

// 代表该指令一共有3部分,每条指令之后都要添加回车与换行符
*3\r\n
// 第一个指令的长度是3
$3\r\n
// 第一个指令是set指令
set\r\n
// 下面的指令以此类推
$4\r\n
name\r\n
$11\r\n
TianLe Zhou\r\n
客户端代码
public class RedisClient {
    /*
        *3
        $3
        set
        $4
        name
        $11
        TianLe Zhou
     */
    public static void main(String[] args) {
        final byte[] FORMAT = "\r\n".getBytes();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(worker,new LoggingHandler(LogLevel.INFO));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            ByteBuf buffer = ctx.alloc().buffer();
                            buffer.writeBytes("*3".getBytes());
                            buffer.writeBytes(FORMAT);

                            buffer.writeBytes("$3".getBytes());
                            buffer.writeBytes(FORMAT);
                            buffer.writeBytes("set".getBytes());
                            buffer.writeBytes(FORMAT);

                            buffer.writeBytes("$4".getBytes());
                            buffer.writeBytes(FORMAT);
                            buffer.writeBytes("name".getBytes());
                            buffer.writeBytes(FORMAT);

                            buffer.writeBytes("$11".getBytes());
                            buffer.writeBytes(FORMAT);
                            buffer.writeBytes("TianLe Zhou".getBytes());
                            buffer.writeBytes(FORMAT);

                            // 发送命令给Redis执行
                            ctx.channel().writeAndFlush(buffer);
                        }

                        //获取Redis返回的结果
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf buf = (ByteBuf) msg;
                            System.out.println(buf.toString(Charset.defaultCharset()));
                        }
                    });
                }
            });
            // 连接到redis
            ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 6379)).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("client error");
        } finally {
            worker.shutdownGracefully();
        }
    }
}
运行结果
INFO: [id: 0xd679c3af, L:/127.0.0.1:59257 - R:localhost/127.0.0.1:6379] WRITE: 41B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 34 0d |*3..$3..set..$4.|
|00000010| 0a 6e 61 6d 65 0d 0a 24 31 31 0d 0a 54 69 61 6e |.name..$11..Tian|
|00000020| 4c 65 20 5a 68 6f 75 0d 0a                      |Le Zhou..       |
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 2:26:19 PM io.netty.handler.logging.LoggingHandler flush
INFO: [id: 0xd679c3af, L:/127.0.0.1:59257 - R:localhost/127.0.0.1:6379] FLUSH
Sep 21, 2022 2:26:19 PM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0xd679c3af, L:/127.0.0.1:59257 - R:localhost/127.0.0.1:6379] READ: 5B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 2b 4f 4b 0d 0a                                  |+OK..           |
+--------+-------------------------------------------------+----------------+

自定义协议

组成要素
  • 魔数:作为判定协议是否有效的依据,例如Java起始字节码是CAFEBABE

  • 版本号:可以支持协议的升级

  • 序列化算法:消息正文到底采用哪种序列化反序列化方式

    • 如:json、protobuf、hessian、jdk
  • 指令类型:与业务相关

  • 请求序号:为了双工通信,提供异步能力

  • 正文长度

  • 消息正文:序列化,一般是json

自定义编解码协议实现

编解码器代码

//通过泛型制定编解码的对象
public class MessageCodec extends ByteToMessageCodec<Message> {

    //出栈时进行编码
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        //魔数 BAKAZHOU 占八个字节
        out.writeBytes("BAKAZHOU".getBytes());

        //版本 1 占四个字节
        out.writeInt(1);
        //序列化算法  0代表Json 1代表jdk 占四个字节
        out.writeInt(0);

        //指令类型 int类型占四个字节
        out.writeInt(msg.getMessageType());

        //请求序号 占四个字节
        out.writeInt(msg.getSequenceId());

        //将消息正文站位bytes
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] msgBytes = bos.toByteArray();

        //正文长度 占四个字节
        out.writeInt(msgBytes.length);

        //正文前一共有28个字节
        //写入正文
        out.writeBytes(msgBytes);
    }

    //入栈时进行解码
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //魔数 BAKAZHOU 8字节
        String magicNum = in.readBytes(8).toString(Charset.defaultCharset());

        //版本号 4字节
        int version = in.readInt();

        //序列化算法 
        int serializationAlgorithm = in.readInt();

        int messageType = in.readInt();

        //请求序号
        int sequenceId = in.readInt();

        //正文长度
        int length = in.readInt();

        //正文内容
        byte[] msg = new byte[length];
        in.readBytes(msg,0,length);
        //判断序列化方式
        switch (serializationAlgorithm){
            case 0:
                ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(msg));
                Message message = (Message) ois.readObject();

                //传给下一个处理器使用
                out.add(message);
                break;
            case 1:
                break;
            default:
                break;
        }

    }
}
  • 编码器与解码器方法源于父类ByteToMessageCodec,通过该类可以自定义编码器与解码器,泛型类型为被编码与被解码的类。此处使用了自定义类Message,代表消息

    public class MessageCodec extends ByteToMessageCodec<Message>Copy
    
  • 编码器负责将附加信息与正文信息写入到ByteBuf中,其中附加信息总字节数最好为2n,不足需要补齐。正文内容如果为对象,需要通过序列化将其放入到ByteBuf中

  • 解码器负责将ByteBuf中的信息取出,并放入List中,该List用于将信息传递给下一个handler

测试代码

@Slf4j
public class TestMessageCodec {
    public static void main(String[] args) throws Exception {
        MessageCodec messageCodec = new MessageCodec();
        EmbeddedChannel channel = new EmbeddedChannel(
                // 添加解码器,避免粘包半包问题
                new LengthFieldBasedFrameDecoder(1024, 28, 4, 4, 0),
                new LoggingHandler(LogLevel.INFO),
                messageCodec);

        //encode
        LoginRequestMessage loginUser = new LoginRequestMessage("bakazhou", "123456");
        channel.writeOutbound(loginUser);

        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
        System.out.println(byteBuf);
        messageCodec.encode(null, loginUser, byteBuf);
        channel.writeInbound(loginUser);
        System.out.println(byteBuf);
    }
}

运行结果

第一行0-7的位置即为魔数BAKAZHOU
第一行8-b为版本号
....
以此类推
INFO: [id: 0xembedded, L:embedded - R:embedded] WRITE: 285B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 42 41 4b 41 5a 48 4f 55 00 00 00 01 00 00 00 00 |BAKAZHOU........|
|00000010| 00 00 00 00 00 00 00 00 00 00 01 01 ac ed 00 05 |................|
|00000020| 73 72 00 41 63 6f 6d 2e 63 6e 2e 74 77 2e 67 72 |sr.Acom.cn.tw.gr|
|00000030| 61 64 75 61 74 65 2e 62 61 6b 61 7a 68 6f 75 2e |aduate.bakazhou.|
|00000040| 50 72 61 63 74 69 63 65 33 2e 6d 65 73 73 61 67 |Practice3.messag|
|00000050| 65 2e 4c 6f 67 69 6e 52 65 71 75 65 73 74 4d 65 |e.LoginRequestMe|
|00000060| 73 73 61 67 65 76 36 80 36 45 1a d9 d3 02 00 02 |ssagev6.6E......|
|00000070| 4c 00 08 70 61 73 73 77 6f 72 64 74 00 12 4c 6a |L..passwordt..Lj|
|00000080| 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b |ava/lang/String;|
|00000090| 4c 00 08 75 73 65 72 6e 61 6d 65 71 00 7e 00 01 |L..usernameq.~..|
|000000a0| 78 72 00 35 63 6f 6d 2e 63 6e 2e 74 77 2e 67 72 |xr.5com.cn.tw.gr|
|000000b0| 61 64 75 61 74 65 2e 62 61 6b 61 7a 68 6f 75 2e |aduate.bakazhou.|
|000000c0| 50 72 61 63 74 69 63 65 33 2e 6d 65 73 73 61 67 |Practice3.messag|
|000000d0| 65 2e 4d 65 73 73 61 67 65 d6 50 c5 58 ac 0f 63 |e.Message.P.X..c|
|000000e0| 63 02 00 02 49 00 0b 6d 65 73 73 61 67 65 54 79 |c...I..messageTy|
|000000f0| 70 65 49 00 0a 73 65 71 75 65 6e 63 65 49 64 78 |peI..sequenceIdx|
|00000100| 70 00 00 00 00 00 00 00 00 74 00 06 31 32 33 34 |p........t..1234|
|00000110| 35 36 74 00 08 62 61 6b 61 7a 68 6f 75          |56t..bakazhou   |
+--------+-------------------------------------------------+----------------+


PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)
//成功进行了解码buf填充了289
PooledUnsafeDirectByteBuf(ridx: 0, widx: 289, cap: 512)

练习四 简易的IM通讯系统

任务说明

实现一个简单的IM通讯系统,包括用户的登录,用户间的消息收发,用户群组间的消息收发,以及群组相关的系列操作,例如创建群聊,加入群聊,退出群聊等

基本架构图

基本架构.png

包结构

image.png

  • client: 与客户端相关的文件
  • command: 客户端的所有基本操作指令
  • message: 所有类型的请求信息和返回信息
  • protocol: 自定义编解码器
  • server: 与服务端相关的文件
  • handler: 服务端处理入栈请求的处理器
  • session: 缓存用户与channel的关系,用户与群组之间的关系

代码实现

git repository practice3模块为具体实现代码