Java的三种IO模式

378 阅读4分钟

BIO

BIO是传统的IO模型,阻塞模型。每个连接都需要创建一个线程,并且在IO操作期间线程是阻塞的,做不了其他事情。

image.png

服务端代码

public class BIOServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        Socket clientSocket = null;

        try {
            //创建服务端
            serverSocket = new ServerSocket(8888);
            System.out.println("服务端已启动,等待客户端连接...");

            while (true){
                // 监听客户端请求,接收不到请求会一直等待
                clientSocket = serverSocket.accept();
                int port = clientSocket.getPort();
                InetAddress inetAddress = clientSocket.getInetAddress();
                System.out.println("客户端 "+inetAddress+":"+port+" 连接成功!");
                //处理客户端消息
                new Thread(new ServerThread(clientSocket)).start();
            }
        } catch (IOException e) {
            System.out.println("客户端连接失败:" + e.getMessage());
        } finally {
            try {
                if (clientSocket != null) {
                    clientSocket.close();
                }
                if (serverSocket != null) {
                    serverSocket.close();
                }
            } catch (IOException e) {
                System.out.println("关闭资源失败:" + e.getMessage());
            }
        }
    }
}

/**
 * 服务端线程处理类
 */
class ServerThread  implements Runnable{

    private Socket clientSocket;

    public ServerThread(Socket clientSocket) {
        this.clientSocket = clientSocket;
    }

    @Override
    public void run() {
        //获取客户端输入流以便接收客户端数据
        try {
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            //获取客户端输出流以便向客户端发送数据
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream());

            int port = clientSocket.getPort();
            InetAddress inetAddress = clientSocket.getInetAddress();
            String address = inetAddress+":"+port;

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                //接收客户端消息
                System.out.println("客户端"+address+"发来消息:" + inputLine);
                //给客户端发送消息
                out.println("服务端已接收到消息并回复:"+inputLine);
                out.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端代码

public class BIOClient {
    public static void main(String[] args) throws IOException {
        Socket clientSocket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            //绑定服务端ip和端口号
            clientSocket = new Socket("localhost", 8888);
            System.out.println("连接服务端成功!");
            //获取输入流,接收服务端消息
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            //获取输出流,给服务端发送消息
            out = new PrintWriter(clientSocket.getOutputStream(), true);

            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.print("给服务端发送消息:");
                String msg = scanner.nextLine();
                out.println(msg);

                String response;
                if ((response = in.readLine()) != null) {
                    //接收服务端响应
                    System.out.println("服务端响应:" + response);
                }
            }
        } catch (IOException e) {
            System.out.println("连接服务端失败:" + e.getMessage());
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
                if (clientSocket != null) {
                    clientSocket.close();
                }
            } catch (IOException e) {
                System.out.println("关闭资源失败:" + e.getMessage());
            }
        }
    }
}

服务端为每一个客户端都要创建一个线程进行处理。

NIO

NIO采用的三个组件

  • Channel 通道,可以同时进行读写操作的

  • Buffer 缓冲区

  • Selector 选择器: Selector是NIO中用于监控多个Channel的选择器,可以实现单线程管理多个Channel。Selector可以检测多个Channel是否有事件发生,包括连接、接收、读取和写入等事件,并根据不同的事件类型进行相应处理。Selector可以有效地减少单线程管理多个Channel时的资源占用,提高程序的运行效率。

    Channel是一个数据读写的通道,所有的数据都通过Buffer来处理,避免将字节直接写入通道,多线程模式下,一个线程可以处理多个请求,客户端的连接请求注册到多路复用器上,由多路复用器轮询到连接有IO请求的时候进行处理。

那么为什么需要Buffer不能直接把数据读写使用Channel呢?

首先Buffer有指针管理,能够更好的对一块的数据进行管理,反转,清除等。一次IO就要触发一次系统调用,涉及到用户态切换到内核态,Buffer可以把很多小操作合成一次大操作,减少系统调用的次数。NIO支持非阻塞IO,缓冲区做的就是一个提前准备的功能,能够提高IO的效率。

服务端代码

public class NIOServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        // 创建一个ServerSocketChannel并绑定到指定的端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(9999));
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 将ServerSocketChannel注册到Selector上,并监听OP_ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器已启动,等待客户端连接...");
​
        while (true) {
            // 阻塞,等待事件发生
            selector.select();
​
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
​
                if (key.isAcceptable()) {     // 处理连接请求事件
                    SocketChannel client = serverSocketChannel.accept();
                    client.configureBlocking(false);
                    //监听OP_ACCEPT事件
                    client.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    client.getRemoteAddress();
                    //分配缓存区容量
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    client.read(buffer);
                    String output = new String(buffer.array()).trim();
​
                    Socket socket = client.socket();
                    InetAddress inetAddress = socket.getInetAddress();
                    int port = socket.getPort();
                    String clientInfo = inetAddress+":"+port;
                    String message = String.format("来自客户端 %s , 消息:%s", clientInfo , output);
                    System.out.println(message);
​
                    System.out.print("回复消息: ");
                    writeMessage(selector, client, buffer);
                }
​
                keyIterator.remove();
            }
        }
    }
​
    private static void writeMessage(Selector selector, SocketChannel client, ByteBuffer buffer) throws IOException {
        Scanner scanner = new Scanner(System.in);
        String message = scanner.nextLine();
        buffer.clear();
        buffer.put(message.getBytes());
        //从写模式切换到读模式
        buffer.flip();
        while (buffer.hasRemaining()) {
            client.write(buffer);
        }
​
        //  重新监听OP_ACCEPT事件
        client.register(selector, SelectionKey.OP_READ);
    }
}

客户端代码

public class NIOClient {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("localhost", 9999));
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
​
        while (true) {
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
​
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
​
                if (key.isConnectable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    if (client.isConnectionPending()) {
                        client.finishConnect();
                    }
​
                    System.out.print("Enter message to server: ");
                    Scanner scanner = new Scanner(System.in);
                    String message = scanner.nextLine();
                    ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
                    client.write(buffer);
​
                    client.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    client.read(buffer);
                    String output = new String(buffer.array()).trim();
                    System.out.println("来自客户端的消息: " + output);
​
                    System.out.print("输入消息: ");
                    // 和服务端代码一样
                    writeMessage(selector, client, buffer);
                }
                keyIterator.remove();
            }
        }
    }
}

客户端和服务端通信的时候,不需要新建线程,通过Selector IO多路复用。

AIO

NIO已经是非阻塞模型,但是其实Selector在选择的这个过程也是一定程度的阻塞。

AIO完全基于异步模式,是真正非阻塞的模型。但是应用还不广泛,并且依赖于操作系统的实现。epoll和netty都是基于NIO模型,AIO需要创建回调函数,只有在大量IO操作中有很明显的效率提升。

在AIO模型中,当一个异步操作完成后,会通知相关线程进行后续处理,这种处理方式称为“回调”。回调函数可以由开发者自行定义,用于处理异步操作的结果。