NIO 库是在JDK 1.4 中引入的,为弥补了原先 I/O 的不足,它在标准Java代码中提供了高速的、面向块的 I/O。NIO可以翻译成 no-blocking io或者new io
JDK提供的NIO和BIO之间最大的一个区别是,BIO是面向流的,而NIO则是面向缓冲区的。BIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。除此之外,它也不能前后移动流中的数据。如果需要前后移动从流中读取的数据,则需要先将它缓存到一个缓冲区。而NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据
BIO属于同步阻塞模型,意味着,当一个线程调用read()或者write()方法时,该线程被阻塞,直到有一些数据被读取,或数据完全写入,否则该线程在此期间不能再干任何事情了
NIO属于非阻塞模型,一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而且不会让线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)
NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道
Selector的英文含义是“选择器”,也可以称为为“轮询代理器”、“事件订阅器”、“channel容器管理机”都行
应用程序将向Selector选择器注册需要它关注的Channel,以及对应的Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器
SelectionKey是一个抽象类,表示selectableChannel在Selector中注册的标识.每个Channel 向 Selector 注册时,都将会创建一个 SelectionKey。SelectionKey将Channel与Selector 建立了关系,并维护了channel事件。可以通过cancel方法取消键,取消的键不会立即从selector 中移除,而是添加到cancelledKeys中,在下一次select操作时移除它,所以在调用某个 key时,需要使用isValid进行校验
在向Selector对象注册感兴趣的事件时,NIO共定义了四种:OP_READ、OP_WRITE、 OP_CONNECT、OP_ACCEPT(定义在 SelectionKey类中),分别对应读、写、请求连接、接受连接等网络Socket操作
操作类型 | 就绪条件及说明 |
---|---|
OP_READ | 当操作系统读缓冲区有数据可读时就绪。并非时刻都有数据可读,所 以一般需要注册该操作,仅当有就绪时才发起读操作,有的放矢,避免浪 费 CPU |
OP_WRITE | 当操作系统写缓冲区有空闲空间时就绪。一般情况下写缓冲区都有空 闲空间,小块数据直接写入即可,没必要注册该操作类型,否则该条件不 断就绪浪费 CPU;但如果是写密集型的任务,比如文件下载等,缓冲区很 可能满,注册该操作类型就很有必要,同时注意写完后取消注册 |
OP_CONNECT | 当 SocketChannel.connect()请求连接成功后就绪。该操作只给客户端 使用 |
OP_ACCEPT | 当接收到一个客户端连接请求时就绪。该操作只给服务器使用 |
ServerSocketChannel 和 SocketChannel 可以注册自己感兴趣的操作类型,当对应操作类 型的就绪条件满足时 OS 会通知 channel,下表描述各种 Channel 允许注册的操作类型,Y 表 示允许注册,N 表示不允许注册,其中服务器SocketChannel 指由服务器 ServerSocketChannel.accept()返回的对象
OP_READ | OP_WRITE | OP_CONNECT | OP_ACCEPT | |
---|---|---|---|---|
服务器 ServerSocketChannel | Y | |||
服务器 SocketChannel | Y | Y | ||
客户端 SocketChanne | Y | Y | Y |
服务器启动 ServerSocketChannel,关注 OP_ACCEPT 事件
客户端启动 SocketChannel,连接服务器,关注 OP_CONNECT 事件
服务器接受连接,启动一个服务器的 SocketChannel,这个 SocketChannel 可以关注 OP_READ、OP_WRITE 事件,一般连接建立后会直接关注 OP_READ 事件
客户端这边的客户端 SocketChannel 发现连接建立后,可以关注 OP_READ、OP_WRITE 事件,一般是需要客户端需要发送数据了才关注 OP_READ 事件
连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注OP_READ、 OP_WRITE 事件
Channels称为通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操 作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数 据,也可以通过通道向操作系统写数据,而且可以同时进行读写
通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入
Buffer 用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。 以写为例,应用程序都是将数据写入缓冲,再通过通道把缓冲的数据发送出去,读也是一样, 数据总是先从通道读到缓冲,应用程序再读缓冲的数据
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存( 其实就是数组)。 这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存
capacity
作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里写 capacity 个 byte、long,char 等类型。一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据) 才能继续写数据往里写数据
position
当你写数据到 Buffer 中时,position 表示当前能写的位置。初始的 position 值为 0.当一 个 byte、long 等数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单 元。position 最大可为 capacity – 1
当读取数据时,也是从某个特定位置读。当将 Buffer 从写模式切换到读模式,position 会被重置为 0. 当从 Buffer 的 position 处读取数据时,position 向前移动到下一个可读的位置
limit
在写模式下,Buffer 的 limit 表示你最多能往 Buffer 里写多少数据。 写模式下,limit 等 于 Buffer 的 capacity。当切换 Buffer 到读模式时, limit 表示你最多能读到多少数据。因此,当切换 Buffer 到 读模式时,limit 会被设置成写模式下的 position 值。换句话说,你能读到之前写入的所有数 据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)
堆内存分配
要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有allocate方法,分配 48 字节 capacity 的 ByteBuffer 的例子:
ByteBuffer buf = ByteBuffer.allocate(48);
直接内存分配
HeapByteBuffer 与 DirectByteBuffer,在原理上,前者可以看出分配的 buffer 是在 heap 区域的,其实真正 flush 到远程的时候会先拷贝到直接内存,再做下一步操作;在 NIO 的框 架下,很多框架会采用 DirectByteBuffer 来操作,这样分配的内存不再是在 java heap 上,经 过性能测试,可以得到非常快速的网络交互,在大量的网络交互下,一般速度会比 HeapByteBuffer 要快速好几倍
直接内存与堆内存比较
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
直接内存 IO 读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
向Buffer中写数据
读取Channel写到Buffer
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
通过Buffer的put()方法写到Buffer
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
flip()方法
flip()方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
从Buffer中读取数据
从Buffer读取数据写入到Channel
//发送缓冲区的字节数组
channel.write(writeBuffer);
使用get()方法从Buffer中读取数据
byte aByte = buffer.get()
使用Buffer读写数据常见步骤
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面
limit(), limit(1 0)等 | 其中读取和设置这 4 个属性的方法的命名和 jQuery 中的 val(),val(10)类似,一个负 责 get,一个负责 set |
---|---|
reset() | 把 position 设置成 mark 的值,相当于之前做过一个标记,现在要退回到之前标记 的地方 |
clear() | position = 0;limit = capacity;mark = -1; 有点初始化的味道,但是并不影响底 层 byte 数组的内容 |
flip() | limit = position;position = 0;mark = -1; 翻转,也就是让 flip 之后的 position 到 limit 这块区域变成之前的 0 到 position 这块,翻转就是将一个处于存数据状态的缓 冲区变为一个处于准备取数据的状态 |
rewind() | 把 position 设为 0,mark 设为-1,不改变 limit 的值 |
remaining() | return limit - position;返回 limit 和 position 之间相对位置差 |
hasRemaining () | return position < limit 返回是否还有未读内容 |
compact() | 把从 position 到 limit 中的内容移到 0 到 limit-position 的区域内,position 和 li mit 的取值也分别变成 limit-position、capacity。如果先将 positon 设置到 limit,再 c ompact,那么相当于 clear() |
get() | 相对读,从 position 位置读取一个 byte,并将 position+1,为下次读写作准备 |
get(int index) | 绝对读,读取 byteBuffer 底层的 bytes 中下标为 index 的 byte,不改变 position |
get(byte[] dst, int offset, int len gth) | 从 position 位置开始相对读,读 length 个 byte,并写入 dst 下标从 offset 到 offs et+length 的区域 |
put(byte b) | 相对写,向 position 的位置写入一个 byte,并将 postion+1,为下次读写作准备 |
put(int index, byte b) | 绝对写,向 byteBuffer 底层的 bytes 中下标为 index 的位置插入 byte b,不改变 p osition |
put(ByteBuffer src) | 用相对写,把 src 中可读的部分(也就是 position 到 limit)写入此 byteBuffer |
put(byte[] src, int offset, int len gth) | 从 src 数组中的 offset 到 offset+length 区域读取数据并使用相对写写入此 byteBuffer |
wrap(byte[] array)、 wrap(byte [] array, int offset, int length) | 把一个 byte 数组或 byte 数组的一部分包装成 ByteBuffer |
“反应”即“倒置”,“控制逆转”,具体事件处理程序不调用反应器,而向反应器注册一个事件处理器,表示自己对某些事件感兴趣,有时间来了,具体事件处理程序通过事件处理器对某个指定的事件发生做出反应
注意:Reactor 的单线程模式的单线程主要是针对于 I/O 操作而言,也就是所有的 I/O 的 accept()、read()、write()以及 connect()操作都在一个线程上完成的
但在目前的单线程 Reactor 模式中,不仅 I/O 操作在该 Reactor 线程上,连非 I/O 的业务 操作也在该线程上进行处理了,这可能会大大延迟 I/O 请求的响应。所以我们应该将非 I/O 的业务逻辑操作从 Reactor 线程上卸载,以此来加速 Reactor 线程对 I/O 请求的响应
与单线程 Reactor 模式不同的是,添加了一个工作者线程池,并将非 I/O 操作从 Reactor 线程中移出转交给工作者线程池来执行。这样能够提高 Reactor 线程的 I/O 响应,不至于因 为一些耗时的业务逻辑而延迟对后面 I/O 请求的处理
使用线程池的优势
改进的版本中,所有的 I/O 操作依旧由一个 Reactor 来完成,包括 I/O 的 accept()、read()、 write()以及 connect()操作
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发或大数据量 的应用场景却不合适,主要原因如下:
Reactor 线程池中的每一 Reactor 线程都会有自己的 Selector、线程和分发的事件循环逻辑
mainReactor 可以只有一个,但 subReactor 一般会有多个。mainReactor 线程主要负责接 收客户端的连接请求,然后将接收到的 SocketChannel 传递给 subReactor,由 subReactor 来 完成和客户端的通信
核心流程:
注意:所有的 I/O 操作(包括,I/O 的 accept()、read()、write()以及 connect()操作)依旧还 是在 Reactor 线程(mainReactor 线程 或 subReactor 线程)中完成的。Thread Pool(线程池)仅用 来处理非 I/O 操作的逻辑
多Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样一来就不会因为read()数据量太大而导致后面的客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量
public class NioServerHandle implements Runnable{
private volatile boolean isStarted;
//创建服务端serverSocketChannel
private ServerSocketChannel serverSocketChannel;
//创建选择器
private Selector selector;
public NioServerHandle() {
try {
//创建选择器
selector = Selector.open();
//创建serverSocketChannel
serverSocketChannel = ServerSocketChannel.open();
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(8761));
//注册事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
isStarted=true;
System.out.println("服务器已启动,端口号:" + 8761);
} catch (IOException e) {
isStarted=false;
e.printStackTrace();
}
}
@Override
public void run() {
while(isStarted){
try {
//阻塞,只有当至少一个注册的事件发生的时候才会继续
selector.select();
Set<SelectionKey> selectionKeys= selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
while(it.hasNext()){
SelectionKey selectionKey=it.next();
it.remove();
hanldeInput(selectionKey);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// TODO: handle finally clause
}
}
}
private void hanldeInput(SelectionKey selectionKey) throws IOException {
//先确认是否可用
if(selectionKey.isValid()){
//接收连接事件
if(selectionKey.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel= serverSocketChannel.accept();
socketChannel.configureBlocking(false);
System.out.println("=接受连接,并注册读事件");
socketChannel.register(selector, SelectionKey.OP_READ);
}
//读事件
if(selectionKey.isReadable()){
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read=socketChannel.read(buffer);
if(read>0){
//读取
buffer.flip();
byte[] bytes=new byte[buffer.remaining()];
buffer.get(bytes);
String message = new String(bytes,"UTF-8");
System.out.println("服务器端接收的消息为:" + message);
String response ="Hello,"+message+",Now is "+new java.util.Date(
System.currentTimeMillis()).toString();
//应答消息
doWrite(socketChannel,response);
}else{//链路已经关闭,释放资源
selectionKey.cancel();
socketChannel.close();
}
}
//写事件
if(key.isWritable()){
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer att = (ByteBuffer)key.attachment();
if(att.hasRemaining()){
int count = sc.write(att);
System.out.println("write:"+count+ "byte ,hasR:"
+att.hasRemaining());
}else{
//只监听读事件,移出写事件
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
//发送应答消息
private void doWrite(SocketChannel channel,String response)
throws IOException {
//将消息编码为字节数组
byte[] bytes = response.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//同时注册读和写的事件
channel.register(selector,SelectionKey.OP_WRITE|SelectionKey.OP_READ,
writeBuffer);
}
}
启动服务端
public class NioServer {
private static NioServerHandle nioServerHandle;
public static void start(){
nioServerHandle = new NioServerHandle();
new Thread(nioServerHandle,"Server").start();
}
public static void main(String[] args){
start();
}
}
public class NioClientHandle implements Runnable{
private String host;
private int port;
private volatile boolean started;
private Selector selector;
private SocketChannel socketChannel;
public NioClientHandle() {
try {
/*创建选择器*/
this.selector = Selector.open();
/*打开监听通道*/
socketChannel = SocketChannel.open();
/*如果为 true,则此通道将被置于阻塞模式;
* 如果为 false,则此通道将被置于非阻塞模式
* 缺省为true*/
socketChannel.configureBlocking(false);
started = true;
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//连接服务器
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
/*循环遍历selector*/
while(started){
try {
/*阻塞方法,当至少一个注册的事件发生的时候就会继续*/
selector.select();
/*获取当前有哪些事件可以使用*/
Set<SelectionKey> keys = selector.selectedKeys();
/*转换为迭代器*/
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
/*我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
如果我们没有删除处理过的键,那么它仍然会在事件集合中以一个激活
的键出现,这会导致我们尝试再次处理它。*/
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if(key!=null){
key.cancel();
if(key.channel()!=null){
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
if(selector!=null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*具体的事件处理方法*/
private void handleInput(SelectionKey key) throws IOException {
if(key.isValid()){
/*获得关心当前事件的channel*/
SocketChannel sc =(SocketChannel)key.channel();
/*处理连接就绪事件
* 但是三次握手未必就成功了,所以需要等待握手完成和判断握手是否成功*/
if(key.isConnectable()){
/*finishConnect的主要作用就是确认通道连接已建立,
方便后续IO操作(读写)不会因连接没建立而
导致NotYetConnectedException异常。*/
if(sc.finishConnect()){
/*连接既然已经建立,当然就需要注册读事件,
写事件一般是不需要注册的。*/
socketChannel.register(selector,SelectionKey.OP_READ);
}else System.exit(-1);
}
/*处理读事件,也就是当前有数据可读*/
if(key.isReadable()){
/*创建ByteBuffer,并开辟一个1k的缓冲区*/
ByteBuffer buffer = ByteBuffer.allocate(1024);
/*将通道的数据读取到缓冲区,read方法返回读取到的字节数*/
int readBytes = sc.read(buffer);
if(readBytes>0){
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("客户端收到消息:"+result);
}
/*链路已经关闭,释放资源*/
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
/*进行连接*/
private void doConnect() throws IOException {
/*如果此通道处于非阻塞模式,则调用此方法将启动非阻塞连接操作。
如果连接马上建立成功,则此方法返回true。
否则,此方法返回false,
因此我们必须关注连接就绪事件,
并通过调用finishConnect方法完成连接操作。*/
if(socketChannel.connect(new InetSocketAddress("127.0.0.1",8761))){
/*连接成功,关注读事件*/
socketChannel.register(selector,SelectionKey.OP_READ);
}
else{
socketChannel.register(selector,SelectionKey.OP_CONNECT);
}
}
/*写数据对外暴露的API*/
public void sendMsg(String msg) throws IOException {
doWrite(socketChannel,msg);
}
private void doWrite(SocketChannel sc,String request) throws IOException {
byte[] bytes = request.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
}
}
启动客户端
public class NioClient {
private static NioClientHandle nioClientHandle;
public static void start(){
nioClientHandle = new NioClientHandle();
new Thread(nioClientHandle,"Server").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
nioClientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) throws Exception {
start();
Scanner scanner = new Scanner(System.in);
while(NioClient.sendMsg(scanner.next()));
}
}