Java Socket IO演进(一)-BIO/NIO/AIO

1. 概览

Java中主要有三种IO模型,分别是同步阻塞IO(BIO)、同步非阻塞IO(NIO)、异步非阻塞IO(AIO)。

Java IO的演进,其实是依赖于操作系统的IO操作接口实现。比如在Linux 2.6以后,Java中NIO和AIO都是通过epoll来实现的;而在Windows上,AIO是通过IOCP来实现的。

Linux(UNIX)操作系统中共有五种IO模型,分别是:阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动IO模型、异步IO模型。

回顾
同步与异步

  • 同步:发起一个调用后,被调用者未处理完请求前,不向调用者返回响应。
  • 异步:发起一个调用后,被调用者收到请求后立刻向调用者回应已接收到请求,但是被调用者并没有返回结果,此时调用者可以处理其他操作,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。

阻塞和非阻塞

  • 阻塞:发起一个请求后,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞:发起一个请求后,调用者不用一直等着结果返回,可以先去干其他事情。

这里介绍Java BIO/NIO/AIO 是结合Socket网络I/O而谈,位于 java.net.* 包下。BIO 使用 java.io.* 包下阻塞IO;NIO使用 java.nio.* 包下非阻塞IO;AIO也就是NIO2.0版,是在NIO基础上提供异步支持。

NIO 是 JDK1.4 提供的API。nio中n有两层含义:

  • new: 表示新的io接口
  • Non-blocking: 非阻塞

AIO 是 JDK1.7 在 java.nio.* 包下提供的API

2. BIO

Blocking I/O,同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
我们常说的I/O一般都是指BIO。

2.1. BIO 基本模型

BIO基本模型.png

2.1.1. 特点描述

Socket 的连接(accept()),数据的读写(read()/write()),都是阻塞的。请求一旦建立连接,就无法再接收其他连接。

2.1.2. 代码示例

public class SocketIO {

    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(9090); // #1

        System.out.println("1. new ServerSocket(9090)");

        Socket client = server.accept(); // 阻塞1 // #2

        System.out.println("2. client connect\t" + client.getPort());

        InputStream in = client.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        System.out.println(reader.readLine()); // 阻塞2 // #3

        while (true) {
        }
    }
}

上述代码中,有两处阻塞,分别是 server.accept()reader.readLine()

使用 strace -ff -o outlog java SocketIO 命令查看JVM调用了内核(kernel)的方法过程

#1 位置 new ServerSocket(9090),调用了内核:

socket(···) = 5  # 创建一个未连接的Socket,5是个fd(文件描述符)指向这个socket
bind(5, 9090, ···)  # 绑定9090端口到socket上
listen(5, ···)  # 把这个socket监听起来

#2 位置 server.accept(),调用了内核:

poll([{fd=5, ···}], 1, -1) = 1 ([{fd=5, revents=POLLIN}])  # 等待请求过来连接。如果没有请求将阻塞,-1表示无限阻塞;有请求时,5fd退出监听交给accept建立连接
accept(5, {客户端信息}, [28]) = 6  # 从5fd中创建一个新的socket,连接client和server,释放5fd,让其继续处于监听状态,等待下一个连接
# 如果使用jdk1.4之前的版本运行,只调用了内核的 accept 方法,接收连接阻塞在此处

#3 位置 server.accept(),调用了内核:

recvfrom(6, "hahahaha\n", 8192, 0, NULL, NULL) = 9  # 从6fd这个socket上读取接收到的数据,如果socket上没有消息,将在此阻塞
# 如果使用jdk1.4之前的版本运行,调用了内核的 recv 方法,读数据阻塞在此处

2.1.3. 问题瓶颈

主要问题突出点:

  1. 接收客户端连接阻塞
  2. 读取/写入数据阻塞
  3. 只能接收一个连接

2.2. BIO 传统模型

BIO服务通信模型.png

2.2.1. 特点描述

服务端使用一个Acceptor线程,用于监听接收请求,收到请求后,创建Socket和一个新线程来服务客户端。
这种模型特点是请求数与线程数1:1

2.2.2. 代码示例

public class SocketIO2 {

    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(9090);

        System.out.println("1. new ServerSocket(9090)");

        while (true) {
            Socket client = server.accept(); // 阻塞1
            System.out.println("2. client connect\t" + client.getPort());
            Thread thread = new Thread(() -> {
                try {
                    InputStream in = client.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        System.out.println(line); // 阻塞2
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("client closed\t" + client.getPort());
            });
            thread.start();
        }
    }
}

