NIO学习笔记(Scalable IO in Java)

本文介绍了Java中的传统BIO模型及其在高并发下的问题,进而引入了Reactor模型,包括单线程和多线程版本,解释了NIO的选择器、通道和缓冲区概念,并指出Reactor模型如何提高服务的可扩展性,为Netty的学习打下基础。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

本文是学习 《Scalable IO in Java》这一文章后的一些笔记,同时也是为了更好的学习Netty做一些铺垫,《Scalable IO in Java》原文地址:https://gee.cs.oswego.edu/dl/cpjslides/nio.pdf

二、网络服务

在Web服务中,通常有着相同的基本结构分为以下几个步骤

  • Read Request(读取请求)
  • Decode Request(请求解码/解析)
  • Process Service(处理服务)
  • Encode Reply(对响应编码)
  • Send Reply(发送响应)

1、传统Service设计

image.png
(对应到Java中也就是我们经常说的BIO,Block IO)
从图中可以看出每个Client在服务端都会有一个Handler与之对应(每个Handler对应一个线程)。这种模式有着很明显的优缺点:
优点:理解起来非常简单,代码实现起来也比较简单。在一些用户量不大的情况下还是可以使用的。
缺点:

  1. 由于服务器内一个线程对应一个连接,当连接数比较大的时候会消耗大量服务器资源;且服务器端线程数过多导致上下文切换频繁对CPU也不友好。
  2. 由于是阻塞模型,客户端和服务端建联连接后哪怕什么都不做,服务端也一直维护这该线程所以导致资源浪费。

总结一下:该模型的优点简单好理解;缺点并发量不大,浪费资源。

代码演示
public class BIOTest   {

    public static void main(String[] args) throws IOException {
        System.out.println("socket tcp服务器端启动....");
        ServerSocket serverSocket = new ServerSocket(8080);
        // 等待客户端请求
        try {
            while (true) {
                System.out.println("服务器等待新连接。。。。。。。。。。");
                Socket accept = serverSocket.accept();
                System.out.println("服务器有了新连接。。。。。。。。。。");
                new Thread(() -> {
                    try {
                        InputStream inputStream = accept.getInputStream();
                        // 转换成string类型
                        byte[] buf = new byte[1024];
                        System.out.println("线程开始read。。。。。。。。。。");
                        int len = inputStream.read(buf);
                        System.out.println("线程结束read。。。。。。。。。。");
                        String str = new String(buf, 0, len);
                        System.out.println("服务器接受客户端内容:" + str);
                    } catch (Exception e) {
                    }
                }).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            serverSocket.close();
        }
    }

}

class TcpClient {
    public static void main(String[] args) throws IOException {
        System.out.println("socket tcp 客户端启动....");
        Socket socket = new Socket("127.0.0.1", 8080);
        System.out.println("客户端线程getOutputStream。。。。。。。。。。");
        OutputStream outputStream = socket.getOutputStream();
        System.out.println("客户端线程write。。。。。。。。。。");
        outputStream.write("我是客户端".getBytes());
        socket.close();
    }
 

}

说明:该demo出自 https://blog.youkuaiyun.com/xyjy11/article/details/115116265 博客!
代码本身也是比较简单的,结合这上面的图可以很好的理解,就是一个连接过来服务器对应开启一个新的线程去处理,这里不多赘述。

2、Reactor模型

基于传统IO模型的缺点我们应该怎么解决呢?首先是一个线程对应一个客户端连接这明显是很浪费的,那能不能用一个线程去管理多个客户端连接呢?**客户端如果阻塞住服务端是否可以做别的事情呢?**带着这两个疑问我们继续往下。
在讲述Reactor模型之前,我们可以先了解一下Java中一个相对冷门的技术ATW(界面编程),在AWT中会有按钮和点击事件
image.png
这是一个很典型的基于“事件驱动”模型,点击按钮的时候事件监听器会从事件队列中查询到当前“按钮”所绑定的事件,进入执行对应的操作。
从这个例子我们可以引申到I/O模型里,我们多个连接是否也可以像“事件”一样注册到某个地方,然后等待一个“时机”通过监听器来触发某些事件呢?这里就引出了Reactor模型
image.png
基础的Reactor模型(单线程版本)
Reactor:通过分派适当的处理程序来响应IO事件
Handler:(黄色圆圈,一行表示一个handler):处理器,用于处理业务
Acceptor:负责统一管理连接。
流程说明:当客户端想要和服务端进行连接时,Reactor会根据请求类型来分发请求;如果是链接请求则会交给acceptor去处理,acceptor会创建一个Handler并且绑定当前连接。如果不是连接请求则会交由特定的Handler处理。

代码演示

Java在JDK1.4之后也支持了NIO(Non-blocking IO:非阻塞IO),这里有几个概念需要提前说明:
Channels:管道,支持非阻塞读取的文件、套接字等的连接。一个客户端连接对应一个Channel
Buffers:缓冲区,NIO是面向缓冲区的,因为客户端和服务端的交互是通过Channel而Channel和客户端之间数据的传输则是通过Buffer缓冲区,所以说NIO是面向缓冲区的。
Selectors :选择器,告诉一组通道中的哪些具有IO事件。
SelectionKeys :维护IO事件状态和绑定
这里借用一张图:
image.png
原图出自:https://www.cnblogs.com/mikechenshare/p/16587635.html
案例代码来源于《Scalable IO in Java》可做参考

class Reactor implements Runnable {
    final Selector selector;
    final ServerSocketChannel serverSocket;

