网络IO基础—网络IO编程的演进 & 高性能网络编程之Reactor模式

185 阅读7分钟

网络IO编程的演进

演进:BIO--->NIO--->IO多路复用

  1. BIO编程

    阻塞式IO

    1. 阶段一:一个线程处理所有网络连接,缺陷:若线程在处理某个连接时阻塞了,则其他连接就得不到处理,服务器吞吐量极低。
    2. 阶段二:一个线程处理一个网络连接(经典的Connection Per Thread) ,相比于【阶段一】极大地提高了系统吞吐量,但也有缺陷:海量连接就要海量线程,非常吃系统资源,系统可能承受不住,另外线程的创建、销毁和上下文切换开销也很大,所以这种模式也很有局限性
  2. NIO编程

    非阻塞式IO

    一个线程可以很好地处理多个网络连接(若当前连接可读/可写,就进行读/写(读/写完处理下一个连接),不可读/不可写也不会阻塞,可以继续处理下一个连接),这能让系统用少量线程处理大量连接,解决了BIO需要海量线程的问题,大大提高了服务器吞吐量,但也有局限性:每个线程每次read/write系统调用只能判断一个连接是否可读/可写,会有大量的系统调用,也有很大开销,一定程度限制了服务器的性能和吞吐量。

  3. IO多路复用

    一次系统调用可以判断很多连接是否可读/可写

    一个线程可以很好地处理多个网络连接,且一次系统调用能处理多个连接,大大减少了系统调用次数,减少了很大一部分开销,解决了NIO的局限性,极大地提高了服务器性能和吞吐量。

高性能网络编程之Reactor模式

两个角色:

  1. Reactor反应器:负责监听IO事件,并分发给Handler处理。(监听+分发)
  2. Handler处理器:负责处理IO事件,承担主要业务逻辑处理。(处理业务)

可以看到,Reactor模式的思想很简单事件驱动

优缺点:

优点:

  1. 少量线程就能处理海量连接
  2. 性能好,服务器吞吐量大
  3. 事件驱动,扩展性高

缺点:

  1. 复杂
  2. 需要操作系统底层支持IO多路复用。(如今大多数操作系统已经支持了)

Reactor模式3种实现方式

推荐 Scalable IO in Java

单Reactor单线程

Reactor和Handler处理都是在一个线程中,即一个线程既负责监听所有连接的IO事件,又负责处理所有连接的IO事件。

缺陷:

  1. 单线程,不能充分利用多核优势,性能差:当某个连接的Handler处理逻辑重、处理慢时,就会阻塞其他连接的处理和接受新连接,从而影响服务器整体性能和吞吐量。

单Reactor单线程瓶颈在【单线程】上。

单Reactor多线程

仍然只有一个Reactor线程监听和分发IO事件,但使用多线程执行Handler处理

仍然有局限性:

一个Reactor线程负责监听所有连接的IO事件,在高并发场景下,就会有很多事件需要响应,可能会存在性能瓶颈:一个Reactor线程来不及响应IO事件,那就会影响连接的处理速度,且会影响新连接的接入速度

单Reactor多线程瓶颈在【单Reactor】上。

多Reactor多线程

mainReactor(主线程) :负责连接事件。(通常只需要1个,因为通常只需要监听一个服务端套接字)

subReactor(子线程) :负责读写事件。(通常会有多个,因为一般会有大量连接)

即主从Reactor

优点:解决了单Reactor的局限性,有多个Reactor负责响应IO事件,能很好的应对高并发场景,吞吐量更大。

一个例子理解为什么会有3种实现

餐厅:网络服务器

前台:Reactor

服务员:Handler

单Reactor单线程:

适合场景:吞吐量不大时

相当于,前台和服务员是同一个人(同一个线程) ,当一下来很多客人时,会来不及接待客人(接收新连接) ,也会来不及提供服务(即handler执行业务处理)


单Reactor多线程:

适合场景:吞吐量比较大时

前台是一个人,服务员也有多个人


多Reactor多线程:

适合场景:吞吐量很大时

前台有多个人,服务员也有多个人


总之,单Reactor单线程--->单Reactor多线程--->多Reactor多线程演进的核心思想就是【人多力量大】,各自有不同的应用场景。

单Reactor单线程实现demo

实现EchoServer回显服务器,接收客户端发送的数据,并返回给客户端。

思考:

  1. Reactor监听到IO事件后怎么知道分发给哪个handler?

    感觉Reactor不应该理解分发给哪个handler,Reactor本身只是单纯的监听和分发,但具体分发给哪个handler其实是业务逻辑,不应该由Reactor负责,Reactor应该约定一个协议,比如,Reactor规定注册到我这的Channel必须实现什么协议,我按照这个协议去分发IO事件到handler,但我不理解具体的业务逻辑,我只遵守这个协议
    这就是面向接口编程的好处(接口就是协议):高内聚低耦合

server端程序

@Slf4j
public class EchoServer {
    ServerSocketChannel serverSocketChannel;
    Selector selector;// 作为Reactor监听IO事件

