【硬核Netty系列】IO多路复用底层原理详解,Java面试大厂必问

一、Socket

在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

Socket读缓冲和写缓冲

在这里插入图片描述
平时用的socket(套接字)只是一个引用,这个socket对象实际上是放在操作系统内核中。socket内部有两个重要的缓冲结构,一个是读缓冲,一个是写缓冲,读写缓冲都是有限大小的数组结构

当我们对客户端的socket写入字节数组时,是将字节数组拷贝到内核区socket的write buffer中,内核网络模块会有单独的线程负责不停地将write buffer的数据拷贝到网卡硬件,网卡硬件再将数据送到网线,经过一些列路由器交换机,最终送达服务器的网卡硬件中。

同样,服务器内核的网络模块也会有单独的线程不停地将收到的数据拷贝到socket的read buffer中等待用户层来读取。最终服务器的用户进程通过socket引用的read方法将read buffer中的数据拷贝到用户程序内存中进行反序列化成请求对象进行处理。然后服务器将处理后的响应对象走一个相反的流程发送给客户端。

阻塞和非阻塞

socket读写也是分阻塞和非阻塞

因为write buffer空间都是有限的,所以当write buffer 满了,写操作就会阻塞,直到空间被腾出来。当有了NIO(非阻塞IO),写操作也可以不阻塞,假设write buffer空间还剩1M,此时一个客户端请求写数据是2M,那么就会先写进去1M,然后返回客户端,告知写进去多少,还剩1M没有写进去的内容用户进程会缓存起来,后续会继续重试写入。

读操作也是一样,read buffer的内容可能会是空的。这样socket的读操作也会阻塞,直到read buffer中有了足够的内容才会返回。有了NIO,就可以有多少读多少,不会阻塞。

Socket API简单使用

客户端简单实例代码

public class TCPClient {
    public static void main(String[] args) throws IOException {
        Socket s=new Socket("127.0.0.1",6666);
        OutputStream os=s.getOutputStream();
        //将数据写入到socket
        DataOutputStream dos=new DataOutputStream(os);
        dos.writeUTF("Hello,server!");
        //读取从服务端传回来的数据
        DataInputStream dis=new DataInputStream(s.getInputStream());
        System.out.println(dis.readUTF());

    }
}

服务端简单实例代码

/**
 * Socket服务端Demo
 */
public class TCPServerDemo {
    public static void main(String[] args) throws IOException {
        //创建一个ServverSocket监听6666端口
        ServerSocket ss=new ServerSocket(6666);
        while (true){
            Socket s=ss.accept();
            System.out.println("A client connected!");
            //从socket中获取数据流
            DataInputStream dis=new DataInputStream(s.getInputStream());
            //将数据流中的数据写入到socket
            DataOutputStream dos=new DataOutputStream(s.getOutputStream());
            String str=null;
            if ((str=dis.readUTF())!=null){
                //读取从客户端传来的数据
                System.out.println(str);
                System.out.println("from"+s.getInetAddress()+",port #"+s.getPort());
            }
            //写入数据
            dos.writeUTF("Hello,"+s.getInetAddress()+",port#"+s.getPort());
            dis.close();
            dos.close();
            s.close();
        }
    } 

代码实例很简单,这里就不再详细说明,我们就看下运行结果:
服务端运行结果
在这里插入图片描述
客户端运行结果
在这里插入图片描述

二、I/O多路复用

什么是I/O多路复用

I/O多路复用,I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。连起来理解就是很多个网络I/O复用一个或少量的线程来处理这些连接。

为了大家更好的理解,我就用一个生动形象的例子来说明:

第一年情人节

今天是情人节,蛋蛋带着月月去了她最喜欢的烤肉店吃饭,一进去,发现有很多服务员,这时一个服务员走过来,说只为我们一桌服务,我说这感情好啊,vip的待遇呀,但发现,每桌都会有一个服务员,我去,这餐厅这么土豪吗,我有点担心它这样能撑多久。

第二年情人节

又过了一年,又到了情人节,蛋蛋发现去年去的那家烤肉店还在开着,于是又带着月月去了这家店,一进去就发现,没那么多服务员了,一个服务员会服务很多桌,我心想这老饭终于知道节省成本,去除多余的配置了,但是有了新的问题,就是服务员服务的桌数多了之后,等上餐的时候,不知道这个餐对应哪桌了,就会一桌一桌的问,这就有点尴尬了。

第三年情人节

第三年情人节,蛋蛋和月月还是去了之前几年去的那家店,这次来又有了新发现,发现餐厅终于把去年出现的问题给解决了,服务员会对每桌客人记录客人坐的桌号,这样上餐的时候根据桌号她就一下就能找到这个餐要往哪桌上,非常聪明,哈哈哈。

对应到编程界,在最开始的时候,为了实现一个服务器可以支持多个客户端连接,人们就想出了fork/thread等方法,当一个连接到来时,就fork/thread一个进程/线程去接收并处理请求,可能是那个年代用电脑的人都很少,所以一直都没有什么大问题。

到了1980年代,发明了一种叫做IO多路复用的模型select,poll),这个模型的好处就是没必要开那么多线程和进程了,少量的线程和进程就能搞定。但是随着计算机的发展,这种IO多路复用模型有点僵化,回想下蛋蛋和月月第二年去吃饭的场景:
一个餐厅只有少量服务员服务很多顾客。
一个服务员上餐的时候需要一个一个问这个餐是谁的。

