深入学习Netty(1)——传统BIO编程

前言

  之前看过Dubbo源码,Nacos等源码都涉及到了Netty,虽然遇到的时候查查资料,后面自己也有私下学习Netty并实践,但始终没有形成良好的知识体系,Netty对想要在Java开发上不断深入是十分重要的。所以借此博客平台记录下自己的学习思考的过程,形成自己的知识体系,以后学习深入源码更加得心应手!

  参考资料《Netty In Action》、《Netty权威指南》(有需要的小伙伴可以评论或者私信我)

  博文中所有的代码都已上传到Github,欢迎Star、Fork

 


 

一、Linux中常见的I/O模型

1.5种I/O模型

根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型

(1)阻塞I/O模型

  如果还未获取数据报则一直等待,直到数据报准备好了再进行复制数据报等接下来的操作。

(2)非阻塞I/O模型

  轮询检查是否有数据准备好的状态,如果数据准备好就进行接下来的操作

(3)I/O复用模型

  Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll顺序扫描fd是否就绪,但是select支持的fd数量有限(1024*8个)Linux提供了epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。Java NIO的Selector基于epoll的多路复用技术实现

(4)信号驱动I/O模型

  首先开启套接字信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数。当数据准备就绪时,就为进程生成一个SIGIO信号,通过信号回调通知应用程序调用来读取数据。

(5)异步I/O

  告知内核启动某个操作,并让内核在整个操作完成后通知我们,这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成。

2.I/O多路复用技术

(1)I/O多路复用技术应用场景

  在处理多客户端连接I/O请求时,往往有两种方式:一种是传统的多线程处理,另一种就是I/O多路复用技术进行处理,但是与传统的多线程处理比较,I/O多路复用最大的优势就是在于系统开销小,不需要创建额外的线程或进程处理客户端连接,节省了系统资源,I/O多路复用技术主要的应用场景如下:

  • 服务器需要同时处理多个处于监听状态或多个连接状态的套接字
  • 服务器需要同时处理多种网络协议的套接字,比如又要处理UDP、又要处理TCP

(2)epoll的优势

  在Linux系统中,采用了I/O多路复用技术调用有select、poll、epoll,在Linux网络编程的过程中。select、poll、epoll介绍这里不再讲解,我也是参考了很多资料才把三者关系弄清楚,大家可以自行Google,起初使用select做轮询,但是select有一些缺陷,不得不选择epoll替代了selectepollselect的基础上做了如下的改进:

 1)支持一个进程打开的socket描述符(FD)不受限制

  select最大缺陷就是单个进程打开FD是有限制的,epoll是没有的,而poll除了没有限制(基于链表存储,所有理论上没有限制)以外跟select没啥区别

 2)I/O效率不会随着FD数目的增加而线性下降

  传统的select/poll的有一个致命缺点:当有一个很大的socket集合时,由于网络延迟或链路空闲,任一时刻只有少部分的socket是“活跃”的,但是select/poll仍然会线性扫描全部的集合,导致效率降低。epoll只会针对“活跃”的socket进行操作,这是因为epoll根据每个fd上的回调函数callback函数实现的,只有“活跃”的socket才会主动调用该函数,其它则不会

 3)使用mmap加速内核与用户空间的消息传递

  无论是select、poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制显得十分重要,epoll通过内核把用户空间mmap同一块内存来实现的。

 4)epoll使用的API更加简单

 

二、传统BIO编程

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。  

很明显,这种模型缺乏弹性伸缩能力,当客户端并发量增加后,线程数也随着增加,可能会造成线程堆栈溢出、创建新线程失败等问题

1.通信模型

  BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户客户端的连接,接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完之后通过输出流返回应答给客户端,最后线程销毁,这是典型的一请求一应答通信模型

    

2.代码实践

(1)服务端代码(代码已上传到Github

/**
 * BIO通信服务端:
 * 由一个独立的Acceptor线程负责监听客户客户端的连接,
 * 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,
 * 处理完之后通过输出流返回应答给客户端,最后线程销毁,这是典型的一请求一应答通信模型
 */
public class BioServer {

    public static void main(String[] args) {
        bioServer(8082);
    }

    /**
     * @param port
     */
    public static void bioServer(int port) {
        ServerSocket server = null;
        try {
            // ServerSocket负责绑定IP地址,启动监听端口
            server = new ServerSocket(port);
            System.out.println("The bio server is start in port : " + port);
            // Socket负责发起连接操作
            Socket socket = null;
            // 无限循环监听客户端的连接,若没有则主线程阻塞在ServerSocket的accept操作上
            while (true) {
                socket = server.accept();
                new Thread(new BioServerHandler(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (server != null) {
                System.out.println("The bio server close");
            }
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            server = null;
        }
    }
}

(2)客户端代码(代码已上传到Github

/**
 * BIO通信客户端:
 * 由一个独立的Acceptor线程负责监听客户客户端的连接,
 * 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,
 * 处理完之后通过输出流返回应答给客户端,最后线程销毁,这是典型的一请求一应答通信模型
 */
public class BioClient {

    public static void main(String[] args) throws InterruptedException {
        String host = "127.0.0.1";
        int port = 8082;
        bioClient(host, port);
    }

    public static void bioClient(String host, int port) {
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            socket = new Socket(host, port);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            // 发送查询当前时间指令
            out.println("QUERY CURRENT TIME ORDER");
            System.out.println("Client send order to server succeed.");
            // 返回应答
            String resp = in.readLine();
            System.out.println("Now is : " + resp);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                out.close();
                out = null;
            }
            // 释放socket套接字句柄资源
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                socket = null;
            }
        }

    }
}

(3)处理器(代码已上传到Github

public class BioServerHandler implements Runnable {

    private Socket socket;

    public BioServerHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(), true);
            String currentTime = null;
            String body = null;
            while (true) {
                body = in.readLine();
                if (body == null) {
                    break;
                }
                System.out.println("The server receive order: " + body);
                currentTime = "QUERY CURRENT TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() :
                        "BAD ORDER";
                out.println(currentTime);

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                out.close();
                out = null;
            }
            // 释放socket套接字句柄资源
            if (this.socket != null) {
                try {
                    this.socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                this.socket = null;
            }
        }
    }
}

(4)测试结果

运行服务端,再运行客户端:

服务端Console:

客户端Console

 同时netstat命令查看TCP监听端口8082

 

此外,通过dump thread查看,发现服务端线程一直阻塞在accept方法上,处于RUNNABLE状态

  为了解决同步阻塞I/O的缺点(处理链路:功能线程=1:1),后端通过一个线程池处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N。这就是通过Java线程池处理任务,而不是每次生成一个Thread。在《Netty权威指南》中称之为“伪异步I/O”

 

posted @ 2021-07-05 15:28  JJian  阅读(999)  评论(0编辑  收藏  举报