欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Netty详解之一:IO模型与Java NIO

程序员文章站 2022-04-24 10:52:05
...

Netty是一个网络IO框架,想要掌握Netty,必须要对于TCP协议、Socket这些基础知识有一个了解。这方面的经典书籍莫过于《UNIX网络编程卷1:套接字联网API》和《TCP-IP详解卷1》。当然,没有这么深厚的基础知识,也不妨碍你使用Netty。

由于Netty是Java领域的框架,因此本系列文章在涉及相关概念时,会优先使用java领域的说法。因此有可能在某些地方显得不够严谨,这首先是本人水平有限所致,有时也是有意为之。

IO模型

在Java网络编程领域,我们通常有三种网络IO模型:bio(Blocking io),nio(non-blocking io),aio。

bio:同步阻塞模型

所有的IO操作都是阻塞的,所以服务端必须为每个连接创建一个线程,实现简单;不能胜任存在大量连接的业务场景。

nio:同步非阻塞模型

也叫selector模型,所有操作都是同步的,但可用一个selector来查询多个连接,哪个链接有响应处理哪个,能够避免阻塞地等待网络事件,一个线程能同时为多个socket连接服务。

aio:异步响应模型

完全的异步响应式模型,底层操作系统做了很多事情,上层程序只需等待系统的响应回调即可;比较新(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相关的知识。

核心类型:

java nio有三个核心角色:Channel,Buffer,Selector

  • Channel:对socket连接的抽象;
  • Buffer:channel和socket之间增加一个缓冲区,实现非阻塞;
  • Selector:selector管理一组channel的状态变化,并选择有事件发生channel进行处理。

nio buffer:

buffer是一个内存块,用于业务代码和nio之间传递数据,所有的buffer类型都有4个属性(定义在抽象基类Buffer中):

  • capacity:数据容量,创建时指定,不能修改;
  • limit:当前操作模式下的有效终点位置,也即操作不能超过该位置;
  • position:下一次操作(读、写)的位置,满足约束position<limit;
  • mark:记住当前的position,已备后续恢复(有点备忘录模式的思想);

Buffer的设计追求性能,所以它的接口可读性有点差,使用buffer必须充分理解它的内部结构和工作方式。

nio channel

一个抽象的IO读写通道,它一般基于底层流来工作,常用的实现有:FileChannel,DatagramChannel(UDP),ServerSocketChannel,SocketChannel。

nio selector:

selector管理多个channel,也叫多路复用选择器;当一个channel有事件发生的时候,selector能够发现它并处理它;所以就达到了用一个线程处理多个channel的效果,减少了系统开销。

Selector是一个抽象类,核心方法:

  • open():静态工厂方法,打开一个选择器;
  • selectXXX():监控所有注册的channel,查看是否有事件发生;
  • wakeup():唤醒一个阻塞的selector(比如调用了阻塞的select方法);

SelectionKey

SelectionKey代表channel和selector之间的注册关系,在channel注册时创建并返回给使用者。SelectionKey内部有两个状态集合(bit位表示),一个代表selector应当检测的channel状态集合(注册时指定);一个代表selector当前处于ready的channel状态。不同的channel支持的操作是不同的。

这里所说的“状态“,是指channel当前可以执行的操作,比如“可读、可写“,状态变化又称之为“事件”;因此在不同的语义场景下,“状态”、“操作”、“事件”这几个词可能被交替使用。

NIO示例

用一个简单回响程序来展示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<SelectionKey> 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<SelectionKey> 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的工作方式还是必要的。