关于netty:跟闪电侠学Netty阅读笔记-Netty入门程序解析

96次阅读

共计 10182 个字符,预计需要花费 26 分钟才能阅读完成。

引言

上一节[[《跟闪电侠学 Netty》浏览笔记 – 开篇入门 Netty]] 中介绍了 Netty 的入门程序,本节如题目所言将会一步步剖析入门程序的代码含意。

思维导图

服务端最简化代码

 public static void main(String[] args) {ServerBootstrap serverBootstrap = new ServerBootstrap();

        NioEventLoopGroup boos = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        serverBootstrap
            .group(boos, worker)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {protected void initChannel(NioSocketChannel ch) {ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println(msg);
                        }
                    });
                }
            })
            .bind(8000);
    }

两个 NioEventLoopGroup

服务端一上来先构建两个对象 NioEventLoopGroup,这两个对象将间接决定 Netty 启动之后的工作模式,在这个案例中boos 和 JDK 的 NIO 编程一样负责进行新连贯的“轮询”,他会定期检查客户端是否曾经筹备好能够接入。worker则负责解决 boss 获取到的连贯,当查看连贯有数据能够读写的时候就进行数据处理。

NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();

那么应该如何了解?其实这两个 Group 对象简略的看成是 线程池 即可,和 JDBC 的线程池没什么区别。通过浏览源码能够晓得,bossGroup只用了一个线程来解决近程客户端的连贯,workerGroup 领有的线程数默认为 2 倍的 cpu 外围数

那么这两个线程池是如何配合的?boss 和 worker 的工作模式和咱们平时下班,老板接活员工干活的模式是相似的。bossGroup 负责接待,再转交给 workerGroup 来解决具体的业务

整体概念上贴合 NIO 的设计思路,不过它要做的更好。

ServerBootstrap

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.
    .xxx()
    .xxx()

服务端疏导类是ServerBootstrap,疏导器指的是疏导开发者更不便疾速的启动 Netty 服务端 / 客户端,

这里应用了比拟经典的建造者设计模式

group 设置

.group(boos, worker)

group 办法绑定 boos 和 work 使其各司其职,这个操作能够看作是绑定线程池。

留神 gorup 办法一旦确定就意味着 Netty 的线程模型被固定了,中途不容许切换,整个运行过程 Netty 会依照代码实现计算的线程数提供服务。

上面是 group 的 api 正文:

Set the EventLoopGroup for the parent (acceptor) and the child (client). These EventLoopGroup’s are used to handle all the events and IO for ServerChannel and Channel’s.

机翻过来就是:为父(acceptor)和子(client)设置 EventLoopGroup。这些 EventLoopGroup 是用来解决 ServerChannelChannel的所有事件和 IO 的。留神这里的 Channel's 是 Netty 中的概念,初学的时候能够简略的类比为 BIO 编程的 Socket 套接字。

channel

.channel(NioServerSocketChannel.class)

设置底层编程模型或者说底层通信模式,一旦设置中途不容许中途更改。所谓的底层编程模型,其实就是 JDK 的 BIO,NIO 模型(Netty 摈弃了 JDK 的 AIO 编程模型),除此之外 Netty 还提供了本人编写的 Epoll 模型,当然日常工作中是用最多的还是 NIO 模型。

childHandler

childHandler办法次要作用是初始化和定义解决链来解决申请解决的细节。在案例代码当中咱们增加了 Netty 提供的字符串解码 handler(StringDecoder)和由 Netty 实现的 SimpleChannelInboundHandler 繁难脚手架,脚手架中自定义的解决逻辑为打印客户端发送的申请数据。

.childHandler(new ChannelInitializer<NioSocketChannel>() {protected void initChannel(NioSocketChannel ch) {ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println(msg);
                        }
                    });
                }
            })

