引言
上一节[[《跟闪电侠学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重试绑定直到所有的端口耗尽。
思路
实现思路
服务端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实现)
- alloc取得内存管理器
- byte[] 数据填充二进制数据
- writeAndFlush 刷缓存
服务端读取客户端数据
逻辑处理器继承适配类
- 逻辑处理器能够通过继承适配类
ChannelInboundHandlerAdapter
实现简化开发
- 逻辑处理器能够通过继承适配类
- 接收数据和服务端读取数据相似
构建ByteBuf
- alloc取得内存管理器
- byte[] 数据填充二进制数据
- writeAndFlush 刷缓存
- 通过
writeAndFlush
写出数据给客户端
服务端返回数据给客户端
逻辑处理器继承适配类
- 逻辑处理器能够通过继承适配类ChannelInboundHandlerAdapter实现简化开发
- 接收数据和服务端读取数据相似
构建ByteBuf
- alloc取得内存管理器
- byte[] 数据填充二进制数据
- writeAndFlush 刷缓存
- 通过writeAndFlush写出数据给客户端
客户端读取服务端数据
- 和服务端读取客户端数据思路相似
- 要害是须要笼罩channelRead() 办法
外围概念
- Netty当中,childHanlder 和 handler 对应客户端和服务端解决逻辑
- ByteBuf 数据载体,相似隧道两端的两头小推车。JDK官网实现
java.nio.ByteBuffer
存在各种问题,Netty官网从新实现了io.netty.buffer.ByteBuf
。 - 服务端读取对象根本单位为Object,如果须要读取其余对象类型通常须要强转。
- 逻辑处理器都能够通过继承适配器实现,客户端和服务端笼罩对应办法实现被动接管或者被动推送。
问题
客户端API比照服务端少了什么内容?
- “group”。
- 客户端只有
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