netty 入门

220 阅读9分钟

1.概述

2.Hello World

2.1 目标

​ 开发一个简单的服务器和客户端

  • 客户端向服务器发送hello ,world

  • 服务仅接受,不反回

加入依赖

 <dependency>
     <groupId>io.netty</groupId>
     <artifactId>netty-all</artifactId>
     <version>4.1.6.Final</version>
 </dependency>

2.2 服务器端


        //1. 启动器,负责组装netty组件,启动服务器
        new ServerBootstrap()
            //2. BoosEventLoopGroup workEventLoopGroup(selectod,thread) ,group 组
            .group(new NioEventLoopGroup())
            //3.选择服务器的 ServerSocketChannel 实现
            .channel(NioServerSocketChannel.class)      // OIO BIO
            //4.  boss 负责处理连接worker(child) 负责处理读写,决定了worker(child) 能执行哪些操作(handler)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                // 5 channel 代表和客户端进行数据读写的通道 Initializer 初始化,负责添加别的handler
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    // 6 添加具体的handler
                    ch.pipeline().addLast(new StringDecoder()); // 将bytebuf 转为字符串
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){   //自定义handler
                        // 读事件
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            //打印上一步转换好的字符串
                            System.out.println(msg);
                        }
                    });
                }
                //7 绑定监听端口
            }).bind(8088);

2.3 客户端

 //1 启动类
new Bootstrap()
    // 2. 添加EventLoopGroup
    .group(new NioEventLoopGroup())
    //3. 选择客户端channel类
    .channel(NioSocketChannel.class)
    //4. 添加处理器
    .handler(new ChannelInitializer<NioSocketChannel>() {

        @Override   //在连接监建立后 调用
        protected void initChannel(NioSocketChannel ch) throws Exception {
            ch.pipeline().addLast(new StringEncoder());
        }
    })
    .connect("127.0.0.1",8088)
    .sync()
    //向服务器 发送消息
    .channel().writeAndFlush("hello world");

3.组件

3.1 EventLoop

事件循环对象

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

它的继承关系比较复杂

  • 一条线是继承自j.u.c.ScheduledExecutorServer 因此包含了线程池中所有方法
  • 另一条线是继承自netty自己的OrderEventExecutor,
    • 提供了boolean inEventLoop(Thread thread)方法判断一个线程是否属于EventLoopGroup
    • 提供了parent方法来看看自己属于哪个EventLoopGroup

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

  • 继承自Netty自己的EventLoopGroup
    • 实现了Iterable 接口提供遍历EventLoop 能力
    • 另有next方法获取集中下一个EventLoop

以一个简单实例为例:

    // 1. 创建事件循环组
    EventLoopGroup group = new NioEventLoopGroup(2);     // IO 事件,普通任务, 定时任务
    // EventLoopGroup defaultGroup = new DefaultEventLoopGroup();  //普通任务,定时任务

    // 2. 获取下一个事件循环
    System.out.println(group.next());


    // 3 执行普通任务
    group.next().submit(()->{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("ok");
    });

    //4 执行定时任务
    group.next().scheduleAtFixedRate(() ->{
        log.debug("ok");
    },0,1, TimeUnit.SECONDS);
    log.debug("main");

3.2 Channel

channel的主要作用

  • close()可以用来关闭channel

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

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

  • write()方法将数据写入

  • writeAndFlush()方法将数据写入并刷出

这是刚才客户端代码

 //1 启动类
Channel channel = new Bootstrap()
    // 2. 添加EventLoopGroup
    .group(new NioEventLoopGroup())
    //3. 选择客户端channel类
    .channel(NioSocketChannel.class)
    //4. 添加处理器
    .handler(new ChannelInitializer<NioSocketChannel>() {

        @Override   //在连接监建立后 调用
        protected void initChannel(NioSocketChannel ch) throws Exception {
            ch.pipeline().addLast(new StringEncoder());
        }
    })
    .connect("127.0.0.1", 8088)
    .sync().channel();

3.3 Future & Promise

在异步处理时,经常要用到这两个接口

