NIO同步非阻塞网络编程
NIO 的全称是 non-blocking IO。从 JDK 1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO。NIO 是同步非阻塞的。NIO 的相关类都被存储在 java.nio 包及其子包下,具有 3 个核心组件:Buffer(缓冲区)、Channel(通道)和 Selector(选择器)。
Buffer(缓冲区)
缓冲区本质上是一个可以写入数据的内存块(类似于数组),可以再次被读取。此内存块包含在 NIO Buffer 对象中,该对象提供了一组方法,可以更轻松地使用内存块。使用 Buffer 进行数据写入和读取,需要进行如下4个步骤:
-
将数据写入缓冲区中。
-
调用 buffer.flip() 方法,转化为读取模式。
-
缓冲区读取数据。
-
调用 buffer.clear() 方法或者 buffer.compact() 方法清除缓冲区。
Buffer(缓冲区)具有 3 个重要属性。
-
capacity 容量:作为一个内存块,Buffer 具有一定的固定大小,也被称作容量。
-
position 位置:写入模式时代表写数据的位置。读取模式时代表读取数据的位置。
-
limit 限制:写入模式,限制等于 buffer 的容量。读取模式下,limit 等于写入的数据量。
Channel(通道)
Channel(通道)的 API 涵盖了 UDP/TCP 网络和文件 IO。和标准 IOStream 操作的区别如下:
-
在一个通道内进行读取和写入。
-
stream 通道是单向的(input 或 output)。
-
可以非阻塞读取和写入通道。
-
通道始终读取和写入缓冲区。
SocketChannel 用于建立 TCP 网络连接,类似于 Socket。创建 SocketChannel 有两种方式:一种是客户端主动发起和服务端的连接,另一种是服务端获取的新连接。
ServerSocketChannel 可以监听新建的 TCP 连接通道,类似于 ServerSocket。
Selector(选择器)
Selector(选择器)是一个 Java NIO 组件,可以检查一个或多个 NIO 通道,并确定哪些通道已准备好进行读取或者写入。实现单个线程可以管理多个通道,从而管理多个网络连接。
一个线程使用 Selector(选择器)监听多个 channel 的不同事件:4 个事件分别对应 SelectionKey 的 4 个常量。SelectionKey 的 4 个常量如下。
-
SelectionKey.OP_CONNECT:Connect 连接。
-
SelectionKey.OP_ACCEPT:Accept 准备就绪。
-
SelectionKey.OP_READ:Read 读取。
-
SelectionKey.OP_WRITE:Write 写入。
下面使用 NIO 分别编码实现客户端和服务器端。客户端的代码如下:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class NIOClient {
public static void main(String[] args) throws IOException {
SocketChannel scl = SocketChannel.open();
scl.configureBlocking(false);
scl.connect(new InetSocketAddress("127.0.0.1", 8080));
while (!scl.finishConnect()) {
// 如果没有连接到服务器,就一直等待
Thread.yield();
}
Scanner scanner = new Scanner(System.in);
System.out.println("请输入:");
// 发送内容
String msg = scanner.nextLine();
ByteBuffer bbw = ByteBuffer.wrap(msg.getBytes());
while (bbw.hasRemaining()) {
scl.write(bbw);
}
// 读取响应
System.out.println("收到服务器响应:");
ByteBuffer bba = ByteBuffer.allocate(1024);
while (scl.isOpen() && scl.read(bba) != -1) {
// 长连接情况下,需要手动判断数据有没有读取结束
// 此处做一个简单的判断,超过 0 字节认为请求结束了
if (bba.position() > 0)
break;
}
bba.flip();
byte[] b = new byte[bba.limit()];
bba.get(b);
System.out.println(new String(b));
scanner.close();
scl.close();
}
}
服务器端的代码如下:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建网络服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 设置为非阻塞模式
ssc.socket().bind(new InetSocketAddress(8080)); // 绑定端口
System.out.println("服务器已启动");
while(true) {
SocketChannel sca = ssc.accept(); // 获取新 tcp 连接通道
// tcp 请求读取/响应
if (sca != null) {
System.out.println("获取新连接:" + sca.getRemoteAddress());
sca.configureBlocking(false); // 默认阻塞,设置为非阻塞
ByteBuffer bba = ByteBuffer.allocate(1024);
while (sca.isOpen() && sca.read(bba) != -1) {
// 长连接情况下,需要手动判断有没有读取结束
// 此处做一个简单判断,超过0字节就认为请求结束
if (bba.position() > 0) {
break;
}
}
if (bba.position() == 0)
continue; // 如果没数据了,则不继续之后的处理
bba.flip();
byte[] b = new byte[bba.limit()];
bba.get(b);
System.out.println("收到数据:" + new String(b) + ",来自:" + sca.getRemoteAddress());
// 响应结果
String str = "Hello!";
ByteBuffer bbw = ByteBuffer.wrap(str.getBytes());
while (bbw.hasRemaining()) {
sca.write(bbw); // 非阻塞
}
}
}
}
}
先运行服务器端,再运行客户端。
客户端输出到控制台上的内容如下:
请输入:
hello
收到服务器端响应:
Hello!
服务器端输出到控制台上的内容如下:
服务器已启动
获取新连接:/127.0.0.1:60364
收到数据:hello,来自:/127.0.0.1:60364