计算机网络——传输层

UDP协议

TCP和UDP有哪些区别?

一般面试的时候我问这两个协议的区别,大部分人会回答,TCP是面向连接的,UDP是面向无连接的。

什么叫面向连接,什么叫无连接呢?在互通之前,面向连接的协议会先建立连接。例如,TCP会三次握手,而UDP不会。为什么要建立连接呢?你TCP三次握手,我UDP也可以发三个包玩玩,有什么区别吗?

所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。

例如,TCP提供可靠交付。通过TCP连接传输的数据,无差错、不丢失、不重复、并且按序到达。我们都知道IP包是没有任何可靠性保证的,一旦发出去,就像西天取经,走丢了、被妖怪吃了,都只能随它去。但是TCP号称能做到那个连接维护的程序做的事情,这个下两节我会详细描述。而UDP继承了IP包的特性,不保证不丢失,不保证按顺序到达。

再如,TCP是面向字节流的。发送的时候发的是一个流,没头没尾。IP包可不是一个流,而是一个个的IP包。之所以变成了流,这也是TCP自己的状态维护做的事情。而UDP继承了IP的特性,基于数据报的,一个一个地发,一个一个地收。

还有TCP是可以有拥塞控制的。它意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。UDP就不会,应用让我发,我就发,管它洪水滔天。

因而TCP其实是一个有状态服务,通俗地讲就是有脑子的,里面精确地记着发送了没有,接收到没有,发送到哪个了,应该接收哪个了,错一点儿都不行。而UDP则是无状态服务。通俗地说是没脑子的,天真无邪的,发出去就发出去了。

我们可以这样比喻,如果MAC层定义了本地局域网的传输行为,IP层定义了整个网络端到端的传输行为,这两层基本定义了这样的基因:网络传输是以包为单位的,二层叫帧,网络层叫包,传输层叫段。我们笼统地称为包。包单独传输,自行选路,在不同的设备封装解封装,不保证到达。基于这个基因,生下来的孩子UDP完全继承了这些特性,几乎没有自己的思想。

UDP包头是什么样的?

我们来看一下UDP包头。

当我发送的UDP包到达目标机器后,发现MAC地址匹配,于是就取下来,将剩下的包传给处理IP层的代码。把IP头取下来,发现目标IP匹配,接下来呢?这里面的数据包是给谁呢?

发送的时候,我知道我发的是一个UDP的包,收到的那台机器咋知道的呢?所以在IP头里面有个8位协议,这里会存放,数据里面到底是TCP还是UDP,当然这里是UDP。于是,如果我们知道UDP头的格式,就能从数据里面,将它解析出来。解析出来以后呢?数据给谁处理呢?

处理完传输层的事情,内核的事情基本就干完了,里面的数据应该交给应用程序自己去处理,可是一台机器上跑着这么多的应用程序,应该给谁呢?

无论应用程序写的使用TCP传数据,还是UDP传数据,都要监听一个端口。正是这个端口,用来区分应用程序,要不说端口不能冲突呢。两个应用监听一个端口,到时候包给谁呀?所以,按理说,无论是TCP还是UDP包头里面应该有端口号,根据端口号,将数据交给相应的应用程序。
img

当我们看到UDP包头的时候,发现的确有端口号,有源端口号和目标端口号。因为是两端通信嘛,这很好理解。但是你还会发现,UDP除了端口号,再没有其他的了。和TCP头比起来,这个简直简单得一塌糊涂啊!

UDP的三大特点

UDP就像小孩子一样,有以下这些特点:

第一,沟通简单,不需要一肚子花花肠子(大量的数据结构、处理逻辑、包头字段)。前提是它相信网络世界是美好的,秉承性善论,相信网络通路默认就是很容易送达的,不容易被丢弃的。

第二,轻信他人。它不会建立连接,虽然有端口号,但是监听在这个地方,谁都可以传给他数据,他也可以传给任何人数据,甚至可以同时传给多个人数据。

第三,愣头青,做事不懂权变。不知道什么时候该坚持,什么时候该退让。它不会根据网络的情况进行发包的拥塞控制,无论网络丢包丢成啥样了,它该怎么发还怎么发。

UDP的三大使用场景

基于UDP这种“小孩子”的特点,我们可以考虑在以下的场景中使用。

第一,需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用。这很好理解,就像如果你是领导,你会让你们组刚毕业的小朋友去做一些没有那么难的项目,打一些没有那么难的客户,或者做一些失败了也能忍受的实验性项目。

DHCP就是基于UDP协议的。一般的获取IP地址都是内网请求,而且一次获取不到IP又没事,过一会儿还有机会。PXE可以在启动的时候自动安装操作系统,操作系统镜像的下载使用的TFTP,这个也是基于UDP协议的。在还没有操作系统的时候,客户端拥有的资源很少,不适合维护一个复杂的状态机,而是因为是内网,一般也没啥问题。

第二,不需要一对一沟通,建立连接,而是可以广播的应用。咱们小时候人都很简单,大家在班级里面,谁成绩好,谁写作好,应该表扬谁惩罚谁,谁得几个小红花都是当着全班的面讲的,公平公正公开。长大了人心复杂了,薪水、奖金要背靠背,和员工一对一沟通。

UDP的不面向连接的功能,可以使得可以承载广播或者多播的协议。DHCP就是一种广播的形式,就是基于UDP协议的,而广播包的格式前面说过了。

对于多播,我们在讲IP地址的时候,讲过一个D类地址,也即组播地址,使用这个地址,可以将包组播给一批机器。当一台机器上的某个进程想监听某个组播地址的时候,需要发送IGMP包,所在网络的路由器就能收到这个包,知道有个机器上有个进程在监听这个组播地址。当路由器收到这个组播地址的时候,会将包转发给这台机器,这样就实现了跨路由器的组播。