首先要说明netty中的Future与jdk中future同名,但是是两个接口,netty的Future继承自jdk的Future,而promise 又对netty Future进行了扩展

  • Jdk Future只能同步等待任务结束(或成功、或失败)才能得到结果
  • netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但是都要等到任务结束
  • netty Promise 不仅有Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
功能/名称jdk Futurenetty FuturePromise
cancel取消任务
isCancel任务是否取消
isDone任务是否完成
get任务是否完成,不能区分成功失败
getNow获取任务结果,阻塞等待获取任务结果,非阻塞,还未产生结果时,返回null
await等待任务结束,如果任务失败,不会抛异常,而是通过isSuccess判断
sync等待任务结束,如果任务失败,抛出异常
isSuccess判断任务是否成功
cause
addLintener
setSuccess设置成功
setFailure设置失败

jdkFuture 的使用

 //1、 线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);

//2、 提交任务
Future<Integer> future = executorService.submit(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        log.debug("执行计算");
        Thread.sleep(1000);
        return 50;
    }
});
// 3、主线程通过future来获取结果
log.debug("等待结果" );
log.debug("结果是{}",future.get());

NettyFuture 的使用

NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
Future<Integer> future = nioEventLoopGroup.submit(() -> {
    log.debug("准备执行");
    return 70;
});
//        log.debug("等待执行结果");
//        log.debug("结果是{}",future.get());
//        future.get();

future.addListener(future1 -> log.debug("结果是{}", future1.getNow()));

PromiseFuture 的使用

EventLoop eventLoop = new NioEventLoopGroup().next();
DefaultPromise<Object> promise = new DefaultPromise<>(eventLoop);

new Thread(()->{
    log.debug("开始执行计算...");
    try {
        int i = 1/0;		//执行结果出错,抛出异常
        Thread.sleep(1000);
        promise.setSuccess(80);
    } catch (Exception e) {
        e.printStackTrace();
        promise.setFailure(e);	// promise出错异常消息
    }
}).start();

log.debug("准备执行");
log.debug("结果是{}",promise.get());	// 获取结果

3.4 Handler & Pipeline

ChannleHandler用来处理Channel上的各种事件,分为入站、出站两种。所有ChannelHandler被连成一串,就是Pipeline

  • 入站处理器通常是ChannelInboundHandlerAdapter的子类,主要用来读取客户端数据,写回结果
  • 出站处理器通常是ChannelOutboundHandlerAdapter的子类,主要对写会结果进行加工

打个比喻,每个Channel是一个产品的加工车间,Pipeline是车间中的流水线,ChannelHandler就是流水线上的各道工序,ByteBuf是原材料,经过很多工序的加工:先经过一道道入站工序,在经过一道道出站工序最终变成产品

服务端

new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                       // 1. 通过channel 拿到pipeline
                        ChannelPipeline pipeline = ch.pipeline();
                        // 2. 添加处理器   head -> h1 -> - h2 -> tail
                        pipeline.addLast("h1",new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("1");
                             	 ByteBuf byteBuf = (ByteBuf) msg;
                                String s = byteBuf.toString(CharsetUtil.UTF_8);
                                super.channelRead(ctx, s);  //向下传递,h2 能获取到s的值
                                super.channelRead(ctx, msg);
                            }
                        });

                        pipeline.addLast("h2",new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("2");
                                log.debug("h1向下传递的值是:{}",msg.toString());
                                   // 如果入站不触发写操作,出站不会执行
                                ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server..".getBytes()));
                                super.channelRead(ctx, msg);
                            }
                        });
 

                        pipeline.addLast("h3",new ChannelOutboundHandlerAdapter(){
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("3");
                                super.write(ctx, msg, promise);
                            }
                        });
                        pipeline.addLast("h4",new ChannelOutboundHandlerAdapter(){
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("4");
                                super.write(ctx, msg, promise);
                            }
                        });

                    }
                }
    ).bind(8088);