    public EchoServer(SocketAddress socketAddress) throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(socketAddress);
        selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 绑定Handler
        selectionKey.attach(new AcceptHandler());
    }

    public void run() throws IOException {
        log.info("EchoServer run...");
        while (true) {
            int select = selector.select();
            if (select <= 0) {
                continue;
            }
            log.info("EchoServer select:{}", select);

            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();

                // 重点:协议约定每个selectionKey的attachment都是Handler
                ((Handler) selectionKey.attachment()).Dispatch(selectionKey);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        EchoServer echoServer = new EchoServer(Const.socketAddress);
        echoServer.run();
    }
}

interface Handler {
    // 分发并处理SelectionKey,由子Handler具体实现
    void Dispatch(SelectionKey selectionKey) throws IOException;
}

@Slf4j
class AcceptHandler implements Handler {

    @Override
    public void Dispatch(SelectionKey selectionKey) throws IOException {
        if (selectionKey.isAcceptable()) {
            SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
            if (socketChannel == null) {
                return;
            }
            log.info("accept success {}", socketChannel.getRemoteAddress());
            socketChannel.configureBlocking(false);
            SelectionKey newKey = socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ);
            // 绑定Handler
            newKey.attach(new IOHandler());
        }
    }
}

@Slf4j
class IOHandler implements Handler {

    ByteBuffer readBuffer;

    public IOHandler() {
    }

    @Override
    public void Dispatch(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

        // 大致逻辑:先读取客户端发送的数据,再回写给客户端
        if (selectionKey.isReadable()) {
            // 读取客户端发送的数据 TODO 怎么知道客户端发完一份完整数据呢?(涉及粘包和半包问题)
            // 这里假设一次读取就收到了客户端一份完整的数据(很简单的处理)
            readBuffer = ByteBuffer.allocate(1024);
            int read = socketChannel.read(readBuffer);
            if (read == -1) {
                log.info("close socketChannel [{}]", socketChannel.getRemoteAddress());
                socketChannel.close();
                return;
            }
            if (read == 0) {
                log.info("read nothing");
                return;
            }
            byte[] bufferArray = readBuffer.array();
            byte[] data = new byte[read];
            System.arraycopy(bufferArray, 0, data, 0, read);
            log.info("read data from client[{}] read:{}, msg:{}", socketChannel.getRemoteAddress(), read, new String(data, StandardCharsets.UTF_8));
            readBuffer.flip();

            // 读完之后需要回写
            selectionKey.interestOps(SelectionKey.OP_WRITE);
        }

        if (selectionKey.isWritable()) {
            // 如果没写完要继续写
            if (readBuffer.hasRemaining()) {
                int write = socketChannel.write(readBuffer);
                log.info("send client data:{}", write);
            } else {
                log.info("echo end to client[{}]", socketChannel.getRemoteAddress());
                readBuffer = null;
                // 写完了再读 TODO 能否同时读写呢?
                selectionKey.interestOps(SelectionKey.OP_READ);
            }
        }
    }
}

client端程序

@Slf4j
public class EchoClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.connect(Const.socketAddress);
        Selector selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_CONNECT);

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

            log.info("EchoClient select:{}", select);
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey curKey = iterator.next();
                iterator.remove();
                SocketChannel curChannel = (SocketChannel) curKey.channel();

                if (curKey.isConnectable()) {
                    log.info("connect success {}", curChannel.finishConnect());
                    // 连接完则写数据
                    curKey.interestOps(SelectionKey.OP_WRITE);
                }

                if (curKey.isWritable()) {
                    String identify = curChannel.getLocalAddress().toString();
                    String msg = "hello, this is " + identify;
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));
                    // 简单处理,不考虑缓冲区满的问题,认为能一次写入
                    int write = curChannel.write(buffer);
                    log.info("client send:{}", write);

                    // 写完后再读来自服务端响应的数据
                    curKey.interestOps(SelectionKey.OP_READ);
                }

                if (curKey.isReadable()) {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    // 认为能一次读取完
                    int read = curChannel.read(byteBuffer);
                    if (read == -1) {
                        log.info("read close");
                        continue;
                    }
                    if (read == 0) {
                        log.info("read nothing");
                        continue;
                    }
                    byte[] bufferArray = byteBuffer.array();
                    byte[] data = new byte[read];
                    System.arraycopy(bufferArray, 0, data, 0, read);
                    log.info("receive data from server: {}", new String(data));

                    // 读完服务端响应直接close
                    curChannel.close();
                }
            }
        }
    }
}

多Reactor多线程实现demo

有时间再实现

Java NIO编程很底层,比较难

从做demo的过程中可以看出来,想要使用Java NIO去开发网络服务器其实是挺困难的,需要处理很多细节问题(因为Java NIO还是比较底层的),问题有

  1. 需要自己解决粘包半包问题、定义消息格式、消息编码和解码

  2. 需要自己实现Reactor模式

  3. ......

感悟

简单复杂这中间是非常多的细节,将细节隐藏就简单,将细节暴露就复杂,这也是抽象具体的关系。