Handler 解决一个 I / O 事件或拦挡一个 I / O 操作,并将其转发给其 ChannelPipeline 中的下一个解决 Handler,以此造成经典的解决链条。比方案例外面 StringDecoder 解码解决数据之后将会交给 SimpleChannelInboundHandlerchannelRead0办法,该办法中将解码读取到的数据打印到控制台。

借助pipeline,咱们能够定义连贯收到申请后续的数据读写细节和解决逻辑,为了不便了解,这里能够认为 NIoSocketChanne 对应 BIO 编程模型的 Socket 套接字,NioServerSocketChannel 对应 BIO 编程模型的 ServerSocket。

bind

.bind(8000)

bind 操作是一个异步办法,它会返回ChannelFuture,服务端编码中能够通过增加监听器形式在 Netty 服务端启动之后收到回调告诉进行下一步解决,也能够齐全不关怀它是否启动持续往下执行其余业务代码的解决。

Netty 的 ChannelFuture 类正文中有一个简略直观的例子介绍 ChannelFuture 的应用。

// GOOD
  Bootstrap b = ...;
  // Configure the connect timeout option.
  b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
  ChannelFuture f = b.connect(...);
  f.awaitUninterruptibly();
 
  // Now we are sure the future is completed.
  assert f.isDone();
 
  if (f.isCancelled()) {// Connection attempt cancelled by user} else if (!f.isSuccess()) {f.cause().printStackTrace();} else {// Connection established successfully}

这个过程相似里面员把外卖送到指定地点之后打电话告诉咱们。

实际:服务端启动失败主动递增端口号从新绑定端口

第一个案例是通过服务端启动失败主动递增端口号从新绑定端口。

需要

服务端启动必须要关怀的问题是指定的端口被占用导致启动失败的解决,这里的代码实际是利用 Netty 的 API 实现服务端端口在检测到端口被占用的时候 主动 + 1 重试绑定 直到所有的端口耗尽。

思路

实现思路

服务端 API 其余办法

具体介绍和解释 API 集体认为意义不大,这里仅仅对于罕用的 API 进行解释:

  • handler():代表服务端 启动过程 当中的逻辑,服务端启动代码中根本很少应用。
  • childHandler():用于指定每个新连贯数据的读写解决逻辑,相似流水线上安顿每一道工序的解决细节。
  • attr():底层实际上就是一个 Map,用户能够为服务端 Channel 指定属性,能够通过自定义属性实现一些非凡业务。(不举荐这样做,会导致业务代码和 Netty 高度耦合)
  • childAttr():为每一个连贯指定属性,能够应用 channel.attr() 取出属性。
  • option():能够为 Channel 配置 TCP 参数。

    • so_backlog:示意长期寄存三次握手申请队列(syns_queue:半连贯队列)的最大容量,如果连贯频繁解决新连贯变慢,适当扩充此参数。这个参数的次要作用是预防“DOS”攻打占用。
  • childOption():为每个连贯设置 TCP 参数。

    • TCP_NODELAY:是否开启 Nagle 算法,如果须要缩小网络交互次数倡议开启,要求高实时性倡议敞开。
    • SO_KEEPALIVE:TCP 底层心跳机制。

客户端最简化代码

客户端的启动代码如下。


public static void main(String[] args) throws InterruptedException {Bootstrap bootstrap = new Bootstrap();  
    NioEventLoopGroup eventExecutors = new NioEventLoopGroup();  
    // 疏导器疏导启动  
    bootstrap.group(eventExecutors)  
            .channel(NioSocketChannel.class)  
            .handler(new ChannelInitializer<Channel>() {  
                @Override  
                protected void initChannel(Channel channel) throws Exception {channel.pipeline().addLast(new StringEncoder());  
                }  
            });  
  
    // 建设通道  
    Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();  
  
    while (true){channel.writeAndFlush(new Date() + "Hello world");  
        Thread.sleep(2000);  
    }  
  
}

客户端代码最次要的三个关注点是:线程模型、IO 模型、IO 业务解决逻辑,其余代码和服务端的启动比拟相似。这里仍旧是从上往下一条条剖析代码。

Bootstrap

客户端连贯不须要监听端口,为了和服务端辨别间接被叫做Bootstrap,代表客户端的启动疏导器。

Bootstrap bootstrap = new Bootstrap();  

NioEventLoopGroup

Netty 中客户端也同样须要设置线程模型能力和服务端正确交互,客户端的 NioEventLoopGroup 同样能够看作是线程池,负责和服务端的数据读写解决。

NioEventLoopGroup eventExecutors = new NioEventLoopGroup();  

group

客户端 group 线程池的设置只须要设置一个即可,因为次要目标是和服务端建设连贯(只须要一个线程即可)。

.group(eventExecutors)

channel

和服务端设置同理,作用是底层编程模型的设置,官网正文中举荐应用 NIO / EPOLL / KQUEUE 这几种,应用最多的是第一种NIO

.channel(NioSocketChannel.class)

这里比拟好奇如果 用 OIO 模型的客户端连贯 NIO 的服务端会怎么样? 于是做了个试验,把如下代码改为OioServerSocketChannel(生产禁止应用,此形式已被 Deprecated),启动服务端之后启动客户端即可察看成果。

.channel(OioServerSocketChannel.class)
15:24:00.934 [main] WARN  io.netty.bootstrap.Bootstrap - Unknown channel option 'SO_KEEPALIVE' for channel '[id: 0xd0aaab57]'
15:24:00.934 [main] WARN  io.netty.bootstrap.Bootstrap - Unknown channel option 'TCP_NODELAY' for channel '[id: 0xd0aaab57]'

handler

上文介绍服务端的时候提到过 handler()代表服务端 启动过程 当中的逻辑,在这里天然就示意客户端启动过程的逻辑,客户端的 handler() 能够间接看作服务端疏导器当中的childHandler()

这里读者可能会好奇那我客户端代码用 childHandler 呢?答案是 Netty 为了避免使用者误会 Bootstrap 中只有handler,所以咱们能够间接等同于服务端的childHandler()

吐槽:这个 child 不 child 的 API 名称看的比拟蛋疼,不加以辨别有时候的确容易用错。这里生活化了解服务端中的 childHandler 是身上带了连贯,所以在连贯胜利之后会调用,没有 child 则代表此时没有任何连贯,所以会发送在初始化的时候调用。
而客户端为什么只保留 handler() 呢?集体了解是客户端最关注的是连贯上服务端之后所做的解决,减少初始化的时候做解决没啥意义,并且会导致设计变简单。

handler 外部是对于 Channel 进行初始化并且增加 pipline 自定义客户端的读写逻辑。这里同样增加 Netty 提供的 StringEncoder 默认会是用字符串编码模式对于发送的数据进行编码解决。

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

最初 ChannelInitializer 能够间接类比 SocketChannel。

connect

当配置都筹备好之后,客户端的最初一步是启动客户端并且和服务端进行 TCP 三次握手建设连贯。这里办法会返回 Channel 对象,Netty 的 connect 反对异步实现。

Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();  

connect 是一个异步办法,能够通过给返回的 channel 对象调用 addListner 增加监听器,在 Netty 的客户端胜利和服务端建设连贯之后会回调相干办法告知监听器所有数据筹备实现。

除了监听连贯是否胜利之外,还能够是用监听器对于连贯失败的状况做自定义解决逻辑,比方上面的例子将会介绍利用监听器实现客户端连贯服务端失败之后,定时主动重连服务端屡次直到重连次数用完的例子。

实际:客户端失败重连

第二个实际代码是客户端在连贯服务端的时候进行失败重连。失败重连在网络环境较差的时候非常无效,然而须要留神这里的代码中多次重试会逐步减少工夫距离。

客户端失败重连的整体代码如下:

private static void connect(Bootstrap bootstrap, String host, int port, int retry) {bootstrap.connect(host, port).addListener(future -> {if (future.isSuccess()) {System.out.println(new Date() + ": 连贯胜利,启动控制台线程……");  
            Channel channel = ((ChannelFuture) future).channel();  
            startConsoleThread(channel);  
        } else if (retry == 0) {System.err.println("重试次数已用完,放弃连贯!");  
        } else {  
            // 第几次重连  
            int order = (MAX_RETRY - retry) + 1;  
            // 本次重连的距离  
            int delay = 1 << order;  
            System.err.println(new Date() + ": 连贯失败,第" + order + "次重连……");  
            bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit  
                    .SECONDS);  
        }  
    });  
}

