NIO的三个最重要的核心分别为:Channel,Buffer和Selector。
Channel就像是通道,是一个关于程序与操作系统底层I/O服务交互的通道。
比如:我们的程序对系统中某一个文件进行连接,以便于我们对它进行后续的操作。
常见的Channel有以下四种:
当我们有了连接通道,我们需要将拿到的数据放到一个缓冲区域,以便于程序对它的读取/写入操作
常见的Buffer有以下几种:
Selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。
我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。
在selector技术之前,处理socket连接有以下两种方法。
多线程技术
系统为每一个连接分配一个thread(线程),分别去处理对应的socket连接。
这种方法的弊端:
什么是线程上下文切换?
一个CPU在同一个时刻是只能处理一个线程的,由于时间片耗尽或出现阻塞等情况,CPU 会转去执行另外一个线程,这个叫做线程上下文切换。
线程池技术
使用线程池,让线程池中的线程去处理连接
这种方法的弊端:
选择器(Selector)技术
为每个线程配合一个选择器,让选择器去管理多个channel。(注:FileChannel是阻塞式的,因此无法使用选择器。)
让选择器去管理多个工作在非阻塞式下的Channel,获取Channel上的事件,当一个Channel没有任务时,就转而去执行别的Channel上的任务。这种适合用在连接多,流量小的场景。
若事件未就绪,调用 selector 的 select() 方法会阻塞线程,直到 channel 发生了就绪事件。这些事件就绪后,select 方法就会返回这些事件交给 thread 来处理
首先了解ByteBuffer的四个属性:
allocate是ByteBuffer的一个静态方法。通过allocate我们可以给ByteBuffer分配空间,但是这个空间不可以动态变换,如果想要改变ByteBuffer的大小只能重新分配一个。
allocateDirect也是ByteBuffer的一个静态方法。通过allocateDirect我们也可以给ByteBuffer分配空间。
allocate 与 allocateDirect的区别在哪?
可以通过代码来看,首先我们通过这两个静态方法创建两个对象。
public static void main(String[] args) {
System.out.println(ByteBuffer.allocate(10).getClass());
//class java.nio.HeapByteBuffer
System.out.println(ByteBuffer.allocateDirect(10).getClass());
//class java.nio.DirectByteBuffer
}
控制打印的结果是:allocate创建出来的是HeapByteBuffer对象,allocateDirect创建出来的是DirectByteBuffer对象。
那就聊一下HeapByteBuffer和DirectByteBuffer的区别:
put方法可以将数据放入到缓冲区中。操作完成后,position的值会+1,并指向下一个可存放的区域,limit=capacity。
flip方法会切换对当前缓冲区的去操作,写/读->读/写。
当是写模式切换到读模式时,先是limit=position,然后position=0。
当是读模式切换到写模式时,恢复为put时的值。
clean方法就像初始化一样,会把ByteBuffer的里属性值都恢复到最初,并且清除缓冲区里的数据。
compact方法会把已经读取的数据清除,后面未读取的数据向前压缩,然后切换到写模式。
数据前移后,原始位置的数据不会清楚,但是在后面的写入操作中会被覆盖。
rewind方法只能在读模式下使用,使用后,会恢复position、limit和capacity的值
这个两个方法通常都是搭配着使用。
mark会保存当前position的值,reset方法会把mark保存的值重新赋给position。
// 方法一
// 编码:字符串的getByte方法
ByteBuffer buffer = ByteBuffer.allocate(15);
buffer.put(str.getBytes());
// 解码:先切换到读模式,然后通过StandardCharsets的decoder来解码
buffer.flip();
String decodeStr = StandardCharsets.UTF_8.decode(buffer).toString();
// 方法二
// 编码:StandardCharsets的encode方法获取ByteBuffer
ByteBuffer buffer2 = StandardCharsets.UTF_8.encode(str);
// 解码: 通过StandardCharsets的decoder方法解码
String decodeStr2 = StandardCharsets.UTF_8.decode(buffer2).toString();
// 方法三:
ByteBuffer buffer3 = ByteBuffer.wrap(str.getBytes());
// 解码: 通过StandardCharsets的decoder方法解码
String decodeStr3 = StandardCharsets.UTF_8.decode(buffer3).toString();
将数据打包发送给服务端,每条数据以"\n"做分割。
比如现在有这三段话,我要一次性发给服务器
Hello world!\n
I’m LIKEGAKKI!\n
How are you?\n
经过传输后,服务端的产生了两个ByteBuffer:
Hello,world\nI’m LIKEGAKKI\nHo(黏包)
w are you?\n?(半包)
发送方在发送数据时,并不是一条一条地发送数据,而是将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去。
因为我们分配缓冲区的大小是固定,如果空间小于数据量,那就只能先把当前缓冲区里的数据读取完,再去接收剩下的的数据。数据就会出现被截断的断层现象。