1.Java NIO基本介绍
Java NIO全称java non-blocking IO,是指JDK提供的新API,从JDK 1.4开始,Java提供了一系列改进
的输入/输出的新特性,被称为NIO,是同步非阻塞的。
NIO相关类都被放在java.nio包下,并且对原java.io包中的很多类进行了改写。
NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。
NIO是面向缓冲区或者面向块编程的,数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就
增加了处理过程中的灵活性,使用它可以提供非阻塞时的高伸缩性网络。
NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,
如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以,直到数据变得可以
读取之前,该线程可以继续做其他事情,非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要
等待它完全写入,这个线程同时可以去做别的事情。
通俗理解:NIO是可以做到一个线程来处理多个操作的,假设有10000个请求过来,根据实际
情况,可以分配50或100个线程来处理,不像之前的阻塞IO那样,非得分配10000个。
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,并且并发请求的数量比HTTP1.1大了好几个数量级。
2.NIO和BIO的比较
BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多。
BIO是阻塞的,NIO则是非阻塞的。
BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行
操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)
用于监听多个通道的事件(比如:连接请求、数据到达等),因此使用单个线程就可以监听
多个客户端通道。
3.NIO三大核心原理示意图
Selector、Channel和Buffer的关系图
一张图描述NIO的Selector、Channel和Buffer的关系。
关系图的说明
1)每个Channel都会对应一个Buffer
2)Selector对应一个线程,一个线程对应多个Channel
3)该图反映了有三个Channel注册到该Selector程序
4)程序切换到哪个Channel是由事件决定的,event是一个重要的概念
5)Selector会根据不同的事件,在各个通道上切换
6)Buffer就是一个内存块,底层有一个数组
7)数据的读取写入是通过Buffer,Buffer可以读也可以写,需要flip方法切换
8)Channel是双向的,可以返回底层操作系统的情况,比如Linux底层的操作系统通道就是双向的
4.缓冲区(Buffer)
基本介绍
缓冲区(Buffer):本质上是可以读写数据的内存块(数组),其提供了一些方法,能够跟踪跟踪缓冲区的状态变化。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经过缓冲区。
Buffer类及其子类
在NIO中,Buffer是一个顶层父类,它是一个抽象类,其子类包括
-
ByteBuffer(最常用)
-
ShortBuffer
-
CharBuffer
-
IntBuffer
-
LongBuffer
-
DoubleBuffer
-
FloatBuffer
Buffer类有四个属性:
-
capacity:容量,即可以容纳的最大数据量,在缓冲区创建时被设定且不能改变
-
limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作
-
position:表示下一次要被读或写的元素的索引,每次读写缓冲区数据时都会改变,为下次读写做准备
-
mark:标记
Bytebuffer
最常用的是ByteBuffer类(二进制数据),该类的主要方法如下:
public abstract class ByteBuffer { //缓冲区创建相关api public static ByteBuffer allocateDirect(int capacity)//创建直接缓存区 public static ByteBuffer allocate(int capacity) public static ByteBuffer wrap(byte[] array) public static ByteBuffer wrap(byte[] array, int offset, in length) //缓存区存取相关api public abstract byte get() public abstract byte get(int index) public abstract ByteBuffer put(byte b) public abstract ByteBuffer put(int index, byte b) }
5.通道(Channel)
基本介绍
NIO的通道类似于流,但有些区别如下:
-
通道可以同时进行读写,而流只能读或只能写。BIO中stream是单向的,例如FileInputStream只能进行读取数据的操作。而NIO中的channel是双向的,可以读操作,也可以写操作。
-
通道可以实现异步读写数据
-
通道可以从缓冲区读数据,也可以写数据到缓冲区
常用的Channel类:FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel
FileChannel类
FileChannel主要用来对本地文件进行IO操作,常见的方法有
public int read(ByteBuffer dst)//从通道读取数据并放入到缓冲区中 public int write(ByteBuffer src)//把缓冲区的数据写入到通道中 //从目标通道中复制数据到当前通道 public long transferFrom(ReadableByteChannel src, long position, long count) //把数据从当前通道复制给目标通道 public long transferTo(long position, long count, WritableByteChannel target)
关于Buffer和Channel的注意事项和细节
NIO还提供了MappedByeBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步
到文件由NIO来完成。
NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即Scattering和Gatering。
6.Selector(选择器)
基本介绍
Java NIO,用非阻塞的方式,可以用同一个线程,处理多个客户端的链接,就会使用到Selector。
Selector能够检测多个注册的通道是否有事件发生,如果有事件发生,便获取事件针对每个事件进行相应的处理,这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为
每个连接都创建一个线程,不用去维护多个线程,避免了多个线程之间的上下文切换导致的开销。
Selector示意图和特点说明
特点说明:
1)Selector可以同时并发处理成百上千个客户端连接
2)当线程从某客户端socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
3)线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理
多个输入和输出通道
4)由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起
5)一个IO线程可以并发处理N个客户端连接和读写操作,这次根本上解决了传统同步阻塞IO一连接
一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升
Selector类相关方法
Selector类是一个抽象类,常用方法和说明如下:
public static Selector open() throws IOException; public abstract int select() throws IOException; //如果没有事件会一直阻塞 public abstract int selectNow() throws IOException; //非阻塞,立刻返回 public abstract int select(long timeout) throws IOException;//等待一段时间 public abstract Set<SelectionKey> selectedKeys();//有事件发生的SelectionKey的集合 public abstract Selector wakeup();//唤醒
7.NIO非阻塞网络编程原理图
NIO非阻塞网络编程相关的Selector、ServerSocketChannel、SocketChannel关系图
说明:
1)服务端会有个Selector,
2)当服务端启动时,ServerSocketChannel注册到Selector上,然后Selector进行事件循环,对事件进行响应
2)Selector进行事件循环
-
accept事件:当客户端连接时,会有accept事件产生,ServerSocketChannel通过accept得到SocketChannel,并将SocketChannel注册到Selector上
-
read事件:获取到对应的channel,进行读事件处理
-
write事件:获取到对应的channel,进行写事件处理
8.NIO非阻塞网络编程快速入门
9.SelectionKey
SelectionKey,表示Selector和网络通道的注册关系,共四种:
int OP_ACCEPT:有新的网络连接可以accept,值为16
int OP_CONNECT:代表连接已经建立,值为8
int OP_READ:代表读操作,值为1
int OP_WRITE:代表写操作,值为4
源码中:
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
SelectionKey相关方法
public abstract class SelectionKey { public abstract SelectableChannel channel(); public abstract Selector selector(); public final Object attachment() public abstract SelectionKey interestOps(int ops); public final boolean isReadable() public final boolean isWritable() public final boolean isConnectable() public final boolean isAcceptable() }
10.ServerSocketChannel
ServerSocketChannel在服务器端监听新的客户端连接。
常用方法如下:
public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel { public static ServerSocketChannel open() public final ServerSocketChannel bind(SocketAddress local) public abstract SocketChannel accept() public final SelectableChannel configureBlocking(boolean block) public final SelectionKey register(Selector sel, int ops, Object att) }
11.SocketChannel
SocketChannel,网络IO通道,具体负责进行读写操作,NIO把缓冲区的数据写入通道,或者
把通道里的数据读到缓冲区。
常用方法如下:
public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel { public static SocketChannel open() public boolean connect(SocketAddress remote) public boolean finishConnect() public abstract int write(ByteBuffer src) public abstract int read(ByteBuffer dst) public final SelectableChannel configureBlocking(boolean block) public final SelectionKey register(Selector sel, int ops, Object att) }
12.NIO网络编程应用实例——群聊系统
目的:理解NIO非阻塞网络编程机制
要求:
1)编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2)实现多人群聊
3)服务器端:可以检测用户上线、离线,并实现消息转发功能
4)客户端:通过channel可以无阻塞发送消息非其他所有用户,同时可以接受其他用户
发送的消息(由服务器转发得到)
服务器端:
package com.thelight1.groupchat; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; public class GroupChatServer { private Selector selector; private ServerSocketChannel listenChannel; private static final int PORT = 6667; public GroupChatServer() { try { selector = Selector.open(); listenChannel = ServerSocketChannel.open(); listenChannel.socket().bind(new InetSocketAddress(PORT)); listenChannel.configureBlocking(false); listenChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务器启动了......"); } catch (IOException e) { e.printStackTrace(); } } public void listen() { try { while (true) { int count = selector.select(2000); if (count > 0) { Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isAcceptable()) { SocketChannel socketChannel = listenChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); System.out.println(socketChannel.getRemoteAddress() + " 上线了...."); } if (key.isReadable()) { readData(key); } iterator.remove(); } } else { // System.out.println("等待....."); } } } catch (Exception e) { e.printStackTrace(); } finally { } } private void readData(SelectionKey key) { SocketChannel channel = null; try { channel = (SocketChannel)key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int count = channel.read(buffer); if (count > 0) { String msg = new String(buffer.array()); System.out.println("from 客户端:" + msg.trim()); sendInfoToOtherClients(msg, channel); } } catch (IOException e) { try { System.out.println(channel.getRemoteAddress() + "离线了"); //取消注册 key.cancel(); //关闭通道 channel.close(); } catch (IOException e1) { e1.printStackTrace(); } } } private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException { System.out.println("服务器转发消息中......."); for (SelectionKey key : selector.keys()) { Channel channel = key.channel(); if (channel instanceof SocketChannel && channel != self) { SocketChannel socketChannel = (SocketChannel)channel; ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); socketChannel.write(buffer); } } } public static void main(String[] args) { GroupChatServer groupChatServer = new GroupChatServer(); groupChatServer.listen(); } }
客户端:
package com.thelight1.groupchat; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Scanner; public class GroupChatClient { private static final String HOST = "127.0.0.1"; private static final int PORT = 6667; private Selector selector; private SocketChannel socketChannel; private String username; public GroupChatClient() throws IOException { selector = Selector.open(); socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT)); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); username = socketChannel.getLocalAddress().toString().substring(1); System.out.println(username + "is ok"); } public void sendInfo(String info) { info = username + "说:" + info; try { socketChannel.write(ByteBuffer.wrap(info.getBytes())); } catch (IOException e) { e.printStackTrace(); } } public void readInfo() { try { int readChannel = selector.select(); if (readChannel > 0) { Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isReadable()) { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int count = channel.read(buffer); if (count > 0) { String msg = new String(buffer.array()); System.out.println(msg.trim()); } } iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { final GroupChatClient groupChatClient = new GroupChatClient(); Thread thread = new Thread(new Runnable() { @Override public void run() { while (true) { groupChatClient.readInfo(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); thread.setDaemon(true); thread.start(); Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String str = scanner.nextLine(); groupChatClient.sendInfo(str); } } }
13.NIO与零拷贝(TODO)
14.Java AIO基本介绍
JDK7引入了Asynchronous I/O,即AIO,在进行I/O编程中,常用到两种模式:Reactor和Proactor。
Java的NIO就是Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO即NIO2.0,也叫异步不阻塞的IO。AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效地请求才启动线程, 它的特点是先由操作系统完成后才通知服务器端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
目前AIO还没有广泛使用,Netty也是基于NIO,而不是AIO。
有兴趣的可以参考《Java新一代网络编程模型AIO原理及Linux系统AIO介绍》http://www.52im.net/thread-306-1-1.html
BIO、NIO、AIO对比表
举例说明:
同步阻塞:到理发店理发,就一直等理发师,知道轮到自己理发
同步非阻塞:到理发店理发,发现前面有其他人理发,给理发师说下,先干其他事情,一会过来看是否
轮到自己
异步非阻塞:给理发师打电话,让理发师上门服务,自己干其他事情,理发师来你家给你理发