首先,讲讲UDP编程为什么不需要用到系统的epoll函数, RichardStevens在不朽的经典《Unix网络编程卷一》中已经说了:大多数情况下,TCP服务器是并发的,UDP的服务器是迭代的。 上面这句话可能不太容易理解。 我基于自己的理解,用大白话跟大家讲,为什么UDP用不到epoll的系统函数。 首先借用一句话,内核不是解决方案,而是问题所在。 linux操作系统为什么需要提供epoll函数,在epoll之前还有selectpoll函数。这些函数是干什么的。 首先,多路复用这个词是针对面向链接协议的说法,一个客户端连接就是一路。 但是实际上,在IP层,或者说在网络底层,根本就没有什么多路,只有一路。没错,只有一路。 TCP服务器一般是这样的流程,一个线程处理一个客户端连接: C音视频开发学习资料:点击领取音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP) 创建服务器描述符sfdsfdsocket()把服务器fd绑定地址端口bind(sfd)正式开始监听listen()阻塞等待TCP3次握手cfdaccept(sfd)开启新线程处理cfd的读写。 UDP服务器程序一般是这样的流程:创建服务器描述符sfdsfdsocket()把服务器fd绑定地址端口bind(sfd)直接接受客户端数据包recvfrom() 可以看到,UDP实际上是TCP的简化版。recvfrom是一个阻塞函数。IP层有数据包到了,就检查IPheader的协议字段,判断IPbody里是不是udp的数据,如果是就激活recvfrom(),这样UDP就能拿到客户端的数据。这个激活,好像是一个软中断还是一个信号,具体在linux4。4内核的哪一块代码,我需要花点时间找一找,先埋个坑,后续补充,暂时理解为激活就行。 从UDP跟IP层的交互来看,你可以看到,实际上没有什么多路,所有的客户端数据都在一路里面传递给IP层,再由IP层传给UDP层recvfrom()。 既然如此,那为什么TCP会出现多路,而且因为多路带来性能问题,需要再用epoll解决。 这里就需要先讲解一下什么是面向连接的协议? 我们在教科书经常看到,TCP是面向连接的,而UDP不是。 实际上上面这句话是什么意思呢? 首先,TCP的实现是在linux内核代码里面的,所以TCP属于linux内核的一部分。 首先讲解一下,连接状态,它并不是特别真实的一个东西,这个东西比较虚,连接状态,只是内存里面的一个变量,而且还不是实时更新的。 为什么说它不是实时更新的,就是当底层网络,通信链路不可达的时候,什么是通信链路不可达?直接把中间路由的网线拔开就是了。 当通信链路不可达的时候,TCP里面的链接状态不会非常快的更改为断开状态。之前在《视频传输协议设计》演示过,把网线拔开之后,TCP要经过多次重传失败,才会认定底层通信链路不可达,然后返回一个信号给调用层。当网线断开的时候,TCP客户端从重试到确认链接断开用了19秒才知道网线断开。 所以说TCP里面的连接状态只是内存的一个变量,一个虚拟的东西。 再来说说,为什么协议栈的发展,会演变出连接状态这个内存变量? 要讲这个问题,就需要先讲为什么广域网的协议,基本上都有3次握手,TCP,UDT,SRT,你可以看到这些协议都有3次握手。 TCP的3次握手是自带的,UDT是基于UDP的协议,自己实现3次握手,SRT是基于UDT的。 这个先抛出个问题,UDT既然是用UDP实现的,UDP本身没有3次握手,没有不是更好,可以少几次RTT的时间,通信更快。 为什么UDT还要自己搞一套3次握手,不用3次握手不是更快吗? 从直觉来讲,没有3次握手,确实会更快,例如IP层,MAC层的各种协议,就没有什么3次握手。 但是,需要注意的是,IP层MAC层,他是工作在路由器,交换机里面的,他的数据包发给下一站就完事了,通常下一站不是太远。 而UDT,TCP,他们的传输,可能是从东大陆到北极,100M的数据,从东大陆某个主机发出来,经过那么多路由器,交换机,跨越海洋,到达北极的某台服务器。如果没有3次握手,提取沟通一下双方的一些情况,100M的传输效率会极其低下,并不会因为没有3次握手而更快,相反,由于上层协议栈没有握手导致一些信息掌握不全面,发送跟接受策略不行,会导致IP层丢包,大量重传。 所以3次握手,干的是什么的事?就是提前沟通一下双方的信息,例如我的MTU是多少,你的MTU是多少之类的。 这里再抛出另一个问题,为什么是3次握手,不是4次,2次? 这个问题实际上是一个ACK设计问题,由于IP层不可靠会丢包,特别是经过那么多路由器转发,丢包的概率会增加,例如路由器负载太高,他就会丢弃数据包减轻负载。 所以基于IP层的协议,想要在广域网上实现可靠传输,都需要自己实现一套ACK机制,TCP自带ACK,UDT协议是自己实现ACK。 因为ACK机制的出现,再加上广域网上传输可靠数据需要提前沟通,沟通好才开始正式开始传输数据。 ACK提前沟通,就形成了3次握手,举个例子。 客户端要开始传数据给服务器,客户端先发一个UDP包,里面有他的MTU等信息,然后服务器端收到了这个UDP包,因为要有确认机制,所以服务器需要发一个ACK的UDP包给客户端,告诉客户端,服务器已经收到了他MTUUDP包。但是同时,服务器也需要告诉客户端自己的一些信息,为了提高效率,服务器的MTU信息也会放进去ACK里面一起返回去给客户端。 然后客户端收到了服务器的ACK,客户端知道MTU信息服务器端拿到了,客户端就不会重传。同时,客户端也收到服务器的MTUUDP包,客户端需要回复服务器一个ACK,说自己拿到了。 上面这些流程,就是3次握手,也就是TCP里面的SYNACKSYNACK。 回到之前的问题,为什么UDP不需要多路复用,是因为UDP他本身没实现3次握手跟链接状态的功能。那是不是如果一个基于UDP的协议栈实现了3次握手跟建立连接,就会需要多路复用,其实也不是。 C音视频开发学习资料:点击领取1音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP) 举个例子,基于UDP设计一个协议。我把他叫UDL。 简洁的流程如下: 1,服务器recvfrom()阻塞等待IP层把IPbody的数据丢上来。 2,服务器recvfrom()拿到了客户端的UDP数据,有端口,有客户端IP,有客户端MTU这些数据。 3,服务器开始回复ACK,3次握手省略,3次握手成功之后然后我服务器创建一个map,把客户端IPPORT作为一个key传进去map,代表这个客户端IPPORT已经建立连接了。可以正式传输数据。 4,服务器继续阻塞在recvfrom()等待数据。 5,服务器再次从recvfrom()拿到UDP包,检测UDP包里面有没有SYN,如果有就是开一个新的链接,没有SYN就在map里面找客户端IPPORT,如果存在就继续走,如果不存在就代表之前没建立连接,没沟通好,不能传数据,直接把RST标记放进去UDP发回去。 6,这里服务器每次从recvfrom()拿到UDP包,都是丢给线程池处理,主线程不阻塞处理。 上面的流程,我基于UDP实现了一个3次握手,跟链接状态,大家觉得里面有没用到多路复用?我觉得没有,由此至终都是有一个路,所有客户端数据都是通过 recvfrom()拿到了,然后传给线程池处理。 那为什么TCP有多路复用跟EPOLL,是因为TCP把3次握手,链接状态等等东西,封装进内部逻辑,做了抽象,方便调用层使用。 上层协议栈,实现了3次握手跟链接状态的功能并不会出现多路复用,而是由于对这些东西做了封装才会出现多路复用,这个具体是什么意思呢? 我再仔细讲解一下,Linux里面有个谚语一切皆文件,包括TCP的socket也是一个文件描述符,tcp的socketfd是跟文件fd,复用了一个内核的数据结构,由于linux内核打开的文件描述符是有限制了,这个封装通用设计,就会导致一些问题。 大家可以看看上面的UDL协议设计,并没打开什么文件描述符,我只是申请了块内存,来存客户端IPPORT的链接状态。 因为TCP的内部实现用了文件描述符这个数据结构,所以他受到了文件描述符的数量限制。这是TCP实现没考虑的问题。TCP实现是linux内核实现的一部分。 TCP里面的客户端socketfd,实际上跟上面的UDL一样,只是把客户端IPPORT转成一个socketfd,一个整数,方便调用层使用。 讲了这么多,还没开始讲,TCP为什么就有多路,而且要复用,而我们上面自己基于UDP实现的类似协议,却没有多路,也不需要复用。 其实这个问题我自己也不是很清楚,估计是因为TCP里面的网络fd跟操作系统的文件fd是共用的某个数据结构,所以一个TCP网络fd,跟文件fd是一样的,写一个文件就是一路,写多个文件就是多路。系统的文件描述符可以监听变化,TCP服务器会生成很多的clientfd,每个线程阻塞监听clientfd效率太低,所以出了epoll统一监听所有网络fd。 所以说,估计是因为TCP的内部实现跟操作系统的文件实现耦合太严重了,导致他出现多路,需要epoll来解决。 TCP应用早期有C10k问题,但是运营商的NAT硬件防火墙,要处理的TCP链接,肯定超过10k,他是怎么解决的?修改linux内核,或者不用linux内核,把硬件性能压榨到极致。TCP标准只是定义了TCP的行为,没有强制一定要像linux内核那样跟文件系统通用数据结构。可以自行实现TCP的行为,符合标准就行。