// 执行结果

		/***
         * 09:59:55.494 [nioEventLoopGroup-2-2] DEBUG pipeline.TestPipeLine - 1
         * 09:59:55.496 [nioEventLoopGroup-2-2] DEBUG pipeline.TestPipeLine - 4
         * 09:59:55.496 [nioEventLoopGroup-2-2] DEBUG pipeline.TestPipeLine - 3
         * 09:59:55.498 [nioEventLoopGroup-2-2] DEBUG pipeline.TestPipeLine - 2
         * 
         * 
         */



EmbeddedChannel 模拟入站出站操作

   ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.debug("1");
                super.channelRead(ctx, msg);
            }
        };

        ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter(){
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.debug("2");
                super.channelRead(ctx, msg);
            }
        };

        ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.debug("3");
                super.write(ctx, msg, promise);
            }
        };

        ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.debug("4");
                super.write(ctx, msg, promise);
            }
        };

        EmbeddedChannel channel = new EmbeddedChannel(h1,h2,h3,h4);
        //模拟入站操作
//        channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes())); // h1 h2
        //模拟出站操作
        channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world".getBytes()));  //h4 h3

3.5 ByteBuf

是对字节数据的封装

1)创建

// ByteBuf 是可扩充的字节数 默认是:256
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
log.info(byteBuf);

上面代码创建了一个默认ByteBuf(池化基于直接内存的ByteBuf),初始容量是10

输出

容量是:PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 10)

其中log方法参考如下

 private static void log(ByteBuf buffer){
        int length = buffer.readableBytes();
        int rows = length / 16 + (length % 15 == 0 ? 0:1) + 4;
        StringBuilder buf = new StringBuilder(rows * 80 * 2)
                .append("read index:").append(buffer.readerIndex())
                .append("write index:").append(buffer.writerIndex())
                .append("capacity:").append(buffer.capacity())
                .append(NEWLINE);
            appendPrettyHexDump(buf,buffer);
        System.out.println(buf.toString());

    }

// ByteBuf 是可扩充的字节数 默认是:256
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);

log(byteBuf);
StringBuilder sb = new StringBuilder();
for(int i=0;i<300;i++){
    sb.append("a");
}
byteBuf.writeBytes(sb.toString().getBytes());
log(byteBuf);

2 ) 直接内存 vs 堆内存

可以使用下面的代码来创建池化基于堆的ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);

也可以使用下面的代码来创建池化基于直接内存的ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
  • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池功能一起用
  • 直接内存堆GC压力小,因为这部分内存不受JVM垃圾回收的管理,但也要主要及时主动释放

3)池化 vs 非池化

池化的最大意义在于可以重用ByteBuf,有点有

  • 没有池化, 则每次都地创建新的ByteBuf实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加GC压力
  • 有了池化,则可以重用翅中ByteBuf 实例,并且采用了与jemalloc类似的内存分配算法提升分配效率。
  • 高并发时,池化功能更节约内存,减少内存溢出的可能

池化功能是否开启,可以通过下面的系统环境变量来设置

-Dio.netty.allocator.type={unpooled|pooled}
  • 4.1 以后,非Android平台默认启用池化实现,Android 平台启用非池化实现
  • 4.1 之前,池化功能还不成熟,默认是非池化实现

4) 组成

ByteBuf 由四部分组成

最开始读写指针都在0位置

5)写入

方法列表,省略一些不重要方法

方法签名含义备注
writeBoolean(boolean value)写入boolean值用一字节01||00 代表true | false
writeByte(int value)写入byte值
writeShort(int value)写入short值
writeInt(int value)写入int值Big Endian,即0x250,写入后00 00 02 50
writeIntLE(int value)写入int值Little Endian,即0x250,写入后50 02 00 00
writerBytes(ByteBuffer src)写入nio的ByteBuffer
int writeCharSequence(CharSequence sequence,Charset charset)写入字符串

注意

  • 这些方法的未指明返回值的,其返回值都是ByteBuf,意味着可以链式调用
  • 网络传输,默认习惯是Big Endian

先写入4个字节

buffer.writeBytes(new byte[]{1,2,3,4});
log(buffer);

结果是:

