引言
上一节[[《跟闪电侠学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
是用来处理ServerChannel
和Channel
的所有事件和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解码处理数据之后将会交给SimpleChannelInboundHandler
的channelRead0
方法,该方法中将解码读取到的数据打印到控制台。
借助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重试绑定直到所有的端口耗尽。
思路
实现代码如下:
public class NettyServerStart { public static void main(String[] args) { ServerBootstrap serverBootstrap = new ServerBootstrap(); NioEventLoopGroup boss = new NioEventLoopGroup(); NioEventLoopGroup worker = new NioEventLoopGroup(); int port = 10022; serverBootstrap .group(boss, worker) .channel(NioServerSocketChannel.class) .handler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { // 指定服务端启动过程的一些逻辑 System.out.println("服务端启动当中"); } }) // 指定自定义属性,客户端可以根据此属性进行一些判断处理 // 可以看作给Channel维护一个Map属性,这里的channel是服务端 // 允许指定一个新创建的通道的初始属性。如果该值为空,指定键的属性将被删除。 .attr(AttributeKey.newInstance("hello"), "hello world") // 给每个连接指定自定义属性,Channel 进行属性指定等 // 用给定的值在每个 子通道 上设置特定的AttributeKey。如果该值为空,则AttributeKey将被删除。 // 区别是是否是 子channel,子Channel代表给客户端的连接设置 .childAttr(AttributeKey.newInstance("childAttr"), "childAttr") // 客户端的 Channel 设置TCP 参数 // so_backlog 临时存放已完成三次握手的请求队列的最大长度,如果频繁连接可以调大此参数 .option(ChannelOption.SO_BACKLOG, 1024) // 给每个连接设置TCP参数 // tcp的心跳检测,true为开启 .childOption(ChannelOption.SO_KEEPALIVE, true) // nagle 算法开关,实时性要求高就关闭 .childOption(ChannelOption.TCP_NODELAY, true) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.err.println(msg); } }); } }); bind(serverBootstrap, port); } /** * 自动绑定递增端口 * @param serverBootstrap * @param port */ public static void bind(ServerBootstrap serverBootstrap, int port){ serverBootstrap.bind(port).addListener(future -> { if(future.isSuccess()){ System.out.println("端口绑定成功"); System.out.println("绑定端口"+ port +"成功"); }else{ System.out.println("端口绑定失败"); bind(serverBootstrap, port+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业务处理逻辑,其他代码和服务端的启动比较类似。这里依旧是从上往下一条条分析代码。
《跟闪电侠学Netty》阅读笔记 - Netty入门程序解析(二)https://developer.aliyun.com/article/1395287