private static void startConsoleThread(Channel channel) {ConsoleCommandManager consoleCommandManager = new ConsoleCommandManager();  
    LoginConsoleCommand loginConsoleCommand = new LoginConsoleCommand();  
    Scanner scanner = new Scanner(System.in);  
  
    new Thread(() -> {while (!Thread.interrupted()) {if (!SessionUtil.hasLogin(channel)) {loginConsoleCommand.exec(scanner, channel);  
            } else {consoleCommandManager.exec(scanner, channel);  
            }  
        }  
    }).start();}

退出失败重连代码之后,客户端的启动代码须要进行稍微调整,在链式调用中不再应用间接connection,而是传递疏导类和相干参数,通过递归的形式实现失败重连的成果:

connect(bootstrap, "127.0.0.1", 10999, MAX_RETRY);

客户端 API 其余办法和相干属性

attr()

三种 TCP 关联参数

SO_KEEPALIVE

对应源码定义如下:

public static final ChannelOption<Boolean> SO_KEEPALIVE = valueOf("SO_KEEPALIVE");

次要为关联 TCP 底层心跳机制。

TCP_NODELAY

Nagle 算法解释

这个参数的含意是:是否开启 Nagle 算法。首先须要留神这个参数和 Linux 操作系统的默认值不一样,true 传输到 Linux 是敞开调 Nagle 算法。