协议VXLAN,也是需要用到组播,也是基于UDP协议的。

第三,需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候。记得曾国藩建立湘军的时候,专门招出生牛犊不怕虎的新兵,而不用那些“老油条”的八旗兵,就是因为八旗兵经历的事情多,遇到敌军不敢舍死忘生。

同理,UDP简单、处理速度快,不像TCP那样,操这么多的心,各种重传啊,保证顺序啊,前面的不收到,后面的没法处理啊。不然等这些事情做完了,时延早就上去了。而TCP在网络不好出现丢包的时候,拥塞控制策略会主动的退缩,降低发送速度,这就相当于本来环境就差,还自断臂膀,用户本来就卡,这下更卡了。

当前很多应用都是要求低时延的,它们可不想用TCP如此复杂的机制,而是想根据自己的场景,实现自己的可靠和连接保证。例如,如果应用自己觉得,有的包丢了就丢了,没必要重传了,就可以算了,有的比较重要,则应用自己重传,而不依赖于TCP。有的前面的包没到,后面的包到了,那就先给客户展示后面的嘛,干嘛非得等到齐了呢?如果网络不好,丢了包,那不能退缩啊,要尽快传啊,速度不能降下来啊,要挤占带宽,抢在客户失去耐心之前到达。

由于UDP十分简单,基本啥都没做,也就给了应用“城会玩”的机会。就像在和平年代,每个人应该有独立的思考和行为,应该可靠并且礼让;但是如果在战争年代,往往不太需要过于独立的思考,而需要士兵简单服从命令就可以了。

曾国藩说哪支部队需要诱敌牺牲,也就牺牲了,相当于包丢了就丢了。两军狭路相逢的时候,曾国藩说上,没有带宽也要上,这才给了曾国藩运筹帷幄,城会玩的机会。同理如果你实现的应用需要有自己的连接策略,可靠保证,时延要求,使用UDP,然后再应用层实现这些是再好不过了。

基于UDP的“城会玩”的五个例子

我列举几种“城会玩”的例子。

网页或者APP的访问

原来访问网页和手机APP都是基于HTTP协议的。HTTP协议是基于TCP的,建立连接都需要多次交互,对于时延比较大的目前主流的移动互联网来讲,建立一次连接需要的时间会比较长,然而既然是移动中,TCP可能还会断了重连,也是很耗时的。而且目前的HTTP协议,往往采取多个数据通道共享一个连接的情况,这样本来为了加快传输速度,但是TCP的严格顺序策略使得哪怕共享通道,前一个不来,后一个和前一个即便没关系,也要等着,时延也会加大。

QUIC(全称Quick UDP Internet Connections快速UDP互联网连接)是Google提出的一种基于UDP改进的通信协议,其目的是降低网络通信的延迟,提供更好的用户互动体验。

QUIC在应用层上,会自己实现快速连接建立、减少重传时延,自适应拥塞控制,是应用层“城会玩”的代表。

流媒体的协议

现在直播比较火,直播协议多使用RTMP,而这个RTMP协议也是基于TCP的。TCP的严格顺序传输要保证前一个收到了,下一个才能确认,如果前一个收不到,下一个就算包已经收到了,在缓存里面,也需要等着。对于直播来讲,这显然是不合适的,因为老的视频帧丢了其实也就丢了,就算再传过来用户也不在意了,他们要看新的了,如果老是没来就等着,卡顿了,新的也看不了,那就会丢失客户,所以直播,实时性比较比较重要,宁可丢包,也不要卡顿的。

另外,对于丢包,其实对于视频播放来讲,有的包可以丢,有的包不能丢,因为视频的连续帧里面,有的帧重要,有的不重要,如果必须要丢包,隔几个帧丢一个,其实看视频的人不会感知,但是如果连续丢帧,就会感知了,因而在网络不好的情况下,应用希望选择性的丢帧。

还有就是当网络不好的时候,TCP协议会主动降低发送速度,这对本来当时就卡的看视频来讲是要命的,应该应用层马上重传,而不是主动让步。因而,很多直播应用,都基于UDP实现了自己的视频传输协议。

实时游戏

游戏有一个特点,就是实时性比较高。快一秒你干掉别人,慢一秒你被别人爆头,所以很多职业玩家会买非常专业的鼠标和键盘,争分夺秒。

因而,实时游戏中客户端和服务端要建立长连接,来保证实时传输。但是游戏玩家很多,服务器却不多。由于维护TCP连接需要在内核维护一些数据结构,因而一台机器能够支撑的TCP连接数目是有限的,然后UDP由于是没有连接的,在异步IO机制引入之前,常常是应对海量客户端连接的策略。

另外还是TCP的强顺序问题,对战的游戏,对网络的要求很简单,玩家通过客户端发送给服务器鼠标和键盘行走的位置,服务器会处理每个用户发送过来的所有场景,处理完再返回给客户端,客户端解析响应,渲染最新的场景展示给玩家。

如果出现一个数据包丢失,所有事情都需要停下来等待这个数据包重发。客户端会出现等待接收数据,然而玩家并不关心过期的数据,激战中卡1秒,等能动了都已经死了。

游戏对实时要求较为严格的情况下,采用自定义的可靠UDP协议,自定义重传策略,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成的影响。

IoT物联网

一方面,物联网领域终端资源少,很可能只是个内存非常小的嵌入式系统,而维护TCP协议代价太大;另一方面,物联网对实时性要求也很高,而TCP还是因为上面的那些原因导致时延大。Google旗下的Nest建立Thread Group,推出了物联网通信协议Thread,就是基于UDP协议的。

移动通信领域