上述代码,可以实现多个客户都安连接,但是还存在两处阻塞。一个连接,占用一个线程。

2.1.3. 问题瓶颈

主要问题突出点:

  1. 接收客户端连接阻塞
  2. 读取/写入数据阻塞
  3. 服务器不能连接过多请求(C10K问题)

2.3. BIO 伪异步模型

BIO伪异步模型.png

2.3.1. 特点描述

服务端的连接响应,使用线程池管理。可实现请求数与线程m:n(m可以大于n)

服务端性能的优化,取决于线程池的优化

2.3.2. 代码示例

public class SocketIO3 {
    private static int THREAD_POOL_SIZE = 1;
    private static ThreadPoolExecutor THREAD_POOL = getThreadPoolExecutor();

    private static ThreadPoolExecutor getThreadPoolExecutor() {  // 服务性能的优化,取决于线程池的优化
        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger mThreadNum = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "server-thread-" + mThreadNum.getAndIncrement());
            }
        };
        return new ThreadPoolExecutor(THREAD_POOL_SIZE, THREAD_POOL_SIZE, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(THREAD_POOL_SIZE), threadFactory);
    }

    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(9090);
        System.out.println("1. new ServerSocket(9090)");

        while (true) {
            Socket client = server.accept(); // 阻塞1
            System.out.println("2. client connect\t" + client.getPort());

            try {
                THREAD_POOL.execute(() -> responseHandler(client)); // 响应处理
            } catch (RejectedExecutionException e) {
                e.printStackTrace();
                rejectedHandler(client);    // 线程池已满,拒绝处理
            }
        }
    }

    private static void responseHandler(Socket client) {
        try {
            InputStream in = client.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line); // 阻塞2
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("client closed\t" + client.getPort());
    }

    private static void rejectedHandler(Socket client) throws Exception {
        OutputStream out = client.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
        writer.write("请求已达上线");
        writer.close();
        client.close();
    }
}

2.3.3. 问题瓶颈

主要问题突出点:

  1. 接收客户端连接阻塞
  2. 读取/写入数据阻塞
  3. 服务器不能连接过多请求(比 2.2 会好一点)

2.4 总结

BIO从底层看,如果不能从底层根本上结果阻塞问题,其性能问题就不能得到有效解决。这种模型单一简单,适用于连接不多的场景。

3. NIO

  • Non-blocking: NIO 流是非阻塞 IO 而 IO 流是阻塞 IO,非阻塞体现在 socket网络,内核机制上。
  • new: NIO 有3个核心组件/特性:Channel、Buffer、Selector,体现在JDK上。

3.1. NIO 基本模型

3.1.1. 特点描述

Java中非阻塞IO,JDK中可通过 sockeChannel.configureBlocking(false)accept()read()/write() 设置成非阻塞,调用系统内核时,如果没有连接/数据读写,就返回-1。

SocketChannel 中提供数据的流入流出两个通道。数据读写时,先直接由操作系统将数据放入Buffer缓冲区中。JVM中Server对数据的读写直接访问Byffer缓冲区。

NIO数据读写模型.png

Channel 与 Buffer 搭配

  • 多个channel配一个buffer
  • 一个channel配一个buffer(使用最多配置)
  • 一个channel配两个buffer(一个读一个写)

Buffer 缓冲区创建位置

  • JVM内(堆内)
  • JVM外(堆外,由OS管理分配)

服务器与外部收发数据,首先由OS管理,由系统将网卡数据放到对外,如果JVM想要使用,需要将数据拷贝到JVM内部,存在JVM内外来回拷贝,影响性能还浪费空间。
Linux内核支持共享空间(mmap),可以将网卡数据直接放到共享空间内存,这样JVM和OS就可以共同使用。Java中的 零拷贝 就是基于此。

ByteBuffer 的创建位置,和 channel 的搭配方式根据实际应用场景灵活选择

3.1.2. 代码示例

public class SocketNIO {

    private static final String CHARSET = "UTF-8";

    public static void main(String[] args) throws Exception {
        List<SocketChannel> clientList = new LinkedList<>();

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(9090)); // 完成端口绑定,socket监听
        ssc.configureBlocking(false); // false指定该ServerSocketChannel为非阻塞 // #1
        System.out.println("1. ServerSocketChannel 9090 non-blocking");