Nagele 算法的呈现和以前的网络带宽资源无限无关,为了尽可能的利用网络带宽,TCP 总是心愿尽可能的发送足够大的数据,Nagle 算法就是为了尽可能发送大块数据,防止网络中充斥着许多小数据块

为了了解 Nagle 算法,咱们须要理解 TCP 的缓冲区通常会设置 MSS 参数。

MSS 参数:除去 IP 和 TCP 头部之后,一个网络包所能包容的 TCP 数据的最大长度;最大 1460。
MTU:一个网络包的最大长度,以太网中个别为 1500 字节;

为什么最大为 1460 个字节?因为 TCP 传输过程中都会要求绑定 TCP 和 IP 的头部信息,这样服务端能力回送 ACK 确认收到包正确。

也就是说传输大数据包的时候,数据会依照 MSS 的值进行切割。回到 Nagle 算法,它的作用就是定义任意时刻只能有一个未被 ACK 确认的小段(MSS 对应切割的一个块)。

这就意味着当有多个未被 ACK 确认的小段的时候,此时 client 端会小小的提早一下期待合并为更大的数据包才发送。

Netty 默认敞开了这个算法,意味着一有数据就了解发送,满足低提早和高并发的设计。

Netty 源码关联

TCP_NODELAY 配置选项定义如下:

public static final ChannelOption<Boolean> TCP_NODELAY = valueOf("TCP_NODELAY");

此参数的配置介绍能够从 SocketChannelConfig 关联的配置中获取。

/**  
 * Gets the {@link StandardSocketOptions#TCP_NODELAY} option.  Please note that the default value of this option  
 * is {@code true} unlike the operating system default ({@code false}). However, for some buggy platforms, such as  
 * Android, that shows erratic behavior with Nagle's algorithm disabled, the default value remains to be * {@code false}.  
 */
 boolean isTcpNoDelay();