在4G网络里,移动流量上网的数据面对的协议GTP-U是基于UDP的。因为移动网络协议比较复杂,而GTP协议本身就包含复杂的手机上线下线的通信协议。如果基于TCP,TCP的机制就显得非常多余,这部分协议我会在后面的章节单独讲解。

总结

  • 如果将TCP比作成熟的社会人,UDP则是头脑简单的小朋友。TCP复杂,UDP简单;TCP维护连接,UDP谁都相信;TCP会坚持知进退;UDP愣头青一个,勇往直前;
  • UDP虽然简单,但它有简单的用法。它可以用在环境简单、需要多播、应用层自己控制传输的地方。例如DHCP、VXLAN、QUIC等。

TCP协议

TCP包头格式

我们先来看TCP头的格式。从这个图上可以看出,它比UDP复杂得多。

img

首先,源端口号和目标端口号是不可少的,这一点和UDP是一样的。如果没有这两个端口号。数据就不知道应该发给哪个应用。

接下来是包的序号。为什么要给包编号呢?当然是为了解决乱序的问题。不编好号怎么确认哪个应该先来,哪个应该后到呢。编号是为了解决乱序问题。既然是社会老司机,做事当然要稳重,一件件来,面临再复杂的情况,也临危不乱。

还应该有的就是确认序号。发出去的包应该有确认,要不然我怎么知道对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。作为老司机,做事当然要靠谱,答应了就要做到,暂时做不到也要有个回复。

TCP是靠谱的协议,但是这不能说明它面临的网络环境好。从IP层面来讲,如果网络状况的确那么差,是没有任何可靠性保证的,而作为IP的上一层TCP也无能为力,唯一能做的就是更加努力,不断重传,通过各种算法保证。也就是说,对于TCP来讲,IP层你丢不丢包,我管不着,但是我在我的层面上,会努力保证可靠性。

这有点像如果你在北京,和客户约十点见面,那么你应该清楚堵车是常态,你干预不了,也控制不了,你唯一能做的就是早走。打车不行就改乘地铁,尽力不失约。

接下来有一些状态位。例如SYN是发起一个连接,ACK是回复,RST是重新连接,FIN是结束连接等。TCP是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。

不像小时候,随便一个不认识的小朋友都能玩在一起,人大了,就变得礼貌,优雅而警觉,人与人遇到会互相热情的寒暄,离开会不舍的道别,但是人与人之间的信任会经过多次交互才能建立。

还有一个重要的就是窗口大小。TCP要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力,别发送的太快,撑死我,也别发的太慢,饿死我。

作为老司机,做事情要有分寸,待人要把握尺度,既能适当提出自己的要求,又不强人所难。除了做流量控制以外,TCP还会做拥塞控制,对于真正的通路堵车不堵车,它无能为力,唯一能做的就是控制自己,也即控制发送的速度。不能改变世界,就改变自己嘛。

作为老司机,要会自我控制,知进退,知道什么时候应该坚持,什么时候应该让步。

通过对TCP头的解析,我们知道要掌握TCP协议,重点应该关注以下几个问题:

  • 顺序问题 ,稳重不乱;
  • 丢包问题,承诺靠谱;
  • 连接维护,有始有终;
  • 流量控制,把握分寸;
  • 拥塞控制,知进知退。

TCP的三次握手

所有的问题,首先都要先建立一个连接,所以我们先来看连接维护问题。

TCP的连接建立,我们常常称为三次握手。

A:您好,我是A。

B:您好A,我是B。

A:您好B。

我们也常称为“请求->应答->应答之应答”的三个回合。这个看起来简单,其实里面还是有很多的学问,很多的细节。

首先,为什么要三次,而不是两次?按说两个人打招呼,一来一回就可以了啊?为了可靠,为什么不是四次?

我们还是假设这个通路是非常不可靠的,A要发起一个连接,当发了第一个请求杳无音信的时候,会有很多的可能性,比如第一个请求包丢了,再如没有丢,但是绕了弯路,超时了,还有B没有响应,不想和我连接。

A不能确认结果,于是再发,再发。终于,有一个请求包到了B,但是请求包到了B的这个事情,目前A还是不知道的,A还有可能再发。

B收到了请求包,就知道了A的存在,并且知道A要和它建立连接。如果B不乐意建立连接,则A会重试一阵后放弃,连接建立失败,没有问题;如果B是乐意建立连接的,则会发送应答包给A。

当然对于B来说,这个应答包也是一入网络深似海,不知道能不能到达A。这个时候B自然不能认为连接是建立好了,因为应答包仍然会丢,会绕弯路,或者A已经挂了都有可能。

而且这个时候B还能碰到一个诡异的现象就是,A和B原来建立了连接,做了简单通信后,结束了连接。还记得吗?A建立连接的时候,请求包重复发了几次,有的请求包绕了一大圈又回来了,B会认为这也是一个正常的的请求的话,因此建立了连接,可以想象,这个连接不会进行下去,也没有个终结的时候,纯属单相思了。因而两次握手肯定不行。

B发送的应答可能会发送多次,但是只要一次到达A,A就认为连接已经建立了,因为对于A来讲,他的消息有去有回。A会给B发送应答之应答,而B也在等这个消息,才能确认连接的建立,只有等到了这个消息,对于B来讲,才算它的消息有去有回。

当然A发给B的应答之应答也会丢,也会绕路,甚至B挂了。按理来说,还应该有个应答之应答之应答,这样下去就没底了。所以四次握手是可以的,四十次都可以,关键四百次也不能保证就真的可靠了。只要双方的消息都有去有回,就基本可以了。

好在大部分情况下,A和B建立了连接之后,A会马上发送数据的,一旦A发送数据,则很多问题都得到了解决。例如A发给B的应答丢了,当A后续发送的数据到达的时候,B可以认为这个连接已经建立,或者B压根就挂了,A发送的数据,会报错,说B不可达,A就知道B出事情了。

