Netty 深入浅出

Netty介绍

Netty是一个NIO客户端服务器框架,可以快速方便地开发协议服务器和客户端等网络应用程序。它大大简化和流线网络编程,如TCP和UDP套接字服务器。

img

NIO三大核心组件

Buffer 暂存数据的缓冲区

  • ByteBuffer(常用)
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

Channel 数据流

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

Selector

使用多线程技术

为每个连接分别开辟一个线程,分别去处理对应的socke连接

img

缺点

  • 内存占用高
    • 每个线程都需要占用一定的内存,当连接较多时,会开辟大量线程,导致占用大量内存
  • 线程上下文切换成本高
  • 只适合连接数少的场景
    • 连接数过多,会导致创建很多线程,从而出现问题

使用线程池技术

使用线程池,让线程池中的线程去处理连接

img

缺点

  • 阻塞模式下,线程同一时间内仅能处理一个连接
    • 线程池中的线程获取任务(task)后,只有当其执行完任务之后(断开连接后),才会去获取并执行下一个任务
    • 若socke连接一直未断开,则其对应的线程无法处理其他socke连接
  • 仅适合短连接场景
    • 短连接即建立连接发送请求并响应后就立即断开,使得线程池中的线程可以快速处理其他连接

使用Selector

selector 的作用就是配合一个线程来管理多个 channel(fileChannel因为是阻塞式的,所以无法使用selector),获取这些 channel 上发生的事件(可连接,可读,可写),这些 channel 工作在非阻塞模式下,当一个channel中没有执行任务时,可以去执行其他channel中的任务。适合连接数多,但流量较少的场景

img

若事件未就绪,调用 selector 的 select() 方法会阻塞线程,直到 channel 发生了就绪事件。这些事件就绪后,select 方法就会返回这些事件交给 thread 来处理

Selectotrap:netty的辅助启动器,netty客户端和服务器的入口,Bootstrap是创建客户端连接的启动器,ServerBootstrap是监听服务端端口的启动器,跟tomcat的Bootstrap类似,程序的入口。

Bootstrap:netty的辅助启动器,netty客户端和服务器的入口,Bootstrap是创建客户端连接的启动器,ServerBootstrap是监听服务端端口的启动器,跟tomcat的Bootstrap类似,程序的入口。

EventLoop:netty最核心的几大组件之一,就是我们常说的reactor,人为划分为boss reactor和worker reactor。通过EventLoopGroup(Bootstrap启动时会设置EventLoopGroup)生成,最常用的是nio的NioEventLoop,就如同EventLoop的名字,EventLoop内部有一个无限循环,维护了一个selector,处理所有注册到selector上的io操作,在这里实现了一个线程维护多条连接的工作。

Channel:关联jdk原生socket的组件,常用的是NioServerSocketChannel和NioSocketChannel,NioServerSocketChannel负责监听一个tcp端口,有连接进来通过boss reactor创建一个NioSocketChannel将其绑定到worker reactor,然后worker reactor负责这个NioSocketChannel的读写等io事件。

ChannelPipeline:netty最核心的几大组件之一,ChannelHandler的容器,netty处理io操作的通道,与ChannelHandler组成责任链。write、read、connect等所有的io操作都会通过这个ChannelPipeline,依次通过ChannelPipeline上面的ChannelHandler处理,这就是netty事件模型的核心。ChannelPipeline内部有两个节点,head和tail,分别对应着ChannelHandler链的头和尾。

ChannelHandler:netty最核心的几大组件之一,netty处理io事件真正的处理单元,开发者可以创建自己的ChannelHandler来处理自己的逻辑,完全控制事件的处理方式。ChannelHandler和ChannelPipeline组成责任链,使得一组ChannelHandler像一条链一样执行下去。ChannelHandler分为inBoundoutBound,分别对应io的readwrite的执行链。ChannelHandler用ChannelHandlerContext包裹着,有prevnext节点,可以获取前后ChannelHandler,read时从ChannelPipeline的head执行到tail,write时从tail执行到head所以head既是read事件的起点也是write事件的终点,与io交互最紧密。

