目录
- 为什么我们需要 NIO
- 传统 IO 的缺点 - 阻塞
- NIO 的优点 - 非阻塞
- NIO 的核心组成部分
- NIO 的工作原理
- 代码实例 - 单个服务端线程,处理多个客户端连接
- 参考
为什么我们需要 NIO
- 传统 IO 的缺点 - 阻塞
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。
因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
- NIO 的优点 - 非阻塞
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
NIO 的核心组成部分
NIO 的组件主要包括:缓冲区(Buffer)、通道(Channel)、选择器(Selector)。
- Buffer
Buffer 的作用
存放数据,底层是数组。
Buffer 的 3 个常用方法
- put():存数据到缓存区,写数据模式。
- flip():切换到读数据模式。
- get():从缓冲区中拿数据。
Buffer 的 7 种常用类型
ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer
Buffer 的获取
通过 XxxBuffer.allocate(int bufferSize) 获取 Buffer(非直接缓冲区)。
- Channel
Channel 的作用
作为数据交换的通道,即连接程序和 IO 设备(文件、套接字…)。
Channel 的主要实现类
-
FileChannel 类
本地文件IO通道,用于读取、写入、映射和操作文件的通道。
-
SocketChannel 类
网络套接字IO通道,TCP协议,针对面向流的连接套接字的可选择通道,一般用在客户端。
-
ServerSocketChannel 类
网络通信IO操作,TCP协议,针对面向流的监听套接字的可选择通道,一般用于服务端。
-
DatagramChannel 类
针对面向数据报套接字的可选择通道,能够发送和接受UDP数据包的Channel。
Channel 的获取
- java 针对支持通道的类提供了 getChannel() 方法
本地文件IO的Channel类有:FileInputStream/FileOutStream,RandomAccessFile
网络套接字IO的Channel类:SocketChannel、ServerSocketChannel、DatagraSocket - 在JDK7.0 中的 NIO2 针对各个通道提供静态方法 open()
- 在JDK7.0 中的 NIO2 的 Files 工具类的 newByteChannel()
Channel 的读取和写入方法
- read(Buffer dst):将数据从 Channel 中读出,写入到 Buffer 中。
- write(Buffer src):将数据写入到 Channel 中,数据来源于 Buffer。
- Selector
Selector 的作用
用于监控 SelectableChannel 的 IO 状况。利用 selector 可以实现在一个线程中管理多个 Channel,selector是非阻塞IO的核心。
Selector 的获取
Selector.open()
向 Selector 注册 Channel
先设置通道为非阻塞的,channel.configureBlocking(false),
然后注册,SelectableChannel.register(Selector sel, int ops),其中 ops 的参数是监听事件。
4 种监听事件
- 读取操作:SelectionKey.OP_READ,数值为 1;
- 写入操作:SelectionKey.OP_WRITE,数值为 4;
- 客户端连接服务端事件:SelectionKey.OP_CONNECT,数值为 8;
- 服务端接收客户端连接事件:SelectionKey .OP_ACCEPT,数值为 16;
若注册时不止监听一个事件,可以使用“| 位或”操作符连接。
选择键 SelectionKey
是 SelectableChannel 在 Selector 中注册的标志,每次向 Selector 注册 Channel 的时候就会生成一个 SelectionKey。
常用方法有
- SelectableChannel channel():获取注册通道
- Selector selector():返回选择器
- boolean isReadable():检查Channel中读事件是否就绪
- boolean isWriteable():检测Channel 中写事件是否就绪
- boolean isConnectable():检测Channel中连接是否就绪
- boolean isAcceptable():检测Channel中接收是否就绪
Selector 常用方法
- Set< SelectionKey > keys():返回所有的 SelectionKey,代表注册在该 Selector 上的所有 Channel
- int select(long timeout) 可以设置超时时长的select()操作
- void close() 关闭该选择器
NIO 的工作原理
首先,获取用于连接的 Channel 以及用于容纳数据的 Buffer。
然后,利用 Selector 监控多个 Channel 的 IO 状况,需要时,操作 Channel 和 Buffer,对数据进行处理。
缺个图来进行说明,作者不知道该画什么图。
代码实例 - 单个服务端线程,处理多个客户端连接
服务端
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class Server_ {
private static final int BUF_SIZE = 1024;
private static final int PORT = 9090;
private static final int SERVER_TIMEOUT = 3000;
public static void main(String[] args){
selector();
}
public static void selector() {
Selector selector = null;
ServerSocketChannel ssc = null;
try{
selector = Selector.open();
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true){
if(selector.select(SERVER_TIMEOUT) == 0){
System.out.println("===== nothing =====");
continue;
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
SelectionKey key = null;
while(iter.hasNext()){
key = iter.next();
if(key.isAcceptable()){
handleAccept(key);
}
if(key.isReadable()){
handleRead(key);
}
if(key.isWritable() && key.isValid()){
handleWrite(key);
}
if(key.isConnectable()){
System.out.println("isConnectable = true");
}
iter.remove();
}
}
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(selector!=null){
selector.close();
}
if(ssc!=null){
ssc.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
public static void handleAccept(SelectionKey key) throws IOException{
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
// 监听新进来的连接
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUF_SIZE));
}
public static void handleRead(SelectionKey key) throws IOException{
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer buf = (ByteBuffer)key.attachment();
long bytesRead = sc.read(buf);
while(bytesRead>0){
buf.flip();
while(buf.hasRemaining()){
System.out.print((char)buf.get());
}
System.out.println();
buf.clear();
bytesRead = sc.read(buf);
}
if(bytesRead == -1){
sc.close();
}
}
public static void handleWrite(SelectionKey key) throws IOException{
ByteBuffer buf = (ByteBuffer)key.attachment();
buf.flip();
SocketChannel sc = (SocketChannel) key.channel();
while(buf.hasRemaining()){
sc.write(buf);
}
buf.compact();
}
}
客户端1
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.TimeUnit;
public class Client_1 {
static final int BUF_SIZE = 1024;
static final String HOST_NAME = "127.0.0.1";
static final int PORT = 9090;
static final long CLIENT_TIMEOUT = 1;
private static final String INFO = "I'm Client_1";
public static void main(String[] args){
ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(HOST_NAME,PORT));
if(socketChannel.finishConnect()) {
int i=0;
while(true) {
TimeUnit.SECONDS.sleep(CLIENT_TIMEOUT);
buffer.clear();
buffer.put(INFO.getBytes());
buffer.flip();
while(buffer.hasRemaining()){
socketChannel.write(buffer);
}
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally{
try{
if(socketChannel!=null){
socketChannel.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
客户端2(与客户端1代码,几乎一摸一样)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.TimeUnit;
public class Client_2 {
private static final String INFO = "I'm Client_2";
public static void main(String[] args){
ByteBuffer buffer = ByteBuffer.allocate(Client_1.BUF_SIZE);
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(Client_1.HOST_NAME,Client_1.PORT));
if(socketChannel.finishConnect()) {
int i=0;
while(true) {
TimeUnit.SECONDS.sleep(Client_1.CLIENT_TIMEOUT);
buffer.clear();
buffer.put(INFO.getBytes());
buffer.flip();
while(buffer.hasRemaining()){
socketChannel.write(buffer);
}
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally{
try{
if(socketChannel!=null){
socketChannel.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
参考
《Java NIO详解》 https://blog.csdn.net/use_admin/article/details/102679985
《Java NIO?看这一篇就够了!》 https://blog.csdn.net/forezp/article/details/88414741