NIO中的buffer、channel、selector
在网络IO简述中我们简单说过了一下缓存区buffer、通道channel、选择器selector,本章我们就来聊一聊这三个知识点。
缓冲区 Buffer
buffer缓冲区实际上就是一块内存空间,而我们的nio对该内存的读写等操作,封装成了一系列的 Buffer Api。计算机内存我们会分为两份,一份是给操作系统使用的(Linux1G,Windows2G),另一份是给程序使用的;操作系统中的内存也会分配出一部分内存空间用作为buffer缓冲,而这个缓冲区我们称为内存缓冲区;我们的程序进程也有buffer缓冲区内存空间,我们把这个缓冲区叫做进程缓冲区;一般情况下,我们进程读数据都是先从核心缓冲区中复制到进程缓冲区再读取进程缓冲区中的数据;写数据也是把数据从进程缓冲区复制到核心缓冲区再写出到目标中去;但我们NIO Buffer Api 有两种创建方式,一种就是创建进程缓冲区的,另一种是创建核心缓冲区的,我们接下来详细说一下 NIO Buffer Api。
buffer缓冲区既然实际上就是一块内存,而我们之前在Go语言基础的文章中讲过,内存都是用来存储数据的,而我们的buffer内存也是如此,存储的是数组类型的数据,而我们 JAVA NIO 的 buffer 接口有多个实现类,每个实现类都是所支持的类型,分别有:
- ShortBuffer
- FloatBuffer
- IntBuffer
- DoubleBuffer
- ByteBuffer
- LongBuffer
- CharBuffer
上述这些不同类型的Buffer,其实本质上就是数组(如 CharBuffer 底层数据类型 char[] hb),所以 Buffer Api 只是一套操作对应类型数组的方法。
为了方便使用buffer缓冲区内存,我们JAVA NIO对buffer操作封装了一套Api,我们通过熟悉这些Api就能使用缓冲区了,我们先来看一下buffer的一些属性的概念意思:
- capacity 容量,buffer缓冲区的大小(数组的大小)
- limit 界限,buffer缓冲区中元素可以被操作的范围(比如 capacity10,limit5,表示buffer数组的前五个元素可以被访问)。
- position 位置,get/put操作时,下一个可以被操作的元素的位置(有点像iterator.next(),只不过next()获取的是下一个元素,而position()是获取数组中的位置)。
- mark 标记,把当前position的位置记录到mark中。
- rest 重置,把position下一个元素的下标位置变更为mark所记录的下标位置。
我们来看一下Buffer的两种创建方式:
- xxxBuffer.allocateDirect(capacity) 直接缓冲区,就是核心缓冲区,直接再其虚拟地址映射空间中去完成IO操作,不需从核心缓冲区中复制到进程缓冲区再操作缓冲区buffer的数据。
- xxxBuffer.allocate(capacity) 缓冲区(非直接缓冲区),在进程中创建一个缓冲区buffer,从核心缓冲区复制到进程缓冲区进行读写操作。
Buffer Api 常用方法:
- get 获取缓冲区数据(get()、get(xxx[] array)、get(index)position不会变、…等)
- put 写入数据到缓冲区(put(xxx)、put(xxx[] array)、…等)
- Buffer clear() 清除,position=0、limit=capacity、mark=-1、return this。
- Buffer flip() 翻转,limit=positoin、position=0、mark=-1、return this,写入切换为读取(重置操作下标为0)。
- int capacity() 容量,获取缓冲区容量大小。
- boolean hasRemaining() 缓冲区是否还有剩余的元素(position<limit)。
- int limit() 获取界限。(上述以解释过界限的意思了)
- Buffer limit(int n) 设置界限。
- Buffer mark() 把position的值记录到mark中。
- int position() 获取下一个可访问的元素下标位置。
- Buffer position(int n) 设置下一个可访问元素的下标位置。
- int remaining() 获取position与limit之间的元素个数。
- Buffer rest() 把mark标记的下标赋值到position。
- Buffer rewind() 倒带,重置位置信息 position=0,mark=-1,return this。
通道 channel
通道channel,可以理解为它是用于缓冲区和另一端(文件、套接字)传送数据的,它即可往缓冲区里写入数据,也可以从缓冲区里读取数据。
java channel 可以大致分为两类,File(文件)通道 和 Socket(套接字)通道:
- FileChannel 用于读取、写入、映射、操作文件。
- SocketChannel 通过TCP读写网络中的数据。
- ServerSocketChannel 监听TCP连接,为每个新连接创建一个SocketChannel。
- DatagramChannel 通过UDP读写网络中的数据。
Channel类获取的方法,通过以下类调用 getChannel():
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket
FileChannel的常用方法:
- int read(ByteBuffer dst) 从channel中读取数据到dst中。
- long read(ByteBuffer[] dsts) 将channel中的数据分散到dsts中。
- int write(ByteBuffer src) 把src中的数据写入channel中。
- long write(ByteBuffer[] srcs) 把srcs中的数据聚集到channel中。
- long position() 返回channel中的文件位置。
- FileChannel position(long p) 设置channel中的文件位置。
- long size() 返回channel中当前位置文件的大小。
- FileChannel truecate(long s) 把channel中的文件截取到给定的大小。
- void force(boolean metaData) 强制将通道中所有文件跟新写入到设备中。
选择器 selector
selector选择器是selectableChannel的多路复用器,当监测连接进入服务器后,会为其创建并绑定一个channel,再把channel注册到selector选择器上(指明监控事件),选择器监控到事件后,使用创建好的一个线程去处执行I/O操作。选择器可以创建多个,且一个选择器可以绑定多个通道,在监控到事件的时候线程才会去执行I/O操作;这在BIO中是不存在,这就能很大程度的节省系统的线程开销,对性能有很大的优化。所以websocket中常用的也是NIO模式(多连接数据小)