Unsafe:顾名思义这个类就是不安全的意思,但并不是说这个类本身不安全,而是不要在应用程序里面直接使用Unsafe以及他的衍生类对象,实际上Unsafe操作都是在reactor线程中被执行。Unsafe是Channel的内部类,并且是protected修饰的,所以在类的设计上已经保证了不被用户代码调用。Unsafe的操作都是和jdk底层相关。EventLoop轮询到read或accept事件时,会调用unsafe.read(),unsafe再调用ChannelPipeline去处理事件;当发生write事件时,所有写事件都会放在EventLoop的task中,然后从ChannelPipeline的tail传播到head,通过Unsafe写到网络中所以,头是Unsafe(), 尾也是Unsafe()。

使用Netty的框架

RPC(dubbo、HSF)Hadoop、Spark

MQ(RocketMQ等)Zookeeper等

几乎所有的基于java的分布式中间件都是采用netty作为通信工具的,(ps:redis是用c写的,采用了跟netty相同的epoll模型)

4.X 使用

JavaDoc4.1
面临的问题

我们并不经常使用HTTP服务来交换大文件,邮箱信息和实时消息(商业信息/多玩家游戏数据)等, 以上数据需要一个高优化的协议来执行, 例如,您可能希望实现一个针对基于ajax的聊天应用程序、媒体流或大文件传输进行优化的HTTP服务器。甚至可以设计并使用一个全新的协议来精确地解决你的需求.

另一个不可避免的情况是,您必须处理遗留的专有协议,以确保与旧系统的互操作性。在这种情况下,重要的是在不牺牲结果应用程序的稳定性和性能的情况下,我们能多快地实现该协议。

解决方案

Netty工程致力于提供一个异步的事务驱动型网络应用框架, 并且是一个用于快速开发可维护、高性能和高可伸缩性协议服务器和客户端的工具。

另一方面, Netty是一个NIO客户端服务框架, 可以快速简单的开发网络应用, 比如协议服务器和客户端.

Netty的优势: philosophy

Netty从一开始就为您提供了最舒适的API和实现体验, 让一切变得更加简单.

开始

安装Netty和 JDK1.6以上的版本

Netty安装网址

Netty的Maven依赖

<dependencies>
  ...
  <dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty</artifactId> <!-- Use 'netty-all' for 4.0 or above -->
    <version>X.Y.Z.Q</version>
    <scope>compile</scope>
  </dependency>
  ...
