IO多路复用
IO多路复用
基本的socket模型
1、服务端首先调用 socket()
,创建传输协议为 TCP 的Socket
2、接着调用 bind()
给这个 Socket 绑定一个IP 地址和端口。绑定端口的目的:当内核收到 TCP 报文,通过 TCP 头里面的端口号,来找到我们的应用程序,然后把数据传递给我们;绑定 IP 地址的目的:一台机器可以有多个网卡,每个网卡都有对应的IP,绑定一个网卡时,内核收到该网卡上的包才会发给我们
3、绑定完 IP 地址和端口后,调用 listen()
进行监听。服务端进入监听状态后通过调 accept()
来从内核获取客户端的连接。如果没有客户端连接则会阻塞等待客户端连接的到来
4、客户端创建好 Socket 后调用 connect()
发起连接,函数参数要指明服务端 IP 和端口号,然后开始 TCP 三次握手
5、TCP连接过程中,服务器内核实际上为每个 Socket 维护了两个队列:一个是「还没完全建立」连接的队列,称为 TCP 半连接队列,这个队列都是没有完成三次握手的连接,此时服务端处于 syn_rcvd
状态;一个是「已建立」连接的队列,称为 TCP 全连接队列,这个队列都是完成了三次握手的连接,此时服务端处于 established
状态
6、当 TCP 全连接队列不为空后,服务端的 accept()
就会从内核中的 TCP 全连接队列里拿一个已完成连接的 Socket 返回应用程序,后续数据传输都用这个 Socket
7、连接建立后客户端和服务端就开始相互传数据,双方都可通过 read()
和 write()
读写数据
IO多路复用
只用一个进程来维护多个 Socket,使用select/poll/epoll系统调用,进程可通过一个系统调用函数从内核中获取多个事件。获取事件时先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中处理这些连接对应的请求即可
select
将已连接的 Socket 都放到一个文件描述符集合,然后调 select 函数将文件描述符集合拷贝到内核里检查是否有网络事件产生,检查方式很粗暴就是通过遍历文件描述符集合,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需再通过遍历找到可读或可写的 Socket再对其处理。
select需进行 2 次「遍历」文件描述符集合,一次在内核态一个在用户态 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中
select 使用固定长度的 BitsMap表示文件描述符集合,而且所支持的文件描述符的个数默认最大值为1024
poll
不再用 BitsMap 来存所关注的文件描述符而用动态数组,以链表形式来组织,突破了select 的文件描述符个数限制。
但 poll 和 select 并没有太大本质区别,都是用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合
epoll
用法:先用epoll_create创一个epoll对象epfd,再通过epoll_ctl将需要监视的socket加到epfd中,最后调epoll_wait等待数据。epoll通过两个方面解决了 select/poll 的问题:
1、epoll在内核里用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl()
加入内核红黑树里,红黑树高效增删改时间复杂度是 O(logn)
。而 select/poll 没有类似 epoll 红黑树这种数据结构,所以每次操作时都传入整个 socket 集合给内核。而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需传一个待检测的socket,减少了内核和用户空间大量的数据拷贝和内存分配。
2、 epoll使用事件驱动机制,内核里维护一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数,内核将其加到这个就绪事件列表中,当用户调 epoll_wait()
时只会返回有事件发生的文件描述符个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,提高了检测效率。
边缘触发和水平触发
1、边缘触发ET:当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此程序要保证一次性将内核缓冲区数据读完(可能会导致饥饿问题,一次没读完就会导致缓冲区数据不被读走导致饥饿)
2、水平触发LT:当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取