NIO基础-Netty系列-1

概览

最近弄几篇NIO基础相关的内容,用于Netty源码解析使用。因为没有这些知识就产生不了问题,也就无法深入一个成熟的网络IO框架源码进行学习。

NIO三大核心组件:

1,Channel

2,Buffer

3,Selector

先概述一下三者的概念和之间的关系,再逐个了解组件的API打个基础。

对于IO通信,必然需要连接起来才能通信,Channel可以理解成一个连接连端的通道,有了通道就可以读写数据了,Buffer是和Channel交互的读写数据的组件,读操作就是从Channel中的数据读到Buffer上,写就是把数据从Buffer上写到Channel上。在通信的流程中,会有很多个通道产生IO事件需要处理,Selector可以理解成由它来监听全部的通道上发生的事件,然后我们通过Selector可以获得我们感兴趣的IO事件,然后进行不同的操作。

Channel

这里就涉及两个Channel类型:

  • SocketServerChannel

    监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道。

  • SocketChannel

    用于Socket套接字TCP连接的数据读写。

先进行连接,然后读写数据,所以对于被连接的一端,是用SocketServerChannel来监听连接请求的,监听到后就创建好SocketChannel,我们可以对SocketChannel进行监听,进行读写操作。这个流程在实现代码中体现,可以先记住这个意思就行。

接下去对关键的API进行解释:

ServerSocketChannel#open

开启一个服务端的Socket Channel。

ServerSocketChannel#bind(SocketAddress)

Server端作为接受连接的一方,怎么说也要先绑定一个端口吧,否则人家想连接你都找不到地址。所以执行open开启一个Channel后一般就执行bind绑定到一个本地地址(SocketAddress)。这样客户端往这个地址连接的时候,服务端的Channel才能接受到。

AbstractSelectableChannel#configureBlocking

这个方法由SocketServerChannelSocketChannel的抽象父类提供,用于设置Channel是阻塞还是非阻塞的。这个设置关系到channel的一些方法是否阻塞的特性,在后面的会被涉及。

ServerSocketChannel#accept

开启一个Channel,绑定好了地址,如果有客户端来连了,就用这个方法来接受并返回一个SocketChannel,和前面概述的内容对应到了,非常的顺利成章,这个方法就涉及前面设置的是否阻塞,如果设置的阻塞,那么这个方法就会阻塞直到有客户端来连接,如果设置的是非阻塞,那么就看此时有没有新连接,如果有就返回一个SocketChannel,否则返回null。

SelectableChannel#register(Selector, int)

Selector前面介绍过是用来监听Channel的,作为被监听者,需要有一个注册的动作,并且在注册的时候告诉监听者监听什么内容,这个内容在SelectionKey中列举,一共也就四个:

  • OP_READ 读就绪
  • OP_WRITE 写就绪
  • OP_CONNECT 连接操作
  • OP_ACCEPT 接受连接

需要理解这些Key表示的是一种就绪的事件,不是事件本身,比如我们监听OP_ACCEPT,那么在后续询问Selector的时候,Selector告诉我们的答案是有一个连接已经过来可以接受这个连接了,所以接下去要做的是去接受这个连接。

SocketChannel#read(ByteBuffer)

从SocketChannel读取数据到ByteBuffer。

SocketChannel.connect(SocketAddress remote)

向远程地址发起连接。作为客户端连接服务端使用。

SocketChannel.finishConnect()

判断前面connect方法是否结束。

Buffer

JDK实现了各种类型的Buffer:

IntBuffer, CharBuffer, FloatBuffer, DoubleBuffer, ShortBuffer, LongBuffer, ByteBuffer

重点关注ByteBuffer就可以了,我们知道它是和Chennel交互数据的,可以稍微了解一下它的结构:

内部有一个byte[]的数据块,读写数据就是操作这个数组,初始化的时候会确定一个容量,用capacity来标识,然后使用position来表示目前读写的位置,比如在读的时候从0开始递增。还有一个limit字段表示读写的最大上限。Buffer有读写模式,会有切换模式的操作,切换模式后positionlimit定义是调整的,所以值也会调整。

后面将仔细分析Buffer的实现原理。

ByteBuffer#allocate

分配一个新的字节缓冲区,初始化操作

ByteBuffer#put(byte)

put方法用于写数据,position会随着写入的数据递增。

ByteBuffer#get()