</dependencies>
写一个Discard Server(对所有请求不返回任何数据)
package com.tencent.nio.Netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Author:nioliu
 * @DATE: 2021/9/1  19:13
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter {
    private Logger logger = LoggerFactory.getLogger(DiscardServerHandler.class);

    /**
     * 当收到信息时,此方法被调用
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 忽略任何data
        ((ByteBuf) msg).release();// ByteBuf是一个引用计数的对象,必须通过release()方法显式解压。
    }

    /**
     * 处理发生错误时的情况
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 关闭连接
        logger.error(cause.getMessage());
        ctx.close();
    }
}

注意: 通常 channelRead 都是用来release message的, 所以基本有如下的格式:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        // Do something with msg
    } finally {
        ReferenceCountUtil.release(msg);
    }
}

而 exceptionCaught 可以通常用来返回error code并计入自己的日志

写一个Main()方法来启动这个Handler服务
package com.tencent.nio.Netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Author:nioliu
 * @DATE: 2021/9/1  19:29
 */
public class DiscardServer {
    private int port;
    private Logger logger = LoggerFactory.getLogger(DiscardServer.class);

    public DiscardServer(int port) {
        this.port = port;
    }

    public void run() {
        // NioEventLoopGroup是一个多线程事件循环I/O操作, 对于不同的传输有不同的EventLoopGroup
        // 使用了多少线程以及如何将它们映射到创建的通道,取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。
        NioEventLoopGroup bossGroup  = new NioEventLoopGroup(); // 通常boss用来接受连接
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();// worker用来处理boss接受的连接的traffic
        try {
            ServerBootstrap b = new ServerBootstrap(); // 用于自动化创建一个服务器(大部分配置已经配好), 也可以自己使用Channel创建, 不过很麻烦
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)// 指定使用NioServerSocketChannel类来创建连接
                    .childHandler(new ChannelInitializer<SocketChannel>() {// 用于初始化Channel, 用来做一些自定义的处理配置
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new DiscardServerHandler());// addLast: 在处理的最后进行操作
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128) // 设置一些套接字选项, 设置对应的是NioServerSocketChannel
                    .childOption(ChannelOption.SO_KEEPALIVE, true);// 设置的是SocketChannel

            // 绑定并启动接受连接
            try {
                ChannelFuture f = b.bind(port).sync();
                // 等待服务器关闭
                f.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                logger.error(e.getMessage());
            }
        }finally {
            // 优雅地关闭这些连接
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        int port = 8888;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new DiscardServer(port).run();
    }
}

这里有一些组件已经是上面我们提到的了,比如ServerBootstrap,server启动器设置一系列参数,比如EventLoopGroup、ChannelHandler、Channel等,最后绑定端口启动服务;EventLoopGroup的主要作用是生成事件循环处理器EventLoop;NioServerSocketChannel是通过反射的方式生成服务端的channel,负责监听一个tcp端口。

最后通过ServerBootstrap的bind(port)方法真正绑定端口,完成服务端启动的工作。

netty的服务端启动主要分为三步:

  • init
  • register
  • bind
初始化源代码
    final ChannelFuture initAndRegister() {        Channel channel = this.channelFactory().newChannel();        try {            this.init(channel);        } catch (Throwable var3) {            channel.unsafe().closeForcibly();            return (new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE)).setFailure(var3);        }        ChannelFuture regFuture = this.group().register(channel);        if (regFuture.cause() != null) {            if (channel.isRegistered()) {                channel.close();            } else {                channel.unsafe().closeForcibly();            }        }        return regFuture;    }
  • 通过jdk的nio原生方法生成ServerSocketChannel对象(最后监听端口就是通过这个对象监听的),绑定到NioServerSocketChannel上,设置ServerSocketChannel为非阻塞方式
  • 调用newUnsafe()方法生成一个Unsafe对象,这里的Unsafe对象是在AbstractNioMessageChannel类中生成的,所以是一个NioMessageUnsafe的实例,不同的Unsafe子类完成的工作不同,NioMessageUnsafe负责处理accept事件。
  • 调用newChannelPipeline()方法生成一个DefaultChannelPipeline对象,DefaultChannelPipeline中默认有head和tail两个节点的ChannelHandler,后续再加ChannelHandler都是在这条链上。

经过这几步操作,完成了NioServerSocketChannel的实例化过程,接下来就是channel的init了。

init channel也分了几步:

  • 获取设置在bootstrap上的options和attrs,将其注入到channel中
  • 获取设置在bootstrap上的ChannelHandler,将其加入到channel的ChannelPipeline的处理链上,这样server channel上的任何事件都会经过这个ChannelHandler处理
  • 最后生成一个ServerBootstrapAcceptor对象,这同样是一个ChannelHandler,将其也加入到channel的ChannelPipeline的处理链上ServerBootstrapAcceptor的作用是:监听到accept事件,生成NioSocketChannel对象,然后将这个对象注册到worker reactor上,让worker reactor负责这条连接的读写。

客户端的bootstrap并没有最后一步生成ServerBootstrapAcceptor的过程,因为客户端不需要监听端口,只需要处理一条连接的读写,这是netty服务端和客户端启动初始化的区别。

netty启动init阶段初始化了一些基本的配置和属性,以及在pipeline上加入了一个接入器,用来专门接受新连接。

而在nio层面,完成了创建一个ServerSocketChannel对象和设置io模式为非阻塞这两步操作。

register注册(channel)

完成了启动初始化工作之后,接下来netty要开始register注册了,register主要是注册channel。

在流程上,从initAndRegister方法出发,调用路径如下:

AbstractBootstrap.initAndRegister() —> EventLoopGroup.register(channel) —> EventLoop.register(channel) —> AbstractUnsafe.register(this, promise) —> AbstractNioUnsafe.doRegister()

到了AbstractNioUnsafe的doRegister方法,netty基本上只做了一件事,jdk原生的serverChannel向selector注册感兴趣的事件

selectionKey = javaChannel().register(eventLoop().selector, 0, this);