        while (true) {
            Thread.sleep(1000);
            SocketChannel client = ssc.accept(); // 接受连接非阻塞
            if (client == null) {
                System.out.print(".");
            } else {
                client.configureBlocking(false); // #2
                System.out.println();
                System.out.println("2. client connect\t" + client.socket().getPort());
                clientList.add(client);
            }

            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);  // 配置缓冲区,可以堆内,可以堆外(allocate())

            for (SocketChannel sc : clientList) {
                int len = sc.read(buffer); // 读取数据,非阻塞
                if (len > 0) {
                    buffer.flip();
                    byte[] bytes = new byte[buffer.limit()];
                    buffer.get(bytes);
                    buffer.clear();

                    String data = new String(bytes, CHARSET);
                    System.out.println();
                    System.out.println(String.format("received data[%-5d]: %s", sc.socket().getPort(), data));
                }
            }
        }
    }
}

#1 位置,让服务端接后客户端连接时 ssc.accept() 非阻塞
#2 位置,与客户端连接的Socket读/写数据时 sc.read(buffer) 非阻塞

运行代码,JVM调用了内核一下方法

socket(···) = 6
bind(6, 9090, ···) 
listen(6, ···)
# 以上三步同BIO一样

# ssc.configureBlocking(false);
fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK) = 0  # 设置6fd为非阻塞

# accept 不再阻塞。没有连接时,返回-1;有连接时,返回建立好的连接
accept(6, xxx, xxx) = -1 EAGAIN (Resource temporarily unavailable)
accept(6, {客户端信息}, [28]) = 7

# read 读取数据不在阻塞。channel没有数据返回-1
read(7, 0x7f92b428e590, 1024) = -1 EAGAIN (Resource temporarily unavailable)
read(7, "hello\n", 1024) = 6

3.1.3. 问题瓶颈

虽然NIO接收客户端连接和读/写数据都不在阻塞,但是上面的额连接、读写都在一个线程,操作是串行化的。高并发下,后面的读写操作降低了可接入连接的性能。可将客户端连接由一个线程处理,数据读写由两一个线程处理。

资源浪费:

  1. while(true) 中,每次都要询问/调用 accept() ,看是否由客户端请求接入。
  2. 如果接入了 1w/100w+ 个连接,每次都需要把所有连接 read() 一下看是否有数据过来(复杂度O(n))。

3.2. NIO 多路复用模型

3.2.1. 特点描述

Selector 多路复用器,JDK中使用Selector对象来注册/管理Channel(包含Server和Client),当有事件触发时,使用 selector.select()selector.selectedKeys() 来获取有事件触发的Channel,我们根据事件类型来做相应处理。
注意Selector获取的是一个Channel状态,数据的读/写还是需要用户自己触发,即读写过程依然是同步。

Selector 的实现依赖内核支持,如:select/poll/epoll等

select/poll: 每次询问是否有事件到达,需要传入所有socket fd,只是所有fd的循环遍历交给了内存来完成(JDK不再干这个事)。
epoll: 先创建一个Selector fd,然后所有socket都添加到这个fd中,只添加一次即可。由这个fd管理所有socket的事件。

3.2.2. 代码示例

ServerSocketChannel 的有效事件为 OP_ACCEPT
SocketChannel 的有效事件为 OP_CONNECTOP_READOP_WRITE

/**
 * NIO,单线程多路复用
 */
public class SocketNIOMultiplexing {

    private ServerSocketChannel server;
    private Selector selector;  // 多路复用器

