java的nio技术初学指南

350 阅读4分钟

一、什么是java的nio?

  • Java NIO(Non-blocking I/O)是从Java 1.4版本开始引入的一个新的I/O API,它提供了一种与标准Java I/O API不同的操作方式。NIO的主要特性是非阻塞I/O操作,这意味着在等待I/O操作完成时,线程不会被阻塞,而是可以继续执行其他任务。

二、NIO 的核心组件

  • Buffer(缓冲区) :用于存放数据。所有数据在被读取或写入时都要经过缓冲区。
  • Channel(通道) :负责将数据从一个地方传输到另一个地方。通道可以读取或写入缓冲区。
  • Selector(选择器) :用于监听多个通道的事件(如可读、可写等),允许在一个线程中处理多个通道。

三,如何使用?

下面是一个简单的java NIO服务端示例,演示如何使用nio来处理客户端的连接请求和数据交换

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 NioServer {
    private static final int PORT = 8080;
    private Selector selector;

    public void startServer() throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
        selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server started on port " + PORT);

        while (true) {
            if (selector.select() == 0) {
                continue;
            }

            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    acceptNewConnection(serverSocketChannel);
                } else if (key.isReadable()) {
                    readData(key);
                }
                // 重要:删除已处理的 SelectionKey
                iterator.remove();
            }
        }
    }

    private void acceptNewConnection(ServerSocketChannel serverSocketChannel) throws IOException {
        SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

    private void readData(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        int bytesRead = socketChannel.read(buffer);
        if (bytesRead == -1) {
            socketChannel.close();
        } else {
            buffer.flip();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String message = new String(data, "UTF-8");
            System.out.println("Received data from client: " + message);
            sendResponse(socketChannel, message);
        }
    }

    private void sendResponse(SocketChannel socketChannel, String message) throws IOException {
        ByteBuffer buffer = ByteBuffer.wrap(("Hello world from server: " + message).getBytes());
        while (buffer.hasRemaining()) {
            socketChannel.write(buffer);
        }
    }

    public static void main(String[] args) throws IOException {
        new NioServer().startServer();
    }
}

下边是一个简单的客户端示例,用来向服务端发送消息

import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NioClient {
    private static final String DEFAULT_HOST = "localhost";
    private static final int DEFAULT_PORT = 8080;

    private void connect() throws Exception {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            socketChannel.configureBlocking(false);
            socketChannel.connect(new java.net.InetSocketAddress(DEFAULT_HOST, DEFAULT_PORT));

            while (!socketChannel.finishConnect()) {
                // 等待连接完成
            }

            ByteBuffer sendBuffer = ByteBuffer.wrap("Hello world from client!".getBytes("UTF-8"));
            socketChannel.write(sendBuffer);

            // 清空缓冲区以准备好接收服务器的响应
            ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);

            // 等待服务器的响应
            boolean receivedResponse = false;
            while (!receivedResponse) {
                receiveBuffer.clear(); // 准备接收数据
                int bytesRead = socketChannel.read(receiveBuffer);
                if (bytesRead > 0) {
                    receiveBuffer.flip(); // 切换到读模式
                    byte[] data = new byte[receiveBuffer.remaining()];
                    receiveBuffer.get(data);
                    String response = new String(data, "UTF-8");
                    System.out.println("Received response: " + response);
                    receivedResponse = true;
                } else if (bytesRead < 0) {
                    // 如果客户端关闭了连接
                    System.out.println("Connection closed by the server.");
                    break;
                }
            }

        }
    }

    public static void main(String[] args) throws Exception {
        new NioClient().connect();
    }
}

代码解释

  1. 服务端:

    • 打开一个ServerSocketChannel并绑定到端口。
    • 创建一个Selector并注册ServerSocketChannel以便监听新的连接。
    • 循环监听选择器的事件,并根据事件类型(接受新连接或读取数据)处理。
  2. 客户端:

    • 打开一个SocketChannel并连接到服务器。
    • 向服务器发送消息,并读取服务器的响应。

其中有几个关键的地方需要注意:

  1. 第39行的iterator.remove();,需要删除已处理的selectionkey,否则服务端会重复处理消息进而引发死循环。
  2. sendResponse响应客户端方法中的while (buffer.hasRemaining()) { socketChannel.write(buffer); },很多资料中都直接使用socketChannel.write(buffer);这时你可能会遇到一个问题,客户端发送过来的数据收不到响应值,原因可能是缓存区中的数据读取不完整,导致数据发送不完全,客户端无法解析。
  3. 客户端while (!socketChannel.finishConnect()) { // 等待连接完成 },客户端尝试非阻塞地连接到服务端,此处一定要确保连接完成,循环中可以添加一些其他操作,添加日志等,如果添加此段代码,可能会出现客户端发送的数据服务端收不到,原因是连接还未完成就去执行后续的发送数据操作。
  4. 客户端中有如下一段代码
// 等待服务器的响应
boolean receivedResponse = false;
while (!receivedResponse) {
    receiveBuffer.clear(); // 准备接收数据
    int bytesRead = socketChannel.read(receiveBuffer);
    if (bytesRead > 0) {
        receiveBuffer.flip(); // 切换到读模式
        byte[] data = new byte[receiveBuffer.remaining()];
        receiveBuffer.get(data);
        String response = new String(data, "UTF-8");
        System.out.println("Received response: " + response);
        receivedResponse = true;
    } else if (bytesRead < 0) {
        // 如果客户端关闭了连接
        System.out.println("Connection closed by the server.");
        break;
    }
}

此段代码中大部分教程都不会等待服务器的响应,此时如果你的机器性能好,网络带宽好,可能没有感受,但是如果机器差点就会发现客户端无法收到服务端返回的响应消息。

响应示例

1.首先启动服务端NioServer 1722565851892.jpg 2.然后启动客户端NioClient 1722565925408.jpg 3.然后返回服务端查看客户端请求 1722565943881.jpg 三张图片对比可以看到客户端发送请求,服务端收到了请求并做出响应,最后客户端收到了服务端返回的响应。

  • 至此一个简单的nio示例代码已完成,对于初学者来说尤其要注意上边提到的几点,少踩坑。