只是这里注册的opts是0,表示此channel不关注任何类型的事件。(言外之意,register方法只是获取一个selectionKey,具体这个Channel对何种事件感兴趣,可以在稍后操作)

所以真正注册对accept是在后面。

bind绑定

完成了启动的初始化和假注册之后,netty需要真正绑定端口监听了。bind过程主要就是完成真正注册关注accept事件,bind端口完成启动工作。

bind的流程调用有两条路径,通过eventLoop异步执行task,流程如下:

AbstractChannel.bind(localAddress, promise) —> DefaultChannelPipeline.bind(localAddress, promise) —> tail.bind(localAddress, promise) —> head.bind(this, localAddress, promise) —> AbstractUnsafe.bind(localAddress, promise) —> NioServerSocketChannel.doBind(localAddress)

AbstractChannel.bind(localAddress, promise) —> DefaultChannelPipeline.fireChannelActive() —> head.invokeChannelActive() —> head.readIfIsAutoRead() —> AbstractChannel.read() —> DefaultChannelPipeline.read() —> tail.read() —> head.read() —> AbstractUnsafe.beginRead() —> AbstractNioUnsafe.doBeginRead()

第一条路径是执行绑定操作,监听你设置的端口号。最后执行到NioServerSocketChannel的doBind方法,调用jdk的ServerSocketChannel执行bind操作:javaChannel().bind(localAddress, config.getBacklog());

而第二条路径是完成真正关注accept事件,先获取在register阶段获得的selectionKey,然后真正注册对accept事件感兴趣,这样channel就可以接收所有连接请求了,boss reactor将这些请求都通过ServerBootstrapAcceptor连接器处理,把这些请求注册给worker reactor,让worker reactor负责这条连接的读写。

