Jetty、Mina、Netty、ZooKeeper等都是基于NIO方式实现。
一、通道(Channel)
1.通道是对原 I/O 包中的流的模拟,所有数据都必须通过通道。它是一个对象,可以通过它读取和写入数据。借助通道可以用最小的总开销来访问操作系统本身的 I/O 服务。
2.主要类型
分别对应文件IO、UDP和TCP(Server和Client)。
-
FileChannel
-
DatagramChannel
-
SocketChannel
-
ServerSocketChannel
3.通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行
二、缓冲区(Buffer)
1.缓冲区:实质上是一个容器,一个连续的数组(通常指字节数组)。所有数据都通过 Buffer 对象来处理,写入到通道的所有对象都必须先放到缓冲区中;同样地,从通道中读取的任何数据都要先读到缓冲区中。nio其实就是利用缓冲区传输字节。
2.缓冲区类型
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
3.buffer的工作机制
apacity 缓冲区数组的总长度
position 下一个要操作的数据元素的位置
limit 缓冲区数组中不可操作的下一个元素的位置,limit<=capacity
mark 用于记录当前 position 的前一个位置或者默认是 0
(1)初始化buffer数组;
(2)向buffer数组开始写入几个字节的时候,position会移动到数据结束的下一个位置,这时候需要把buffer中的数据写到channel管道中,这时候会调用buffer.flip()方法;
注:buffer.flip()的作用:就会把buffer的当前位置更改为buffer缓冲区的第一个位置,设置为limit。
flip方法涉及到buffer中的Capacity,Position和Limit三个概念:
其中Capacity表示缓冲区大小,在读写模式下都是固定的;
Position类似于读写指针,表示当前读(写)到什么位置;
limit在读模式下表示最多能读多少数据(缓存中的实际数据大小相同),在写模式下表示最多能写入多少数据(和Capacity相同);
(3)调用buffer.flip()后读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。如果不调用flip()方法,就是从文件最后开始读取的,什么也读不到。
(4)再下一次写数据之前,调用clear()方法,缓冲区的索引位置又回到初始位置。(这一步有点像IO中的把转运字节数组 char[] buf = new char[1024]; 不足1024字节的部分给强制刷新出去)
package ali.java.nio.ibm;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class CopyFile {
static public void main(String args[]) throws Exception {
String infile = "c://test/nio_copy.txt";
String outfile = "c://test/result.txt";
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);
// 获取读的通道
FileChannel fread = fin.getChannel();
// 获取写的通道
FileChannel fwrite = fout.getChannel();
// 定义缓冲区,并指定大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
// 清空缓冲区
buffer.clear();
//从通道读取一个数据到缓冲区
int r = fread.read(buffer);
//判断是否有从通道读到数据
if (r == -1) {
break;
}
//将buffer指针指向头部
buffer.flip();
//把缓冲区数据写入通道
fwrite.write(buffer);
}
}
}
三、选择器(Selectors):通过一个线程管理多个通道。
1.为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(4种)
(1)connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
(2)accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
(3)read:读事件,对应值为SelectionKey.OP_READ(1)
(4)write:写事件,对应值为SelectionKey.OP_WRITE(4)
2.服务端代码
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
int n = selector.select();
if (n == 0) continue;
Iterator ite = this.selector.selectedKeys().iterator();
while(ite.hasNext()){
SelectionKey key = (SelectionKey)ite.next();
if (key.isAcceptable()){
SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
clntChan.configureBlocking(false);
//将选择器注册到连接到的客户端信道,
//并指定该信道key值的属性为OP_READ,
//同时为该信道指定关联的附件
clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
}
if (key.isReadable()){
handleRead(key);
}
if (key.isWritable() && key.isValid()){
handleWrite(key);
}
if (key.isConnectable()){
System.out.println("isConnectable = true");
}
ite.remove();
}
}
//注释
(1)创建ServerSocketChannel实例,并绑定指定端口;
(2)创建Selector实例;
(3)将serverSocketChannel注册到selector,并指定事件OP_ACCEPT,其中最底层的socket通过channel和selector建立关联;
(4)如果没有准备好的socket,select方法会被阻塞一段时间并返回0;
(5)如果底层有socket已经准备好,selector的select方法会返回socket的个数,而且selectedKeys方法会返回socket对应的事件(connect、accept、read or write);
(6)根据事件类型,进行不同的处理逻辑;
在步骤3中,selector只注册了serverSocketChannel的OP_ACCEPT事件
1、如果有客户端A连接服务,执行select方法时,可以通过serverSocketChannel获取客户端A的socketChannel,并在selector上注册socketChannel的OP_READ事件。
2、如果客户端A发送数据,会触发read事件,这样下次轮询调用select方法时,就能通过socketChannel读取数据,同时在selector上注册该socketChannel的OP_WRITE事件,实现服务器往客户端写数据。
//TODO
结合netty来看,暂时到这
3.Selector底层原理、Selector是如何做到同时管理多个socket?