查看原文
其他

Netty,从这里入手更易上手(含细节分析)

IT服务圈儿 2023-02-06

The following article is from yes的练级攻略 Author 是Yes呀

作者丨是Yes呀

出品丨yes的练级攻略(ID:yes_java)


经过这个系列前面几篇的阅读,我相信你已经对 Netty 有了整体上的理解,没看过前几篇的同学建议先看了再来看这篇。

学习任何知识得先有宏观的了解,不要一下子进来就扎入到细节中,不然就会迷失方向,然后不了了之,还骂一句:擦,不是人学的。

这篇,我打算从 Netty 的引导器入手,来看看 Netty 的启动到底做了哪些工作,是如何跟底层 JDK 的 NIO 进行联动的。

我们先看一个 HTTP Server 示例,直观地感受下 Netty 的强大,然后再深入分析一波源码。

实现一个简单的 HTTP Server

这里教大家一个技巧,当你要深入学习一个开源项目的时候,把它的源码 down 下来,稍微有点规模的项目都会有 example,这里面就写了很多 demo,供我们测试和研究。

比如,我想要看看 Netty 实现一个简单 HTTP Server 的 demo ,直接找完事儿了~

通过包名划分,很轻松的就能找到。

这里为了使得演示代码更清晰,我删减了一些代码,简单的 demo 就如下所示:

不熟悉的同学看着以上的代码可能会有点懵,没事,下文我都会一个一个盘过去

这几行代码就实现了一个 HTTP Server,启动之后,只要你在浏览器上访问 localhost:8080,那么你就会获得一个典中典之:Hello World!

是吧,非常简单!接下来我们就看看,这个服务端是如何启动且实现这个功能的。

想一下,启动一个服务端需要准备什么?

首先,我们肯定需要配置线程组,需要有线程来接待(accept)新的连接,需要有线程来处理已经建连的连接上的 I/O 请求。

然后我们知道,服务端是需要有个 ServerSocket 来监听连接,那可能需要一些配置,所以需要配置一下 ServerSocket ,比如等待队列大小等等。

建连之后会得到一个子 Socket,这个就是和一个客户端的连接所对应,这可能也需要配置,比如是否keepalive 等。

对了,还有业务处理逻辑别忘了,在 Netty 中对应的就是 handler,我们都知道每个 Channel 都有属于自己的 ChannelPipeline 来编排属于自己的业务逻辑处理,因此不论是 ServerSocketChannel 还是  SocketChannel 都有属于自己的 handler 链。

至此,这些配置都差不多了,最后绑定下端口,服务端就算启动完毕啦。

现在我们将上面这段话对应到启动的代码中,来一个一个分析下。

配置线程组

之前的文章已经提到,会有 bossGroup 和 workerGroup 两个线程组,bossGroup 用来接收新连接(接活),workerGroup 负责处理已经建连的连接上的 I/O 请求(干活)。

这两个线程组都是 NioEventLoopGroup ,我们来看看 new 这个 group 到底做了哪些事情。

NioEventLoopGroup 的父类是 MultithreadEventLoopGroup,从构造函数可以得知,如果没有设置线程组内的线程数,那么默认拿当前服务器 CPU 核心数 * 2 作为线程数。

从上面的示例代码可以看到,我把 bossGroup 的线程数手动设置成 1。

那为什么要把 bossGroup 设为 1 ?多些线程处理不是更快?

一个端口只需配置一个线程监听即可,因为监听建连的操作不耗时,线程多了没用,一个线程就处理的很快了(想想 redis)。当然,如果你的应用同时开放多个端口,那么你就不能只设置一个线程了

回过来,我们接着看下面的操作,关键点我都用中文标注了:

既然是线程组,那肯定需要根据线程数创建多个线程,对应到 Netty 中就创建 NioEventLoop,一个 NioEventLoop 就是一个线程,不断 loop 发生的 Nio 事件。

那创建 NioEventLoop 的过程中,又会操作什么呢?

看到我框起来的没,也就是在新建一个 NioEventLoop 的时候,会创建一个 Selector!跟进去看一下,我就截取一小段的源码:

这个 provider 已经 Java NIO 的包了,即 SelectorProvider 类,通过这个 provider 我们就可以得到一个 selector。而  SelectorProvider 是一个抽象类,不同平台对应的实现类不同,通过不同平台的 jdk 提供不同类型的 provider  来实现跨平台。

比如我现在是 windows 下的,所以提供的实现类是 WindowsSelectorProvider:

Linux 是 EPollSelectorProvider 或者 PollSelectorProvider ,mac 下是 KQueueSelectorProvider 。

总之,我们知道创建 NioEventLoop 就会创建一个 Selector。这个 NioEventLoop 就是不断地等待 Selector 上有感兴趣的事件产生,然后进行相应的操作。

我们再回到创建选择器的那行代码,这里就有细节了,高手都是在细节上决定成败的。

