阻塞与非阻塞
- 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
- Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞IO 的空闲时间用于在其他通道上执行IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
1 使用 NIO 完成网络通信的三个核心
- 通道(Channel):负责链接
- 缓冲区(Buffer):负责数据的存取
- 选择器(Selector):是
SelectableChannel
的多路复用器。用于监控SelectableChannel
的 IO 状况,选择器是针对于网络通信而言的
选择器(Selector)监控的通道:
SocketChannel
(连接TCP网络套接字的通道)
ServerSocketChannel
DatagramChannel
(收发UDP包的通道)
Pipe.SinkChannel
Pipe.SourceChannel
其中不包括FileChannel
,因为不能切换成非阻塞模式,所以不符合网络通信需要处于非阻塞模式的基本规则,所以不包括
2 选择器(Selector)
选择器(Selector)是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心
2.1 SelectableChannle 的结构
2.2 选择器的应用
- **第一步:**通过调用
Selector.open()
方法创建一个Selector。 - **第二步:**切换非阻塞模式_
channel.configureBlocking(false);
,其中true表示阻塞模式,false表示非阻塞模式 - **第三步:**向选择器注册通道_
SelectableChannel.register(Selector sel, int ops)
2.3 SelectionKey 选择键(ops参数)
- 选择键表示SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件。
- 选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
状态 | 常量名称 | 常量值 | 含义 |
---|---|---|---|
读 | SelectionKey.OP_READ | 1 | 一个有数据可读的通道 |
写 | SelectionKey.OP_WRITE | 4 | 等待写数据的通道 |
连接 | SelectionKey.OP_CONNECT | 8 | 某个Channel成功连接到另一个服务器 |
接收 | SelectionKey.OP_ACCEPT | 16 | 一个ServerSocketChannel准备好接收新连接 |
2.4 SelectionKey 的常用方法
方法 | 描述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测Channal 中读事件是否就绪 |
boolean isWritable() | 检测Channal 中写事件是否就绪 |
boolean isConnectable() | 检测Channel 中连接是否就绪 |
boolean isAcceptable() | 检测Channel 中接收是否就绪 |
2.5 Selector 的常用方法
方法 | 描述 |
---|---|
Set <SelectionKey> keys() | 所有的SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() | 被选择的SelectionKey 集合。返回此Selector的已选择键集 |
int select() | 监控所有注册的Channel,当它们中间有需要处理的IO 操作时,该方法返回,并将对应得的SelectionKey 加入被选择的SelectionKey 集合中,该方法返回这些Channel 的数量。 |
int select(long timeout) | 可以设置超时时长的select() 操作 |
int selectNow() | 执行一个立即返回的select() 操作,该方法不会阻塞线程 |
Selector wakeup() | 使一个还未返回的select() 方法立即返回 |
void close() | 关闭该选择器 |
如果想同时监听一个通道的多个状态,使用“位或”操作符连接
eg.int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
3 网络通信中的常用通道
3.1 SocketChannel 与 ServerSocketChannel
- Java NIO 中的 SocketChannel 是一个连接到TCP网络套接字的通道,一般用于客户端发送数据到服务端
- Java NIO 中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道,就像标准 IO 中的 ServerSocket 一样,一般用于服务端监听并接收来自于客户端发来的 TCP 请求
package NIO;
/**
* 阻塞式IO,使用的是 FileChannel(FileChannel不能切换到非阻塞模式)
*/
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class BlockingNIO {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
Server();
}
}.start();
Client();
}
public static void Client() {
SocketChannel sChannel = null;
FileChannel outFileChannel = null;
try {
// 1. 获取通道
sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
// 2. 读取本地文件的通道
outFileChannel = FileChannel.open(Paths.get("D:\\Workspace\\JavaBaseStu\\Notes\\99 Pic\\Collection函数库.jpg"),
StandardOpenOption.READ);
// 3. 获取缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 4. 数据写入缓冲区
while (outFileChannel.read(buf) != -1) {
buf.flip();
sChannel.write(buf);
buf.clear();
}
// 5. 通知服务器发送结束(没有这一步,服务器一直处于阻塞状态,程序无法结束)
sChannel.shutdownOutput();
// 6. 接受服务器返回信息
int len = 0;
while ((len = sChannel.read(buf)) != -1) {
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (sChannel != null) {
try {
sChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outFileChannel != null) {
try {
outFileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void Server() {
ServerSocketChannel ssChannel = null;
FileChannel inFileChannel = null;
SocketChannel sChannel = null;
try {
// 1. 获取通道
ssChannel = ServerSocketChannel.open();
// 2. 绑定端口
ssChannel.bind(new InetSocketAddress(9898));
// 3. 本地文件通道
inFileChannel = FileChannel.open(Paths.get("C:\\Users\\Yorick_PC\\Desktop\\demo\\demo.jpg"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 4. 获取输入流
sChannel = ssChannel.accept();
// 5. 申请缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 6. 写入本地文件
while (sChannel.read(buf) != -1) {
buf.flip();
inFileChannel.write(buf);
buf.clear();
}
// 7. 返回信息
buf.put(new String("收到了").getBytes());
buf.flip();
sChannel.write(buf);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ssChannel != null) {
try {
ssChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inFileChannel != null) {
try {
inFileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (sChannel != null) {
try {
sChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
package NIO;
/**
* 非阻塞式IO
*/
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.time.LocalDateTime;
import java.util.Iterator;
import java.util.Scanner;
public class NonBlockingNIO_TCP {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
Server();
}
}.start();
Client();
}
public static void Client() {
SocketChannel sChannel = null;
Scanner scanner = null;
try {
scanner = new Scanner(System.in);
// 创建通道
sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
// 将通道切换成非阻塞模式
sChannel.configureBlocking(false);
// 创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
while (scanner.hasNext()) {
// 将信息写入缓冲区
String output = new String();
output = LocalDateTime.now().toString() + "\n" + scanner.next();
buf.put(output.getBytes());
// 切换缓冲区模式为读模式
buf.flip();
// 写入通道
sChannel.write(buf);
// 清空缓冲区
buf.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sChannel != null) {
try {
sChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void Server() {
ServerSocketChannel ssChannel = null;
SocketChannel sChannel = null;
try {
// 获取通道
ssChannel = ServerSocketChannel.open();
// 切换为非阻塞模式
ssChannel.configureBlocking(false);
// 绑定端口
ssChannel.bind(new InetSocketAddress(9898));
// 创建选择器
Selector selector = Selector.open();
// 将通道注册到选择器上,并监听通道的接收事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
// 创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 当选择器上存在需要监听的通道时,轮询注册到选择器中的通道状态
while (selector.select() > 0) {
Iterator<SelectionKey> selectionkeys = selector.selectedKeys().iterator();
while (selectionkeys.hasNext()) {
SelectionKey sk = selectionkeys.next();
// 判断当前通道的状态:已经提交就绪
if (sk.isAcceptable()) {
// 读取通道
sChannel = ssChannel.accept();
// 将通道转换为非阻塞状态
sChannel.configureBlocking(false);
// 将通道注册到选择器上,并监听读就绪状态
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
// 获取通道
sChannel = (SocketChannel) sk.channel();
// 读取通道中的数据
int len = 0;
while ((len = sChannel.read(buf)) > 0) {
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
}
selectionkeys.remove();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ssChannel != null) {
try {
ssChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (sChannel != null) {
try {
sChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.2 DatagramChannel
- Java NIO 中的 DatagramChannel 是一个能发送 UDP 包的通道
package NIO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Scanner;
public class NonBlockingNIO_UDP {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
Receive();
}
}.start();
Send();
}
public static void Send() {
DatagramChannel dc = null;
Scanner scanner = null;
try {
scanner = new Scanner(System.in);
String input = new String();
dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
while (scanner.hasNext()) {
input = scanner.next();
buf.put((LocalDateTime.now() + "\n" + input).getBytes());
buf.flip();
dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
buf.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (dc != null) {
try {
dc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void Receive() {
DatagramChannel dc = null;
try {
dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
ByteBuffer buf = ByteBuffer.allocate(1024);
while (selector.select() > 0) {
Iterator<SelectionKey> selectionkeys = selector.selectedKeys().iterator();
while (selectionkeys.hasNext()) {
SelectionKey sk = selectionkeys.next();
if (sk.isReadable()) {
dc.receive(buf);
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
selectionkeys.remove();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (dc != null) {
try {
dc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}