为什么要介绍 Netty
如今优秀的开源项目非常多,仅在 Java 服务器端开发领域,优秀的开源项目就不胜枚举。比如从十年前就开始流行到现在依旧十分活跃的 Spring Framework,如今已经发展为一个覆盖服务器端、大数据等多个领域的平台级开源项目。还有早年间的 MVC 框架、ORM 框架,到现在涉及各个服务器开发领域的开源技术。
但这次我要介绍的是 Netty 这个网络 IO 框架,而非 Spring 这样的流行项目。原因在于,Netty 这样的框架所实现的功能相比 Spring Framework 来说来说更为基础。因为对于服务器端开发来说,Spring Framework 核心的 IoC 和 AOP 技术其实并不是必须的。当然没有这两个技术,开发复杂的项目会很困难,但这两者只是充分而非必要的条件。但 Netty 这样的技术其实对于服务器端开发来说是必须的。
从产品的角度讲,没有像 Netty 这样的网络 IO 框架,就不会有优秀的 Web 服务器、应用服务器等等的平台,而没有这些平台,仅靠 IoC 和 AOP 等技术是不可能产生优秀的软件产品。一个服务器端软件,即便没有直接使用 Netty,那往往也是间接地使用了 Netty 这样的框架。所以 Netty 这样的框架对于一个优秀的服务器端项目或产品来说十分重要。
同时,因为 Netty 不仅被广泛地直接使用,还被很多优秀的技术,例如 Thrift、Storm、Spark 等采用,作为其基础的 IO 层的实现技术。所以,深入了解 Netty 还可以帮助开发人员更好地使用这些技术,在遇到一些技术问题时也可以从更深地角度去分析。
从程序员学习的角度讲,像 Netty 这样的网络 IO 框架,它们大量使用了 IO 和并发这两个技术。而这两个技术,对于优秀的服务器端开发人员来说是必备的。通过深入了解 Netty 的实现原理,能让程序员从实践的角度去学习优秀的 IO 和并发设计。除了 IO 和并发这两个对于服务器端开发非常重要的技术,了解 Netty 的实现原理还可以让程序员去学习如何设计实现一个复杂的框架,学习到设计模式、数据结构等的应用技巧。所以,深入了解 Netty 对于提高非常有帮助。
从工作的角度讲,国内外的很多公司也同样广泛使用 Netty,比如Twitter、Facebook、华为、阿里巴巴等等。所以在日常工作中也很容易接触到 Netty 的使用。
此外,Netty 的文档相对 Spring 这样的技术来说还是偏匮乏。同时,Netty 3/4/5 这几个版本的变化还是非常大的,尤其是从 3 到 4。这给使用者带来了很多的问题,而深入了解 Netty 会帮助使用者解决这些问题。
所以,总结一下,深入了解 Netty 的好处有:
- 提高程序员在网络 IO 和并发,以及设计模式、数据结构等领域的内功
- 学习如何设计复杂的框架
- 帮助解决使用 Netty 以及将 Netty 作为基础的技术时遇到的问题
目录
- Java NIO 介绍
- Netty 主要组件和类图
- 源码分析之 Server Side Channel Init and Register
- 源码分析之 Server Side Channel Port Bind
- 源码分析之 Server Side Accept Client Connect
- 源码分析之 Read Message
- 源码分析之 Write Message
- Netty 线程模型
Java NIO 介绍
Java NIO 对于 Netty 来说是基础技术(Java NIO 是基于 Linux epoll 等技术),所以下面将介绍一下 Java NIO 方面的基础知识。
对于并发技术,因为它相比较 Java NIO 来说在工作中被直接使用到的机会要多得多,所以这里就不做介绍了。但是后续会介绍 Netty 中的并发设计。
示例
Selector selector = null;
ServerSocketChannel server = null;
try {
selector = Selector.open(); // 打开 Selector
server = ServerSocketChannel.open(); // 打开 ServerSocketChannel
server.socket().bind(new InetSocketAddress(port)); // 绑定端口
server.configureBlocking(false); // 设置为非阻塞模式
server.register(selector, SelectionKey.OP_ACCEPT); // 将 ServerSocketChannel 注册到 Selector 上
while (true) {
selector.select();
for (Iterator<SelectionKey> i = selector.selectedKeys().iterator(); i.hasNext();) {
SelectionKey key = i.next();
i.remove();
if (key.isConnectable()) {
((SocketChannel)key.channel()).finishConnect();
}
if (key.isAcceptable()) {
// accept connection
SocketChannel client = server.accept(); // 接受 TCP 连接
client.configureBlocking(false);
client.socket().setTcpNoDelay(true);
client.register(selector, SelectionKey.OP_READ); // 将 SocketChannel 注册到 Selector 上
}
if (key.isReadable()) {
// ...read messages...
}
}
}
} catch (Throwable e) {
throw new RuntimeException("Server failure: "+e.getMessage());
} finally {
try {
selector.close();
server.socket().close();
server.close();
stopped();
} catch (Exception e) {
// do nothing - server failed
}
}
上面这段代码介绍了 Java NIO 的基本使用方式。比如,ServerSocketChannel
和 SocketChannel
要注册在 Selector
上,并且这两个 SelectableChannel
要通过 configureBlocking(boolean block)
方法将其设置为非阻塞模式;通过不断轮询 Selector
的方式,通过 Selector.selectedKeys()
获得例如客户端连接、读写消息等事件,并做相应处理。因为在一个 Selector
上可以注册多个 SelectableChannel
,所以实现了一个线程处理多个连接的目的。
上面就是 Java NIO 的大致的使用方法。
技术基础
Java 这样编程语言级的 IO 如同线程一样,最终还是基于操作系统的实现支持。在 Linux 中,是靠 select、poll 和 epoll 等技术支持的。
select(poll 类似)
在非阻塞 IO 出现之前,我们是用阻塞的方式使用 IO。当一个流(文件、套接字等)无法读写的时候,操作便被阻塞,线程被挂起。于是,对于一个流的操作,必须独占一个线程。虽然可以用多进程或多线程的方式去并发处理,但是这样所带来的大量的资源使用、线程上下文的切换,都使得这种方式十分低效。所以便出现了非阻塞 IO。
最先出现的非阻塞 IO 技术是 select 和 poll。它们的原理简单来说不断地轮询多个流,看是否有流可以被操作,从而实现了非阻塞的 IO 操作。
但这两种方式也有明显的缺点。其缺点在于,select 和 poll 对流的轮询的方式很“傻”。它俩在轮询时并不会区分一个流上是否真的有数据,而是一股脑地轮询所有的流。因此这两者的性能会随着流的增加而线性下降。
epoll
epoll 的意思是 event poll。epoll 相较 select 和 poll 的改进在于不像前者去轮询所有的流,而是只去轮询有实际事件发生的流。这一点的实现是基于硬件中断实现的。
组成介绍
Channel
Channel
代表了 IO 操作的通道。Channel
通过后面提到的 Buffer
进行数据的读写。Channel
有一类我们后面会经常提到的 SelectableChannel
。配合 Selector
,能够实现非阻塞 IO。后续将被经常提到的 ServerSocketChannel
和 SocketChannel
都是 SelectableChannel
的子类。
Selector
选择器,也可被称为多路复用器,是实现非阻塞 IO 的关键。通过调用 Selector.open()
,便可得到一个当前操作系统下的默认 Selector
实现。通过 SelectorProvider
修改这一行为,自定义实现。
SelectableChannel
可以通过 register(Selector, int)
方法将自己注册在 Selector
上,并提供其所关注的事件类型。
通过地调用 Selector
的 select()
(阻塞)或 selectNow()
(非阻塞)方法,Selector
会将从上次 select()
方法调用之后的所有就绪状态通道上的 SelectionKey
放入一个集合中。同时,select()
方法所返回的 int 值代表了有多少 Channel 自上次之后便为就绪状态。
在调用 select()
之后,通过调用 Selector
的 selectedKeys()
得到就绪状态通道的 SelectionKey
。通过遍历这一集合,再通过 SelectionKey
便可得到所有就绪状态的 SelectableChannel
,进一步便可以做相应的操作。
Buffer
Java NIO 以 Buffer
最为数据传输的载体。通过使用 Buffer
,Java NIO 提高了数据传输的效率。而 Netty 的很多改进也是围绕 Buffer
进行的,比如 Buffer
的池化。
后面会提到的 SocketChannel
,其使用了 Buffer
的一个子类 ByteBuffer
来进行数据的读写。
接下来
接下来会介绍 Netty 中一些主要的类,以及它们的作用以及关系。