这个 chooser 就是一个选择器,因为一个 group 可能会有多个 NioEventLoop ,所以为了负载均衡,让 建连完毕的 channel 均衡的分配给 group 内的 NioEventLoop ,因此弄了这个 chooser。

从代码可以看到,根据线程数的不同分别会创建两个不同的选择器实现,如果是线程数是 2 的整数次幂则会选择 PowerOfTwoEventExecutorChooser,反之选 GenericEventExecutorChooser。

我们先来看下 isPowerOfTwo 这个方法,如果是你来实现判断 2 的整数次幂,你会如何写代码?

你可能会利用循环,根据余数和商来判断:

while (true) {
 if(x == 1) {
  return true;
 }

 if(x % 2 == 1) {
  return false;
 } else {
  x = x/2;
 } 
}

如果你稍微有点算法刷题经历,你应该会用下面这行代码来实现

x & (x-1) == 0

x & (x-1)的含义是消除 x (二进制下)从右到左遇到的第一个 1,而 2 的整数次幂的二进制只有一个 1,因此被消除之后是 0 ,所以利用一行代码即可实现判断,且位运行效率更高!

而 Netty 是同样也是用一行代码来实现的:

val & -val 这个式子是只保留从右到左的第一个 1,这样一来如果 val 只有一个 1 则会保持原值不变,2 的整数次幂只会有一个1,所以通过这个式子也可以判断。

是不是长见识了?说实话,我也是第一次看到这样的判断方式,不过也很正常,毕竟用位运算的骚操作有太多了,之后有机会专门出一期位运算骚操作合集。

然后,我们再看下具体的 chooser 是如何实现的,如果是你要实现每次调用选择不同的 NioEventLoop ,你是不是就来个递增取余?我想大部分人都会写这样的代码来逐一选择,这也是普通的选择器实现:

而如果是总数设置的是 2 的整数次幂,则可以利用位运算,更快地得到结果,也就是 powerOfTwo 的实现:

以上这些就是细节,具体原理的实现,由于篇幅有限,我就不深入了,自己上网查查。

可以看出 Netty 对这种极致性能优化还是很有追求的,我们可以学学,到时候炫下技?(比如面试笔试的时候

小结下 new NioEventLoopGroup 的主要操作:

  • 创建规定数量的 NioEventLoop 保存至 children 数组中
  • 每创建一个 NioEventLoop 都会对应调用底层的 selectorProvider 获得一个 selector
  • 通过工厂创建一个选择器,用来均匀分配子 channel 给 group 内的 NioEventLoop

option 和 childOption

option 和 childOption 是用来配置 channel 的属性。

option 配置的是 ServerSocketChannel(下文称:父 channel)。

childOption 配置的是由 ServerSocketChannel 创建的 SocketChannel(下文称:子channel)。

对应的配置有挺多的,这里就不一一列举了,后面有提到再谈谈

(如果你了解 ServerSocket 和  Socket 应该对这个很容易理解)

设置 channel 类型

我们的演示代码写的是 NioServerSocketChannel.class,其实一共有 4 个 channel 的类型:

oio 就是以前的阻塞 I/O,epoll 是 linux 下的优化版 selector,kqueue 是 mac 下的。

不同的 ServerSocketChannel 就会创建对应的 SocketChannel,所以不需要有 childChannel 这样的配置。

如果要替换 I/O ,我们只需要更换 channel(xxServerSocketChannel.class)即可,非常得方便和灵活。

handler 和 childHandler

handler 是对应父 channel 的逻辑处理,也就是其加入的是父 channel 的 ChannelPipeline 中,像示例代码中,我们父 channel 的 handler 配置了 LoggingHandler,启动的时候就会输出日志。

下图就是父 channel 注册且准备完毕的日志。

childHandler 是对应子 channel 的逻辑处理,也就是其加入的是子 channel 的 ChannelPipeline 中。示例代码中,我们配置了 HttpServerCodec 和 HttpHelloWorldServerHandler 这两个 handler,也正是这两个 handler,使得我们访问localhost:8080,会得到一个:Hello World!

这里要注意,每个 channel 都会有一个属于自己独有的 ChannelPipeline。

bind

最后就是绑定端口了,而这一步才是真正的启动触发方法,前面的只是一些配置操作,在这一步才会真正的应用使得配置生效。

但 bind 方法还是有点复杂的,这篇已经 5000 多字了,所以我放到下篇再详谈 Netty 真正的启动。

最后

这篇我们大致已经把 Netty 启动需要关注的核心点都过了一遍,想必你心里应该对启动需要设置的东西已经有了概念了,然后现在再看上面一坨启动代码心里就有数了。



1、用70行Python编写一个概率编程语言

2、继承和组合,究竟我要选哪个?

3、不用描述符,不算懂 Python

4、C++ Trick:右值引用、万能引用傻傻分不清楚

5、静态代码块、构造代码块、构造函数、普通代码块,还傻傻的分不清?

点分享

点点赞

点在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存