当然你可以说A比较坏,就是不发数据,建立连接后空着。我们在程序设计的时候,可以要求开启keepalive机制,即使没有真实的数据包,也有探活包。

另外,你作为服务端B的程序设计者,对于A这种长时间不发包的客户端,可以主动关闭,从而空出资源来给其他客户端使用。

三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是TCP包的序号的问题

A要告诉B,我这面发起的包的序号起始是从哪个号开始的,B同样也要告诉A,B发起的包的序号起始是从哪个号开始的。为什么序号不能都从1开始呢?因为这样往往会出现冲突。

例如,A连上B之后,发送了1、2、3三个包,但是发送3的时候,中间丢了,或者绕路了,于是重新发送,后来A掉线了,重新连上B后,序号又从1开始,然后发送2,但是压根没想发送3,但是上次绕路的那个3又回来了,发给了B,B自然认为,这就是下一个包,于是发生了错误。

因而,每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个32位的计数器,每4ms加一,如果计算一下,如果到重复,需要4个多小时,那个绕路的包早就死翘翘了,因为我们都知道IP包头里面有个TTL,也即生存时间。

好了,双方终于建立了信任,建立了连接。前面也说过,为了维护这个连接,双方都要维护一个状态机,在连接建立的过程中,双方的状态变化时序图就像这样。

img

一开始,客户端和服务端都处于CLOSED状态。先是服务端主动监听某个端口,处于LISTEN状态。然后客户端主动发起连接SYN,之后处于SYN-SENT状态。服务端收到发起的连接,返回SYN,并且ACK客户端的SYN,之后处于SYN-RCVD状态。客户端收到服务端发送的SYN和ACK之后,发送ACK的ACK,之后处于ESTABLISHED状态,因为它一发一收成功了。服务端收到ACK的ACK之后,处于ESTABLISHED状态,因为它也一发一收了。

TCP四次挥手

好了,说完了连接,接下来说一说“拜拜”,好说好散。这常被称为四次挥手。

A:B啊,我不想玩了。

B:哦,你不想玩了啊,我知道了。

这个时候,还只是A不想玩了,也即A不会再发送数据,但是B能不能在ACK的时候,直接关闭呢?当然不可以了,很有可能A是发完了最后的数据就准备不玩了,但是B还没做完自己的事情,还是可以发送数据的,所以称为半关闭的状态。

这个时候A可以选择不再接收数据了,也可以选择最后再接收一段数据,等待B也主动关闭。

B:A啊,好吧,我也不玩了,拜拜。

A:好的,拜拜。

这样整个连接就关闭了。但是这个过程有没有异常情况呢?当然有,上面是和平分手的场面。

A开始说“不玩了”,B说“知道了”,这个回合,是没什么问题的,因为在此之前,双方还处于合作的状态,如果A说“不玩了”,没有收到回复,则A会重新发送“不玩了”。但是这个回合结束之后,就有可能出现异常情况了,因为已经有一方率先撕破脸。

一种情况是,A说完“不玩了”之后,直接跑路,是会有问题的,因为B还没有发起结束,而如果A跑路,B就算发起结束,也得不到回答,B就不知道该怎么办了。另一种情况是,A说完“不玩了”,B直接跑路,也是有问题的,因为A不知道B是还有事情要处理,还是过一会儿会发送结束。

那怎么解决这些问题呢?TCP协议专门设计了几个状态来处理这些问题。我们来看断开连接的时候的状态时序图

img

断开的时候,我们可以看到,当A说“不玩了”,就进入FIN_WAIT_1的状态,B收到“A不玩”的消息后,发送知道了,就进入CLOSE_WAIT的状态。

A收到“B说知道了”,就进入FIN_WAIT_2的状态,如果这个时候B直接跑路,则A将永远在这个状态。TCP协议里面并没有对这个状态的处理,但是Linux有,可以调整tcp_fin_timeout这个参数,设置一个超时时间。

如果B没有跑路,发送了“B也不玩了”的请求到达A时,A发送“知道B也不玩了”的ACK后,从FIN_WAIT_2状态结束,按说A可以跑路了,但是最后的这个ACK万一B收不到呢?则B会重新发一个“B不玩了”,这个时候A已经跑路了的话,B就再也收不到ACK了,因而TCP协议要求A最后等待一段时间TIME_WAIT,这个时间要足够长,长到如果B没收到ACK的话,“B说不玩了”会重发的,A会重新发一个ACK并且足够时间到达B。

A直接跑路还有一个问题是,A的端口就直接空出来了,但是B不知道,B原来发过的很多包很可能还在路上,如果A的端口被一个新的应用占用了,这个新的应用会收到上个连接中B发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来B发送的所有的包都死翘翘,再空出端口来。

等待的时间设为2MSL,MSLMaximum Segment Lifetime报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL域,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。协议规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

还有一个异常情况就是,B超过了2MSL的时间,依然没有收到它发的FIN的ACK,怎么办呢?按照TCP的原理,B当然还会重发FIN,这个时候A再收到这个包之后,A就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送RST,B就知道A早就跑了。

TCP状态机

将连接建立和连接断开的两个时序状态图综合起来,就是这个著名的TCP的状态机。学习的时候比较建议将这个状态机和时序状态机对照着看,不然容易晕。

img

在这个图中,加黑加粗的部分,是上面说到的主要流程,其中阿拉伯数字的序号,是连接过程中的顺序,而大写中文数字的序号,是连接断开过程中的顺序。加粗的实线是客户端A的状态变迁,加粗的虚线是服务端B的状态变迁。

如何做个靠谱的人?