对应的编程模型就是:一个连接来了,就必须遍历所有已注册的文件描述符,来找到那个需要处理信息的文件描述符,如果已经注册了几万个文件描述符,遍历完了估计cpu也要歇菜了。

直到2002年,,互联网时代大爆炸,请求量呈指数级增长,人们通过改进IO多路复用模型,进一步优化,发明了叫做epoll的方法。这个方法就相当于蛋蛋和月月第三年去吃饭餐厅做的优化。
在这里插入图片描述
这是当年的并发图。我们可以看到蓝色的线是epoll,随着连接数的增加性能几乎不受影响。

文件描述符fd

inux的内核将所有外部设备都可以看做一个文件来操作。那么我们对与外部设备的操作都可以看做对文件进行操作。
我们对一个文件的读写,都是通过调用内核提供的系统调用,内核给我们返回一个文件描述符。而对一个socket的读写
也会有相应的描述符,称为socketfd(socket描述符)。描述符只是一个数字,指向内核中一个结构体(文件路径,数据区等一些属性)。我们的应用程序对文件的读写就通过对描述符的读写完成。

select函数

select函数监视的描述符分3类,分别是writefds、readfds、和exceptfds
在linux中,我们可以使用select函数实现I/O端口的复用,传递给select函数的参数会告诉内核:

  • 我们所关心的文件描述符
  • 对每个描述符,我们所关心的状态
  • 我们要等待多长时间

从select函数返回后,内核告诉我们以下信息:

  • 对我们的要求已经做好准备的描述符的个数
  • 对于三种条件哪些描述符已经做好准备(读,写,异常)
    有了这些返回信息,我们就可以调用合适的I/O函数(通常是read或write),并且这些函数不会再阻塞。

select函数接口

#include <sys/select.h>
 int select(int maxfdp1,fd_set*readset,fd_set*writeset,fd_set*exceptset,struct timeval*timeout);

返回值:做好准备的文件描述符的个数,超时为0,错误为-1.
方法中传参详解:

  1. maxfdp1:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1。
  2. 中间的三个参数readset,writeset,exceptset指向描述符集。这些参数指明了我们关心哪些描述符,和需要满足什么条件(可读,可写,异常)。一个文件描述符集保存在fd_set类型中,fd_set其实就是位图!
  3. timeval*timeout:它指明我们要等待的时间。
struct timeval(
long tv_sec;/*秒*/
long tv_usec;/*微秒*/
)

有三种情况:

  • timeout==NULL 等待无限长时间。
  • timeout->tv_sec == 0 && timeout -> tv_usec == 0 不等待,直接返回。(非阻塞)
  • timeout->tv_sec != 0 || timeout -> tv_usec!=0 等待指定的时间。

select具体工作流程

假如说,服务器端收到3个客户端的accept连接事件,构建出3个与客户端对接的socket,那么服务器程序接下来需要监听这3个客户端的读事件了。
在这里插入图片描述
服务器端会创建一个ServerSocket,队列中就是3个代处理的客户端连接,每个客户端都会对应一个socket。