get方法用于从Buffer获取数据

Buffer#flip

反转操作,也就是切换模式,具体操作是把limit设置成当前的position,position设置成0。

Buffer#clear

清理操作,position设置成0,limit设置成capacity。

Selector

Selector 用于监控Channel上的就绪事件,这个能力是实现多路复用的IO模型的关键。我们可以在Channel通过register方法注册到Selector上的时候传入感兴趣的就绪事件,然后通过select方法检测是否有就绪事件。

Selector#select

选择出就绪事件,返回数量,大于0表示有就绪事件。无参数的方法是阻塞一直到可以返回数量或者被weakup或线程被中断,我们可以通过重载的方法(select(long timeout))中参数调整这个方法的阻塞时间,也可以选择selectNow()执行一次选择操作,立即返回。

Selector#wakeup

效果就是把前面select方法阻塞还未返回的操作唤醒立即返回。如果此时没有调用select的那么下次的调用立即返回。

Selector#selectedKeys

获得事件就绪的SelectionKey类型集合,我们可以通过SelectionKey获得事件就绪的Channel,然后就可以对这些Channel进行相应的读写等操作。

SelectionKey#interestOps(int)

设置监听的事件,这个方法可以调整对应的SocketChannel监听的感兴趣事件。

SelectionKey#interestOps()

获取SelectionKey的就绪事件兴趣集,一般先调用这个方法在调用前面的方法调整。

SelectionKey#attach(Object ob)

绑定一个对象

SelectionKey#attachment()

返回前面绑定的对象

入门代码

先代码入个门,我们已经了解了一些NIO的核心组件的关键Api,那么我们为了实现一个网络通信的功能,应该组装起这些API呢?先实现一个监听端口,能够获得连接,并且从连接读取数据这样一个基本的功能代码。

服务端代码:

public class NioServer {
​
    public static void start() throws IOException {
        // 开启选择器
        Selector selector = Selector.open();
        // 开启server socket channel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置成非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // server 监听端口绑定
        serverSocketChannel.bind(new InetSocketAddress(12022));
        // channel 注册到选择器上,IO事件为Accept
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // select 操作 阻塞等待Accept状态就绪
        while (selector.select() > 0) {
            // 获取全部就绪的selectedKeys
            Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
            // 遍历selectedKeys
            while (selectionKeys.hasNext()) {
                SelectionKey selectionKey = selectionKeys.next();
                // 就绪状态为Accept
                if (selectionKey.isAcceptable()) {
                    // 接受一个连接 得到一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 设置成非阻塞模式
                    socketChannel.configureBlocking(false);
                    // 把SocketChannel 注册到Selector 感兴趣的事件是读事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    // 分配一个新的字节缓冲区
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    // 从Channel中读取数据到Buffer
                    while ((socketChannel.read(byteBuffer)) > 0) {
                        // 翻转Buffer
                        byteBuffer.flip();
                        // 清理Buffer
                        byteBuffer.clear();
                    }
                    String readStr = new String(byteBuffer.array());
                    System.out.print("" + readStr);
                    socketChannel.close();
                }
                selectionKeys.remove();
            }
//            serverSocketChannel.close();
        }
    }
​
    public static void main(String[] args) throws IOException {
        start();
    }

客户端代码:

public static void start() throws IOException {
    InetSocketAddress inetSocketAddress = new InetSocketAddress(12022);
    // 开启一个Socket Channel 并且连接远程地址
    SocketChannel socketChannel = SocketChannel.open();
    // 设置为非阻塞
    socketChannel.configureBlocking(false);
    // 连接远程地址
    socketChannel.connect(inetSocketAddress);
    // 等待连接成功
    while (!socketChannel.finishConnect()) {
    }
    // 分配Buffer空间
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    // 写入Buffer
    byteBuffer.put("hello world".getBytes());
    // 切换成读模式
    byteBuffer.flip();
    // 把Buffer 写入Socket Channel
    socketChannel.write(byteBuffer);
    // 关闭写连接
    socketChannel.shutdownOutput();
    // 关闭socket Channel
    socketChannel.close();
}
​
public static void main(String[] args) throws IOException {
    start();
}

参考:

https://www.cnblogs.com/yungyu16/p/13065194.html

posted on 2022-10-07 23:33  每当变幻时  阅读(25)  评论(0编辑  收藏  举报

导航