    Reactor(int port) throws IOException { //Reactor初始化
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        serverSocket.configureBlocking(false); //非阻塞
        SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT); //分步处理,第一步,接收accept事件
        sk.attach(new Acceptor()); //attach callback object, Acceptor
    }


    public void run() {
        try {
            while (!Thread.interrupted()) {
                selector.select();
                Set<SelectionKey> selected = selector.selectedKeys();
                for (SelectionKey selectionKey : selected) {
                    //Reactor负责dispatch收到的事件
                    dispatch(selectionKey);
                }
                selected.clear();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 分发请求
     *
     * @param k
     */
    void dispatch(SelectionKey k) {
        Runnable r = (Runnable) (k.attachment()); //调用之前注册的callback对象
        if (r != null)
            r.run();
    }
    /**
     * Acceptor组件
     */
    class Acceptor implements Runnable { // inner
        public void run() {
            try {
                SocketChannel c = serverSocket.accept();
                if (c != null)
                    new Handler(selector, c);
            } catch (IOException ex) { /* ... */ }
        }
    }
}


final class Handler implements Runnable {
    final SocketChannel socket;
    final SelectionKey sk;
    ByteBuffer input = ByteBuffer.allocate(1024);
    ByteBuffer output = ByteBuffer.allocate(1024);
    static final int READING = 0, SENDING = 1;
    int state = READING;


    Handler(Selector sel, SocketChannel c) throws IOException {
        socket = c;
        c.configureBlocking(false);
        // Optionally try first read now
        sk = socket.register(sel, 0);
        sk.attach(this); //将Handler作为callback对象
        sk.interestOps(SelectionKey.OP_READ); //第二步,接收Read事件
        sel.wakeup();
    }

    boolean inputIsComplete() {
        return true;
    }

    boolean outputIsComplete() {
        return true;
    }

    void process() { /* ... */ }


    public void run() {
        try {
            if (state == READING) read();
            else if (state == SENDING) send();
        } catch (IOException ex) { /* ... */ }
    }

    void read() throws IOException {
        socket.read(input);
        if (inputIsComplete()) {
            process();
            state = SENDING;
            // Normally also do first write now
            sk.interestOps(SelectionKey.OP_WRITE); //第三步,接收write事件
        }
    }
    void send() throws IOException {
        socket.write(output);
        if (outputIsComplete()) sk.cancel(); //write完就结束了, 关闭select key
    }
}

3、多线程版本实现Reactor

如果你能明白上面的所写的单线程版本的Reactor,那么多线程版本其实也不难。在学习多线程版本之前,我们需要知道一个概念,计算机是一门“工程类学科”,我们讲究的是模块化的开发各自负责各自的模块。再回到多线程版本的Reactor模型中来,单线程的版本有什么问题呢?Reactor本身及负责处理连接又负责处理请求,那我们是不是有更好的方案来解决这一问题呢?答案是有的,思想也很简单我们添加一个 工作线程池来处理具体的业务请求,Reactor本身只负责管理分发请求。
用一个通俗的例子来说,我们去饭店吃饭,前台的服务员只负责将我们带进饭店,到了饭店内部会有其他工作人员为我们服务。模型如下
image.png
可以看到具体的业务逻辑放到了一个新的线程池去处理,通常我们会称之为WorkerThread(工人线程)
一个完整的多线程模型如下图:
image.png
主Reactor只负责接收连接,而真正干活的则是SubReactor。

三、结束语

在学习了《Scalable IO in Java》之后,我们对NIO,Reactor模型有了一些新的认识,当然我们实际开发过程中很少会使用JDK原生的NIO API,因为比较晦涩难懂,我们更多的是使用Netty来做。今天这些知识点也是为后续Netty学习系列文章做铺垫,希望对你有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值