目录
想象这样一个场景:老师站在讲台上提问,下面100个学生把答案写在纸上,谁写完谁举手示意,让老师来检查,完成的好就可以放学回家。如果学生张三举手,李四也举手,就表示他们已经完成了,老师就立即依次去检查张三和李四的答案,检查完毕,老师就可以返回讲台休息或者溜达等等,接着王五,赵四儿又举手,然后老师马上去检查他们的答案。。。以此往复。

如上这种生活现象就是 I/O 多路复用模型,Linux下的 select、poll,和epoll 就是实现的这种机制,这样就避免了大量的无用操作,比如,老师不需要依次的等待一个学生写完了,然后检查一个学生,检查完毕,再去等待下一个学生。。。(对应多客户端单线程模型),也不需要请100个老师,每个老师对应1个学生(一客户端一线程的 BIO 模型),而是让所有学生先自己闷头写答案,写完才主动举手示意,老师在去检查答案,处理完毕,老师就可以走了,继续等待其它学生举手,全程一个老师就能处理(epoll 函数),这就是所谓的非阻塞模式。另外,老师也不需要顺序的询问每个学生的问题完成情况(select 函数)只需要看谁举手。。。这样老师不烦躁,学生也能专心答题。

类比到通信,整个I/O过程只在调用 select、poll、epoll 这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,从而使得系统在单线程(进程)的情况下,可以同时处理多个客户端请求,这就是I/O 多路复用模型。与传统的多线程(单线程)模型相比,I/O多路复用的最大优势就是系统开销小,系统不需要创建新的额外线程,也不需要维护这些线程的运行、切换、同步问题,降低了系统的开发和维护的工作量,节省了时间和系统资源。
主要的应用场景,服务器需要同时处理多个处于监听状态或多个连接状态的套接字,服务器需要同时处理多种网络协议的套接字。
支持I/O多路复用的系统调用主要有select、pselect、poll、epoll。而当前推荐使用的是epoll,优势如下:
- 支持一个进程打开的socket fd(file description)不受限制
- I/O效率不会随着fd数目的增加而线性下将
- 使用mmap加速内核与用户空间的消息传递。
- epoll拥有更加简单的API。
而常见的一种 I/O 多路复用模型有所谓的 reactor 模式,Netty 就实现了多线程的 reactor 模型(reactor 模型有三种,单线程,多线程和主从),即当有感兴趣的事件(event)发生,就通知对应的事件处理器(ChannelHandler)去处理这个事件,如果没有就不处理。故用一个线程(NioEventLoop)做轮询就可以了。如果要获得更高性能,可以使用少量的线程,一个负责接收请求(boss NioEventLoopGroup),其他的负责处理请求(worker NioEventLoopGroup),对于多 CPU 时效率会更高(Netty 的线程池会默认启动 2 倍的 CPU 核数个线程)。
后续笔记会详细分析。
在网络编程层次,这些Socket函数是操作系统内核实现的,用户代码无法触及,只能使用,这些内核代码把TCP/IP协议栈和网卡封装,暴露出来对用户友好的API,就成了所谓的 Socket 函数,用户代码可以用这些 Socket 函数操纵本地的TCP/IP协议栈和网卡,和服务器通信。
回到网络层次,OSI 的上三层等价于 TCP/IP 协议族的应用层(典型的 Telnet、FTP 等应用), OSI 下两层等价于 TCP/IP 协议族中随系统提供的设备驱动程序和硬件。在一个网络程序中, 对应OSI 模型,上三层处理应用本身的细节,却对应用底层的通信细节了解很少;下四层可以处理所有的底层网络的通信细节。OSI 的上三层可以对应所谓的用户进程,下四层通常对应操作系统内核的一部分,因此,把第4层和第5层之间的接口抽象为 Socket API 是自然而然的一个过程,即所谓的 Socket 所处的位置就是 TCP/IP 协议族应用层和传输层的交界处。

具体分析如下,在连接建立阶段,客户端调用 connect() 函数发起主动连接——触发客户端的 TCP 协议栈发送 SYN 报文,此时客户端处于 SYN-SENT 态,如下。而在此之前,服务端的 Socket 需要已经处于监听态(LISTEN),在 Linux 上就是调用 listen() 函数即可实现监听 Socket。

服务端的 TCP 协议栈收到该 SYN 报文后,发送给处于 LISTEN 状态的服务端 Socket,服务端应用进程通过调用 accept() 函数触发其 TCP 协议栈发送 SYN+ACK 报文返回给客户端,此时服务端从 LISTEN 态转移到 SYN-RCVD 态。

客户端收到服务端的 SYN+ACK 报文后,发送确认的 ACK 报文,此时客户端从 SYN-SENT 态进入 ESTABLISHED 态,当服务端收到客户端的 ACK 报文后,同样会从 SYN-RCVD 态也进入 ESTABLISHED 态,此时服务端的 accept() 函数返回。

经过如上三个报文交互,TCP 连接建立,然后就可以进行数据传输。
在数据传输阶段,客户端的 Socket 可以调用 send() 函数发送数据,然后服务端的 Socket 接到客户端 Socket 传来的请求,调用 read() 函数读取,调用 write() 函数写入响应。
在连接断开阶段,以客户端主动关闭为例子。

客户端的 TCP 协议栈主动发送一个 FIN 报文,主动关闭到服务端方向的连接,此时客户端状态从 ESTABLISHED 态转移到 FIN-WAIT-1 态。通过调用 close() 函数即可实现。

服务端 TCP 协议栈收到 FIN 报文,就发回客户端一个 ACK 报文确认关闭,此时,服务端状态从 ESTABLISHED 态转移到 CLOSE-WAIT 态(因为是被动关闭),和 SYN 一样,一个 FIN 也占用一个序号,同时服务端还向客户端传送一个文件结束符。当客户端接受到服务端确认关闭的报文后,客户端状态从 FIN-WAIT-1 态转移到 FIN-WAIT-2 态。

接着这个服务端程序就关闭它的连接,这会导致服务端的 TCP 协议栈也会发送一个 FIN 报文给客户端,这里也能清楚看到,ACK 不消耗序号。此时,服务端状态从 CLOSE-WAIT 转移到 LAST-ACK 态。

客户端收到服务端的 FIN 报文,也必须发回一个ACK 确认报文。此时,客户端状态从 FIN-WAIT-2 态转移到 TIME-WAIT 态。

至此,TCP 连接关闭。
1 fd_set readfd; 2 struct timeval timeout; 3 4 FD_ZERO(&readfd); // 初始化 readfd 5 FD_SET(gps_fd, &readfd); // 把 gps_fd 加入 readfd 6 timeout.tv_sec = 3;
关键字:青岛软件培训
可能你正在寻找一家靠谱的IT培训机构, 渴望突破职业瓶颈, 找一份得体的工作。 恰巧万码学堂正在寻找像你这样不甘平凡的追光者! 我们拒绝纸上谈兵,直接参与真实开发流程!
申请免费试听课程
现在行动,未来可期
立即拨打0532-85025005,预约免费职业规划咨询 前20名咨询者赠送《2025高薪技术岗位白皮书》!
你不是在报名课程,而是在投资五年后的自己!