在这里插入图片描述
当服务器socket处理完这三个acceot事件,在进程的用户态堆栈的fds就是对应的三个客户端socket的文件描述符,rset就是文件描述符集。
在这里插入图片描述
这时发起select函数调用,将用户态堆栈中的rset信息拷贝到内核态堆栈中,因为进程A对应的三个socket都没有数据,所以进程A要从运行队列中出来,进程等待队列中。
在这里插入图片描述
现在客户端1和客户端2向服务器发送数据报文,服务器网卡接收到报文后,通过DMA设备会将数据存入服务器内存当中。
在这里插入图片描述
服务器网卡转发报文后,会向CPU发起硬件中断IR,CPU会立即响应这个中断请求,假设CPU此时正在运行B进程,那么接下来会做什么事呢,接着往下看
在这里插入图片描述
CPU会将进程B当前正在运行的瞬时数据节点(执行的行数,数据等)信息保存到进程描述符。
然后会修改CPU寄存器,完成由用户态切换到内核态。接着根据IRQ向量在向量表中查找合适的中断处理程序,接着开始执行网卡的中断处理程序。
在这里插入图片描述
网卡中断处理程序由一个网卡缓冲区,里面记录了IP信息,端口号,所以可以找到对应的socket,将报文从网卡缓冲区转移到socket缓冲区,接着检查这些socket有没有对应的进程,有的话就会让进程从等待队列中移出。
在这里插入图片描述
进程A就会进入运行队列当中,client1和client3中有数据,所以进程A内核态堆栈中的rset会将这两者的信息拷贝到用户态当中,接着就可以读数据给客户端了。

epoll讲解

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次

基本原理

epoll对文件描述符的操作有两种模式:LT(水平触发)和ET(边缘触发),LT模式时默认模式。

LT(水平触发):事件就绪后,用户可以选择处理或者不处理,如果用户本次未处理,那么下次调用epoll_wait时仍然会将未处理的事件打包给你。
ET(边缘触发): 它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。epoll使用”事件"的就绪通知方式,通过epollctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epollwait便可以收到通知,事件就绪后,用户必须处理,因为内核不给你兜底了,内核把就绪的事件打包给你后,就把对应的就绪事件清理掉了。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。

epoll优点

  1. 没有最大并发连接限制。能打开的fd的上限远大于1024(1G的内存上能监听约10万个端口)。
  2. 效率提升,不是轮询的方式,不会随着fd数目的增加效率下降。
  3. 内存拷贝,利用mmap文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

JDK1.5_update10版本使用epoll替代了传统的select/poll,极大提升了NIO通信的性能。

epoll接口

epoll操作过程需要三个接口,分别如下:

#include <sys/epoll.h>
  int epoll_create(int size);
int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event);
int epoll_wait(int epfd,struct epoll_event*events,int maxevents,int timeout);
epoll_create函数

epoll_create函数是一个系统函数,函数将在内核空间内开辟一块新的空间,可以理解为epoll结构空间,返回值为epoll文件描述符编号,方便后续操作使用。

epoll_ctl 函数

epoll_ctl 是epoll的事件注册函数,epoll与select不同,select函数是调用时指定需要监听的描述符和事件,epoll先将用户感兴趣的描述符事件注册到epoll空间内,此函数是非阻塞函数,作用仅仅是增删改epoll空间内的描述符信息。

  • 参数一:epfd,epoll结构的进程fd编号,函数将依靠该编号找到对应的epoll结构。
  • 参数二: op,表示当前请求类型,由三个宏定义
    (EPOLL_CTL_ADD:注册新的fd到epfd中)、(EPOLL_CTL_MOD:修改已经注册的fd的监听事件)、(EPOLL_CTL_DEL:从epfd中删除一个fd)
  • 参数三:fd,需要监听的文件描述符
  • 参数四:event,告诉内核对该fd资源感兴趣的事件。
    struct epoll_event结构如下:
struct epoll_event{
_uint32_t_events;
  epoll_data_t_data;
}

events可以是以下几个宏的集合:
EPOLLIN、EPOLLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP(挂断)、EPOLLET(边缘触发)、EPOLLONESHOT(只监听一次,事件触发后自动清除该fd,从epoll列表)

epoll_wait函数

epoll_wait等待事件的产生,类似于select()调用。根据参数timeout,来决定是否阻塞。

  • 参数一:epfd,指定感兴趣的epoll事件列表。
  • 参数二:*events,是一个指针,必须指向一个epoll_event结构数组,当函数返回时,内核会把就绪状态的数据拷贝到该数组中!
  • 参数三:maxevents,标明参数二epoll_event数组最多能接收的数据量,即本次操作最多能获取多少就绪数据。
  • 参数四: timeout,单位毫秒。