TCP想成为一个成熟稳重的人,成为一个靠谱的人。那一个人怎么样才算靠谱呢?咱们工作中经常就有这样的场景,比如你交代给下属一个事情以后,下属到底能不能做到,做到什么程度,什么时候能够交付,往往就会有应答,有回复。这样,处理事情的过程中,一旦有异常,你也可以尽快知道,而不是交代完之后就石沉大海,过了一个月再问,他说,啊我不记得了。

对应到网络协议上,就是客户端每发送的一个包,服务器端都应该有个回复,如果服务器端超过一定的时间没有回复,客户端就会重新发送这个包,直到有回复。

这个发送应答的过程是什么样呢?可以是上一个收到了应答,再发送下一个。这种模式有点像两个人直接打电话,你一句,我一句。但是这种方式的缺点是效率比较低。如果一方在电话那头处理的时间比较长,这一头就要干等着,双方都没办法干其他事情。咱们在日常工作中也不是这样的,不能你交代你的下属办一件事情,就一直打着电话看着他做,而是应该他按照你的安排,先将事情记录下来,办完一件回复一件。在他办事情的过程中,你还可以同时交代新的事情,这样双方就并行了。

如果使用这种模式,其实需要你和你的下属就不能靠脑子了,而是要都准备一个本子,你每交代下属一个事情,双方的本子都要记录一下。

当你的下属做完一件事情,就回复你,做完了,你就在你的本子上将这个事情划去。同时你的本子上每件事情都有时限,如果超过了时限下属还没有回复,你就要主动重新交代一下:上次那件事情,你还没回复我,咋样啦?

既然多件事情可以一起处理,那就需要给每个事情编个号,防止弄错了。例如,程序员平时看任务的时候,都会看JIRA的ID,而不是每次都要描述一下具体的事情。在大部分情况下,对于事情的处理是按照顺序来的,先来的先处理,这就给应答和汇报工作带来了方便。等开周会的时候,每个程序员都可以将JIRA ID的列表拉出来,说以上的都做完了,而不用一个个说。

如何实现一个靠谱的协议?

TCP协议使用的也是同样的模式。为了保证顺序性,每一个包都有一个ID。在建立连接的时候,会商定起始的ID是什么,然后按照ID一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的ID,表示都收到了,这种模式称为累计确认或者累计应答cumulative acknowledgment)。

为了记录所有发送的包和接收的包,TCP也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的ID一个个排列,根据处理的情况分成四个部分。

第一部分:发送了并且已经确认的。这部分就是你交代下属的,并且也做完了的,应该划掉的。

第二部分:发送了并且尚未确认的。这部分是你交代下属的,但是还没做完的,需要等待做完的回复之后,才能划掉。

第三部分:没有发送,但是已经等待发送的。这部分是你还没有交代给下属,但是马上就要交代的。

第四部分:没有发送,并且暂时还不会发送的。这部分是你还没有交代给下属,而且暂时还不会交代给下属的。

这里面为什么要区分第三部分和第四部分呢?没交代的,一下子全交代了不就完了吗?

这就是我们之前提到的十个词口诀里的“流量控制,把握分寸”。作为项目管理人员,你应该根据以往的工作情况和这个员工反馈的能力、抗压力等,先在心中估测一下,这个人一天能做多少工作。如果工作布置少了,就会不饱和;如果工作布置多了,他就会做不完;如果你使劲逼迫,人家可能就要辞职了。

到底一个员工能够同时处理多少事情呢?在TCP里,接收端会给发送端报一个窗口的大小,叫Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分,就是已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了。

于是,发送端需要保持下面的数据结构。

img

  • LastByteAcked:第一部分和第二部分的分界线
  • LastByteSent:第二部分和第三部分的分界线
  • LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线

对于接收端来讲,它的缓存里记录的内容要简单一些。

第一部分:接受并且确认过的。也就是我领导交代给我,并且我做完的。

第二部分:还没接收,但是马上就能接收的。也即是我自己的能够接受的最大工作量。

第三部分:还没接收,也没法接收的。也即超过工作量的部分,实在做不完。

对应的数据结构就像这样。

img

  • MaxRcvBuffer:最大缓存的量;
  • LastByteRead之后是已经接收了,但是还没被应用层读取的;
  • NextByteExpected是第一部分和第二部分的分界线。

第二部分的窗口有多大呢?

NextByteExpectedLastByteRead的差其实是还没被应用层读取的部分占用掉的MaxRcvBuffer的量,我们定义为A。

AdvertisedWindow其实是MaxRcvBuffer减去A。

也就是:AdvertisedWindow = MaxRcvBuffer - ( ( NextByteExpected - 1) - LastByteRead )。

那第二部分和第三部分的分界线在哪里呢?

NextByteExpectedAdvertisedWindow就是第二部分和第三部分的分界线,其实也就是LastByteRead加上MaxRcvBuffer

其中第二部分里面,由于受到的包可能不是顺序的,会出现空挡,只有和第一部分连续的,可以马上进行回复,中间空着的部分需要等待,哪怕后面的已经来了。

顺序问题与丢包问题

接下来我们结合一个例子来看。

还是刚才的图,在发送端来看,1、2、3已经发送并确认;4、5、6、7、8、9都是发送了还没确认;10、11、12是还没发出的;13、14、15是接收方没有空间,不准备发的。

在接收端来看,1、2、3、4、5是已经完成ACK,但是没读取的;6、7是等待接收的;8、9是已经接收,但是没有ACK的。

发送端和接收端当前的状态如下:

  • 1、2、3没有问题,双方达成了一致。
  • 4、5接收方说ACK了,但是发送方还没收到,有可能丢了,有可能在路上。
  • 6、7、8、9肯定都发了,但是8、9已经到了,但是6、7没到,出现了乱序,缓存着但是没办法ACK。

