洪宇 2019-12-13
从哪哪哪读(写)数据,数据读(写)到哪里,看看这里或者那里能不能读(写)。
在Java NIO里的所有IO操作都从Channel开始。数据可以从 Channel 读取到 Buffer 中,也可以从 Buffer 写到 Channel 中。如下图所示
Channel 必须配合 Buffer 使用,总是先读取到一个 Buffer 中,又或者是向一个 Buffer 写入。也就是说,我们无法绕过 Buffer ,直接向 Channel 写入数据
Channel的实现
Channel在Java中作为一个接口,java.nio.channels.Channel 定义IO操作的联通与关闭
package java.nio.channels;
import java.io.IOException;
import java.io.Closeable;
public interface Channel extends Closeable {
public boolean isOpen();
//判断通道是否开启
public void close() throws IOException;
//关闭通道
}
Channel实现类众多,最重要的四个Channel实现类如下:
SocketChannel :一个客户端用来发起 TCP 的 Channel 。
ServerSocketChannel :一个服务端用来监听新进来的连接的 TCP 的 Channel 。对于每一个新进来的连接,都会创建一个对应的 SocketChannel 。
DatagramChannel :通过 UDP 读写数据。
FileChannel :从文件中,读写数据。
关于Buffer
Buffer在java.nio包中实现,被定义成抽象类
Buffer有四个重要属性 capacity、limit、 position 、 mark
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity 不变量
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
//省略具体方法的代码
}
一个 Buffer ,本质上是内存中的一块,是不是一下子就想通四个属性存在必要啦
capacity 在Buffer创建时被赋值,永远不能被更改
Buffer分为读、写模式两种。两种模式下
position 和 limit 属性分别代表不同的含义。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
position属性 意为位置,初值为0
(position像是真的数据存入所占的内存所占的相对位置)
在写模式中,往Buffer中写入一个值,position 就自动加 1 ,代表下一次的写入位置。
在读模式中,从 Buffer 中读取一个值,position 就自动加 1 ,代表下一次的读取位置。( 和写模式类似 )
limit属性 意为上限。
写模式下,代表最大能写入的数据上限位置,这个时候 limit 等于 capacity 。
读模式:在Buffer完成数据写入后,通过调用 #flip() 方法,切换到读模式。此时,limit 等于 Buffer 中实际的数据大小(position)。因为 Buffer 不一定被写满,所以不能使用 capacity 作为实际的数据大小。
mark 属性,标记,通过 #mark() 方法,记录当前 position ;通过 reset() 方法,恢复 position 为标记。
写模式下,标记上一次写位置。
读模式下,标记上一次读位置。
四个属性的大小关系遵循:
mark <= position <= limit <= capacity
关于Selector
Selector , 一般称为选择器。它是 Java NIO 核心组件中的一个,用于轮询一个或多个 NIO Channel 的状态是否处于可读、可写。如此,一个线程就可以管理多个 Channel ,也就说可以管理多个网络连接。也因此,Selector 也被称为多路复用器。
将Channel注册到Selector中,Selector就知道他需要管理哪些Channel。
Selector 会不断地轮询注册在其上的 Channel 。如果某个 Channel 上面发生了读或者写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。
① 优点
使用一个线程能够处理多个 Channel 的优点是,只需要更少的线程来处理 Channel 。事实上,可以使用一个线程处理所有的 Channel 。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源( 例如 CPU、内存 )。因此,使用的线程越少越好。
② 缺点
因为在一个线程中使用了多个 Channel ,因此会造成每个 Channel 处理效率的降低。
在Netty中,对Buffer的操作少了#filp()的操作, 他的基本属性设计为:通过readerIndex和writerIndex两个属性操作。
0 <= readerIndex <= writerIndex <= capacity
通过n个线程处理Channel,解决Channel处理效率低的问题。