Netty是一个网络IO框架,想要掌握Netty,必须要对于TCP协议、Socket这些基础知识有一个了解。这方面的经典书籍莫过于《UNIX网络编程卷1:套接字联网API》和《TCP-IP详解卷1》。当然,没有这么深厚的基础知识,也不妨碍你使用Netty。
由于Netty是Java领域的框架,因此本系列文章在涉及相关概念时,会优先使用java领域的说法。因此有可能在某些地方显得不够严谨,这首先是本人水平有限所致,有时也是有意为之。
在Java网络编程领域,我们通常有三种网络IO模型:bio(Blocking io),nio(non-blocking io),aio。
所有的IO操作都是阻塞的,所以服务端必须为每个连接创建一个线程,实现简单;不能胜任存在大量连接的业务场景。
也叫selector模型,所有操作都是同步的,但可用一个selector来查询多个连接,哪个链接有响应处理哪个,能够避免阻塞地等待网络事件,一个线程能同时为多个socket连接服务。
完全的异步响应式模型,底层操作系统做了很多事情,上层程序只需等待系统的响应回调即可;比较新(java 7开始支持),目前运用还不是特别广泛;
虽然java实现了以上3种模型,旦总体来说,IO模型是偏底层的API,在实现具体业务时,仍然需要开发者做大量网络相关的工作。netty刚好帮我们做了这部分工作,它不仅仅对底层IO模型做了封装,更以reactor模式为蓝本,实现了一个完整、易用的网络应用框。
reactor模型的知识大家自行搜索。
虽然Netty底层使用的socket接口随平台而异(Mac OS上可以使用kqueue,linux上使用epoll),但java nio是最典型的方式;在学习Netty的时候,建议大家先聚焦于Netty nio相关部分。
先简单介绍一下java nio相关的知识。
java nio有三个核心角色:Channel,Buffer,Selector
buffer是一个内存块,用于业务代码和nio之间传递数据,所有的buffer类型都有4个属性(定义在抽象基类Buffer中):
Buffer的设计追求性能,所以它的接口可读性有点差,使用buffer必须充分理解它的内部结构和工作方式。
一个抽象的IO读写通道,它一般基于底层流来工作,常用的实现有:FileChannel,DatagramChannel(UDP),ServerSocketChannel,SocketChannel。
selector管理多个channel,也叫多路复用选择器;当一个channel有事件发生的时候,selector能够发现它并处理它;所以就达到了用一个线程处理多个channel的效果,减少了系统开销。
Selector是一个抽象类,核心方法:
SelectionKey代表channel和selector之间的注册关系,在channel注册时创建并返回给使用者。SelectionKey内部有两个状态集合(bit位表示),一个代表selector应当检测的channel状态集合(注册时指定);一个代表selector当前处于ready的channel状态。不同的channel支持的操作是不同的。
这里所说的“状态“,是指channel当前可以执行的操作,比如“可读、可写“,状态变化又称之为“事件”;因此在不同的语义场景下,“状态”、“操作”、“事件”这几个词可能被交替使用。
用一个简单回响程序来展示java nio的用法,服务端代码如下:
public class NIOServer {
public static void main(String[] args) throws IOException {
//打开channel,绑定地址
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(6666));
serverSocketChannel.configureBlocking(false);
//打开Selector,并注册channel,这里关注OP_ACCEPT事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select() > 0) {
Set selectionKeys = selector.selectedKeys();
for (SelectionKey key : selectionKeys) {
if (key.isAcceptable()) {
//说明有连接请求进来
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//接收连接,并将新socketChannel注册到selector
SocketChannel clientChannel = channel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
//新客户端连接有数据进来,读取并打印
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
String text = new String(buffer.array(), 0, buffer.limit()).trim();
System.out.println("接受到数据:" + text);
channel.close();
}
}
selectionKeys.clear();
}
}
}
}
客户端代码如下
public class NIOClient {
public static void main(String[] args) throws IOException {
//打开一个SocketChannel
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
//连接服务端
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
if (!clientChannel.connect(inetSocketAddress)) {
while (!clientChannel.finishConnect()) {
System.out.println("连续尚未完成");
}
}
//打开Selector,并注册channel,这里只关注OP_WRITE(可写)事件
Selector selector = Selector.open();
clientChannel.register(selector, SelectionKey.OP_WRITE);
if (selector.select() > 0) {
Set selectionKeys = selector.selectedKeys();
for (SelectionKey key : selectionKeys) {
if (key.isWritable()) {
//向服务端发送一句话
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.wrap("Hello Netty".getBytes());
channel.write(buffer);
}
}
selectionKeys.clear();
}
clientChannel.close();
}
}
我们学习Netty并不需要先完整地掌握java nio,但是能看懂上面的代码,了解java nio的工作方式还是必要的。