之前有提到Java中如何使用NIO,但是实际上在Java的NIO中,还有很多细节我们并没有去提到,只是简单的了解了一下NIO的使用方法,这篇文章我们来着重的了解一下NIO中的三个核心channe、buffer、selector,对于NIO的操作,都是围绕着这三个核心来进行的

先来回顾一下Java NIO的代码

public class ChatServer {

public static void main(String[] args) throws IOException {

//创建一个selector
Selector selector = Selector.open();

//创建channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

//绑定端口以及设定为非阻塞模式
serverSocketChannel.bind(new InetSocketAddress(13111));
serverSocketChannel.configureBlocking(false);
//将serverSocketChannel注册到selector上 注册为accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true){
selector.select();

Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();

//selector需要不断的去查询数据准备好了没 也可以看作是一种阻塞
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();

//如果是连接事件 即有新的客户端连接过来
if (selectionKey.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
System.out.println("accept new conn:"+socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
//channel 注册到selector上 注册为读事件
socketChannel.register(selector,SelectionKey.OP_READ);
}else if (selectionKey.isReadable()){
SocketChannel ssc = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据阶段的阻塞
int length = ssc.read(buffer);
if (length >0 ){
//切换到读模式
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String content = new String(bytes, "UTF-8");
if (content.equalsIgnoreCase("quit")){
selectionKey.cancel();
ssc.close();
}
}
}
iterator.remove();
}
}

}
}


可以看到,在java代码中,我们open了一个selector,然后将一个服务端的channel,注册到了这个selector上,然后后续所有的accept事件的channel也会被注册到selector上,然后如果是read事件,就会在buffer中读取数据,大致的流程就是这样的。接下来详细的讲一下channel、buffer、selector这三个核心组件

channel

channel可以看做是一条具体的实际存在的连接,就像是家里存在的网线,电话线一样,都是一条条具体的连接,这个连接可以是网络连接,io连接,文件传输的连接,比如说我要给你传输文件,我们之间就要建立一条io连接来传输文件,我想给你发消息,我们之前就会建立一条网络连接,这个我们之间建立起来的连接就可以看做是一条channel。在我眼里channe == 连接

所以,根据channel功能的不同,就分出来很多种类型的channel 比如说上面代码中用到的,

  • ServerSocketChannel(基于TCP协议 仅仅用于服务端的channel,可以监听新进来的TCP连接。对每一个新进来的连接都会创建一个SocketChannel)
  • SocketChannel(基于TCP 协议读写数据,用户客户端与服务端之间通信的channel,监听到客户端连接的时候由服务端建立的)

当然除了这些, 还有一些其他的channel,比如说

  • FileChannel(用于操作文件的channel)
  • DatagramChannel(基于udp协议读写数据的channel)

总而言之,对于channel,很多博客翻译成通道,我觉得反而不好理解,我们只要看作是一个A到B具体的连接,而这个连接又有很多种类型,根据具体的需求来使用不同类型的连接方式,就像我们上网用网线连接,打电话用电话线,这样来描述就很好理解了。

buffer

我们摘取一段示例中的代码,我们从这段代码中可以看出,我们从channel中读取数据,就是对buffer的读取

      SocketChannel ssc = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据阶段的阻塞 对buffer的读取