正文翻译如下。

获取 StandardSocketOptions.TCP_NODELAY 配置。请留神,该选项的默认值为 true,与操作系统的默认值(false)不同。然而,对于一些有问题的平台,比方 Android,在禁用 Nagle 算法的状况下会呈现不稳固的行为,默认值依然为 false。

CONNECTION_TIMEOUT

示意连贯超时工夫,单位为毫秒。

客户端和服务端通信

本局部能够参考作者代码,这里仅仅用笔记归档一下大抵代码编写思路。

https://github.com/lightningMan/flash-netty

客户端写入数据到服务端

  • handler 办法:指定客户端通信解决逻辑
  • initChannel 办法:给客户端增加逻辑处理器
  • pipeline:逻辑解决链增加逻辑处理器

    • addLast 增加自定义 ChannelHandler
    • 逻辑处理器继承自 ChannelHandler

      • 笼罩 channelActive()办法
      • 客户端连贯建设胜利提醒打印
    • 逻辑处理器能够通过继承适配类 ChannelInboundHandlerAdapter 实现简化开发
  • 写数据局部 ByteBuf (Netty 实现)

      1. alloc 取得内存管理器
      1. byte[] 数据填充二进制数据
      1. writeAndFlush 刷缓存

服务端读取客户端数据

  • 逻辑处理器继承适配类

    • 逻辑处理器能够通过继承适配类 ChannelInboundHandlerAdapter 实现简化开发
  • 接收数据和服务端读取数据相似
  • 构建 ByteBuf

      1. alloc 取得内存管理器
      1. byte[] 数据填充二进制数据
      1. writeAndFlush 刷缓存
  • 通过 writeAndFlush 写出数据给客户端

服务端返回数据给客户端

  • 逻辑处理器继承适配类

    • 逻辑处理器能够通过继承适配类 ChannelInboundHandlerAdapter 实现简化开发
  • 接收数据和服务端读取数据相似
  • 构建 ByteBuf

      1. alloc 取得内存管理器
      1. byte[] 数据填充二进制数据
      1. writeAndFlush 刷缓存
  • 通过 writeAndFlush 写出数据给客户端

客户端读取服务端数据

  • 和服务端读取客户端数据思路相似
  • 要害是须要笼罩channelRead() 办法

外围概念

  • Netty 当中,childHanlder 和 handler 对应 客户端 服务端解决 逻辑
  • ByteBuf 数据载体,相似隧道两端的两头小推车。JDK 官网实现 java.nio.ByteBuffer 存在各种问题,Netty 官网从新实现了io.netty.buffer.ByteBuf
  • 服务端读取对象根本单位为Object,如果须要读取其余对象类型通常须要强转。
  • 逻辑处理器都能够通过继承适配器实现,客户端和服务端笼罩对应办法实现被动接管或者被动推送。

问题

客户端 API 比照服务端少了什么内容?

  1. “group”。
  2. 客户端只有childHandler

新连贯接入时候,如何实现服务端被动推送音讯,而后客户端进行回复?

答案是增加监听器,在监听到客户端连贯胜利之后间接被动推送自定义信息。

handler()和 childHandler()有什么区别

初学者比拟容易困扰的问题。handler()childHandler() 的次要区别是:handler()是产生在 初始化的时候 childHandler() 是产生在 客户端连贯之后

“知其所以然”的局部放到后续的源码剖析笔记当中,这里临时跳过,首次浏览只须要记住论断即可。

八股

BIO 的 Socket 和 NIO 的 SocketChannel 区别

实质上都是客户端和服务端进行网络通信的连贯的一种形象,然而应用上有不小的区别。上面的内容摘录自参考资料:

Socket、SocketChannel 区别:
https://blog.csdn.net/A350204530/article/details/78606298

https://segmentfault.com/q/1010000011974154

正文完
 0