read index:0 write index:4 capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04                                     |....            |
+--------+-------------------------------------------------+----------------+

在写入一个int整数,也是4个字节

buffer.writeInt(5);
log(buffer);

结果是

read index:0 write index:8 capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05                         |........        |
+--------+-------------------------------------------------+----------------+

还有一类方法是set开头的一系列方法,也可以写入数据,但不会改变写指针位置

6)扩容

在写入一个int整数时,容量不够了(初始容量是10),这时会引发扩容

buffer.writeInt(6);
log(buffer);

扩容规则是

  • 如何写入后数据大小未超过512,则选择下一个16的整数倍,例如写入后大小为12,则扩容后capacity是16
  • 如果写入后数据大小超过512,则选择下一个2^n,例如写入后大小为513,则扩容后capacity是2^10=1024(2^9-512已经不够了)
  • 扩容不能超过max capacity 会报错

结果是

read index:0 write index:12 capacity:64
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05 00 00 00 06             |............    |
+--------+-------------------------------------------------+----------------+

7 ) 读取

例如读了4次,每次一个字节

System.out.println(byteBuf.readByte());
System.out.println(byteBuf.readByte());
System.out.println(byteBuf.readByte());
System.out.println(byteBuf.readByte());
log(byteBuf);

读过的内容,就属于废弃部分了,再读只能读那些尚未读取的部分

1
2
3
4
read index:4 write index:12 capacity:64
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 05 00 00 00 06                         |........        |
+--------+-------------------------------------------------+----------------+

如果需要重复读取int整数5,怎么办?

可以在read前先做个标记mark

buffer.markReaderIndex();
System.out.println(buffer.readInt());
log(buffer);

结果

5
read index:8 write index:12 capacity:64
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 06                                     |....            |
+--------+-------------------------------------------------+----------------+

这时要重复读取的话,重置到标记位置reset

byteBuf.resetReaderIndex();
log(byteBuf);

这时

read index:4 write index:12 capacity:64
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 05 00 00 00 06                         |........        |
+--------+-------------------------------------------------+----------------+

还有种办法是才用get开头的一系列方法,这些方法不会改变read index

8 ) retain & release

由于Netty中有堆内存的ByteBuf 实现,堆外内存最好是手动来释放,而不是等GC垃圾回收

  • UnpooledHeapByteBuf 使用jvm内存,只需等GC回收内存即可
  • UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来收回内存
  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

回收内存的源码实现,请关注下面方法的不同实现

protected abstract void deallocate()

Netty这里才用了引用计数法来控制回收内存,每个ByteBuf都实现了ReferenceCounted接口

  • 每个ByteBuf 对象的初始计数为1
  • 调用release方法计数减1,如果计数为0,ByteBuf 内存被回收
  • 调用retain 方法计数加1,表示调用者没用完之前,其它handler即使调用了release也不会造成回收
  • 当计数为0时,底层内存会被回收,这时即使ByteBuf 对象还在,其各个方法均无法正常使用

9 ) Slice

【零拷贝】的体现之一,对原始ByteBuf 进行切片或多个ByteBuf,切片后的ByteBuf并没有发生内存复制,还是使用原始ByteBuf的内存,切片后的ByteBuf 维护独立的read,write 指针

例,原始 ByteBuf 进行一些初始操作

ByteBuf byteBuf  = ByteBufAllocator.DEFAULT.buffer(10);
byteBuf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i'});
BufferUtil.log(byteBuf);

// 在切片过程中,没有发生数据复制
ByteBuf f1 = byteBuf.slice(0, 5);
f1.retain();
ByteBuf f2 = byteBuf.slice(5, 4);
f2.retain();

byteBuf.release();

BufferUtil.log(f1);
BufferUtil.log(f2);
System.out.println("================");
f1.setByte(0,'b');

f1.release();
f2.release();
BufferUtil.log(byteBuf);
BufferUtil.log(f1);

输出

