网络IO
2024-08-01 18:36:32 9 举报
AI智能生成
网络IO(Network Input/Output)是指在计算机网络中,数据传输的过程。这个过程涉及到计算机系统与网络设备之间的数据交换。网络IO可以分为两种类型:同步IO和异步IO。在同步IO中,数据处理和传输是在一个线程中完成的,这可能会导致线程阻塞,影响程序的性能。在异步IO中,数据处理和传输在不同的线程中完成,不会导致线程阻塞,可以提高程序的性能。网络IO的实现方式有多种,包括TCP/IP、UDP、HTTP、FTP等。其中,TCP/IP是一种常见的网络协议,提供了可靠的数据传输,但是传输速度较慢。UDP是一种快速的网络协议,但是其传输是不可靠的。
作者其他创作
大纲/内容
Linux下五种IO模型
1.阻塞IO模型
这是一种最传统的一种IO模型,即在读写数据过程中会发生阻塞现象
也是最简单的IO模型,一般表现为进程或县城等待某个条件,如果条件不满足,
则一直等待下去,如果条件满足,则进行下一步操作
应用进程通过系统调用recvfrom接收数据,但由于内核还没准备好数据,应用进程
就会阻塞,知道内核准备好数据,recvfrom完成数据报的复制工作,应用进程才能结束阻塞状态
也是最简单的IO模型,一般表现为进程或县城等待某个条件,如果条件不满足,
则一直等待下去,如果条件满足,则进行下一步操作
应用进程通过系统调用recvfrom接收数据,但由于内核还没准备好数据,应用进程
就会阻塞,知道内核准备好数据,recvfrom完成数据报的复制工作,应用进程才能结束阻塞状态
模型图
2.非阻塞IO模型
应用进程与内核进行交互,目的未达到之前,不再一味地等待,而是直接返回
然后通过轮询的方式,不停地询问内核数据有没有准备好,如果某一次轮询时发现数据
已经准备好了,那么就把数据复制到用户空间
应用进程通过recvfrom不停地与内核交互,直到内核准备好数据。如果没有准备好
内核会返回error,应用进程在得到error后,过一段时间再发送recvfrom请求,
在两次请求的时间间隔,进程可以做别的事情
然后通过轮询的方式,不停地询问内核数据有没有准备好,如果某一次轮询时发现数据
已经准备好了,那么就把数据复制到用户空间
应用进程通过recvfrom不停地与内核交互,直到内核准备好数据。如果没有准备好
内核会返回error,应用进程在得到error后,过一段时间再发送recvfrom请求,
在两次请求的时间间隔,进程可以做别的事情
模型图
3.信号驱动IO模型
应用进程预先向内核注册一个信号处理函数
然后用户进程不阻塞直接返回,当内核数据准备就绪时
会发送一个信号给进程,用户进程在信号处理函数中把数据
复制到用户空间,实现比较复杂
然后用户进程不阻塞直接返回,当内核数据准备就绪时
会发送一个信号给进程,用户进程在信号处理函数中把数据
复制到用户空间,实现比较复杂
模型图
4.IO多路复用模型
多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互
当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据复制到用户空间
IO多路转接是多了一个select函数,多个进程的IO可以注册到同一个select上,当用户进程调用该select时
select会监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好,那么select调用进程会阻塞
当任意一个IO所需要的数据准备好之后,select调用就会返回,然后进程通过recvfrom实现数据复制
这里并没有向内核注册信号处理函数,所以IO复用模型并不是非阻塞的,进程在发出select后
要等select监听的所有IO操作中至少一个需要的数据准备好,才会由返回值,并且需要再次发送请求去执行文件的复制
阻塞IO模型,非阻塞IO模型,IO复用模型和信号驱动IO模型都是同步的IO模型,因为无论哪种模型
真正的数据复制过程都是同步进行的
当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据复制到用户空间
IO多路转接是多了一个select函数,多个进程的IO可以注册到同一个select上,当用户进程调用该select时
select会监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好,那么select调用进程会阻塞
当任意一个IO所需要的数据准备好之后,select调用就会返回,然后进程通过recvfrom实现数据复制
这里并没有向内核注册信号处理函数,所以IO复用模型并不是非阻塞的,进程在发出select后
要等select监听的所有IO操作中至少一个需要的数据准备好,才会由返回值,并且需要再次发送请求去执行文件的复制
阻塞IO模型,非阻塞IO模型,IO复用模型和信号驱动IO模型都是同步的IO模型,因为无论哪种模型
真正的数据复制过程都是同步进行的
select
模型图
核心源码
int fds[] = 存放需要监听的socket
while(1) {
int n = select( fds);
for (int i = 0; i < fds.count; i++) {
if (FD_ISSET(fds[i],....) {
// fds[i]d的数据处理
}
}
}
while(1) {
int n = select( fds);
for (int i = 0; i < fds.count; i++) {
if (FD_ISSET(fds[i],....) {
// fds[i]d的数据处理
}
}
}
优点
1.支持性比较好.几乎所有的操作系统都能支持
2.模型比较简单
缺点
1.性能不好.每当连接中的socket有数据时,需要轮询监听的socket,时间复杂度比较高
2.socket数量限制.受限于宏定义FD_SIZE变量1024的socket,select只能监听1024个socket大小,
因为socket如果过多的话,性能将会下降特别明显,所以做出了数量限制
因为socket如果过多的话,性能将会下降特别明显,所以做出了数量限制
3.如果并发数量比较少,可以使用原生阻塞式的连接方式,相比原生的recvfrom的方式,select多了一次系统调用,多一次用户态和内核态的切换
poll
优点
1.数组结构改为链表形式,突破了select函数中的宏定义的1024数量限制,
缺点
1.和select一样,需要遍历轮询,因为无法准确地知道哪个socket当中有数据
epoll
IO多路复用之epoll模型(https://blog.csdn.net/Cover_sky/article/details/135297671)
水平触发
只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知,epoll默认模式是水平触发
边缘触发
当文件描述符关联的读内核缓冲区非空时,则发出可读信号进行通知,写缓冲区不满时,则发出可写信号进行通知,
边缘触发只会通知一次
边缘触发只会通知一次
两者区别:当有数据可读时,水平触发会一直通知epoll对象告诉我们有数据可以读,触发epoll_wait系统调用,如果我们上一次的socket还没处理完毕,则会造成该系统调用的阻塞,消耗CPU,但是如果我们先让数据留存在缓冲区,等这次epoll_wait处理完毕之后,再去读,此时也有可能不止一个socket中有数据,我们可以一次性读取多个socket,效率比较高
epoll_create
向内核注册一定数量的fd,该数量只是一个建议
epoll_ctl
添加/删除/移除socket,并且告诉内核需要监听什么事件
epoll_wait
返回有数据的socket,只需要针对有数据的socket进行处理即可
eventpoll对象
rdllist就绪列表:维护有数据的socket,底层采用双向链表,它不需要是有序的,只是需要把有数据的socket链接到rdllist中即可
为什么就绪列表是双向链表?而不采用红黑树结构
1.因为它需要快速地将有数据的socket插入进来,
客户端断开连接时也需要将它快速地移除掉,但是并不需要保证有序性,也不需要搜索某个socket,
所以双向链表就可以满足
另外准备就绪的socket数量和需要监听的socket数量相比再同一个时刻相比也会比较少
1.因为它需要快速地将有数据的socket插入进来,
客户端断开连接时也需要将它快速地移除掉,但是并不需要保证有序性,也不需要搜索某个socket,
所以双向链表就可以满足
另外准备就绪的socket数量和需要监听的socket数量相比再同一个时刻相比也会比较少
rbr维护需要监听的socket,保存需要监视的socket,需要快速地添加删除,底层是红黑树
监听所有的socket连接,需要快速插入和删除,还要搜索socket以避免重复添加
再加上数量比较多,所以采用了红黑树结构,搜索和插入删除的事件复杂度都是O(logN)
再加上数量比较多,所以采用了红黑树结构,搜索和插入删除的事件复杂度都是O(logN)
伪代码
int epfd = epoll_create();
// 将所有需要监听的socket添加到epfd中
epoll_ctl(epfd,....)
while(1) {
int n = epoll_wait(...)
for(接收到数据的socket) {
// 处理
}
}
// 将所有需要监听的socket添加到epfd中
epoll_ctl(epfd,....)
while(1) {
int n = epoll_wait(...)
for(接收到数据的socket) {
// 处理
}
}
5.异步IO模型
Linux
应用进程把IO请求传给内核后,完全由内核去完成文件的复制。
内核完成相关操作后,会发送信号告诉应用进程本次IO操作已经完成
用户进程发起aio_read操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等
告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了,
当内核收到aio_read后会立刻返回,然后开始等待数据准备,数据准备好之后,直接把
数据复制到用户空间,然后通知本次IO操作已经完成
内核完成相关操作后,会发送信号告诉应用进程本次IO操作已经完成
用户进程发起aio_read操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等
告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了,
当内核收到aio_read后会立刻返回,然后开始等待数据准备,数据准备好之后,直接把
数据复制到用户空间,然后通知本次IO操作已经完成
模型图
暂时还没有完全支持,还是借用了IO多路复用的方式实现的
Windows
利用IOCP模型去实现
操作系统的IO
CPU是如何知道网卡中有数据需要接收
内核是怎样接收数据的
内核是怎样接收数据的
中断、上半部、下半部
内核和设备驱动是通过中断的方式来处理的。所谓中断,可以理解为,
当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,
以通知CPU来处理数据.计算机程序执行程序时,会有优先级的需求,
比如,当计算机收到断电信号时(电容可以保存少许电量,供CPU运行很短的一小段时间)
它应立即去保存数据,保存数据的程序具有较高的优先级,一般而言,
由硬件产生的信号需要CPU立马做出回应(不然数据可能就丢失),所以它的
优先级很高,CPU理应中断掉正在执行的程序,去做出响应;当CPU完成对硬件
的响应后,再重新执行用户程序。中断的过程如下图,和函数调用差不多,
只不过函数调用是事先定好位置,而中断的位置由"信号"决定
当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,
以通知CPU来处理数据.计算机程序执行程序时,会有优先级的需求,
比如,当计算机收到断电信号时(电容可以保存少许电量,供CPU运行很短的一小段时间)
它应立即去保存数据,保存数据的程序具有较高的优先级,一般而言,
由硬件产生的信号需要CPU立马做出回应(不然数据可能就丢失),所以它的
优先级很高,CPU理应中断掉正在执行的程序,去做出响应;当CPU完成对硬件
的响应后,再重新执行用户程序。中断的过程如下图,和函数调用差不多,
只不过函数调用是事先定好位置,而中断的位置由"信号"决定
以键盘为例,当用户按下键盘某个按键时,键盘会给CPU的中断引脚发出
一个高电平。CPU能够捕获这个信号,然后执行键盘中断程序。同样,
当网卡把数据写入到内存后,网卡向CPU发出一个中断信号,操作系统便能得知
有新数据到来,再通知网卡中断程序去处理数据,对于网络模块来说,
由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,
将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其他设备
例如鼠标和键盘的消息。因此Linux中断处理函数是分为上半部和下半部的
上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许
其他中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理
2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd内核线程全权处理。
和硬中断不同的是,硬中断是通过给CPU物理引脚施加电压变化,而软中断是
通过给内存中的一个变量的二进制值以通知软中断处理程序
一个高电平。CPU能够捕获这个信号,然后执行键盘中断程序。同样,
当网卡把数据写入到内存后,网卡向CPU发出一个中断信号,操作系统便能得知
有新数据到来,再通知网卡中断程序去处理数据,对于网络模块来说,
由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,
将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其他设备
例如鼠标和键盘的消息。因此Linux中断处理函数是分为上半部和下半部的
上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许
其他中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理
2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd内核线程全权处理。
和硬中断不同的是,硬中断是通过给CPU物理引脚施加电压变化,而软中断是
通过给内存中的一个变量的二进制值以通知软中断处理程序
当网卡上收到数据以后,Linux中第一个工作的模块是网络驱动,网络驱动会以DMA
的方式把网卡上收到的帧写到内存里。再向CPU发起一个中断,以通知CPU有数据到达
第二,当CPU收到中断请求后,回去调用网络驱动注册的中断处理函数。
网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU.
ksoftirqd检测到有软中断请求到达,调用poll开始轮询收包,收到后
交由各级协议栈处理,最后会被放到用户socket的接收队列中
的方式把网卡上收到的帧写到内存里。再向CPU发起一个中断,以通知CPU有数据到达
第二,当CPU收到中断请求后,回去调用网络驱动注册的中断处理函数。
网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU.
ksoftirqd检测到有软中断请求到达,调用poll开始轮询收包,收到后
交由各级协议栈处理,最后会被放到用户socket的接收队列中
进程阻塞
了解epoll本质,要从操作系统进程调度的角度来看数据接收
阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)
发生之前的等待状态,recv、select和epoll都是阻塞方法。
了解"进程阻塞为什么不占用CPU资源"?也就能了解这一步。
为简单起见,我们从普通的recv接收开始分析
// 创建socket
int s = socket(....);
// 绑定
bind(s,...);
// 监听
listen(s,.....);
// 接受客户端连接
int c = accept(s,...);
// 接收客户端数据
recv(c, ...);
// 将数据打印出来
printf(.....)
创建socket对象 -> 调用bind -> 调用listen -> 调用accept -> recv
recv是一个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行
那么阻塞的原理是什么?
操作系统为了支持多人物,实现了进程调度的功能,
会把进程分为"运行"和"等待"等几种状态。运行状态是进程获得CPU使用权,
正在执行代码的状态,等待状态是阻塞状态,比如上面代码运行到recv时,
程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统
会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务
因为每个进程获得的CPU时钟周期是不同的
阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)
发生之前的等待状态,recv、select和epoll都是阻塞方法。
了解"进程阻塞为什么不占用CPU资源"?也就能了解这一步。
为简单起见,我们从普通的recv接收开始分析
// 创建socket
int s = socket(....);
// 绑定
bind(s,...);
// 监听
listen(s,.....);
// 接受客户端连接
int c = accept(s,...);
// 接收客户端数据
recv(c, ...);
// 将数据打印出来
printf(.....)
创建socket对象 -> 调用bind -> 调用listen -> 调用accept -> recv
recv是一个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行
那么阻塞的原理是什么?
操作系统为了支持多人物,实现了进程调度的功能,
会把进程分为"运行"和"等待"等几种状态。运行状态是进程获得CPU使用权,
正在执行代码的状态,等待状态是阻塞状态,比如上面代码运行到recv时,
程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统
会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务
因为每个进程获得的CPU时钟周期是不同的
当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象,
这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员.
等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程
当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中
由于工作队列只剩下了进程B和C,依据进程调度,CPU会轮流执行这两个进程的程序,
不会执行进程A的程序。所以进程A被阻塞,不会执行代码,也不会占用CPU资源
这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员.
等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程
当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中
由于工作队列只剩下了进程B和C,依据进程调度,CPU会轮流执行这两个进程的程序,
不会执行进程A的程序。所以进程A被阻塞,不会执行代码,也不会占用CPU资源
操作系统添加等待队列只是添加了对这个"等待中"进程的引用,
以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下
,直接将进程挂到等待队列之下
当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列
该进程变成运行状态,继续执行代码,也由于socket的接收缓冲区已经有了数据,
recv可以返回接收到的数据
以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下
,直接将进程挂到等待队列之下
当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列
该进程变成运行状态,继续执行代码,也由于socket的接收缓冲区已经有了数据,
recv可以返回接收到的数据
零拷贝
什么是零拷贝Zero-copy
该技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域
这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽
1.零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之前
不必要的中间拷贝次数,从而有效地提高数据传输效率
2.零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销
可以看出没有说不需要拷贝,指示说减少冗余[不必要]的拷贝
Kafka、Netty、RocketMQ、Nginx均使用了零拷贝技术
目的:减少IO流程中不必要的拷贝,当然零拷贝需要OS支持,也就是需要kernel暴露API
这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽
1.零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之前
不必要的中间拷贝次数,从而有效地提高数据传输效率
2.零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销
可以看出没有说不需要拷贝,指示说减少冗余[不必要]的拷贝
Kafka、Netty、RocketMQ、Nginx均使用了零拷贝技术
目的:减少IO流程中不必要的拷贝,当然零拷贝需要OS支持,也就是需要kernel暴露API
DMA(Direct Memory Access)
在早期计算机中,用户进程需要读取磁盘数据,需要CPU终端和CPU参与,因此效率比较低
发起IO请求,每次的IO中断,都带来CPU的上下文切换。因此出现了DMA
DMA(Direct Memory Access,直接内存读取)是所有现代电脑的重要特色,它允许不同速度的硬件
装置来沟通,而不需要依赖CPU的大量中断负载.
DMA控制器,接管了数据读写请求,减少CPU的负担,这样一来,CPU能高效工作了,现在硬盘都支持DMA
实际IO读取,涉及两个过程:
1.DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区
2.用户进程,将内核缓冲区的数据COPY到用户空间
发起IO请求,每次的IO中断,都带来CPU的上下文切换。因此出现了DMA
DMA(Direct Memory Access,直接内存读取)是所有现代电脑的重要特色,它允许不同速度的硬件
装置来沟通,而不需要依赖CPU的大量中断负载.
DMA控制器,接管了数据读写请求,减少CPU的负担,这样一来,CPU能高效工作了,现在硬盘都支持DMA
实际IO读取,涉及两个过程:
1.DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区
2.用户进程,将内核缓冲区的数据COPY到用户空间
传统数据传送机制
比如:读取文件,再用socket发送出去,实际经过四次copy,伪代码如下:
buffer = File.read()
Socket.send(buffer)
第一次:将磁盘文件,读取到操作系统内核缓冲区
第二次:将内核缓冲区的数据,copy到应用程序的buffer
第三次:将应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区)
第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输
buffer = File.read()
Socket.send(buffer)
第一次:将磁盘文件,读取到操作系统内核缓冲区
第二次:将内核缓冲区的数据,copy到应用程序的buffer
第三次:将应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区)
第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输
在上述过程中,虽然引入了DMA来接管CPU的中断请求,但四次的copy是存在不必要的拷贝的
实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传回套接字缓冲区之外
什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区,显然,第二次和第三次的数据
copy其实在这种场景下没有什么帮助,反而带来开销,这也正是零拷贝出现的背景和意义
同时,read,send都属于系统调用,每次调用都牵涉到两次上下文切换
总结下,传统的数据传送所消耗的成本:4次拷贝,4次上下文切换
4次拷贝,其中两次是DMA拷贝,两次是CPU拷贝
实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传回套接字缓冲区之外
什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区,显然,第二次和第三次的数据
copy其实在这种场景下没有什么帮助,反而带来开销,这也正是零拷贝出现的背景和意义
同时,read,send都属于系统调用,每次调用都牵涉到两次上下文切换
总结下,传统的数据传送所消耗的成本:4次拷贝,4次上下文切换
4次拷贝,其中两次是DMA拷贝,两次是CPU拷贝
子主题
MMAP内存映射
硬盘上文件的位置和应用缓冲区(application buffer)进行映射(建立一种一一对应关系)
由于mmap()将文件直接映射到用户空间,所以实际文件读取时根据这个映射关系,直接将文件
从硬盘拷贝到用户空间,只进行了一次数据拷贝,不再有文件内容从硬盘拷贝到内核空间的一个
缓冲区
mmap内存映射将会经历:3次拷贝:1次CPU copy 两次 DMA copy
以及4次上下文切换,调用mmap函数两次,write函数两次
由于mmap()将文件直接映射到用户空间,所以实际文件读取时根据这个映射关系,直接将文件
从硬盘拷贝到用户空间,只进行了一次数据拷贝,不再有文件内容从硬盘拷贝到内核空间的一个
缓冲区
mmap内存映射将会经历:3次拷贝:1次CPU copy 两次 DMA copy
以及4次上下文切换,调用mmap函数两次,write函数两次
子主题
sendfile
Linux 2.1支持的sendfile
当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer
直接拷贝到socket buffer;但是数据并未被真正复制到socket关联的缓冲区。取而代之的是,
只有记录数据位置和长度的描述符被加入到socket缓冲区中,DMA模块将数据直接从内核缓冲区
传递给协议引擎,从而消除了遗留的最后一次复制。但是要注意,这个需要DMA硬件设备支持,
如果不支持,CPU就必须介入进行拷贝i
一旦数据全部拷贝到socket buffer,sendfile()系统调用将会return,代表数据转换的完成
socket buffer里的数据就能在网络传输了
sendfile会经历3(2,如果硬件设备支持)次拷贝,1(0,如果硬件设备支持)次CPU copy,2次DMA copy
以及两次上下文切换
当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer
直接拷贝到socket buffer;但是数据并未被真正复制到socket关联的缓冲区。取而代之的是,
只有记录数据位置和长度的描述符被加入到socket缓冲区中,DMA模块将数据直接从内核缓冲区
传递给协议引擎,从而消除了遗留的最后一次复制。但是要注意,这个需要DMA硬件设备支持,
如果不支持,CPU就必须介入进行拷贝i
一旦数据全部拷贝到socket buffer,sendfile()系统调用将会return,代表数据转换的完成
socket buffer里的数据就能在网络传输了
sendfile会经历3(2,如果硬件设备支持)次拷贝,1(0,如果硬件设备支持)次CPU copy,2次DMA copy
以及两次上下文切换
splice
Linux从2.6.17支持splice
数据从磁盘读取到OS内核缓冲区后,在内核缓冲区直接可将其专程内核空间其他数据buffer,
而不用需要拷贝到用户空间
如图所示,从磁盘读取到内核buffer后,在内核空间直接与socket buffer建立pipe管道
和sendfile()不同的是,splice()不需要硬件支持
注意splice()和sendfile不同,sendfile是DMA硬件设备不支持的情况下将磁盘数据加载到
kernel buffer后,需要一次CPU copy,拷贝到socket buffer。而splice()是更进一步,连这个CPU
copy 也不需要了,直接将两个内核空间的buffer进行pipe
splice会经历两次拷贝,0次CPU拷贝,2次DMA copy
以及两次上下文切换
数据从磁盘读取到OS内核缓冲区后,在内核缓冲区直接可将其专程内核空间其他数据buffer,
而不用需要拷贝到用户空间
如图所示,从磁盘读取到内核buffer后,在内核空间直接与socket buffer建立pipe管道
和sendfile()不同的是,splice()不需要硬件支持
注意splice()和sendfile不同,sendfile是DMA硬件设备不支持的情况下将磁盘数据加载到
kernel buffer后,需要一次CPU copy,拷贝到socket buffer。而splice()是更进一步,连这个CPU
copy 也不需要了,直接将两个内核空间的buffer进行pipe
splice会经历两次拷贝,0次CPU拷贝,2次DMA copy
以及两次上下文切换
子主题
DPDK
**数据包直通(DPDK,Data Plane Development Kit)**技术。DPDK 是一个开源项目,它提供了一系列库和驱动程序,用于快速处理数据包。DPDK 旨在提高数据包处理速度,减少用户空间和内核空间之间的交互,从而提高网络应用程序的性能。
DPDK 并不是一个网卡的硬件技术,而是一个软件层面的解决方案,它可以在不修改网络协议栈的情况下,直接在用户空间处理数据包。DPDK 使用了如下的一些技术:
用户空间驱动:DPDK 提供了用户空间网络驱动,绕过了传统的内核网络栈,减少了上下文切换和数据包处理的 overhead。
大页内存:DPDK 使用大页内存来减少页表项的数量,提高内存访问的效率。
无锁数据结构:DPDK 使用无锁队列等数据结构来减少多线程访问时的锁竞争。
批处理:DPDK 支持批处理操作,可以一次性处理多个数据包,减少 CPU 指令周期。
硬件加速:DPDK 可以利用网卡的一些硬件加速功能,如 RSS(Receive Side Scaling)、VMDQ(Virtual Machine Device Queues)等。
DPDK 适用于需要高速数据处理的应用,如网络功能虚拟化(NFV)、负载均衡器、防火墙、入侵检测系统等。通过这些技术,DPDK 能够显著提高网络数据包的处理速度,达到线速处理能力。
DPDK 并不是一个网卡的硬件技术,而是一个软件层面的解决方案,它可以在不修改网络协议栈的情况下,直接在用户空间处理数据包。DPDK 使用了如下的一些技术:
用户空间驱动:DPDK 提供了用户空间网络驱动,绕过了传统的内核网络栈,减少了上下文切换和数据包处理的 overhead。
大页内存:DPDK 使用大页内存来减少页表项的数量,提高内存访问的效率。
无锁数据结构:DPDK 使用无锁队列等数据结构来减少多线程访问时的锁竞争。
批处理:DPDK 支持批处理操作,可以一次性处理多个数据包,减少 CPU 指令周期。
硬件加速:DPDK 可以利用网卡的一些硬件加速功能,如 RSS(Receive Side Scaling)、VMDQ(Virtual Machine Device Queues)等。
DPDK 适用于需要高速数据处理的应用,如网络功能虚拟化(NFV)、负载均衡器、防火墙、入侵检测系统等。通过这些技术,DPDK 能够显著提高网络数据包的处理速度,达到线速处理能力。
Java生态圈中的零拷贝
Linux提工的零拷贝技术,Java并不是全支持,支持两种(内存映射mmap,sendfile)
NIO提工的内存映射MappedByteBuffer
NIO中的FileChannel.map()方法其实就是采用了操作系统中的内存映射方式
底层就是调用Linux mmap()实现
将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射,这种方式适合读取大文件
同时也能对文件内容进行更改,但是如果其后通过SocketChannel发送,还是需要CPU进行
数据的拷贝
底层就是调用Linux mmap()实现
将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射,这种方式适合读取大文件
同时也能对文件内容进行更改,但是如果其后通过SocketChannel发送,还是需要CPU进行
数据的拷贝
Kafka中的零拷贝
Kafka两个重要过程都是用了零拷贝技术,且都是操作系统层面的狭义零拷贝
一是Producer生产的数据存到Broker,二是Consumer从broker读取数据
Producer生产的数据持久化到Broker,broker里采用mmap文件映射,实现
顺序的快速写入
Consumer从broker读取数据,broker里采用sendfile,将磁盘文件读到OS内核
缓冲区后,直接转到socket buffer进行网络发送
一是Producer生产的数据存到Broker,二是Consumer从broker读取数据
Producer生产的数据持久化到Broker,broker里采用mmap文件映射,实现
顺序的快速写入
Consumer从broker读取数据,broker里采用sendfile,将磁盘文件读到OS内核
缓冲区后,直接转到socket buffer进行网络发送
同步与异步、阻塞与非阻塞
同步与异步
区别:在于是否要请求发起方主动获取结果
异步结果通知方式
1.结果占位符Future
2.接口回调Callback
阻塞与非阻塞
区别:在于是否要请求发起方等待
组合
同步阻塞
同步非阻塞
异步阻塞
异步非阻塞
Java中的IO
AIO
NIO
和BIO的区别
面向流与面向缓冲区
Java NIO和BIO之间第一个最大的区别是,BIO是面向流的,NIO是面向缓冲区的。
Java BIO面向流意味着每次从流中读一个或者多个字节,直至读取所有字节,
它们没有被缓存在任何地方。此外,它不能前后移动数据流中的数据。如果需要
前后移动从流中读取数据,需要先将它缓存到一个缓冲区。JavaNIO的缓冲导向方法
略有不同。数据读取到一个它稍后处理的缓冲区。
Java BIO面向流意味着每次从流中读一个或者多个字节,直至读取所有字节,
它们没有被缓存在任何地方。此外,它不能前后移动数据流中的数据。如果需要
前后移动从流中读取数据,需要先将它缓存到一个缓冲区。JavaNIO的缓冲导向方法
略有不同。数据读取到一个它稍后处理的缓冲区。
阻塞与非阻塞IO
Java BIO的各种流是阻塞的,这意味着,当一个线程调用read()或者write()时,
该线程被阻塞,知道有一些数据被读取,或者数据完全写入,该线程在此期间不能再
干任何事情了
JavaNIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到
目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞
所以直至数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此
一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同事可以去做别的
事情,线程通常将非阻塞IO的空闲事件用于在其他通道上执行IO操作,所以一个单独的线程
现在可以管理多个输入和输出通道(Channel)
该线程被阻塞,知道有一些数据被读取,或者数据完全写入,该线程在此期间不能再
干任何事情了
JavaNIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到
目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞
所以直至数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此
一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同事可以去做别的
事情,线程通常将非阻塞IO的空闲事件用于在其他通道上执行IO操作,所以一个单独的线程
现在可以管理多个输入和输出通道(Channel)
核心三大组件
Selector
客户端和服务端均可向Selector注册感兴趣的感兴趣的事件
客户端:感兴趣的事件有OP_READ读事件、OP_CONNECT 连接事件
服务端:感兴趣的事件有OP_READ读事件、OP_ACCEPT 接受请求事件
客户端:感兴趣的事件有OP_READ读事件、OP_CONNECT 连接事件
服务端:感兴趣的事件有OP_READ读事件、OP_ACCEPT 接受请求事件
SelectionKey
OP_READ
当接收缓存区有数据可读时,触发
OP_WRITE
当发送缓冲区有空间可写时,触发
OP_CONNECT
当服务端可以跟客户端建立连接时,触发,只给客户端使用
OP_ACCEPT
当客户端与服务端连接完成之后触发,只给服务端使用
Channel
通道的双方都可以发送数据,既可以读也可以写
Buffer
相比Input/OutputStream,不再需要阻塞整个流,而是阻塞于一个Buffer缓冲区
InputStream是Java socket中提供的默认读写网络流的接口类,其内部由SocketInputStream实现;在调用read方法时如果流还没有准备完成则会阻塞整个调用线程直到流准备完成
NIO架构图
子主题
BIO
阻塞式IO
当客户端向服务端发起连接时,如果客户端没有发送数据,
服务端将会一直阻塞直到客户端发出数据,
其他的客户端必须要等待服务端处理完毕
可以延伸出两种策略:
缺点:受限于服务器的CPU核心数,无法支撑起高并发
当客户端向服务端发起连接时,如果客户端没有发送数据,
服务端将会一直阻塞直到客户端发出数据,
其他的客户端必须要等待服务端处理完毕
可以延伸出两种策略:
缺点:受限于服务器的CPU核心数,无法支撑起高并发
单线程
子主题
一个连接一个线程
子主题
线程池模型处理
子主题
Netty
一个基于事件驱动的主从React模型开发的网络IO框架,对IO模型做了一些封装
核心组件
ChannelPipeline
一系列的ChannelHandler组成的出站/入站事件(ChannelInboundHandler/ChannelOutboundHandler)链表进行处理,也是Netty开发中经常写的业务
ByteBuf
1.一个容器中增加了读指针和写指针,相比于原生的NIO中的ByteBuffer的一个指针方便了很多
2.可以分配堆外内存,使用了零拷贝的技术
3.Netty在一开始就向OS申请了一大块儿内存用来为ByteBuf分配空间
EventLoopGroup
类似于线程池,由EventLoop组成
ChannelOption
可以调整连接中的TCP参数来优化性能,如发送/接收缓冲区大小
粘包/半包
现象描述
TCP
由于TCP协议本身的机制(面向连接的可靠的协议-三次握手机制)客户端与服务端会维持一个连接(channel),
数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,
那么它本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要比UDP高些)
然后再发送(超时或者包大小足够).这样,服务器在接收到消息(数据流)的时候就无法区分哪些数据包时客户端
自己分开发送的,这样就产生了粘包;服务器在接收到数据后,放到缓冲区中,如果消息没有被及时从缓存区取走
下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象
数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,
那么它本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要比UDP高些)
然后再发送(超时或者包大小足够).这样,服务器在接收到消息(数据流)的时候就无法区分哪些数据包时客户端
自己分开发送的,这样就产生了粘包;服务器在接收到数据后,放到缓冲区中,如果消息没有被及时从缓存区取走
下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象
UDP
本身作为无连接的不可靠传输协议(适合频繁发送较小的数据包),它不会对数据包进行合并发送(也就没有Nagle)
它直接是一端发送什么数据,直接就发送出去了,既然它不会对数据进行合并,每一个数据包都是完整的
数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了
它直接是一端发送什么数据,直接就发送出去了,既然它不会对数据进行合并,每一个数据包都是完整的
数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了
具体原因
1.应用程序写入数据的字节大小大于套接字发送缓冲区的大小
2.进行MSS大小的TCP分段。MSS是最大报文段长度的缩写.MSS是TCP报文段中的数据字段的最大长度.
数据字段加上TCP首部才等于整个的TCP报文段.所以MSS并不是TCP报文段的最大长度.
而是:MSS=TCP报文段长度 - TCP首部长度
数据字段加上TCP首部才等于整个的TCP报文段.所以MSS并不是TCP报文段的最大长度.
而是:MSS=TCP报文段长度 - TCP首部长度
MTU(Maximm Transmission Unit),用来通知对方所能接受数据服务单元的最大尺寸,说明发送方能够接受的有效载荷大小
在以太网中是1500个字节
TCP报头占20个字节
IP报头占20个字节
在以太网中是1500个字节
TCP报头占20个字节
IP报头占20个字节
解决方式
1.分隔符。分割每个报文
2.固定长度。截取每个报文
3.消息头+消息体。把每个报文长度写在消息头当中
Netty高并发高性能架构设计精髓
1.主从Reactor线程模型
2.NIO多路复用非阻塞
3.无锁串行化
4.支持高性能序列化协议
5.零拷贝(直接内存的使用)
6.ByteBuf的内存池设计
7.灵活的TCP参数配置能力
8.并发优化
TCP/UDP
OSI七层网络模型、TCP/IP四层模型
TCP
面向连接的、可靠的传输协议
三次握手
为什么是3次而不是两次或者4次
TCP为什么是三次握手?
两次握手:
1.客户端向服务端发起建立连接的请求
2.服务器回应客户端,表示可以接受连接
这时只能说明,此时并不能证明客户端可以响应服务端的请求数据,
而服务端已经在第二次握手中证明了自己可以发送数据和响应数据
所以需要客户端去进行第三次握手来向服务端证明自己可以响应数据
三次握手是从性能和效率的角度触发指定的一套规则。
因为三次握手已经可以达到验证两端数据响应是正常的,
虽然说再多一次握手也没问题,可是却浪费了网络带宽的消耗
两次握手:
1.客户端向服务端发起建立连接的请求
2.服务器回应客户端,表示可以接受连接
这时只能说明,此时并不能证明客户端可以响应服务端的请求数据,
而服务端已经在第二次握手中证明了自己可以发送数据和响应数据
所以需要客户端去进行第三次握手来向服务端证明自己可以响应数据
三次握手是从性能和效率的角度触发指定的一套规则。
因为三次握手已经可以达到验证两端数据响应是正常的,
虽然说再多一次握手也没问题,可是却浪费了网络带宽的消耗
DDos攻击
大量的请求向服务端发起建立连接的请求,这些客户端却无法响应服务端的第二次握手请求,
服务端为了保存客户端第一次握手的状态,当这个数量特别大时,超过了服务端维持TCP第一次握手的队列时,
服务端将无法接收后续客户端请求,从而造成瘫痪
这个参数是cat /proc/sys/net/ipv4/tcp_max_syn_backlog
服务端为了保存客户端第一次握手的状态,当这个数量特别大时,超过了服务端维持TCP第一次握手的队列时,
服务端将无法接收后续客户端请求,从而造成瘫痪
这个参数是cat /proc/sys/net/ipv4/tcp_max_syn_backlog
四次挥手
为什么客户端要等待2*MSL
Windows: 2min
Linux(Ubuntu CentOS): 60s
Unix:30s
主要原因有以下两点:
1.防止TCP断开连接时,服务端发送的断开连接请求报文,客户端可以正常响应.
假设客户端在发送第四次挥手报文的时候,网络发生了波动,导致报文丢失了.
服务端并没有接收到,此时如果客户端不选择等待,客户端会认为服务端侧的连接也断开了,
然而服务端却并没有收到,服务端这个时候会选择重发第三次挥手的报文,在发送之前,
客户端已经等待一段MSL时间了,如果客户端想收到这个重发的挥手报文,则需要再次等待
一段MSL时间,以确保服务端侧的连接可以正常断开
2.防止下一个应用程序启动时重用端口进而接收到上一个应用程序的数据报文
继续上面的例子解释,上一个客户端已经关闭连接了,但是服务端又进行了重发报文,
这个时候又有一个客户端重用了上一个客户端的源端口号,于是两端建立请求之后,又收到了
重传的报文,但是这个报文却不是本次连接中的数据,就给新客户端造成了困扰。
再一个原因,有的客户端是会重用客户端的源端口号的
五元组确定一个通信
源ip 源端口 协议版本号 目标ip 目标端口
客户端在发起连接时,这个源端口号是会随机生成的
Linux(Ubuntu CentOS): 60s
Unix:30s
主要原因有以下两点:
1.防止TCP断开连接时,服务端发送的断开连接请求报文,客户端可以正常响应.
假设客户端在发送第四次挥手报文的时候,网络发生了波动,导致报文丢失了.
服务端并没有接收到,此时如果客户端不选择等待,客户端会认为服务端侧的连接也断开了,
然而服务端却并没有收到,服务端这个时候会选择重发第三次挥手的报文,在发送之前,
客户端已经等待一段MSL时间了,如果客户端想收到这个重发的挥手报文,则需要再次等待
一段MSL时间,以确保服务端侧的连接可以正常断开
2.防止下一个应用程序启动时重用端口进而接收到上一个应用程序的数据报文
继续上面的例子解释,上一个客户端已经关闭连接了,但是服务端又进行了重发报文,
这个时候又有一个客户端重用了上一个客户端的源端口号,于是两端建立请求之后,又收到了
重传的报文,但是这个报文却不是本次连接中的数据,就给新客户端造成了困扰。
再一个原因,有的客户端是会重用客户端的源端口号的
五元组确定一个通信
源ip 源端口 协议版本号 目标ip 目标端口
客户端在发起连接时,这个源端口号是会随机生成的
UDP
面向报文的传输协议
一个Socket组成也指一条连接
源IP,源端口号,协议版本,目标ip,目标端口号,五元组确定一条通信Socket
IP占4个字节
端口号2个字节
HTTP
HTTP1.0
短连接
每次发送请求都要重新建立TCP请求,即三次握手,非常浪费性能
无host头域
HTTP 1.1通过增加更多的请求头和响应头来改进和扩充HTTP 1.0的功能。如,HTTP 1.0不支持Host请求头字段,浏览器无法使用主机头名来明确表示要访问服务器上的哪个WEB站点,这样就无法使用WEB服务器在同一个IP地址和端口号上配置多个虚拟WEB站点。在HTTP 1.1中增加Host请求头字段后,WEB浏览器可以使用主机头名来明确表示要访问服务器上的哪个WEB站点,这才实现了在一台WEB服务器上可以在同一个IP地址和端口号上使用不同的主机名来创建多个虚拟WEB站点
不允许断点续传
不能只传输对象的一部分,要求传输整个对象
HTTP1.1
长连接
HTTP1.1中默认开启Connection: keep-alive,能够更好的利用TCP的慢启动机制
增加缓存处理
HTTP1.0中主要使用 Last-Modified,Expires 来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略:ETag,Cache-Control
增加Host字段
Host 是 HTTP 1.1 协议中新增的一个请求头,主要用来实现虚拟主机技术。
虚拟主机(virtual hosting)即共享主机(shared web hosting),可以利用虚拟技术把一台完整的服务器分成若干个主机,因此可以在单一主机上运行多个网站或服务。
举个栗子,有一台 ip 地址为 61.135.169.125 的服务器,在这台服务器上部署着谷歌、百度、淘宝的网站。为什么我们访问 https://www.google.com 时,看到的是 Google 的首页而不是百度或者淘宝的首页?原因就是 Host 请求头决定着访问哪个虚拟主机
虚拟主机(virtual hosting)即共享主机(shared web hosting),可以利用虚拟技术把一台完整的服务器分成若干个主机,因此可以在单一主机上运行多个网站或服务。
举个栗子,有一台 ip 地址为 61.135.169.125 的服务器,在这台服务器上部署着谷歌、百度、淘宝的网站。为什么我们访问 https://www.google.com 时,看到的是 Google 的首页而不是百度或者淘宝的首页?原因就是 Host 请求头决定着访问哪个虚拟主机
长连接会给服务器造成压力
支持断点续传
HTTP2.0
新的二进制格式
HTTP1.x的解析是基于文本,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合,基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮
header压缩
HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小
服务端推送
例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了
多路复用
HTTP/1.0 每次请求响应,建立一个TCP连接,用完关闭 - HTTP/1.1 「长连接」 若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞; - HTTP/2.0 「多路复用」多个请求可同时在一个连接上并行执行,某个请求任务耗时严重,不会影响到其它连接的正常执行
HTTP3.0
基于google的QUIC协议,而quic协议是使用udp实现的;
减少了tcp三次握手时间,以及tls握手时间;
解决了http 2.0中前一个stream丢包导致后一个stream被阻塞的问题;
优化了重传策略,重传包和原包的编号不同,降低后续重传计算的消耗;
连接迁移,不再用tcp四元组确定一个连接,而是用一个64位随机数来确定这个连接;
更合适的流量控制。
HTTPS
浏览器中输入url发生了什么
1.DNS解析域名获取其IP地址
2.建立TCP连接
3.发送HTTP/HTTPS请求(建立TLS连接)
4.服务器响应请求
5.浏览器解析渲染页面 reflow 回流 确定大小尺寸位置 repain 绘制颜色
6.浏览器断开连接
0 条评论
下一页