网络通信的开发,就涉及到一些开发框架:Java NIO
、Netty
、Mina
等等。
理论上来说,类似于序列化器,可以为其定义一套统一的接口,让不同类型的框架实现,事实上,Dubbo
就是这么干的。
但是,作为一个简单的 RPC 框架,ccx-rpc
就先不统一了,因为基本上网络框架是不会换的,而且统一起来代码量巨大。
ccx-rpc
选择的网络框架是 Netty
,Netty
是一款大名鼎鼎的异步事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。
Netty
在 JDK 自带的 NIO
基础之上进行了封装,解决了 JDK 自身的一些问题,具备如下优点:
下面我们先来介绍一下 Netty
的核心设计吧。
服务收到请求之后,执行的逻辑大致有:编解码、消息派发、业务处理以及返回响应。这些逻辑是放到一个线程串行执行,还是分配到不同线程中执行,会对程序的性能产生很大的影响。优秀的线程模型对一个高性能网络库来说是至关重要的。
Netty 采用了 Reactor 线程模型的设计。
Wikipedia 的定义是:
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.
从上面的定义可以看出有几个重点:
Reactor 线程模型有几个角色:
简单来说,其核心原理是 Reactor 负责监听事件,在监听到事件之后,分发给相关线程的处理器进行处理。
我们先来看看传统阻塞 I/O 模型的缺点:
针对传统阻塞 I/O 模型的两个问题,可以采用如下的方案
Reactor 线程模型的思想就是 线程池 和 I/O复用 的结合。
为了帮助你更好地了解 Netty 线程模型的设计理念,我们将从最基础的单 Reactor 单线程模型开始介绍,然后逐步增加模型的复杂度,最终到 Netty 目前使用的非常成熟的线程模型设计。
Reactor 对象监听客户端请求事件,收到事件后通过进行分发。
具体情况如下图所示:
单 Reactor 单线程的优点就是:线程模型简单,没有引入多线程,自然也就没有多线程并发和竞争的问题。
但其缺点也非常明显,那就是性能瓶颈问题,一个线程只能跑在一个 CPU 上,能处理的连接数是有限的,无法完全发挥多核 CPU 的优势。一旦某个业务逻辑耗时较长,这唯一的线程就会卡在上面,无法处理其他连接的请求,程序进入假死的状态,可用性也就降低了。正是由于这种限制,一般只会在客户端使用这种线程模型。
其流程跟 "单 Reactor 单线程" 的流程差不多,也是 Acceptor 处理连接事件,Handler 处理读写事件。
唯一的区别就是:Handler 处理请求的时候,使用的是 线程池 来处理。
很明显,单 Reactor 多线程的模型可以充分利用多核 CPU 的处理能力,提高整个系统的吞吐量,但引入多线程模型就要考虑线程并发、数据共享等问题。
在这个模型中,只有一个线程来处理 Reactor 监听到的所有 I/O 事件,其中就包括连接建立事件以及读写事件,当连接数不断增大的时候,这个唯一的 Reactor 线程也会遇到瓶颈。
为了解决单 Reactor 多线程模型中的问题,我们可以引入多个 Reactor。
Netty 同时支持上述几种线程模式,Netty 针对服务器端的设计是在主从 Reactor 多线程模型的基础上进行的修改,如下图所示:
Netty 抽象出两组线程池:BossGroup 专门用于接收客户端的连接,WorkerGroup 专门用于网络的读写。
BossGroup 里的线程 会监听连接事件,与客户端建立网络连接后,生成相应的 NioSocketChannel 对象,表示一条网络连接。之后会将 NioSocketChannel 注册到 WorkerGroup 中某个线程上。
WorkerGroup 里的线程会监听对应连接上的读写事件,当监听到读写事件的时候,会通过 Pipeline 添加的多个处理器进行处理,每个处理器中都可以包含一定的逻辑,例如编解码、心跳、业务逻辑等。
介绍完 Netty 优秀的线程模型设计,接下来开始介绍 Netty 的核心组件。
在前面介绍 Netty 线程模型的时候,提到 BossGroup 和 WorkerGroup,他们就是 EventLoopGroup,一个 EventLoopGroup 当中会包含一个或多个 EventLoop,EventLoopGroup 提供 next 接口,可以从一组 EventLoop 里面按照一定规则获取其中一个 EventLoop 来处理任务。EventLoop 从表面上看是一个不断循环的线程。
EventLoop 最常用的实现类是:NioEventLoop,一个 NioEventLoop 包含了一个 Selector 对象, 可以支持多个 Channel 注册在其上,该 NioEventLoop 可以同时服务多个 Channel,每个 Channel 只能与一个 NioEventLoop 绑定,这样就实现了线程与 Channel 之间的关联。
EventLoop 并不是一个纯粹的 I/O 线程,它除了负责 I/O 的读写之外,还兼顾处理以下两类任务:
execute(Runnable task)
方法实现,Netty 有很多系统任务,当 I/O 线程和用户线程同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成任务放入消息队列中,由 I/O 线程负责执行,这样就实现了局部无锁化。schedule(Runnable command, long delay, TimeUnit unit)
方法实现。Channel 是 Netty 对网络连接的抽象,核心功能是执行网络 I/O 操作,是服务端和客户端进行 I/O 数据交互的媒介。
工作流程:
上面介绍 Channel 的时候提到,如果是读事件,则通过 Pipeline 来处理。一个 Channel 对应一个 Pipeline,一个 Pipeline 由多个 Handler 串成一个有序的链表,一个 Handler 处理完,调用 next 获得下一个 Handler 进行处理。
上图黄色部分即为 Handler,一个 Handler 可以是 Inbound、OutBound。处理入站事件时,Handler 按照正向顺序执行。处理出站事件时,Handler 按照反向顺序执行。
常规 Pipeline 的 Handler 注册代码如下:
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// 30 秒之内没有收到客户端请求的话就关闭连接
p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
// 编解码器
p.addLast(new NettyEncoder());
p.addLast(new NettyDecoder());
// RPC 消息处理器
p.addLast(serviceHandlerGroup, new NettyServerHandler());
}
}
在进行跨进程远程交互的时候,我们需要以字节的形式发送和接收数据,发送端和接收端都需要一个高效的数据容器来缓存字节数据,ByteBuf 就扮演了这样一个数据容器的角色。
ByteBuf 类似于一个字节数组,其中维护了一个读索引(readerIndex
)和一个写索引(writerIndex
),分别用来控制对 ByteBuf 中数据的读写操作。还有一个capacity
用来记录缓冲区的总长度,当写数据超过 capacity
时,ByteBuf 会自动扩容,直到 capacity
达到 maxCapacity
。
ByteBuf 的结构如下:
Netty 中主要分为以下三大类 ByteBuf:
Unpooled.buffer()
或者 ctx.alloc().buffer()
Unpooled.directBuffer()
或者 ctx.alloc().directBuffer()
Unpooled.compositeBuffer()
或者 ctx.alloc().compositeBuffer()
Netty 使用 ByteBuf 对象作为数据容器,进行 I/O 读写操作,其实 Netty 的内存管理也是围绕着 ByteBuf 对象高效地分配和释放。从内存管理角度来看,ByteBuf 可分为 Unpooled 和 Pooled 两类。
Unpooled.buffer()
ByteBufAllocator byteBufAllocator = ctx.alloc()
,然后再生成 ByteBuf 对象:byteBufAllocator.buffer()`最后,我们来总结一下 ByteBuf 的优点:
上面我们介绍了 Netty 优秀的线程模型和核心组件,Netty 优秀的设计还有很多,感兴趣的读者可以再去深入了解,以上介绍的已经够写一个 RPC 框架了。
接下来,我们就要讲到网络通信的核心实现了,敬请期待!
ccx-rpc 代码已经开源
Github:https://github.com/chenchuxin/ccx-rpc
Gitee:https://gitee.com/imccx/ccx-rpc
原文:https://www.cnblogs.com/chenchuxin/p/15221478.html