java IO(BIO)、NIO、AIO

什么是IO

什么是 I/O?
当程序需要从网络(如 TCP 套接字)读取数据,或向网络写入数据时,会触发 ​I/O 操作。

​输入:从网络接收数据到应用程序的缓冲区(如 recv() 函数)。
​输出:从应用程序的缓冲区发送数据到网络(如 send() 函数)。
​为什么 I/O 是性能瓶颈?

​等待时间:数据从网络传输到内存需要时间(如网络延迟、带宽限制)。
​阻塞问题:传统的 I/O 操作会阻塞当前线程,导致资源无法被充分利用。

 

BIO

现实比喻

1.你去饭点点菜,点完之后只能在窗口等待,在做好之前,有其他人来了也不能接待,其他人只能干等待,这就是传统的同步阻塞代码。

流程图

 

 

BIO服务端代码例子

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class BioServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建 ServerSocket,绑定端口 8888
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("BIO 服务端启动,监听端口 8888...");

        // 2. 循环监听客户端连接
        while (true) {
            // 阻塞点:等待客户端连接(线程挂起,直到有连接)
            Socket socket = serverSocket.accept();
            System.out.println("客户端连接: " + socket.getRemoteSocketAddress());

            // 3. 为每个客户端连接启动一个线程处理
            new Thread(() -> handleClient(socket)).start();
        }
    }

    private static void handleClient(Socket socket) {
        try (
            // 4. 获取输入输出流
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)
        ) {
            String message;
            // 5. 循环读取客户端发送的数据(阻塞点:read() 时线程挂起)
            while ((message = reader.readLine()) != null) {
                System.out.println("收到客户端消息: " + message);
                // 6. 向客户端返回响应
                writer.println("服务端回复: " + message.toUpperCase());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
View Code

BIO客户端代码例子

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class BioClient {
    public static void main(String[] args) throws IOException {
        // 1. 连接服务端
        Socket socket = new Socket("localhost", 8888);
        System.out.println("连接到服务端...");

        try (
            // 2. 获取输入输出流
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            Scanner scanner = new Scanner(System.in)
        ) {
            while (true) {
                // 3. 从控制台读取用户输入
                System.out.print("请输入消息: ");
                String input = scanner.nextLine();
                if ("exit".equalsIgnoreCase(input)) break;

                // 4. 向服务端发送消息(阻塞点:write() 时线程可能挂起)
                writer.println(input);

                // 5. 读取服务端响应(阻塞点:readLine() 时线程挂起)
                String response = reader.readLine();
                System.out.println("服务端响应: " + response);
            }
        } finally {
            socket.close();
        }
    }
}
View Code

BIO 的核心问题

  • 线程阻塞accept()read()write() 等方法会阻塞当前线程,导致线程资源浪费。
  • 高并发瓶颈:每个连接需要一个线程,线程数过多会导致内存和 CPU 资源耗尽。
  • 适用场景:适合连接数较少且延迟不敏感的场景(如内部工具、小型应用)。

NIO

 

一个线程可以处理多个Socket连接

核心概念

SocketChannel  封装客户端的链接

Selector  轮询器  (select、poll、epoll)

 

Selector

1.由用户空间将fd传给内核空间由内核遍历获取可读状态的channel,然后将这些状态的结果从内核空间切换到用户空间再返回给轮训器,这一次调用可以获取到多个通道的状态。这些通道被一次调用获取到了就叫多路复用。

2.将用户空间遍历交给内核遍历

select 和poll的区别

轮训器将通道的fd描述符传给内核时数量是有限制的(也就是说不能一次把所有的fd传递给内核遍历),而poll没有数量限制,可以一次性都传递

epoll

1.创建ServerSocket

2.绑定端口号 bind

3.监听 listen

4.调用epplll_create(会在内核中开辟2个空间,红黑树、双向链表)

5.调用epoll_ctl(会将要监听的文件描述符放到红黑树空间,并标注监听的是可连接事件)

6.再调用epoll_wail遍历双向链表,获取可连接事件的fd

双向链表数据如何存储

设计中断知识如:

1.当网卡接收到数据时,内核就能检测到网络套接字的状态变化

2.网关设备驱动程序会通过内核会将数据放到对应的fd中的接收缓冲区并修改对应的红黑树文件描述符状态

3.内核就会检查红黑树中的fd文件描述符,发现对应的fd对应的状态是可连接的接着内核就会将该fd插入到双向链表,表示这个文件描述符准好被处理了。

4.这样调用epoll_wailt时就可以从双向链表中获取到了。

5.这样就不需要像select和poll在内核中遍历所有channel了,效率大大提高

代码例子

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NioServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建 Selector(事件监听器)
        Selector selector = Selector.open();

        // 2. 创建 ServerSocketChannel(监听客户端连接的通道)
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("localhost", 8888));
        serverSocket.configureBlocking(false); // 设置为非阻塞模式

        // 3. 将 ServerSocketChannel 注册到 Selector,监听 OP_ACCEPT 事件(新连接)
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("NIO 服务端启动,监听端口 8888...");

        // 4. 事件循环(核心)
        while (true) {
            selector.select(); // 阻塞直到有事件就绪
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove(); // 处理完必须移除,避免重复处理

                if (key.isAcceptable()) { // 处理新连接
                    handleAccept(key, selector);
                } else if (key.isReadable()) { // 处理读数据
                    handleRead(key);
                }
            }
        }
    }

    // 处理新连接
    private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
        ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverSocket.accept(); // 接受客户端连接
        clientChannel.configureBlocking(false); // 设置为非阻塞模式

        // 注册 OP_READ 事件,监听客户端发送的数据
        clientChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("客户端连接: " + clientChannel.getRemoteAddress());
    }

    // 处理读数据
    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配缓冲区

        try {
            int bytesRead = clientChannel.read(buffer); // 读取数据到缓冲区
            if (bytesRead == -1) { // 客户端关闭连接
                clientChannel.close();
                return;
            }

            buffer.flip(); // 切换为读模式
            String message = new String(buffer.array(), 0, bytesRead).trim();
            System.out.println("收到客户端消息: " + message);

            // 向客户端回写数据
            String response = "服务端回复: " + message.toUpperCase();
            ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
            clientChannel.write(responseBuffer);
        } catch (IOException e) {
            clientChannel.close(); // 发生异常时关闭连接
        }
    }
}
View Code

AIO

linux并没有完全实现

与NIO最大区别

再NIO中通过selector获取到可写或者可读的程序后还是需要程序来处理,这种需要自己做的就是同步的意思

再AIO的情况下获取到可读或者可写的事件后内核主动来做,不需要程序来做。这就是异步

 

posted @ 2018-05-09 21:16  意犹未尽  阅读(518)  评论(0)    收藏  举报