目录
3.6.1.?ByteBufAllocator - 抽象工厂模式
3.6.2.?CompositeByteBuf - 组合模式
3.6.3.?ByteBufInputStream - 适配器模式
3.6.4.?ReadOnlyByteBuf - 装饰器模式
3.13.1.?固定长度的拆包器 - FixedLengthFrameDecoder
3.13.2.?行拆包器 - LineBasedFrameDecoder
3.13.3.?分隔符拆包器 - DelimiterBasedFrameDecoder
3.13.4.?基于长度域拆包器 - LengthFieldBasedFrameDecoder
?
? ? Netty 是一款卓越的 Java框架,提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。它用较简单的抽象,隐藏 Java 网络编程底层实现的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
下表总结了 Java IO 和 NIO 之间的主要区别:
IO | NIO |
---|---|
面向流 | 面向缓冲 |
阻塞 IO | 非阻塞 IO |
无 | 选择器 |
传统 IO 和 Java NIO 最大的区别是传统的 IO 是面向流,NIO 是面向 Buffer。
Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
Java NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
? ? Java NIO 的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道,这些通道里已经有可以处理的输入,或者选择已经准备写入的通道,这种选择机制,使得一个单独的线程很容易来管理多个通道。
传统的 IO:
NIO:
JDK 原生也有一套网络应用程序 API,但是存在一系列问题,主要如下:
? ? Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。
Netty 的主要特点有:高性能、可靠性、可定制性、可扩展性。
1.?高性能
2. 可靠性
1>. 链路有效监测(心跳和空闲检测)
2>.?内存保护机制
3>.?优雅停机
3. 可定制性
4. 可扩展性
可以方便进行应用层协议定制,比如 Dubbo、RocketMQ。
? ? 对于网络请求一般可以分为两个处理阶段,一是接收请求任务,二是处理网络请求。根据不同阶段处理方式分为以下几种线程模型:
这个模型中用一个线程来处理网络请求连接和任务处理,当 worker 接受到一个任务之后,就立刻进行处理,也就是说任务接受和任务处理是在同一个 worker 线程中进行的,没有进行区分。这样做存在一个很大的问题是,必须要等待某个 task 处理完成之后,才能接受处理下一个 task。
因此可以把接收任务和处理任务两个阶段分开处理,一个线程接收任务,放入任务队列,另外的线程异步处理任务队列中的任务。
由于任务处理一般比较缓慢,会导致任务队列中任务积压长时间得不到处理,这时可以使用线程池来处理。可以通过为每个线程维护一个任务队列来改进这种模型。
1. 如何理解 NioEventLoop 和 NioEventLoopGroup
2. 每个 NioEventLoop 都绑定了一个 Selector,所以在 Netty 的线程模型中,是由多个 Selector 在监听 IO 就绪事件。而 Channel 注册到 Selector。
3.?一个 Channel 绑定一个 NioEventLoop,相当于一个连接绑定一个线程,这个连接所有的ChannelHandler 都是在一个线程中执行的,避免了多线程干扰。更重要的是 ChannelPipline 链表必须严格按照顺序执行的。单线程的设计能够保证 ChannelHandler 的顺序执行。
4.?一个 NioEventLoop 的 selector 可以被多个 Channel 注册,也就是说多个 Channel 共享一个EventLoop。EventLoop 的 Selecctor 对这些 Channel 进行检查。
? ? Server 端启动时绑定本地某个端口,将自己 NioServerSocketChannel 注册到某个 boss NioEventLoop 的 selector 上。
? ? Server 端包含1个 boss NioEventLoopGroup 和1个 worker NioEventLoopGroup,NioEventLoopGroup 相当于1个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个NioEventLoop 包含1个 selector 和1个事件循环线程。
每个 boss NioEventLoop 循环执行的任务包含3步:
每个 worker NioEventLoop 循环执行的任务包含3步:
? ? Client 端启动时 connect 到 Server 端,建立 NioSocketChannel,并注册到某个 NioEventLoop的 selector 上。
Client 端只包含1个 NioEventLoopGroup,每个 NioEventLoop 循环执行的任务包含3步:
public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
}
});
serverBootstrap.bind(8000);
}
}
总结:创建一个引导类,然后给他指定线程模型,IO 模型,连接读写处理逻辑,绑定端口之后,服务端就启动起来了。
? ? 对于客户端的启动来说,和服务端的启动类似,依然需要线程模型、IO 模型,以及 IO 业务处理逻辑三大参数。
public class NettyClient {
public static void main(String[] args) {
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap
// 1.指定线程模型
.group(workerGroup)
// 2.指定 IO 类型为 NIO
.channel(NioSocketChannel.class)
// 3.IO 处理逻辑
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
}
});
// 4.建立连接
bootstrap.connect("csdn.im", 80).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else {
System.err.println("连接失败!");
}
});
}
}
总结:创建一个引导类,然后给他指定线程模型,IO 模型,连接读写处理逻辑,连接上特定主机和端口,客户端就启动起来了。
ByteBuf 是一个节点容器,里面数据包括三部分:
这三段数据被两个指针给划分出来,读指针、写指针。
ByteBuf 本质上就是,它引用了一段内存,这段内存可以是堆内也可以是堆外的,然后用引用计数来控制这段内存是否需要被释放,使用读写指针来控制对 ByteBuf 的读写,可以理解为是外观模式的一种使用。
基于读写指针和容量、最大可扩容容量,衍生出一系列的读写方法,要注意 read/write 与 get/set 的区别。
多个 ByteBuf 可以引用同一段内存,通过引用计数来控制内存的释放,遵循谁 retain() 谁 release() 的原则。
ByteBuf 和 ByteBuffer 的区别:
ByteBuf 和 设计模式如下。关于设计模式可以参见《24大设计模式总结》。
在 Netty 的世界里,ByteBuf 实例通常应该由 ByteBufAllocator 来创建。
? ? CompositeByteBuf 可以让我们把多个 ByteBuf 当成一个大 Buf 来处理,ByteBufAllocator 提供了 compositeBuffer() 工厂方法来创建 CompositeByteBuf。CompositeByteBuf 的实现使用了组合模式。
? ? ByteBufInputStream 使用适配器模式,使我们可以把 ByteBuf 当做 Java 的 InputStream 来使用。同理,ByteBufOutputStream 允许我们把 ByteBuf 当做 OutputStream 来使用。
? ? ReadOnlyByteBuf 用适配器模式把一个 ByteBuf 变为只读,ReadOnlyByteBuf 通过调用Unpooled.unmodifiableBuffer(ByteBuf) 方法获得:
? ? 我们很少需要直接通过构造函数来创建 ByteBuf 实例,而是通过 Allocator 来创建。从装饰器模式可以看出另外一种获得 ByteBuf 的方式是调用 ByteBuf 的工厂方法,比如:
? ? channelHandler 只会对感兴趣的事件进行拦截和处理,Servlet 的 Filter 过滤器,负责对 IO 事件或者 IO 操作进行拦截和处理,它可以选择性地拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。
? ? Pipeline 与 channelHandler 它们通过责任链设计模式来组织代码逻辑,并且支持逻辑的动态添加和删除。?
ChannelHandler 有两大子接口:
这两个子接口分别有对应的默认实现,ChannelInboundHandlerAdapter,和 ChanneloutBoundHandlerAdapter,它们分别实现了两大接口的所有功能,默认情况下会把读写事件传播到下一个 handler。
事件的传播
? ? AbstractChannel 直接调用了 Pipeline 的 write() 方法,因为 write 是个 outbound 事件,所以DefaultChannelPipeline 直接找到 tail 部分的 context,调用其 write() 方法:
context 的 write() 方法沿着 context 链往前找,直至找到一个 outbound 类型的 context 为止,然后调用其 invokeWrite() 方法:
NioEventLoop 除了要处理 IO 事件,主要还有:
非 IO 操作和 IO 操作各占默认值50%,底层使用 Selector(多路复用器)。
Selector BUG 出现的原因:若 Selector 的轮询结果为空,也没有 wakeup 或新消息处理,则发生空轮询,CPU 使用率100%。
Netty 的解决办法:
通信协议是为了服务端与客户端交互,双方协商出来的满足一定规则的二进制格式:
通信协议的设计:
? ? 内存池是指为了实现内存池的功能,设计一个内存结构 Chunk,其内部管理着一个大块的连续内存区域,将这个内存区域切分成均等的大小,每一个大小称之为一个 Page。将从内存池中申请内存的动作映射为从 Chunk 中申请一定数量 Page。为了方便计算和申请 Page,Chunk 内部采用完全二叉树的方式对 Page 进行管理。
? ? 对象池是指 Recycler 整个对象池的核心实现由 ThreadLocal 和 Stack 及 WrakOrderQueue 构成,接着来看 Stack 和 WrakOrderQueue 的具体实现,最后概括整体实现。
整个设计上核心的几点:
? ? 连接假死的现象是:在某一端(服务端或者客户端)看来,底层的 TCP 连接已经断开了,但是应用程序并没有捕获到,因此会认为这条连接仍然是存在的,从 TCP 层面来说,只有收到四次握手数据包或者一个 RST 数据包,连接的状态才表示已断开。
假死导致两个问题:
通常,连接假死由以下几个原因造成的:
? ? 如果能一直收到客户端发来的数据,那么可以说明这条连接还是活的,因此,服务端对于连接假死的应对策略就是空闲检测。
? ? 简化一下,我们的服务端只需要检测一段时间内,是否收到过客户端发来的数据即可,Netty 自带的 IdleStateHandler 就可以实现这个功能。
? ? IdleStateHandler 的构造函数有四个参数,其中第一个表示读空闲时间,指的是在这段时间内如果没有数据读到,就表示连接假死;第二个是写空闲时间,指的是在这段时间如果没有写数据,就表示连接假死;第三个参数是读写空闲时间,表示在这段时间内如果没有产生数据读或者写,就表示连接假死。写空闲和读写空闲为0,表示我们不关心者两类条件;最后一个参数表示时间单位。在我们的例子中,表示的是:如果 15 秒内没有读到数据,就表示连接假死。
? ? 在一段时间之内没有读到客户端的数据,是否一定能判断连接假死呢?并不能为了防止服务端误判,我们还需要在客户端做点什么。
服务端在一段时间内没有收到客户端的数据有两种情况:
所以我们要排除第二种情况就能保证连接自然就是假死的,定期发送心跳到服务端。
实现了每隔 5 秒,向服务端发送一个心跳数据包,这个时间段通常要比服务端的空闲检测时间的一半要短一些,我们这里直接定义为空闲检测时间的三分之一,主要是为了排除公网偶发的秒级抖动。
为了排除是否是因为服务端在非假死状态下确实没有发送数据,服务端也要定期发送心跳给客户端。
? ? TCP 是个“流”协议,所谓流,就是没有界限的一串数据。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包的问题。
解决方法:
? ? 如果你的应用层协议非常简单,每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 Pipeline 中,Netty 会把一个个长度为 100 的数据包(ByteBuf)传递到下一个 channelHandler。
? ? 从字面意思来看,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。
DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。
? ? 这种拆包器是最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包。
RPC 的目标就是要 2~8 这些步骤都封装起来,让用户对这些细节透明。Java 一般使用动态代理方式实现远程调用。
?