int length = ssc.read(buffer);
if (length >0 ){
//切换到读模式
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String content = new String(bytes, "UTF-8");
if (content.equalsIgnoreCase("quit")){
selectionKey.cancel();
ssc.close();
}


buffer和channel之间的关系

我们可以把buffer看成是一种容器,用来装数据的容器。一般和channel结合在一起使用,那么他和channel有什么关系呢。

前面我们说到,一条channel就是一条具体的连接。那么我们可以在北京到广州之间建立一条铁路,这条铁路就可以看作是一条channel。那么buffer就可以看作是一个这条铁路上跑的一列列火车,至于火车上装的人,就是一个个数据了。Channel可以有很多种类型,这种类型就可以看作是一种种运输方式,拿前面的例子来说,FileChannel就可以说是海路运输方式,DatagramChannel可以看作是火车运输方式。而建立的这种类型的channel,就可以看作是这个类型下面具体的一条线路.

比方说我现在建立了一个FileChannel类型的channel,就可以理解成我开辟了一条北京到广州的航道。因为channel是具体的某条连接,代表了具体的数据流向,北京到广州的航道不止一条,同样的channel也可以建立很多个。而buffer,则是跑在这些具体的线路上的,火车,轮船,飞机等等用来运载的容器

buffer的类型和作用

就像火车有运煤的,运人的,运海鲜的一样,对于buffer来说,也有着不同的类型,除了boolean之外的每一种基本类型都有着对应的buffer类型,ByteBuffer、CharBuffer、IntBuffer。。。。等等。并且buffer是可以进行读写模式的切换的

那么为什么会加入buffer这个组件呢?我们想想,如果你是物流公司的老板,你会有一件快递就发一次车,还是等一批货物到齐了一起装车运输呢。毫无疑问是选择后者,成本会更低,效率也会更高。因此,NIO是面向缓冲区的(buffer)。所以NIO即可以读又可以写 可以切换读写模式,因为buffer只是一个容器嘛,他只是装数据的,我可以把数据装上去,也可以把数据卸下来,所以channel是双向的 buffer读写模式的切换也让channel可以切换读写

BIO则不同了,我们以前学BIO就是各种stream,比如说inputStream,outPutStream,流是单向的,inputStream只能干输出流能干的事,没有NIO的buffer这么灵活,可以随意的切换读写。

bufferr的三个核心参数

buffer有三个核心的参数,分别是limit、capacity、position。他们在读或写模式的时候代表的含义和作用也不一样。

  • capacity 不管是哪个模式下,都是代表着容器的最大可容纳数据
  • position 代表着下一个可用的位置,读模式下是下一个读的位置,写模式下是下一个可写入的位置
  • limit 表示最大可写/可读的的数据,在写模式下、limit=capacity,在读模式下,limit等于当前容器现有的数据长度

【网络IO系列 五】 Java NIO详解以及channel、buffer、selector解析_网络IO

拿上面的图,红色的代表容器已经写入的数据,容器的容量为10,我们举个例子

写模式下,容器长度为10,已经写入四个数据,所以capacity=10,limit=10,position=4。数据下标是从0开始的 所以position为4,代表的是容器的第五个位置可写入。这点要区分开

读模式下, 容器长度为10,已经写入了4个数据,假设从头开始读,那么capacity=10,limit=4(因为只写入了四个数据,读模式下limit代表的是可读数据的长度,所以是4),因为是从头开始读的,所以position为0

以上就是buffer所有的知识点,其实buffer本质上就是一个容器,也没有什么其他特别的东西

selector

前面看过io多路复用和io的五种模型的,应该就知道这个selector的作用是什么,详细的可以去看这两篇文章,因为前面赘述的太多,所以这里只讲总结式的讲一下selector大致的作用。

​【网络IO系列】IO的五种模型,BIO、NIO、AIO、IO多路复用、信号驱动IO​

​【网络IO系列】IO多路复用详解以及select poll epoll之间的区别​

我们前面知道,channel本质上可以看作是一个连接,那么如果有很多个连接连过来,服务端要去处理这几万个连接,是不是就很难处理的过来,如果对于这一万个连接,每个连接都新建一个线程去处理,那服务器线程资源很容易被耗尽。

我们再联系到生活中的案例,我们去吃饭,如果每来一个客人都专门分配一个服务员去处理,那成本得多大呀。所以我们的生活中一般是这样的,客人进门,点菜,然后服务员把这个客人要的菜记下来,然后再来一个客人再记下来。然后由这个服务员去服务这一批客人的点的菜,这样就能做到资源利用率最大化,只需要用一个服务员,就能同时服务很多个客人,客人只需要把菜的需求托管给这个服务员就行了,别的什么也不用管。

在上面说的例子中,服务员所扮演的角色,就和我们的selector差不多。客人点菜告诉服务员吃什么菜,也就相当于channel注册到selector的过程。先看看示例中的代码

//将serverSocketChannel注册到selector上 注册为accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

//channel 注册到selector上 注册为读事件
socketChannel.register(selector,SelectionKey.OP_READ);


从代码种可以看出,不管是服务端channel,serverChannel,还是socketChannel,都是注册到selector进行统一的管理。然后selector会给这个channel一个标志。也就是这一句

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


然后selector就会不断地轮询,当某个channel的事件(读事件,写事件,连接事件等等)准备就绪的时候,就是会找到这个channel对应的SelectionKey,去做相应的操作,读写数据等等。

      Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();

//selector需要不断的去查询数据准备好了没 也可以看作是一种阻塞
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if(selectionKey == '读事件'){
//do something
}else if("写事件"){
//do something
}else if.....
}


所以,selector其实也就相当于一个管家,管理着所有的channel和各种事件,这样新来了连接,就不用服务端来新建线程来服务这个连接,只需要把这个连接注册到selector,由selector代劳注册到自己身上的那些channel去做那些轮询,等某个连接的事件就绪了,就告知那个channel来处理。这样全程只用了selector这一个线程,却能服务大量的channel。这也是java NIO高效的原因。这就是所谓的IO多路复用机制。具体的可以去看上面两个连接的文章,有具体的讲到io多路复用。

总结

Java的NIO中,有三个核心的组件,分别是channel,buffer、selector

  • channel 代表的是实际的一条连接。这个连接有多种类型,可以是io,网络,或者硬件设备连接
  • buffer 是一种容器,和channel结合使用,用来提升channel的运输效率。有三个核心参数,capacity、limit、position
  • selector是一种多路复用器,channel会注册到selector上,由selector来管理和轮询channel的多种事件,当某事件到达时,交由channel处理,实现了一个线程管理多个channel