根据这个例子,我们可以知道,顺序问题和丢包问题都有可能发生,所以我们先来看确认与重发的机制

假设4的确认到了,不幸的是,5的ACK丢了,6、7的数据包丢了,这该怎么办呢?

一种方法就是超时重试,也即对每一个发送了,但是没有ACK的包,都有设一个定时器,超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?这个时间不宜过短,时间必须大于往返时间RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。

估计往返时间,需要TCP通过采样RTT的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断的变化。除了采样RTT,还要采样RTT的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法Adaptive Retransmission Algorithm)。

如果过一段时间,5、6、7都超时了,就会重新发送。接收方发现5原来接收过,于是丢弃5;6收到了,发送ACK,要求下一个是7,7不幸又丢了。当7再次超时的时候,有需要重传的时候,TCP的策略是超时间隔加倍每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍两次超时,就说明网络环境差,不宜频繁反复发送。

超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?

有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。

例如,接收方发现6、8、9都已经接收了,就是7没来,那肯定是丢了,于是发送三个6的ACK,要求下一个是7。客户端收到3个,就会发现7的确又丢了,不等超时,马上重发。

还有一种方式称为Selective AcknowledgmentSACK)。这种方式需要在TCP头里加一个SACK的东西,可以将缓存的地图发送给发送方。例如可以发送ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是7丢了。

流量控制问题

我们再来看流量控制机制,在对于包的确认中,同时会携带一个窗口的大小。

我们先假设窗口不变的情况,窗口始终为9。4的确认来的时候,会右移一个,这个时候第13个包也可以发送了。

img

这个时候,假设发送端发送过猛,会将第三部分的10、11、12、13全部发送完毕,之后就停止发送了,未发送可发送部分为0。

img

当对于包5的确认到达的时候,在客户端相当于窗口再滑动了一格,这个时候,才可以有更多的包可以发送了,例如第14个包才可以发送。

img

如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为0,则发送方将暂时停止发送。

我们假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包6确认后,窗口大小就不能再是9了,就要缩小一个变为8。

img

这个新的窗口8通过6的确认消息到达发送端的时候,你会发现窗口没有平行右移,而是仅仅左面的边右移了,窗口的大小从9改成了8。

img

如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为0。

img

当这个窗口通过包14的确认到达发送端的时候,发送端的窗口也调整为0,停止发送。

img

如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。

这就是我们常说的流量控制。

拥塞控制问题

最后,我们看一下拥塞控制的问题,也是通过窗口的大小来控制的,前面的滑动窗口rwnd是怕发送方把接收方缓存塞满,而拥塞窗口cwnd,是怕把网络塞满。

这里有一个公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} ,是拥塞窗口和滑动窗口共同控制发送的速度。

那发送方怎么判断网络是不是满呢?这其实是个挺难的事情,因为对于TCP协议来讲,他压根不知道整个网络路径都会经历什么,对他来讲就是一个黑盒。TCP发送包常被比喻为往一个水管里面灌水,而TCP的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽。

水管有粗细,网络有带宽,也即每秒钟能够发送多少数据;水管有长度,端到端有时延。在理想状态下,水管里面水的量=水管粗细 x 水管长度。对于到网络上,通道的容量 = 带宽 × 往返延迟。

如果我们设置发送窗口,使得发送但未确认的包为为通道的容量,就能够撑满整个管道。

img

如图所示,假设往返时间为8s,去4s,回4s,每秒发送一个包,每个包1024byte。已经过去了8s,则8个包都发出去了,其中前4个包已经到达接收端,但是ACK还没有返回,不能算发送成功。5-8后四个包还在路上,还没被接收。这个时候,整个管道正好撑满,在发送端,已发送未确认的为8个包,正好等于带宽,也即每秒发送1个包,乘以来回时间8s。

如果我们在这个基础上再调大窗口,使得单位时间内更多的包可以发送,会出现什么现象呢?

我们来想,原来发送一个包,从一端到达另一端,假设一共经过四个设备,每个设备处理一个包时间耗费1s,所以到达另一端需要耗费4s,如果发送的更加快速,则单位时间内,会有更多的包到达这些中间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃,这是我们不想看到的。

这个时候,我们可以想其他的办法,例如这个四个设备本来每秒处理一个包,但是我们在这些设备上加缓存,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加时延,这个缓存的包,4s肯定到达不了接收端了,如果时延达到一定程度,就会超时重传,也是我们不想看到的。

于是TCP的拥塞控制主要来避免两种现象,包丢失超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢?

如果我们通过漏斗往瓶子里灌水,我们就知道,不能一桶水一下子倒进去,肯定会溅出来,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动。

一条TCP连接开始,cwnd设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认cwnd加一,两个确认cwnd加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认cwnd加一,四个确认cwnd加四,于是一次能够发送八个。可以看出这是指数性的增长

涨到什么时候是个头呢?有一个值ssthresh为65535个字节,当超过这个值的时候,就要小心一点了,不能倒这么快了,可能快满了,再慢下来。

每收到一个确认后,cwnd增加1/cwnd,我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加1/8,八个确认一共cwnd增加1,于是一次能够发送九个,变成了线性增长。

但是线性增长还是增长,还是越来越多,直到有一天,水满则溢,出现了拥塞,这时候一般就会一下子降低倒水的速度,等待溢出的水慢慢渗下去。

拥塞的一种表现形式是丢包,需要超时重传,这个时候,将sshresh设为cwnd/2,将cwnd设为1,重新开始慢启动。这真是一旦超时重传,马上回到解放前。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿。

前面我们讲过快速重传算法。当接收端发现丢了一个中间包的时候,发送三次前一个包的ACK,于是发送端就会快速的重传,不必等待超时再重传。TCP认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd减半为cwnd/2,然后sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。