read index:0 write index:9 capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 66 67 68 69                      |abcdefghi       |
+--------+-------------------------------------------------+----------------+
 read index:0 write index:5 capacity:5
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65                                  |abcde           |
+--------+-------------------------------------------------+----------------+
 read index:0 write index:4 capacity:4
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 66 67 68 69                                     |fghi            |
+--------+-------------------------------------------------+----------------+
================
 read index:0 write index:9 capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 62 63 64 65 66 67 68 69                      |bbcdefghi       |
+--------+-------------------------------------------------+----------------+
 read index:0 write index:5 capacity:5
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 62 63 64 65                                  |bbcde           |
+--------+-------------------------------------------------+----------------+

Process finished with exit code 0


10 ) duplicate

【零拷贝】的体现之一,就好比截取了原始ByteBuf 所有内容,并且没有max capacity 限制,也是与原始ByteBuf 使用同一块底层内存,只是读写指针独立的

11) copy

会将底层内存数据进行深拷贝,因此无论读写,都与原始ByteBuf 无关

13) Unpooled

Unpooled是一个工具类,类如其名,提供了非池化的ByteBuf创建、组合、复制等操作

ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1,2,3,4,5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6,7,8,9,10});


//当包装ByteBuf 个数超过一个时,底层使用了 CompositeByteBuf
ByteBuf buf3 = Unpooled.wrappedBuffer(buf1,buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));

ByteBuf 优势

  • 池化-可以重用池中ByteBuf实例,更节约内存,减少内存溢出的可能
  • 读写指针分离,不需要像ByteBuffer 一样切换读写模式
  • 可以自动扩容
  • 支持链式调用,使用更流畅
  • 很多地方体现零拷贝,例如Slice、duplicate 、 CompositeByteBuf

4.双向通信

4.1 练习

实现一个 echo server

编写server

new ServerBootstrap()
    .group(new NioEventLoopGroup())
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<NioServerSocketChannel>() {
        @Override
        protected void initChannel(NioServerSocketChannel ch) throws Exception {
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
					
                    ByteBuf buffer = msg instanceof  ByteBuf ? (ByteBuf)msg : null;
                    System.out.println(buffer.toString(Charset.defaultCharset()));
//                    super.channelRead(ctx, msg);
                    
                    // 思考需要释放buffer吗
                }
            });
        }
    }).bind(8088);


怎么回应了? 只需要为channelRead 添加逻辑

  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
      // ....
      
      //建议使用ctx.alloc() 创建ByteBuf
      ByteBuf response = ctx.alloc().buffer(20);
      response.writeBytes("hello word".getBytes());
      ctx.writeAndFlush(response);
      
      //思考:需要释放 response 吗
                  
  }

编写client,这次使用了channelActive 事件, 它会在连接建立后触发

  new Bootstrap()
      .group(new NioEventLoopGroup())
      .channel(NioSocketChannel.class)
      .handler(new ChannelInitializer<NioSocketChannel>() {
          @Override
          protected void initChannel(NioSocketChannel ch) throws Exception {
              ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                  @Override
                  public void channelActive(ChannelHandlerContext ctx) throws Exception {
                      //建议使用ctx.alloc() 创建bytebuf
                      ByteBuf buffer = ctx.alloc().buffer(10);
                      //首次建立连接,发送hello 信息
                      buffer.writeBytes("hello".getBytes());
                      ctx.writeAndFlush(buffer);
                      super.channelActive(ctx);

                      // 思考: 需要释放buffer吗
                  }
              });
          }
      }).connect("127.0.0.1",8088);

客户端接收,与服务器接收代码类似,在客户端ChannelInboundHandlerAdapter中加入 channelRead事件

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

    ByteBuf buffer = msg instanceof  ByteBuf ? (ByteBuf)msg : null;
    System.out.println(buffer.toString(Charset.defaultCharset()));
    //                    super.channelRead(ctx, msg);

    // 思考需要释放buffer吗
}

三. Netty 进阶

1.粘(nian)包与半包

1.1 粘包现象

只要使用tcp 协议 ,都会出现半包 粘(nian)包问题 ,udp 不会出现