    public void initServer() throws IOException {
        server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(9090));
        server.configureBlocking(false); // accept非阻塞
        selector = Selector.open();
        server.register(selector, SelectionKey.OP_ACCEPT); // 将ServerSocketChannel注册到Selector中,注册过程创建SelectionKey,表示Channel向Selector中注册的token
        System.out.println("1. Server started [ServerSocketChannel Selector 9090]");
    }

    public void start() throws IOException {
        initServer();
        while (true) {
            if (selector.select(0) < 1) { // 0表示每次询问不阻塞,立即返回;非0表示阻塞时间
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 取出有效的key
            System.out.println("2. selector 中有事件状态进来 - size=" + selectionKeys.size());
            for (Iterator<SelectionKey> it = selectionKeys.iterator(); it.hasNext(); it.remove()) { // 注意,这里key处理后要从keys中remove,不然下次会会获取没有事件key,poll进入死循环
                SelectionKey selectionKey = it.next();
                if (selectionKey.isAcceptable()) { // selectionKey中持用的Socket状态是可连接
                    acceptHandler(selectionKey); // 从socket(Server)中建立连接
                } else if (selectionKey.isReadable()) { // selectionKey中持用的Socket状态是可读
                    readHandler(selectionKey); // 从socket(Client)中读取数据
                }
            }
        }
    }

    private void acceptHandler(SelectionKey selectionKey) {
        ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
        System.out.println("3.1 接受连接");
        try {
            SocketChannel client = ssc.accept();
            client.configureBlocking(false); // 非阻塞
            ByteBuffer buffer = ByteBuffer.allocate(1024); // 为该client配置一个buffer,堆内
            client.register(selector, SelectionKey.OP_READ, buffer); // 将SocketChannel注册到Selector中
            System.out.println("一个新客户端连接: client=" + client.getRemoteAddress());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readHandler(SelectionKey selectionKey) {
        SocketChannel sc = (SocketChannel) selectionKey.channel();
        ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();

        System.out.println("3.2 读取数据");

        buffer.clear(); // 读之前默认清空一次buffer
        try {
            while (true) {
                int len = sc.read(buffer); // 读到数据返回数据字节长度;流没有结束,没有数据返回0,流结束返回-1
                if (len > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        sc.write(buffer); // 把数据直接返回去
                    }
                    buffer.clear();
                } else if (len == 0) {
                    break;
                } else { // -1  注意bug,当tcp连接处于 close_wait 时,selectionKey.isReadable()返回true,这里出现死循环,cpu飙高
                    sc.close(); // 去掉bug,这里client close,主要是使用 key.cancel(); 从多路复用器的key集合中移除
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        SocketNIOMultiplexing server = new SocketNIOMultiplexing();
        server.start();
    }
}

运行代码,JVM调用了内核一下方法

socket(···) = 6
bind(6, 9090, ···) 
listen(6, ···)
# 以上三步同BIO一样

fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK) = 0  # 设置6fd非阻塞

pipe([7, 8])  # 创建一个channel,7fd表示管道的读取端,8fd表示管道的写端
fcntl(7, F_SETFL, O_RDONLY|O_NONBLOCK) = 0  # 设置7fd只读端口非阻塞
fcntl(8, F_SETFL, O_WRONLY|O_NONBLOCK) = 0  # 设置8fd只写端口非阻塞

# 创建 Selector 实例
epoll_create(256) = 9  # 创建一个新的epoll实例9fd

# channel.register(selector)
epoll_ctl(9, EPOLL_CTL_ADD, 7, ···) = 0  # 将7fd注册到9fd上,并将事件连接到7fd上
epoll_wait(9, ···)  # 阻塞,9fd上等待事件到来触发

# client连接
rt_sigaction(SIGRT_30, ···, 8) = 0  # 信号安装登记
accept(6, {客户端信息}, [28]) = 10

3.2.3. 问题瓶颈

上面的模型不需要每次询问 accept() 和所有client的 read(),每次只需要询问一次多路复用器Selector即可,显然复杂度降为O(1),解决了资源浪费问题。
但是Selector询问、Channel客户端连接、数据读写依然串行化。可使用多线将这个三者分开,Netty的模型就是基于此设计。

3.3. NIO 多路复用模型-多线程

3.3.1. 特点描述

3.2 中的问题,进行多线程分开。boss线程接收连接请求后,快速记录,交给worker线程,worker线程负责耗时的accept和数据的读写操作。(这里就有些netty的味道了)

3.3.2. 代码示例

/**
 * NIO,多路复用,多线程
 */
public class SocketNioMultiplexing2 {

    private static final String CHARSET = "UTF-8";

    private ServerSocketChannel server;
    private NioThread boss;
    private NioThread[] workers;

    static class NioThread extends Thread {

        private static volatile int workerNum = 0; // worker线程数量
        private static BlockingQueue<SocketChannel>[] workerQueues; // SocketChannel队列,长度=workerNum
        private static AtomicInteger workerBalance = new AtomicInteger();

        private Selector selector; // 多路复用器
        private Integer tid; // 线程ID,null表示boss线程

        public void config(Selector selector, Integer tid) {
            this.selector = selector;
            this.tid = tid;
            if (tid == null) {
                System.out.println("boss thread ready");
                this.setName("boss");
            } else {
                System.out.println("worker-" + tid + " thread ready");
                this.setName("worker-" + tid);
            }
        }

        @Override
        public void run() {
            System.out.println(">> " + Thread.currentThread().getName() + " start");
            try {
                if (tid == null) {
                    bossLoop();
                } else {
                    workerLoop();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        private void bossLoop() throws IOException {
            while (true) {
                if (selector.select(10) < 1) { // 询问是否有事件到达,最多阻塞10ms
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                System.out.println("2. boss-selector 中有请求事件状态进来 - size=" + selectionKeys.size());
                for (Iterator<SelectionKey> it = selectionKeys.iterator(); it.hasNext(); it.remove()) {
                    SelectionKey selectionKey = it.next();
                    if (!selectionKey.isAcceptable()) {
                        continue;
                    }
                    ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();

                    try {
                        SocketChannel client = ssc.accept();
                        client.configureBlocking(false);
                        int tid = workerBalance.getAndIncrement() % workerNum; // 负载均衡
                        workerQueues[tid].add(client);
                        System.out.println("3. 连接请求加入队列-" + tid);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        private void workerLoop() throws IOException {
            while (true) {
                acceptHandler(workerQueues[tid]); // 建立连接

                if (selector.select(10) < 1) {
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                System.out.println("2. worker-selector 中有读取事件状态进来 - size=" + selectionKeys.size());
                for (Iterator<SelectionKey> it = selectionKeys.iterator(); it.hasNext(); it.remove()) {
                    SelectionKey selectionKey = it.next();
                    if (selectionKey.isReadable()) {
                        readHandler(selectionKey); // 读取数据
                    }
                }
            }
        }

        private void acceptHandler(BlockingQueue<SocketChannel> queue) throws IOException {
            SocketChannel client = queue.poll();
            if (client == null) {
                return;
            }
            // 建立Channel连接,配置缓冲器
            ByteBuffer buffer = ByteBuffer.allocate(1024); // 字节对齐?
            client.register(this.selector, SelectionKey.OP_READ, buffer);
            System.out.println("4. 一个新客户端连接: client=" + client.getRemoteAddress());
        }

        private void readHandler(SelectionKey selectionKey) {
            SocketChannel sc = (SocketChannel) selectionKey.channel();
            ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();

            System.out.println("3 读取数据");

            buffer.clear(); // 读之前默认清空一次buffer
            try {
                while (true) {
                    int len = sc.read(buffer);
                    if (len > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) { // Echo应答
                            ByteBuffer prefix = ByteBuffer.allocate(20);
                            prefix.put("Echo:".getBytes(CHARSET));
                            prefix.flip();
                            sc.write(new ByteBuffer[]{prefix, buffer});
                        }
                        buffer.clear();
                    } else if (len == 0) {
                        break;
                    } else {
                        sc.close();
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    public SocketNioMultiplexing2 init(NioThread boss, NioThread[] workers) throws IOException {
        this.boss = boss;
        this.workers = workers;

        server = ServerSocketChannel.open();
        server.configureBlocking(false);

        Selector boosSelector = Selector.open();
        boss.config(boosSelector, null);
        server.register(boosSelector, SelectionKey.OP_ACCEPT);

        int workerNum = workers.length;
        NioThread.workerNum = workerNum;
        NioThread.workerQueues = new LinkedBlockingQueue[workerNum];
        for (int i = 0; i < workerNum; i++) {
            workers[i] = new NioThread();
            workers[i].config(Selector.open(), i); // worker 线程,指定线程ID
            NioThread.workerQueues[i] = new LinkedBlockingQueue<>();
        }

        return this;
    }

    public SocketNioMultiplexing2 port(int port) throws IOException {
        server.bind(new InetSocketAddress(port));
        return this;
    }

    public void start() throws IOException {
        boss.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (NioThread worker : workers) {
            worker.start();
        }
        System.out.println(String.format("1. Server started [boss: %s, workers: %d]", server.getLocalAddress().toString(), workers.length));
    }

    public static void main(String[] args) throws IOException {
        NioThread boss = new NioThread(); // boss 线程
        NioThread[] workers = new NioThread[2]; // worker 线程
        SocketNioMultiplexing2 server = new SocketNioMultiplexing2();
        server.init(boss, workers)
        .port(9090)
        .start();
    }
}

4. AIO

Asynchronous I/O,异步非阻塞I/O模型。这里的 异步 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

在大多数业务场景中,我们往往在读/写数据时需要阻塞,获取读取到的数据/写入数据状态等。

目前来看,Linux上 AIO 的应用还不真正的异步,可以说时伪异步,Netty 之前也尝试使用过 AIO,不过又放弃回归到NIO上。


参考

Linux的五种IO模型:https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41&scene=21#wechat_redirect

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341