认识Netty
1.引言
在单体应用下,因为不同业务是部署在同一个进程中的,当我们需要调用依赖业务的时候我们只需要用 本地方法调用
即可。随着业务的逐渐庞大和访问量的增多等诸多问题的出现,我们会将应用按业务的不同或者公共依赖等方式拆分成多个服务后构建出分布式或者微服务的架构,这时不同的服务会部署在不同的进程中,服务间就不能使用本地方法进行依赖服务的调用了。 那么服务间如何调用就成为了服务拆分后需要解决的公共关注点问题之一。为了区分本地方法调用,我们也将服务间的调用称为 远程方法调用
。
服务间的远程方法调用,顾名思义其中 服务间的远程调用
涉及了一个做为客户端,一个做为服务端,以及客户端和服务端之间的交互。为了解决服务间如何调用,我们就需要解决好几个问题:如何建立服务间的网络连接?连接建立好了数据如何传输?接收方如何处理接收到的数据?当我们能够解决好这几个问题时,服务间调用的问题也就基本上解决了。
2.如何解决这些问题即引出Netty
在引言中有提到 服务间的远程调用
面临的问题,我们在解决这类问题时有两种思路:自己搭建一套网络通信方案、使用开源项目。我会根据这两种思路进行分别介绍。
首先先说自己搭建一套网络通信方案,做为一名JAVA工程师,第一种方案:我们会使用一对Socket套接字来实现基于同步阻塞式的IO「也称之为BIO」
。下面这张图是使用BIO来实现网络通信的简图,从图中可以看出之所以称为同步阻塞,其中同步的点在于:客户端或者服务端在获取数据时需要主动进行获取,阻塞的点在于:服务端需要阻塞等待客户端发起连接,需要阻塞等待客户端发送数据。
第二种方案:使用JDK的NIO来实现基于同步非阻塞式的IO「也称之为NIO」
用来处理客户端和服务端连接的建立和交互。JDK的NIO提供了三大组件:Buffer、Channel、Selector来实现IO多路复用「一个线程处理多个客户端连接和请求」
的能力。下面这张图是使用NIO来实现网络通信的简图,我们把主要关注点关注在Server端。从图中可以看出与同步阻塞的区别:服务端不需要在阻塞等待客户端发起连接和发送数据了,而是非阻塞事件驱动的方式通过监听一个一个的事件来处理客户端的请求。另外一方面可以看出NIO模型相较于BIO会复杂许多。
第三种方案:使用JDK的AIO来实现基于异步非阻塞式的IO,这种方式不是本文的重点,后文也不在阐述,感兴趣的同学可以自行百度。
再说后者使用开源项目,目前市面上主要流行的通信框架有:Netty、Mina。Mina和Netty的作者实际上是同一个人,Mina不是本文介绍的重点,感兴趣的同学可以自行百度。在上文中介绍了最原始的JDK实现自己的网络通行框架,终于引出本文的主题 Netty
,后面我将会从以下几个方面介绍Netty:Netty是什么?Netty能做什么?Netty去哪下?Netty怎么玩?Netty相关组件有哪些?Dubbo是如何使用Netty的?那么接下来让我们直接进入主题吧。
3.Netty入门
3.1.Netty是什么
这里我用Netty官方的描述来简单的说明:Netty是一个基于JAVA的NIO的通信框架并提供了异步、事件驱动、多协议支持等能力,可用于快速、简单地开发网络应用程序「包括客户端和服务端」
。
3.2.Netty能做什么
正如前面所谈及为了解决服务间可以相互调用,我们需要构建服务间的相互通信。我们完全可以用Netty来实现服务间的连接建立与通信。目前在朴朴使用的技术栈:Dubbo、ES、Redisson、Spark也使用了Netty做为底层的通信框架,并且Netty也是Dubbo默认的底层通信框架
3.3.Netty从哪下
从官方的角度,有三种方式可以去了解Netty和下载Netty相关的源码:
- 官网:Netty。
官网也提供了相关的用户指南可作为入门文档:User Guide
- github:Netty
- 书籍:《Netty IN ACTION》由Netty主要开发者编写
Netty目前的主要版本系列是4.1.X,我们关注这个系列即可。3.X和4.1.X有很大区别,就连包名都完全不一样了,5.X已被Netty的相关开发人员放弃了,感兴趣的同学可以自行百度了解具体原因。
3.4.Netty怎么玩
在介绍这章内容前,我们有必要先来了解一下Netty的架构图,以便后续我们更好的理解Netty的相关内容。下图就是用Netty来实现服务端的架构图(摘自百度图库)。我们先关注一下图中的重点字眼Selector
、accept
、select
、channel
、SelectedKey
、read
、write
,我们对这些字眼是不是有似曾相识的感觉「印象模糊的同学可以回到2.中描述的第二种方案」
。下面我会结合这张图与对应的代码对Netty的交互流程简单通俗的介绍:
Netty架构图对应的简单Netty代码如下,代码中涉及的类可以和上图中的关键字眼相对应起来,便于理解:
public void start() throws Exception {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(8080))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
new EchoServerHandler());
}
});
ChannelFuture f = bootstrap.bind().sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully().sync();
bossGroup.shutdownGracefully().sync();
}
}
- 首先服务端启动后会创建两个类分别是BossGroup和WorkerGroup,我们先把它们理解为两个不同的线程池即可,只不过线程池中的线程是用来处理网络连接和读写请求的,不过还是需要额外提一下,BossGroup只负责处理客户端连接请求,WorkerGroup负责处理客户端的读写请求
- 创建一个服务端的引导启动类ServerBootStrap,并通过构建者模式往其中填充一些属性,重点关注Channel和ChannelHandler,由于是服务端并且我们使用的是NIO的通信方式所以我们使用的是NIOServerSocketChannel
「如果想使用BIO的方式可修改为OIOServerSocketChannel,BIO不是本文的重点不在赘述」
。在ChannelHandler中我们获取到了SocketChannel,并进一步通过SocketChannel获取到了它的Pipeline,并向其中添加了一个EchoServerHandler「用来处理实际业务的代码」
,这段代码会在有新连接建立时进行回调的,我们有个印象就好 - 在创建好ServerBootStrap后,我们调用了bind方法,bind方法实际上会去做很多事情,我们只需要知道:将服务端绑定好端口,并将BossGroup线程池中的线程用来监听连接
- 后续Client发起连接后,Netty服务端会在BossGroup中的某一个线程的Select通过轮询出该连接请求后,并获取到客户端连接SocketChannel,往WorkerGroup中的某一线程的Selector注册read事件,用于后续该线程轮询并处理该客户端的写请求
- WorkerGroup中的某一线程接收到了客户端的写请求时,该线程会轮询到该事件,并将事件分发到与当前客户端连接SocketChannel关联的Pipeline中,Pipeline存储这一个一个的ChannelHandler,写请求事件会在Pipeline中的ChannelHandler向火炬一样向后流转并处理,这里用到了责任链的模式
其中3-5是Netty服务端处理数据的简单流程,我们可以结合
2.第三种方案
和Netty架构图
进行更深入的理解
注意因为Netty提供了异步的能力,但是有些情况下我们又不得不异步转同步等待。在上面代码中我们可以看到了bind的方法是异步的,我们需要主动调用sync,如果我们在这里没有调用sync的话,会导致后面的代码出错,异常退出程序的
3.5.Netty相关组件
在对Netty的整体架构有了一定的了解之后,现在我对架构图中涉及的一些重点关键字眼进行一一简单介绍,希望大家可以对Netty相关组件有更深的认识以便后续更好的学习,那下面让我们开始吧:
- NioEventLoopGroup
内部维护了一组线程「可以称之为IO线程」
,默认线程个数为当前机器CPU核数*2。每一个线程会绑定一个Selector理解为就是JDK的NIO中的Selector选择器
,用来轮询发生的事件 - Channel
可以分为ServerSocketChannel和SocketChannel,在Netty中作为服务端使用的ServerSocketChannel会与BossGroup进行绑定,并用来接收客户端的连接。服务端接收到客户端的每一个连接都是一个SocketChannel,每一个SocketChannel会交给WorkGroup中的线程进行绑定 - Pipeline
每一个SocketChannel都会有一个Pipeline「也称为ChannelPipeline」
。WorkerGroup中轮询到的SocketChannel的某个事件会交由Pipeline进行处理 - ChannelHandler
Pipiline可以看作为一个链表,链表中存储的数据阈实际上是一个一个的ChannelHandler「实际会封装成ChannelHandlerContext」
,在Pipeline接收到WorkerGroup传递过来的事件后会通过内部存储的一个一个ChannelHandler向后传递,直到数据被ChannelHandler中的方法进行处理。我们通常所说的Netty是基于事件驱动的模型,实际上就是当事件发生后,ChannelHandler中的方法会被回调进行数据的处理,举个例子:ChannelHandler中的ChannelRead方法就是用来处理服务端接收到的客户端发送的数据
这里需要单独说明一下Pipeline中数据流向的相关问题,这里我都是针对服务端对象来说的:
- 从客户端向服务端发送的事件:比如连接请求、写请求我们可以称之为
入站
事件 - 从服务端向客户端发送的数据的过程,我们称之为
出站
事件
介绍完组件后,现在我把它们之间的关系关联起来,可以得到下面这张图:
Netty的组件关系
3.6.在Dubbo中是如何使用Netty的
通过Netty的相关描述,想必大家对Netty的事件轮询,以及Netty是如何处理事件的机制有了一定认识。在这里我还想在提一点Netty上使用的注意事项:经常可以听到不要在ChannelHandler中做一些耗时的事情
。那么是为什么?究其原因,在上面描述Netty组件的过程中有说道,当NioEventLoopGroup中的线程「即IO线程」
轮询到事件后,会将事件派发给Pipeline中的ChannelHandler进行处理,此时的处理还是在IO线程上进行,如果这个处理需要非常耗时,势必会影响IO线程轮询下一个事件。此时可以考虑转异步线程池中的线程去处理,我们也将这个异步线程池称之为业务线程
。
目前朴朴在服务间的调用使用的是Dubbo框架,针对上述的描述,大家是否已经联想到了Dubbo框架中是如何解决上面这个问题的呢。我在这里给个提示点:在Dubbo中提出了多种的线程模型可供选择,以及提供了对应的业务线程池来解决这里提到的Netty注意事项。对Dubbo这块内容想更深入了解的同学可以自行百度。
4.总结
本编文章从服务化的拆分导致的业务间调用的差异开始,并引出这种差异所带来的问题。为了解决问题,我们了解了原生JDK的解决方案,以及使用开源项目来解决。并重点讲解了目前火热的通信框架Netty,当然随着Netty的发展,Netty的知识体系也是非常庞大的,本文篇幅有限不可能面面俱到,还有很多基础知识「比如Reactor模式、Netty的编解码,拆包粘包、用户事件等」
在本文并没有谈及到,因为本文意在为大家在网络通信这一块扫盲,我也非常期待后续能够还有机会给大家进一步分享Netty。感兴趣的同学可以通过上述在3.3中提供的方式进一步了解Netty