服务端代码

  void start(){

      NioEventLoopGroup boss = new NioEventLoopGroup(1);
      NioEventLoopGroup work = new NioEventLoopGroup();
      try {
          ServerBootstrap serverBootstrap = new ServerBootstrap();
          serverBootstrap.group(boss,work);
          serverBootstrap.channel(NioServerSocketChannel.class);
          serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
              @Override
              protected void initChannel(SocketChannel ch) throws Exception {
                  ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                  ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                      @Override
                      public void channelActive(ChannelHandlerContext ctx) throws Exception {
                          logger.debug("connected {}" ,ctx.channel());
                          super.channelActive(ctx);
                      }

                      @Override
                      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                          super.channelRead(ctx, msg);
                      }
                  });
              }
          });
          ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();
          channelFuture.channel().closeFuture().sync();
      } catch (InterruptedException e) {
          logger.error("server error",e);
      }finally {
          boss.shutdownGracefully();
          work.shutdownGracefully();
      }

客户端代码

NioEventLoopGroup worker = new NioEventLoopGroup();
try {
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.channel(NioSocketChannel.class);
    bootstrap.group(worker);
    bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) throws Exception {
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                // 会在连接channle 建立成功后,会触发active 事件
                @Override
                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                    for(int i =0 ;i < 10;i++ ){
                        ByteBuf buffer = ctx.alloc().buffer(16);
                        buffer.writeBytes(new byte[] {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
                        ctx.writeAndFlush(buffer);
                    }
                    //                        super.channelActive(ctx);
                }
            });
        }
    });
    ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8088).sync();
    channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
    //            e.printStackTrace();
    logger.debug("client sesrver",e);
}finally {
    worker.shutdownGracefully();
}

结果是: 发生粘包 , 一次发送16字节,结果一次收到160个字节

DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x2cade680, L:/127.0.0.1:8088 - R:/127.0.0.1:55461] RECEIVED: 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 |................|
+--------+-------------------------------------------------+----------------+
11:31:41.603 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 160, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.

半包

//服务器加上 接收缓冲区
serverBootstrap.option(ChannelOption.SO_RCVBUF,10);

结果是:

11:37:28.364 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 36B
         +-------------------------------------------------+
         |  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                                     |....            |
+--------+-------------------------------------------------+----------------+
11:37:28.364 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 36, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
11:37:28.365 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 40B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000010| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000020| 04 05 06 07 08 09 0a 0b                         |........        |
+--------+-------------------------------------------------+----------------+
11:37:28.365 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 40, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 40B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 0c 0d 0e 0f 00 01 02 03 04 05 06 07 08 09 0a 0b |................|
|00000010| 0c 0d 0e 0f 00 01 02 03 04 05 06 07 08 09 0a 0b |................|
|00000020| 0c 0d 0e 0f 00 01 02 03                         |........        |
+--------+-------------------------------------------------+----------------+
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 40, cap: 512) that reached at the tail of the pipeline. Please check your pipeline configuration.
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 40B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000010| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000020| 04 05 06 07 08 09 0a 0b                         |........        |
+--------+-------------------------------------------------+----------------+
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 40, cap: 512) that reached at the tail of the pipeline. Please check your pipeline configuration.
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 4B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 0c 0d 0e 0f                                     |....            |
+--------+-------------------------------------------------+----------------+
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 4, cap: 496) that reached at the tail of the pipeline. Please check your pipeline configuration.

1.2

1.3 现象分析

粘包

  • 现象,发送abc def,接受 abcdef
  • 原因
    • 应用层: 接收方ByteBuf 设置太大(Netty 默认1024)
    • 滑动窗口:假设发送方256bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这256bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
    • Nagle算法: 会造成粘包

半包

  • 现象, 发送abcdef,接受abc edf
  • 原因
    • 应用层:接收方Bytebuf 小于实际发送数据量
    • 滑动窗口: 假设接收方的窗口只剩了128bytes,发送方的报文大小是256 bytes, 这时放不下了,只能先发送前128 bytes, 等待ack后法能发送剩余部分,这就造成半包
    • MSS 限制: 当发送的数据超过MSS 限制后,会将数据切分发送, 就会造成半包