img

就像前面说的一样,正是这种知进退,使得时延很重要的情况下,反而降低了速度。但是如果你仔细想一下,TCP的拥塞控制主要来避免的两个现象都是有问题的。

第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。

第二个问题是TCP的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实TCP只要填满管道就可以了,不应该接着填,直到连缓存也填满。

为了优化这两个问题,后来有了TCP BBR拥塞算法。它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。

img

总结

  • TCP包头很复杂,但是主要关注五个问题,顺序问题,丢包问题,连接维护,流量控制,拥塞控制;
  • 连接的建立是经过三次握手,断开的时候四次挥手,一定要掌握的我画的那个状态图。
  • 顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的,这其实就相当于你领导和你的工作备忘录,布置过的工作要有编号,干完了有反馈,活不能派太多,也不能太少;
  • 拥塞控制是通过拥塞窗口来解决的,相当于往管道里面倒水,快了容易溢出,慢了浪费带宽,要摸着石头过河,找到最优值。

Socket

基于TCP协议的Socket程序函数调用过程

两端创建了Socket之后,接下来的过程中,TCP和UDP稍有不同,我们先来看TCP。

TCP的服务端要先监听一个端口,一般是先调用bind函数,给这个Socket赋予一个IP地址和端口。

为什么需要端口呢?

要知道,你写的是一个应用程序,当一个网络包来的时候,内核要通过TCP头里面的这个端口,来找到你这个应用程序,把包给你。

为什么要IP地址呢?

有时候,一台机器会有多个网卡,也就会有多个IP地址,你可以选择监听所有的网卡,也可以选择监听一个网卡,这样,只有发给这个网卡的包,才会给你。

当服务端有了IP和端口号,就可以调用listen函数进行监听。在TCP的状态图里面,有一个listen状态,当调用这个函数之后,服务端就进入了这个状态,这个时候客户端就可以发起连接了。

在内核中,为每个Socket维护两个队列。

一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于established状态;

一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于syn_rcvd的状态。

接下来,服务端调用accept函数,拿出一个已经完成的连接进行处理。如果还没有完成,就要等着。

在服务端等待的时候,客户端可以通过connect函数发起连接。先在参数中指明要连接的IP地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept就会返回另一个Socket。

这是一个经常考的知识点,就是监听的Socket和真正用来传数据的Socket是两个,一个叫作监听Socket,一个叫作已连接Socket

连接建立成功之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西一样。

这个图就是基于TCP协议的Socket程序函数调用过程。

img

说TCP的Socket就是一个文件流,是非常准确的。因为,Socket在Linux中就是以文件的形式存在的。除此之外,还存在文件描述符。写入和读出,也是通过文件描述符。

在内核中,Socket是一个文件,那对应就有文件描述符。每一个进程都有一个数据结构task_struct,里面指向一个文件描述符数组,来列出这个进程打开的所有文件的文件描述符。文件描述符是一个整数,是这个数组的下标。

这个数组中的内容是一个指针,指向内核中所有打开的文件的列表。既然是一个文件,就会有一个inode,只不过Socket对应的inode不像真正的文件系统一样,保存在硬盘上的,而是在内存中的。在这个inode中,指向了Socket在内核中的Socket结构。

在这个结构里面,主要的是两个队列,一个是发送队列,一个是接收队列。在这两个队列里面保存的是一个缓存sk_buff。这个缓存里面能够看到完整的包的结构。看到这个,是不是能和前面讲过的收发包的场景联系起来了?

整个数据结构画了一张图。

img

基于UDP协议的Socket程序函数调用过程

对于UDP来讲,过程有些不一样。UDP是没有连接的,所以不需要三次握手,也就不需要调用listenconnect,但是,UDP的的交互仍然需要IP和端口号,因而也需要bind。UDP是没有维护连接状态的,因而不需要每对连接建立一组Socket,而是只要有一个Socket,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,都调用sendtorecvfrom,都可以传入IP地址和端口。

这个图的内容就是基于UDP协议的Socket程序函数调用过程。

img

服务器如何接更多的项目?

会了这几个基本的Socket函数之后,你就可以轻松地写一个网络交互的程序了。就像上面的过程一样,在建立连接后,进行一个while循环。客户端发了收,服务端收了发。

当然这只是万里长征的第一步,因为如果使用这种方法,基本上只能一对一沟通。如果你是一个服务器,同时只能服务一个客户,肯定是不行的。这就相当于老板成立一个公司,只有自己一个人,自己亲自上来服务客户,只能干完了一家再干下一家,这样赚不来多少钱。

那作为老板你就要想了,我最多能接多少项目呢?当然是越多越好。

我们先来算一下理论值,也就是最大连接数,系统会用一个四元组来标识一个TCP连接。

{本机IP, 本机端口, 对端IP, 对端端口}

服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,服务端端TCP连接四元组中只有对端IP, 也就是客户端的IP和对端的端口,也即客户端的端口是可变的,因此,最大TCP连接数=客户端IP数×客户端端口数。对IPv4,客户端的IP数最多为2的32次方,客户端的端口数最多为2的16次方,也就是服务端单机最大TCP连接数,约为2的48次方。

当然,服务端最大并发TCP连接数远不能达到理论上限。

首先主要是文件描述符限制,按照上面的原理,Socket都是文件,所以首先要通过ulimit配置文件描述符的数目;

另一个限制是内存,按上面的数据结构,每个TCP连接都要占用一定内存,操作系统是有限的。

所以,作为老板,在资源有限的情况下,要想接更多的项目,就需要降低每个项目消耗的资源数目。

方式一:将项目外包给其他公司(多进程方式)

