「硬核Netty系列」IO多路复用底层原理详解,Java面试大厂必问
liebian365 2024-10-30 04:47 23 浏览 0 评论
文章目录
一、Socket
- Socket读缓冲和写缓冲
- 阻塞和非阻塞
- Socket API简单使用
二、I/O多路复用
什么是I/O多路复用?
文件描述符fd
select函数
select函数接口
select具体工作流程
epoll讲解
- 基本原理
- epoll优点
- epoll接口
- **epoll_create函数**
- epoll_ctl 函数
- epoll_wait函数
一、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.
方法中传参详解:
- maxfdp1:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1。
- 中间的三个参数readset,writeset,exceptset指向描述符集。这些参数指明了我们关心哪些描述符,和需要满足什么条件(可读,可写,异常)。一个文件描述符集保存在fd_set类型中,fd_set其实就是位图!
- 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优点
- 没有最大并发连接限制。能打开的fd的上限远大于1024(1G的内存上能监听约10万个端口)。
- 效率提升,不是轮询的方式,不会随着fd数目的增加效率下降。
- 内存拷贝,利用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个数。
相关推荐
- 精品博文嵌入式6410中蓝牙的使用
-
BluetoothUSB适配器拥有一个BluetoothCSR芯片组,并使用USB传输器来传输HCI数据分组。因此,LinuxUSB层、BlueZUSB传输器驱动程序以及B...
- win10跟这台计算机连接的前一个usb设备工作不正常怎么办?
-
前几天小编闲来无事就跑到网站底下查看粉丝朋友给小编我留言询问的问题,还真的就给小编看到一个问题,那就是win10跟这台计算机连接的一个usb设备运行不正常怎么办,其实这个问题的解决方法时十分简单的,接...
- 制作成本上千元的键盘,厉害在哪?
-
这是稚晖君亲自写的开源资料!下方超长超详细教程预警!!全文导航:项目简介、项目原理说明、硬件说明、软件说明项目简介瀚文智能键盘是一把我为自己设计的——多功能、模块化机械键盘。键盘使用模块化设计。左侧的...
- E-Marker芯片,USB数据线的“性能中枢”?
-
根据线缆行业的研究数据,在2019年搭载Type-C接口的设备出货量已达到20亿台,其中80%的笔记本电脑和台式电脑采用Type-C接口,50%的智能手机和平板电脑也使用Type-C接口。我们都知道,...
- ZQWL-USBCANFD二次开发通讯协议V1.04
-
修订历史:1.功能介绍1.1型号说明本文档适用以下型号: ZQWL-CAN(FD)系列产品,USB通讯采用CDC类实现,可以在PC机上虚拟出一个串口,串口参数N,8,1格式,波特率可以根据需要设置(...
- win10系统无法识别usb设备怎么办(win10不能识别usb)
-
从驱动入手,那么win10系统无法识别usb设备怎么办呢?今天就为大家分享win10系统无法识别usb设备的解决方法。1、右键选择设备管理器,如图: 2、点击更新驱动程序,如图: 3、选择浏览...
- 微软七月Win8.1可选补丁有内涵,含大量修复
-
IT之家(www.ithome.com):微软七月Win8.1可选补丁有内涵,含大量修复昨日,微软如期为Win7、Win8.1发布7月份安全更新,累计为6枚安全补丁,分别修复总计29枚安全漏洞,其中2...
- 如何从零开始做一个 USB 键盘?(怎么制作usb)
-
分两种情况:1、做一个真正的USB键盘,这种设计基本上不涉及大量的软件编码。2、做一个模拟的USB键盘,实际上可以没有按键功能,这种的需要考虑大量的软件编码,实际上是一个单片机。第一种设计:买现成的U...
- 电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题
-
电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题注意:有些方法会清除USB设备里的数据,请谨慎操作,如果不想丢失数据,可以先连接到其他电脑,看能否将数据复制出来,或者用一些数据恢复软件去扫...
- 未知usb设备设备描述符请求失败怎么解决
-
出现未知daousb设备设备描述符请求失du败解决办zhi法如下:1、按下Windows+R打开【运行】;2、在版本运行的权限输入框中输入:services.msc按下回车键打开【服务】;2、在服务...
- 读《飘》47章20(飘每章概括)
-
AndAhwouldn'tleaveMissEllen'sgrandchildrenfornotrashystep-patobringup,never.Here,Ah...
- 英翻中 消失的过去 37(消失的英文怎么说?)
-
翻译(三十七):消失的过去/茱迪o皮考特VanishingActs/JodiPicoult”我能做什么?“直到听到了狄利亚轻柔的声音,我才意识到她已经在厨房里站了好一会儿了。当她说话的时候,...
- RabbitMQ 延迟消息实战(rabbitmq如何保证消息不被重复消费)
-
现实生活中有一些场景需要延迟或在特定时间发送消息,例如智能热水器需要30分钟后打开,未支付的订单或发送短信、电子邮件和推送通知下午2:00开始的促销活动。RabbitMQ本身没有直接支持延迟...
- Java对象拷贝原理剖析及最佳实践(java对象拷贝方法)
-
作者:宁海翔1前言对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。Java对象拷贝分为深拷贝和浅拷贝,目前常用的...
- 如何将 Qt 3D 渲染与 Qt Quick 2D 元素结合创建太阳系行星元素?
-
Qt组件推荐:QtitanRibbon:遵循MicrosoftRibbonUIParadigmforQt技术的RibbonUI组件,致力于为Windows、Linux和MacOSX提...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)