本质是因为tcp是流失协议,消息无边界

粘包半包_解决短连接

解决 定长-解码器 LineBasedFrameDecoder

粘包半包-解决 -行解码器

服务端

 void start(){

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup work = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            //调整系统的接受缓冲器(滑动窗口)
//            serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
            //调整 netty 的接收缓冲区(bytebuf)
            serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss,work);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); //加入行解码器
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        // 会在连接 channel 建立成功后,会触发active事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            logger.debug("connected {}" ,ctx.channel());
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            super.channelRead(ctx, msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            logger.error("server error",e);
        }finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
        }
    }

客户端

//行解码器 
public static  StringBuilder makeString(char c,int len){
        StringBuilder sb = new StringBuilder(len+2);
        for(int i = 0; i < len; i++ ){
            sb.append(c);
        }
        sb.append("\n");
        return sb;
    }


    private static void send() {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        // 会在连接channle 建立成功后,会触发active 事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            char c = '0';
                            ByteBuf buf = ctx.alloc().buffer();
                            Random r = new Random();
                            for(int i = 0 ;i < 10 ;i++ ){
                                StringBuilder sb = makeString(c, r.nextInt(256) + 1);
                                c++;
                                buf.writeBytes(sb.toString().getBytes());
                            }
                            ctx.writeAndFlush(buf);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8088).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
//            e.printStackTrace();
//            logger.debug("client sesrver",e);
        }finally {
            worker.shutdownGracefully();
        }
    }

粘包-半包-解决LTC解码器 LengthFieldBasedFrameDecoder www.bilibili.com/video/BV1py…

lengthFieldOffset  - 长度字段偏移量
lengthFieldLength  - 长度字段长度
lenegthAdjustment  - 长度字段为基准,还有几个字节是内容
initialBytesToStrip - 从头剥离几个字节    

测试字节消息


main(){

    EmbeddedChannel channel = new EmbeddedChannel(new LengthFieldBasedFrameDecoder(
                1024,0,4,1,4),
                new LoggingHandler(LogLevel.DEBUG));

        // 4 个字节的内容长度, 实际内容
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        send(buffer, "Hello, world!");
        send(buffer, "Hi!");
        channel.writeInbound(buffer);
}


  private static void send(ByteBuf buffer, String content) {
        byte[] bytes = content.getBytes();  //实际内容
        int length = bytes.length;  // 实际内容长度
        buffer.writeInt(length);
        buffer.writeByte(1);	//
        buffer.writeBytes(bytes);
    }


2. 协议设计与解析

使用netty测试http协议

  NioEventLoopGroup boos = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boos,worker);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new HttpServerCodec());
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {  //泛型,处理HttpRequest 请求
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
                            // 获取请求
                            log.debug("获取请求 {}",msg.uri());

                            // 返回响应
                            DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
                            byte[] bytes = "<h1>Hello world</h1>".getBytes();
                            response.headers().setInt(CONTENT_LENGTH,bytes.length);
                            response.content().writeBytes(bytes);
                            //写回响应
                            ctx.writeAndFlush(response);
                        }
                    });
         /*           ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.debug("{}",msg.getClass());
//                            super.channelRead(ctx, msg);
                            if(msg instanceof HttpRequest){     //请求行,请求头
                            }else if(msg instanceof HttpContent){   //请求体
                            }
                        }
                    });*/
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            boos.shutdownGracefully();
            worker.shutdownGracefully();
        }

2.1 自定义协议要素

  • 魔数,用来在第一时间判定是否是无效数据包
  • 版本号,用来支持协议的升级
  • 序列算法, 消息正文到底才用哪用序列化反序列化方法,可以由此扩展, 例如: json ,protobuf、hession、jdk
  • 指令类型, 是登陆、注册、单聊、群聊.....跟业务相关
  • 请求序号, 为了双工通信,提供异步能力
  • 正文长度
  • 消息正文

image.png

解码

image.png

已观看

https://www.bilibili.com/video/BV1py4y1E7oA?p=142