这就相当于你是一个代理,在那里监听来的请求。一旦建立了一个连接,就会有一个已连接Socket,这时候你可以创建一个子进程,然后将基于已连接Socket的交互交给这个新的子进程来做。就像来了一个新的项目,但是项目不一定是你自己做,可以再注册一家子公司,招点人,然后把项目转包给这家子公司做,以后对接就交给这家子公司了,你又可以去接新的项目了。

这里有一个问题是,如何创建子公司,并如何将项目移交给子公司呢?

在Linux下,创建子进程使用fork函数。通过名字可以看出,这是在父进程的基础上完全拷贝一个子进程。在Linux内核中,会复制文件描述符的列表,也会复制内存空间,还会复制一条记录当前执行到了哪一行程序的进程。显然,复制的时候在调用fork,复制完毕之后,父进程和子进程都会记录当前刚刚执行完fork。这两个进程刚复制完的时候,几乎一模一样,只是根据fork的返回值来区分到底是父进程,还是子进程。如果返回值是0,则是子进程;如果返回值是其他的整数,就是父进程。

进程复制过程画在这里。

img

因为复制了文件描述符列表,而文件描述符都是指向整个内核统一的打开文件列表的,因而父进程刚才因为accept创建的已连接Socket也是一个文件描述符,同样也会被子进程获得。

接下来,子进程就可以通过这个已连接Socket和客户端进行互通了,当通信完毕之后,就可以退出进程,那父进程如何知道子进程干完了项目,要退出呢?还记得fork返回的时候,如果是整数就是父进程吗?这个整数就是子进程的ID,父进程可以通过这个ID查看子进程是否完成项目,是否需要退出。

方式二:将项目转包给独立的项目组(多线程方式)

上面这种方式你应该也能发现问题,如果每次接一个项目,都申请一个新公司,然后干完了,就注销掉这个公司,实在是太麻烦了。毕竟一个新公司要有新公司的资产,有新的办公家具,每次都买了再卖,不划算。

于是你应该想到了,我们可以使用线程。相比于进程来讲,这样要轻量级的多。如果创建进程相当于成立新公司,购买新办公家具,而创建线程,就相当于在同一个公司成立项目组。一个项目做完了,那这个项目组就可以解散,组成另外的项目组,办公家具可以共用。

在Linux下,通过pthread_create创建一个线程,也是调用do_fork。不同的是,虽然新的线程在task列表会新创建一项,但是很多资源,例如文件描述符列表、进程空间,还是共享的,只不过多了一个引用而已。

img新的线程也可以通过已连接Socket处理请求,从而达到并发处理的目的。

上面基于进程或者线程模型的,其实还是有问题的。新到来一个TCP连接,就需要分配一个进程或者线程。一台机器无法创建很多进程或者线程。有个C10K,它的意思是一台机器要维护1万个连接,就要创建1万个进程或者线程,那么操作系统是无法承受的。如果维持1亿用户在线需要10万台服务器,成本也太高了。

其实C10K问题就是,你接项目接的太多了,如果每个项目都成立单独的项目组,就要招聘10万人,你肯定养不起,那怎么办呢?

方式三:一个项目组支撑多个项目(IO多路复用,一个线程维护多个Socket)

当然,一个项目组可以看多个项目了。这个时候,每个项目组都应该有个项目进度墙,将自己组看的项目列在那里,然后每天通过项目墙看每个项目的进度,一旦某个项目有了进展,就派人去盯一下。

由于Socket是文件描述符,因而某个线程盯的所有的Socket,都放在一个文件描述符集合fd_set中,这就是项目进度墙,然后调用select函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在fd_set对应的位都设为1,表示Socket可读或者可写,从而可以进行读写操作,然后再调用select,接着盯着下一轮的变化。。

方式四:一个项目组支撑多个项目(IO多路复用,从“派人盯着”到“有事通知”)

上面select函数还是有问题的,因为每次Socket所在的文件描述符集合中有Socket发生变化的时候,都需要通过轮询的方式,也就是需要将全部项目都过一遍的方式来查看进度,这大大影响了一个项目组能够支撑的最大的项目数量。因而使用select,能够同时盯的项目数量由FD_SETSIZE限制。

如果改成事件通知的方式,情况就会好很多,项目组不需要通过轮询挨个盯着这些项目,而是当项目进度发生变化的时候,主动通知项目组,然后项目组再根据项目进展情况做相应的操作。

能完成这件事情的函数叫epoll,它在内核中的实现不是通过轮询的方式,而是通过注册callback函数的方式,当某个文件描述符发送变化的时候,就会主动通知。

img

如图所示,假设进程打开了Socket m, n, x等多个文件描述符,现在需要通过epoll来监听是否这些Socket都有事件发生。其中epoll_create创建一个epoll对象,也是一个文件,也对应一个文件描述符,同样也对应着打开文件列表中的一项。在这项里面有一个红黑树,在红黑树里,要保存这个epoll要监听的所有Socket。

epoll_ctl添加一个Socket的时候,其实是加入这个红黑树,同时红黑树里面的节点指向一个结构,将这个结构挂在被监听的Socket的事件列表中。当一个Socket来了一个事件的时候,可以从这个列表中得到epoll对象,并调用call back通知它。

这种通知方式使得监听的Socket数据增加的时候,效率不会大幅度降低,能够同时监听的Socket的数目也非常的多了。上限就为系统定义的、进程打开的最大文件描述符个数。因而,epoll被称为解决C10K问题的利器

总结

  • 你需要记住TCP和UDP的Socket的编程中,客户端和服务端都需要调用哪些函数;
  • 写一个能够支撑大量连接的高并发的服务端不容易,需要多进程、多线程,而epoll机制能解决C10K问题。
原文地址:https://www.cnblogs.com/wwj99/p/12891451.html