protected void doBeginRead() throws Exception {    // Channel.read() or ChannelHandlerContext.read() was called    final SelectionKey selectionKey = this.selectionKey;    if (!selectionKey.isValid()) {        return;    }     readPending = true;     final int interestOps = selectionKey.interestOps();    if ((interestOps & readInterestOp) == 0) {        // 真正注册对accept事件感兴趣        selectionKey.interestOps(interestOps | readInterestOp);    }}

最后,netty客户端的启动过程跟服务端启动流程相似,客户端的是init、register和connect,而且客户端的启动比服务端简单,感兴趣可以自行关注。

查看接收的数据

现在已经有了第一个服务器, 我们需要测试一下: 最简单的方法是使用tenlnet命令.

但是由于目前是Discard, 所以不有任何响应, 下面改变一下channelRead.class

package com.tencent.nio.Netty;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;/** * @Author:nioliu * @DATE: 2021/9/1  19:13 */public class DiscardServerHandler extends ChannelInboundHandlerAdapter {    private Logger logger = LoggerFactory.getLogger(DiscardServerHandler.class);    /**     * 当收到信息时,此方法被调用     *     * @param ctx     * @param msg     * @throws Exception     */    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        // 忽略任何data        // ((ByteBuf) msg).release();// ByteBuf是一个引用计数的对象,必须通过release()方法显式解压。        ByteBuf in = (ByteBuf) msg;        try {            while (in.isReadable()) {                logger.info(in.toString(io.netty.util.CharsetUtil.US_ASCII));                System.out.flush();            }        } finally {            in.release();        }    }    /**     * 处理发生错误时的情况     *     * @param ctx     * @param cause     * @throws Exception     */    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {        // 关闭连接        logger.error(cause.getMessage());        ctx.close();    }}
使用telnet连接该端口
# telnet localhost 8080
连接结果

这时在键入任何信息, 均可以在服务端打印输出

键入"Ctrl+]" 进入回显模式
image-20210902095914974
点击了解更多关于Telnet的用法
open使用 openhostname 可以建立到主机的 Telnet 连接。
close使用命令 close 命令可以关闭现有的 Telnet 连接。
display使用 display 命令可以查看 Telnet 客户端的当前设置。
send : 支持以下指令使用 send 命令可以向 Telnet 服务器发送命令。
ao放弃输出命令。
ayt“Are you there”命令。
esc发送当前的转义字符。
ip中断进程命令。
synch执行 Telnet 同步操作。
brk发送信号。
写一个Echo Server

到现在为止我们的服务器还不能返还给client任何东西, 但是通常我们是需要这么做的. 下面就来学习如何通过使用 ECHO 协议写一个response message返回给客户端.

    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        // 忽略任何data        // ((ByteBuf) msg).release();// ByteBuf是一个引用计数的对象,必须通过release()方法显式解压。        // 输出收到的语句        ByteBuf in = (ByteBuf) msg;        try {            while (in.isReadable()) {                System.out.print((char) in.readByte());                System.out.flush();                //上述语句等同于//                System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII));        } finally {            // 返回给client响应            ctx.write(msg);            ctx.flush();        }    }

1.ChannelHandlerContext对象提供了不同的操作, 可以触发不同的I/O事件和操作.

调用write(Object)发给Client回应, 这里没有release()操作是因为write()把这件事干了.

2.ctx.write(Object)不会将消息写入到网络。它在内部进行缓冲,然后由ctx.flush()将其清除到连线。或者,为了简洁起见,可以调用ctx.writeAndFlush(msg)。

写一个Time Server

Time Server使用的是TIME协议, 不同于前面的例子, 该服务发送一个包含32-bit的整数message, 一旦message发送完毕就不再接收任何请求并关闭连接.

因为我们需要忽略任何接受的信息, 但在一个连接建立时要立即发送一个message, 就不能使用 channelRead()方法了. 我们需要重写channelActive()方法来完成这个任务

    /**     * 当一个连接建立并准备产生通道时, 该方法被调用     *     * @param ctx     */    @Override    public void channelActive(ChannelHandlerContext ctx){        final ByteBuf time = ctx.alloc().buffer(4);// 分配一个new buffer来写message(32-bit对应8个bytes)        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));        // 写一个结构化的信息, 但是注意:ByteBuf没有flip()方法, 因为它有两个指针:一个是用于读操作,另一个是写操作        // 当向ByteBuf写入内容时,写入器索引增加,而读取器索引没有改变。阅读器索引和写入器索引分别表示消息开始和结束的位置。        // 在Netty统统都是用这种简单的双指针来代替NIO中的flip()        final ChannelFuture f = ctx.writeAndFlush(time);// ChannelFuture表示一个I/P操作还没有发生, 这意味着,任何请求的操作可能还没有执行,因为在Netty中所有操作都是异步的        f.addListener(new ChannelFutureListener() {            /**             * 当请求完成时, 此方法被调用             * @param channelFuture             */            @Override            public void operationComplete(ChannelFuture channelFuture) {                assert f == channelFuture;                ctx.close();            }        });    }
写一个Time Client

我们需要写一个Client来传递32-bit的二进制data

在Netty中, client唯一和server不同的是: Bootstrap和Channel的使用方法

package com.tencent.nio.Netty;import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import org.slf4j.Logger;import org.slf4j.LoggerFactory;/** * @Author:nioliu * @DATE: 2021/9/2  10:36 */public class TimeClient {    private static final Logger logger = LoggerFactory.getLogger(TimeClient.class);    public static void main(String[] args) {        String host = args[0];        int port = Integer.parseInt(args[1]);        NioEventLoopGroup workerGroup = new NioEventLoopGroup();        try {            Bootstrap b = new Bootstrap();// 非服务器channel            b.group(workerGroup)                    .channel(NioSocketChannel.class)// create a client-side Channel.                    .option(ChannelOption.SO_KEEPALIVE, true)                    .handler(new ChannelInitializer<SocketChannel>() {                        @Override                        protected void initChannel(SocketChannel socketChannel) throws Exception {                            socketChannel.pipeline().addLast(new TimeClientHandler());                        }                    });            // 开启client            ChannelFuture f = b.connect(host, port);            try {                // 等待连接关闭                f.channel().closeFuture().sync();            } catch (Exception e) {                logger.error(e.getMessage());            }        } finally {            workerGroup.shutdownGracefully();        }    }}
写一个ClientHandler来处理Client接收到的消息
package com.tencent.nio.Netty;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.Date;/** * 用于接收一个32位的整数, 并将其转换成可读形式, 然后关闭连接 * * @Author:nioliu * @DATE: 2021/9/2  10:44 */public class TimeClientHandler extends ChannelInboundHandlerAdapter {    private static Logger logger = LoggerFactory.getLogger(TimeClientHandler.class);        @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        ByteBuf m = (ByteBuf) msg;        try {            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;            System.out.println(new Date(currentTimeMillis));            ctx.close();        } finally {            m.release();        }    }    /**     * 目前的handler可能会发生IndexOutOfBoundsException     * @param ctx     * @param cause     * @throws Exception     */    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {        logger.error(cause.getMessage());        ctx.close();    }}

处理一个基于流的传输

Socket Buffer的一个小警告

类似于TCP/IP的基于流的传输所接收的数据存储在一个socket receive buffer内, 这个buff个人并不是一个包队列, 二十一个bytes队列. 这就意味着尽管你发送两个独立包messages, 操作系统不会把它们当成两个messages对待, 只看成一串bytes. 因此, 不能保证你读到的信息就是remote peer所写的信息

举个例子

现在假设一个操作系统的TCP/IP栈接收到三个packets

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c85NMwpq-1630914139399)(https://github.com/djxhero/some_little_thing/raw/master/res/images/netty/1.png)]

由于基于流协议的属性, 很可能会以以下的形式对messages进行分段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m60gBf7t-1630914139399)(https://github.com/djxhero/some_little_thing/raw/master/res/images/netty/2.png)]

因此,接收部分,无论它是服务器端还是客户端,都应该将接收到的数据整理成一个或多个有意义的、应用程序逻辑容易理解的帧。在上面的例子中,接收到的数据应该如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-28svQgSH-1630914139400)(https://github.com/djxhero/some_little_thing/raw/master/res/images/netty/3.png)]

解决办法
第一个解决方案

在刚刚的TIME Client中就会发生这个错误: 一个32位的整数是个非常小的data, 不可能经常被分片. 但问题是,它可能会被碎片化,而且随着流量的增加,碎片化的可能性也会增加。

最简单的解决办法是创建一个内部累计缓冲区(internal cumulative buffer)并且直到整个4bytes都被内部累计缓冲区接收到时才停止等待

package com.tencent.nio.Netty;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.Date;/** * 用于接收一个32位的整数, 并将其转换成可读形式, 然后关闭连接 * * @Author:nioliu * @DATE: 2021/9/2  10:44 */public class TimeClientHandler extends ChannelInboundHandlerAdapter {    private ByteBuf buf;    /**     * handler的生命周期--添加时     *     * @param ctx     * @throws Exception     */    @Override    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {        buf = ctx.alloc().buffer(4);    }    /**     * handler的生命周期--移除时     *     * @param ctx     * @throws Exception     */    @Override    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {        buf.release();        buf = null;    }    private static Logger logger = LoggerFactory.getLogger(TimeClientHandler.class);    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        ByteBuf m = (ByteBuf) msg;        buf.writeBytes(m); // 所有msg都要被累计入buf        m.release();        if (buf.readableBytes() >= 4) {            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;            System.out.println(new Date(currentTimeMillis));            ctx.close();        }    }    /**     * 目前的handler可能会发生IndexOutOfBoundsException     *     * @param ctx     * @param cause     * @throws Exception     */    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {        logger.error(cause.getMessage());        ctx.close();    }}
第二个解决方案

上述解决方案并不能解决变长字段, 我们可以通过添加更多ChannelHandler到ChannelPipeline中, 因此可以将一个单片的ChannelHandler分割成多个模块化, 以降低应用程序的复杂性。例如,可以将TimeClientHandler分成两个处理程序

TimeDecoder----处理分片问题(也就是TimeClientHandler最开始的样子)

Netty提供一个可扩展类来帮助实现:

package com.tencent.nio.Netty;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import java.util.List;/** * @Author:nioliu * @DATE: 2021/9/2  11:19 */public class TimeDecoder extends ByteToMessageDecoder {// ByteToMessageDecoder类专门用来解决分片问题    /**     * 一旦有data进入, 就会调用这个方法     * @param channelHandlerContext 上下文     * @param in 进入的数据     * @param out 输出的数据     * @throws Exception     */    @Override    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {        if (in.readableBytes() < 4) {            return;        }        out.add(in.readBytes(4));    }}

现在可以将这个Handler添加到ChannelPipeline中了, 注意handler的顺序.

.handler(new ChannelInitializer<SocketChannel>() {                        @Override                        protected void initChannel(SocketChannel socketChannel) throws Exception {                            socketChannel.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());                        }                    });

If you are an adventurous person, you might want to try the ReplayingDecoder which simplifies the decoder even more. You will need to consult the API reference for more information though.

public class TimeDecoder extends ReplayingDecoder<Void> {    @Override    protected void decode(            ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {        out.add(in.readBytes(4));    }}

此外,Netty提供了开箱即用的解码器,使您能够非常容易地实现大多数协议,并帮助您避免最终使用不可维护的单片处理程序实现。更详细的例子,请参考以下包:

Speaking in POJO instead of ByteBuf

POJO对象

POJO_百度百科

POJO对象的优势有很多: 通过将从ByteBuf提取信息的代码从处理程序中分离出来,处理程序变得更易于维护和可重用。在TIME客户端和服务器示例中,我们只读取一个32位整数,直接使用ByteBuf不是主要问题。然而,在实现真实的协议时,有必要进行分离。

定义一个新类 UnixTime

package com.tencent.nio.Netty;import java.util.Date;/** * @Author:nioliu * @DATE: 2021/9/2  13:58 */public class UnixTime {    private final long value;    public UnixTime() {        this(System.currentTimeMillis() / 1000L + 2208988800L);    }    public UnixTime(long value) {        this.value = value;    }    public long value() {        return value;    }    @Override    public String toString() {        return new Date((value() - 2208988800L) * 1000L).toString();    }}

现在修改TimeDecoder来生成一个UnixTime而不是一个ByteBuf。

@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {    if (in.readableBytes() < 4) {        return;    }    out.add(new UnixTime(in.readUnsignedInt()));}

在TimeClientHandler中改变对象的解析方式

@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {    UnixTime m = (UnixTime) msg;    System.out.println(m);    ctx.close();}

同理, server handler也可以作这种转换

@Overridepublic void channelActive(ChannelHandlerContext ctx) {    ChannelFuture f = ctx.writeAndFlush(new UnixTime());    f.addListener(ChannelFutureListener.CLOSE);}

对UnixTime编码成ByteBuf在网络中传递

package com.tencent.nio.Netty;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelOutboundHandlerAdapter;import io.netty.channel.ChannelPromise;/** * @Author:nioliu * @DATE: 2021/9/2  14:10 */public class TimeEncoder extends ChannelOutboundHandlerAdapter {    @Override    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {        UnixTime m = (UnixTime) msg;        ByteBuf encoded = ctx.alloc().buffer(4);        encoded.writeInt((int) m.value());        ctx.write(encoded, promise);// 将原始ChannelPromise原样传递,以便当编码数据实际写入到网络时,Netty将其标记为成功或失败    }}/** * 简化版 MessageToByteEncoder类 *///public class TimeEncoder extends MessageToByteEncoder<UnixTime> {//    @Override//    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {//        out.writeInt((int)msg.value());//    }//}

剩下的最后一个任务是在TimeServerHandler之前将一个TimeEncoder插入到服务器端的ChannelPipeline中

.childHandler(new ChannelInitializer<SocketChannel>() {// 用于初始化Channel, 用来做一些自定义的处理配置                        @Override                        protected void initChannel(SocketChannel socketChannel) throws Exception {                            socketChannel.pipeline().addLast(new TimeEncoder(), new DiscardServerHandler());// addLast: 在处理的最后进行操作                        }                    })
关闭应用

shutdownGracefully(): 当EventLoopgroup已经完全停止并且该Group下所有的Chaneels都已经关闭后,返回一个 Future.

总结

In this chapter, we had a quick tour of Netty with a demonstration on how to write a fully working network application on top of Netty.

There is more detailed information about Netty in the upcoming chapters. We also encourage you to review the Netty examples in the io.netty.example package.

Please also note that the community is always waiting for your questions and ideas to help you and keep improving Netty and its documentation based on your feedback.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值