0: 表示立即返回,非阻塞调用。
-1: 阻塞调用,直到有用户感兴趣的事件就绪为止。
大于0:阻塞调用,阻塞指定时间内如果有事件就绪则提前返回,否则等待指定时间返回。
返回值: 本次就绪的fd个数。

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 《程序员必知的硬核知识大全》是一本面向程序员的综合性知识手册,涵盖了各个领域的关键知识点,旨在帮助程序员提升技术水平和解决实际问题。该书以PDF格式出版,便于读者在电脑、手机等设备上随时查阅。 该书内容包括以下几个方面的硬核知识: 1. 编程语言知识:介绍了主流的编程语言,如Java、C++、Python等,包括语法、数据结构、算法等方面的内容。 2. 操作系统和计算机原理:详细介绍了操作系统的基本原理和常见问题解决方法,以及计算机组成原理和计算机网络等相关知识。 3. 数据库和存储知识:讲解了数据库设计和管理的基本原理,介绍了关系型数据库如MySQL和非关系型数据库如MongoDB等的使用方法和优化技巧。 4. 网站和网络开发知识:包括Web开发的基本原理、前后端开发技术、网络安全和性能优化等方面的内容。 5. 软件工程和开发方法论:介绍了软件工程的基本概念和常用开发方法,包括敏捷开发、测试驱动开发和持续集成等。 6. 设计模式和架构知识:详细介绍了常用的设计模式和软件架构,帮助程序员设计可维护、可扩展和高效的软件系统。 除了以上几个方面的内容,该书还涵盖了其他与程序员工作密切相关的技术和知识,如版本控制、软件部署、性能调优等。《程序员必知的硬核知识大全》适合本科或者有一定编程经验的程序员阅读,对于提高技术实力和职业发展都有很大帮助。 ### 回答2: "程序员必知的硬核知识大全 pdf"是一份提供程序员必备知识的电子书,PDF格式可以方便地在各种设备上阅读。这本书包含了各个方面的硬核知识,帮助程序员提高技术能力和解决问题的能力。 这本电子书的内容包括了数据结构和算法,编程语言,操作系统,网络通信,数据库管理等各方面的知识。对于程序员而言,这些都是非常重要的基础知识,能够帮助他们理解和设计高效的程序。 在数据结构和算法部分,程序员将学习到各种基础的数据结构,如数组、链表、栈和队列,以及常见的算法,如排序和搜索算法。这些知识对于程序的效率和性能优化至关重要。 编程语言部分将介绍多种编程语言,如C、C++、Java和Python等。这些语言在不同的领域有各自的优点和适用范围,程序员需要了解它们的特点和使用方法,以便在开发项目时选择合适的语言。 操作系统部分将深入讲解操作系统的原理和设计。程序员将了解到进程管理、内存管理、文件系统等重要概念,这些对于编写具有高可靠性和高性能的程序至关重要。 网络通信部分将介绍计算机网络的基本原理和常见的协议,如TCP/IP和HTTP等。程序员需要理解网络通信的基础知识,以便与其他系统进行数据交换和通信。 数据库管理部分将详细介绍关系型数据库和非关系型数据库的原理和使用方法。程序员需要了解数据库的设计和优化,以提高数据的存储和检索效率。 总之,这本电子书涵盖了程序员必备的硬核知识,对于提高他们的技术能力和解决问题的能力非常有帮助。 ### 回答3: 《程序员必知的硬核知识大全》是一本汇集了程序员必备的核心知识的书籍,可以帮助程序员提升自己的技术水平。这本书涵盖了计算机科学的各个领域和重要概念,包括数据结构与算法、操作系统、编程语言、网络通信、数据库、Web开发、软件工程等。 在数据结构与算法部分,书中介绍了常用的数据结构如链表、栈、队列以及各种排序和搜索算法,帮助程序员理解和应用这些经典的算法。在操作系统方面,书中讲解了进程、线程、内存管理、文件系统等重要概念,帮助程序员深入了解计算机系统的工作原理。 在编程语言方面,书中列举了多种编程语言的特性和应用场景,如C++、Java、Python等,有助于程序员选择适合自己的编程语言并掌握其特性。在网络通信部分,书中介绍了TCP/IP协议、HTTP协议等重要的网络通信协议和技术,帮助程序员理解网络通信的基本原理。 此外,书中还介绍了数据库的相关知识,包括关系数据库、SQL语言、数据备份与恢复等内容,有助于程序员设计和管理数据库。在Web开发方面,书中介绍了前端开发、后端开发、服务器部署等关键技术,帮助程序员构建高效、安全的Web应用程序。 最后,在软件工程方面,书中讲解了软件开发的生命周期、需求分析、设计模式、测试和持续集成等内容,有助于程序员理解和掌握软件开发过程中的重要环节。 总的来说,这本《程序员必知的硬核知识大全》提供了一站式的学习资料,涵盖了程序员必备的核心知识,可以帮助程序员系统地学习和应用这些知识,提升自己的技术能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员蛋蛋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值