Java NIO同步非阻塞网络编程(附带实例)
NIO 的全称是 non-blocking IO。从 JDK 1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO。
NIO 是同步非阻塞的。NIO 的相关类都被存储在 java.nio 包及其子包下,具有 3 个核心组件,分别是 Buffer(缓冲区)、Channel(通道)和 Selector(选择器)。
使用 Buffer 进行数据写入和读取,需要进行如下 4 个步骤:
Buffer(缓冲区)具有 3 个重要属性:
SocketChannel 用于建立 TCP 网络连接,类似于 Socket。创建 SocketChannel 有两种方式:
ServerSocketChannel 可以监听新建的 TCP 连接通道,类似于 ServerSocket。
一个线程使用 Selector(选择器)监听多个 channel 的不同事件:4 个事件分别对应 SelectionKey 的 4 个常量。
SelectionKey 的 4 个常量如下:
下面使用 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(选择器)是一个 JavaNIO 组件,可以检查一个或多个 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); bba.clear(); 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