Netty快速入门,一看就懂!
很早以前就写过关于 Netty 的使用,最近发现还有网友在看之前写的那篇 Netty 文章,个人感觉那时候写的很粗糙,怕影响同行的阅读质量,所以决定重新写一些关于 Netty 的文章,补充以前的不足。
图片来自 Pexels
Netty 能做啥
简单说就是用来处理网络编程,写一款能进行网络通信的服务端和客户端程序。如果没有 Netty,在 Java 的世界中如何处理网络编程呢?
Java 自带的工具有:java.net 包,用于处理网络通信,后面 Java 提供了 NIO 工具包用于提供非阻塞的通信。
与 Netty 同级别的第三方工具包:Mina,在设计上与 Netty 有些许不同,但是核心都是提供网络通信的能力。
传统网络通信模型
说 Netty 之前还是先讲一下传统的网络编程是什么样子。传统的 Socket 编程开发步骤很简单,只需要使用 Socket 类创建客户端和服务端即可。
同步阻塞线程模型的问题在于一个请求必须绑定一个线程去处理,并且所有的请求都是同步操作,意味着该请求未处理完之前这个连接不会被释放,如果并发高的情况必然会导致系统压力过大。
Netty 的新线程模型
基于此,Java 新增了非阻塞的 IO 操作包 NIO, NIO 的线程模型采用了 Reactor 模式,即异步非阻塞的方式,解决了之前同步阻塞带来的问题。
NIO 的全称是 NoneBlocking IO,非阻塞 IO,区别与 BIO,BIO 的全称是 Blocking IO,阻塞 IO。
那这个阻塞是什么意思呢?
Accept 是阻塞的,只有新连接来了,Accept 才会返回,主线程才能继。
Read 是阻塞的,只有请求消息来了,Read 才能返回,子线程才能继续处理。
Write 是阻塞的,只有客户端把消息收了,Write 才能返回,子线程才能继续读取下一个请求。
服务器在处理响应的设计模式方面目前主要分为两种:
线程驱动
事件驱动
同步阻塞就是线程驱动的模式,最明显的例子就是 Tomcat;对于事件驱动来说,没有必要为每一个连接都创建一个线程去维护。
参考观察者模式,可以设置一个事件池,用一个单线程去循环监听当前池中是否有完成的事件,如果有则取出该事件。
简单说一下 Reactor 模式是如何解决线程等待问题的:在等待 IO 的时候,线程可以先退出不用一直等待 IO 操作。
但是如果不等待那么 IO 处理完成之后返回给谁呢?Reactor 模型采用了事件驱动机制,要求线程在退出前向 event loop 注册回调函数,这样 IO 完成之后 event loop 就可以调用回调函数完成数据返回。
在 Reactor 中有 4 个角色,所有的数据流入的处理统一称为 Channel,就像是一个水管,Reactor 模型将每一种事件拆分为一个 event,相同类型的 event 归为一类,这一类的统一处理逻辑被称为一个 handler。
那么怎么去让一个或者多个线程去监听所有的 Channel 呢?所以就有 Selector。
Selector 就像是一个管理者,你可以将多个 Channel 注册到 一个 Selector 线程上,它会使用一个阻塞方法去捕获当前 Channel 上是否有事件发生,如果有则取出事件交给对应的 Handler 去处理。
Netty 是建立在 NIO 之上的,并且 Netty 在 NIO 上面又提供了更多高层次 API 的封装。
为什么不用 JDK 提供的 NIO
JDK 已经给我们提供了 NIO 的包,也是使用了 Reactor 模型来实现的异步非阻塞模式,那我们为啥在日常开发中没有听到谁直接使用 NIO 来开发网络编程呢?
实际上大家不使用的原因是因为它太难控制。Java NIO 类库中主要提供的功能包括:
缓冲区 Buffer
通道 Channel
多路复用器 Selector
缓冲区 Buffer 其实就是一个对象,即所有流入或者流出的数据都在 Buffer 中存在。
新 IO 与老的面向流 IO 的区别在于老 IO 直接面向字节流进行处理,新 IO 是面向缓冲区进行处理,读写数据都是先读写到缓冲区中。
Channel 通道,所有 Buffer 内的数据都会往 Channel 上流,数据通过 Channel 流向处理逻辑,通过 Channel 将处理过的数据返回给客户端。
所以 Channel 是全双工的,可以支持读写,这是它与 Stream 的区别。如果你使用 Stream,读数据只能使用 InputStream 进行操作,写数据只能使用 OutputStream 进行操作。
用现实世界中的事物比喻的话,传统 IO 犹如水管,水流只能沿着管道往下流;NIO 犹如一条双向公路,两个方向都可以行车。
另外也正是因为 Buffer 的引入我们才能随意的控制每次传输读多少数据,如果上次读取失败,那么应该从多少偏移量重新读取,这是传统 I/O 流无法比拟的。
Selector 选择器,它是 NIO 的核心,一个 Selector 就是一个线程,NIO 允许一个 Selector 管理多个 Channel,即将 Channel 注册到 Selector 上。
关于 NIO 的代码我就不写了,是很庞大的一堆,大家百度一下就能看到。总之基于这个思想来进行网络编程肯定是面对当今流量洪峰的最佳方式。而正好 Netty 底层基于 NIO 去做的封装,已经给你屏蔽了这一大坨操作。
网络编程还有一个问题就是跨平台性,NIO 底层是依赖系统的 IO API,不同的系统可能对 IO API 的实现也是不一样的,这里如何你使用 NIO 那么就需要考虑系统兼容性问题了。
另外还有一个问题就是 NIO 有个很著名的 bug,JDK 的 NIO 底层由 epoll 实现,若 Selector 的轮询结果为空,也没有 wakeup 或新消息处理,则发生空轮询,CPU 使用率 100%。
这个 Bug 官方声明已经修复,事实上没有被 Fix, 只是出现的概率会降低一些。
Netty 也对该 Bug 进行了处理:对 Selector 的 Select 操作周期进行统计,每完成一次空的 Select 操作进行一次计数,若在某个周期内连续发生 N 次空轮询,则触发了 Epoll 死循环 Bug。
那么这个时候就重建 Selector,判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的 Selector 上去除注册,重新注册到新的 Selector 上,并将原来的 Selector 关闭。
网络编程应该注意什么
既然说要学习 Netty, 它本身是基于 NIO 的封装用于网络通信,那么在编写一段用于网络通信的代码我们应该注意一些什么呢?弄清楚这些问题,我们大概就知道 Netty 都做了什么。
谈到网络就不能避免说到 OSI 7 层模型和 TCP / IP 4 层模型:
Java 网络编程主要使用的是 Socket 套接字编程,基于 4 层协议的网络编程,即基于 TCP/ UDP 协议的封装。
编写一个 Socket 通信都有哪些步骤呢?
创建一个 ServerSocket,监听并绑定一个端口。
一系列客户端来请求这个端口。
服务器使用 Accept,获得一个来自客户端的 Socket 连接对象。
启动一个新线程处理连接:①读 Socket,得到字节流;②解码协议,得到 HTTP 请求对象;③处理 HTTP 请求,得到一个结果,封装成一个 HTTPResponse 对象;④编码协议,将结果序列化字节流;④写 Socket,将字节流发给客户端。
继续循环步骤 3。
如何约定字节流长度格式,以保证每次读到的字节流都是最新的而不会和上次重复。
传输字节流的编解码问题。
一个服务端肯定会有多个客户端链接,如何管理众多的客户端链接,比如如何维护断线重连,连接超时以及关闭机制。
Netty 核心组件
当前网络连接的通道的状态(例如是否打开,是否已连接)。
网络连接的配置参数 (例如接收缓冲区大小)。
提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。
调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
支持关联 I/O 操作与对应的处理程序。
NioSocketChannel,异步的客户端 TCP Socket 连接。
NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
NioDatagramChannel,异步的 UDP 连接。
NioSctpChannel,异步的客户端 Sctp 连接。
NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。
ChannelInboundHandler 用于处理入站 I/O 事件。
ChannelOutboundHandler 用于处理出站 I/O 操作。
ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。
ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。
ChannelDuplexHandler 用于处理入站和出站事件。
作者:rickiyang
编辑:陶家龙
出处:https://www.cnblogs.com/rickiyang/
精彩文章推荐: