目录
UDP
协议格式
UDP协议的格式如下图所示:

任何协议,都要解决两个问题,一个是报头和有效载荷如何分离的问题,另外一个则是有效载荷分用的问题。
对于第一个问题,UDP采用固定长度的报头,前8个字节为UDP报头,其中,16为UDP长度就可以表示整个UDP报文的长度,以此就可以解决报头和有效载荷分离的问题。对于第二个问题,UDP报头中有一个16位的目的端口号,通过这个就可以实现分用。
同时,UDP不存在数据报粘包问题,因为UDP报文长度都是固定的,UDP能识别内部的数据,不存在粘包的问题。
UDP协议在操作系统中本质也是一个结构体,通信的双方可以互传结构体变量。封装的过程就是将结构体变量进行拷贝的过程。
在操作系统内部,可能会同时存在多个报文,那么操作系统就要对这么多的报文进行进行管理,管理的方式为"先描述,再组织",这个报文的数据结构在内核中为sk_buff,同时,任何一个报文都会在内核中开辟一个缓冲区,对这些报文进行管理,本质就是对这些sk_buff进行再组织,报文贯穿整个协议栈,封装和解包的过程,最核心的就是移动指针的过程。
面向数据报
UDP是面向数据报的,应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并。如果发送端一次调用sendto,发送100个字节,那么接收端也必须调用对应的以此recvfrom,接收100个字节。
缓冲区
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;UDP有接收缓冲区,但这个接收缓冲区不能保证到UDP报的顺序和发送UDP报的顺序一致,如果缓冲区满了,再到达的UDP数据就会被丢弃。
注意事项
UDP协议首部有一个16位最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部),如果需要传输的数据超过64K,就需要在应用层进行分包,多次发送。
TCP
协议格式
TCP协议的格式如下图所示:

TCP报文的报头是可以携带选项的,标准TCP报头的长度为20字节,但是由于选项的存在,这个报头长度是浮动的。在报头中存在4位首部长度,取值范围为0000-1111,也就是0-15,但是这个范围过小,为了能将报头整体衡量出来,因此首部长度是以4字节为单位,写入时要乘4,读取时要除以4,因此,4位首部长度对应的取值范围为[0,60],又因为标准报文的长度为20,所以报头长度的取值范围为[20,60],4为首部长度的范围也就为[5,15]。通过这个就可以实现报头和有效载荷的分离,报头中有一个目的端口号,通过目的端口号就可以实现分用。
可靠性传输
TCP协议为可靠性协议,可靠性最本质的一个特点,就是要对发出去的报文,要有应答,收到应答后,历史报文100%被对方收到。发送报文的过程中,只需要保证历史报文的可靠性即可,通信的双反,发送报文,对报文进行应答,就能保证报文本身的可靠性,这就是确认应答机制,不管接收方有没有收到报文,发送方都得知道。同时,TCP可以在两个朝向上保证可靠性,这也是全双工式的可靠性,如下图所示:

可靠性的表现并不是保证报文能100%收到,这是网络所需要的保证,而是在接收方收到报文后,发送方得知道;相应地,如果发送方没有收到应答报文时,也得知道,发送方没有收到报文,有两种可能,一种是报文真的丢失了,这种发送方肯定不会收到应答;另一种可能是应答丢失了,这种发送方还是不会收到应答,无论是那种情况,发送方都会判定报文丢失了。
但是发送方如何知道自己会不会收到应答呢?这时发送方会设置一个超时时间,超过这个时间发送方就会判定丢包。规定这个超时时间,主要是因为,站在发送方的视角,无法判断是数据真的丢失了,还是应答丢失了,对发送方而言只是没有应答,都需要进行重传,重传是对没有收到报文之后的补发措施。
序号与确认序号
若发送方发送的报文真的丢失了,只需要重传就行了;但若是应答丢失了,接收方就会收到重复的报文,收到一样的报文就需要去重,所以报文中需要带序号,序号的核心用途就是用来去重。
在TCP通信中,通信双方的地位是对等的,发送报文的方式有常见的两种,第一种是发送方发送报文后,收到接收方的应答后才能接着发送后面的报文,整个过程是串行的,如下图所示:

这是最朴素最简单的一种方式。但是这种发送方式效率过低,因此不用。真实情况是,A给B发一批报文,B对每一个报文进行应答,如下所示:

由于网络情况复杂,不同的报文的传输路径可能不一样,在这种情况下,会出现接收方收数据的乱序问题,乱序是一种不可靠,因此必须要解决乱序问题,需要通过序号来保证报文的按序到达。因此,通信双方的无论是发报文还是发应答,都需要带序号。
B收到报文后对每一个报文进行应答,如果某一个应答丢了,如果不对丢失的应答加以区分,那么A也就不知道哪一个报文丢失了,此时就需要确认序号,确认序号是自己收到的序号+1,表示的含义是,该序号之前的所有数据都收到了。例如,A发送的报文中的序号seq为1000,那么应答报文中的序号ack seq为1001,代表1001序号之前的所有数据都收到了,同时也告诉发送方下一次报文可以从1001开始发了。再来看一个场景,A向B发送了序号为1000,2000,3000,4000的报文,那么B就要向A发送确认序号为1001,2001,3001,4001的应答,若3001的应答丢了,此时A还需要重传序号为3000的报文吗?答案是不要的,因为A已经收到了确认序号为4001的应答,也就是说B一定收到了4001之前的报文,那么也就是说不是报文丢失了,而是应答丢失了,这时就不需要重传了,这个机制可以有效减少报文的重发次数。
在双方通信的过程中,可能还会有一种情况,A给B发报文的同时,B给A也会发送报文,B给A发送应答的时候,可能也会将数据发送A,也就是B在给A应答的时候也给A发消息,这这种情况称为捎带应答,由于捎带应答的存在,发送数据和发送确认就可以同时进行了,因此,就必须区分序号和确认序号。
窗口大小
之前介绍TCP发送和接收的过程中,有一个示意图如下所示:

如果发送的太快,接收缓冲区可能来不及接收,因此,接收方需要衡量自己的接收能力,而衡量接收能力的,就是接收缓冲区中剩余空间的能力。TCP报头中有一个16位窗口大小,这个是接收缓冲区的大小,A与B通信时,A给B发送报文,B必须要有应答,B应答时会发送对应的TCP报头,会包含16位窗口的大小,会将自己的接收能力,填写到应答报文的16位窗口大小中,A收到对应的报文后,就知道B的接收能力是多少了,A就可以根据B的接收能力来动态调整发送数据的速度了。同样的,一开始A给B发数据时,也会携带自己16位窗口大小的信息,确保B能知道A的窗口大小。
标志位
TCP中的报文也分为各种类型,有应答报文、建立连接的报文、断开连接的报文、数据报文等,报文本身也是有类型的,报文类型不同,对报文的处理也就不同,因此TCP协议报头中,就必须有对应的字段表明报文的类型,而标志位就是区分报文类型的。
第一个标志位ACK,ACK用来确认号是否有效。当主机B给A发送应答报文时,主机A通过B发送的报文中的ACK来确认报文是否为应答报文,ACK为1为应答报文。在TCP报文中存在捎带应答,因此,大部分报文ACK都为1,只有少数一开始发送的报文ACK为0。
SYN报文为连接建立的请求,将携带SYN标识的称为同步报文段。建立连接的过程要经理三次握手的过程,三次握手是为了确保通信的双方既能收也能发,确保全双工通信,如下图所示:

两次握手建立连接,能确保发送方发数据和收数据正常,但是接收方只能确保接受数据正常,不能保证发送数据正常,这也是三次的原因。在网络中,双方操作系统会存在大量连接,操作系统要管理这些连接,管理的方式依旧为"先描述,再组织",通过构建连接结构体来表明双方的连接。建立连接是有成本的,包括花费的内存空间,以及初始化和管理工作等,都是需要空间和时间的。
FIIN为结束连接的请求,用于通知对方,本端要关闭了。TCP连接终止时,要进行四次挥手的过程,如下图所示:

四次挥手是以最小成本的方式争得了对方的同意,之后操作系统内建立的连接结构体就会被自动释放,在代码层面的表现就是通信双方调用了close函数,将文件的相关信息释放,四次挥手的本质就是关闭全双工。
同时,四次挥手也可以将中间两次进行压缩,变为捎带应答,四次挥手也可以转为三次挥手如下所示:

三次握手,本质也是四次握手,因为,建立连接的整个过程,即便是一方主动,整个过程也需要征得双方同意,本质是双方主机建立共识的过程;同时,在这个过程中,也验证全双工,验证网络是否通畅。建立连接的过程,服务器必须无条件答应,所以通常是三次握手,但断开连接的过程可能还有上层业务以及通信逻辑要处理,所以是四次挥手。
RST标志位为连接复位标志位,将携带RST标识的称为复位报文段。在三次握手建立连接的过程中,最后一次发送ACK没有应答,因此这个报文能不能被服务器收到是不确定的,客户端只要把最后一个携带ACK的报文发送出去了,客户端就认为连接已经建立了,若最后一个携带ACK的报文丢失了,服务端此时处于一个半连接的状态,但是,客户端此时可能就要开始发送数据了,服务器此时收到了正常的数据,但由于服务器还没有完全建立起连接,此时,服务器向客户端发送一个携带RST=1的报文,告诉客户端连接建立异常。如下图所示:

在访问浏览器时,可能会出现连接被重置的错误信息,这种情况的其中一个原因就是路由器出现了故障,服务器长时间收不到客户端发来的最后一个ACK报文,客户端直接服务器发消息,此时服务器就会给客户端发送携带RST的报文,要求重新连接。
PSH标志位全称为PUSH,当双方通信的时候,若服务端发送的报文中窗口大小为0,说明此时服务端就不能再接收数据了,此时客户单无法向服务端发送数据,但此时客户端此时可以向服务端发报头,周期性地询问服务端的缓冲区是否有空间,若长时间还是没有空间,此时客户端就会向服务端发送一个携带PSH=1的报文,要求服务端通知应用层尽快把数据取走,PSH就是要求对端立即把数据取走,保证剩余空间,PSH本质就是让操作系统告诉应用进程,数据已经准备就绪了。
URG标志位代表紧急指针是否有效,16位紧急指针本质是一种偏移量,用来标识哪部分数据是紧急数据,这里的指针指的是序列号,如下图所示:

确认应答机制
通信的双方一方发送数据时,另一方必须进行应答,如下图所示:

A和B的通信地位是对等的,A通过收到收到应答,就可以保证A向B的可靠性,B同样通过一样的方式确保B向A的可靠性。在确认应答机制中,需要保证的是数据,而不是确认应答,收到应答,是对历史报文的可靠性保证。
超时重传机制
若出现了数据丢失问题,可能有两种情况,一种是数据丢失了,如下图所示:

如果数据丢失了,主机A一定不会有应答,A一直等待应答时没有意义的。
另外一种情况是确认应答丢失了,如下图所示:

应答本身不保证可靠性,此后,主机B短期就不会再发确认应答。此时A一直等待应答也是没有意义的。
对于A来讲,无论是数据丢失还是应答丢失,作为发送方,永久等待应答是没有意义的。因此,必须规定一个特定的时间间隔,到了特定时间间隔没有收到应答,就判定为丢包,此时就进行超时重传。
如果超时时间设定的太长,会影响整体的重传效率;如果超时时间设的太短,有可能会频繁发送重复的包,TCP为了保证无论在任何环境下都能比较高性能的通信,会动态计算这个最大超时时间。Linux中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。如果重发一次之后,仍然得不到应答,等到2*500ms后再进行重传,如果仍然得不到应答,等待4*500ms进行重传,依次类推,以指数形式递增。累计达到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
若仅仅是应答报文丢失,接收方收到重复的报文根据序号进行去重,原则上是去除老数据,保留新数据。
连接管理机制
TCP通信之前要通过三次握手来建立连接,断开前要通过四次挥手来断开连接,如下图所示:

在三次握手建立连接的过程中,客户端和服务端是有对应的状态变化的,客户端只要一发送SYN,客户端此时就处在SYS_SENT同步发送状态,服务器收到了SYN,服务器就处于SYN_RCVD同步收到状态,此时会立即发送SYN+ACK应答,客户端收到后,变为ESTABLISHED连接建立的状态,三次握手的过程,每一次状态变化都是从"发出"或者"收到"开始算的,只考虑自己即可,不用考虑网络的问题。三次握手的过程是由双方TCP层自主完成,应用层的相关函数只是触发或者获得对应的信息,不参与三次握手。
双方通信时,阻碍双方通信可能是双方是否愿意通信,以及网络是否通畅的问题。三次握手可以将网络问题先验证,在正式通信前,客户端和服务端保证一定能发数据和收数据,因为客户端发送了SYN,服务端也发送了对应的响应报文,双方都验证了自己既能收数据也能发数据,验证了自己的全双工,也就以最小成本验证了网络问题。至于双方是否愿意通信,客户端发送了携带SYN的报文,服务端收到后发送了ACK应答,这一过程说明c端得到了s端的许诺,同时服务端发送携带SYN的报文,客户单收到后发送了ACK应答,这一过程就说明s端得到了c端的许诺,此时双方都愿意和对方通信。而服务器对正常请求,总是愿意的,所以中间两次就成了捎带应答,三次握手可以以最小方式验证双方意愿的问题。
在正式通信的过程中,write和read相关函数,只是将数据进行拷贝而已,至于数据啥时候发,出错了如何控制,都是由TCP层自己控制,同时,往网络写数据时,ACK应答等用户也感知不到,用户只需要关心被ACK过的数据,通过read读取数据即可,不需要知道中间的过程。应用层只是发起或者参与一部分过程。
若双方不想通信了,就要断开连接,此时就要进行四次挥手过程,双方都要得到的对方的许诺才能断开连接,四次挥手时双方都可以作为主动断开连接的一方,以客户端主动断开连接为例,客户端不写数据时,就会告诉服务端,客户端对服务端的信道就可以关掉了,服务端收到后就会发送应答,发送完应答后,可能服务端还会接着发送数据,当服务端发送数据完成后,也会告诉客户端数据发送完成,服务端对客户端的信道就可以关掉了。断开连接的本质,就是一方不写了。在双方的状态都变为CLOSED前,连接的结构体并没有被释放,连接还存在,只是状态改变,双反不写数据了而已,断开连接,不一定是彻底释放,只是不写用户数据,不代表不发送报头和标志位。
在没有accpet的时候,连接能够建立成功,但是接收端能都维持的暂时不用的accept到应用层的连接个数是有上限的,这个上限为listen的第二个参数backlog+1,暂时不用的连接保存在全连接队列中。
四次挥手之后,主动断开连接的一方,进如TIME_WAIT状态,被动断开立即释放连接,此时主动断开连接的一方还没有关闭,端口号依旧被占着,可以使用setsocket地址复用,将处于TIME_WAIT状态的端口号复用,主动断开的一方要等待一个2MSL(报文最大生存时间)的时间,才会进入CLOSED状态,这是因为客户端最后一次发送ACK应答时,要确保这个应答到达,等待2MSL,是为了保证最后这个应答可以送到对端,即便这个报文丢失,对端会重新发送携带FIN的报文。等待2MSL,一方面是为了确保双方4次挥手都尽可能正确完成,另外一方面,是为了让陈旧报文在网络中尽可能消散。
流量控制
接收端处理数据的能力是有限的,如果发送端发的速度过快,导致接受端缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。因此TCP需要根据接收端的处理能力,来决定发送端的发送速度,这也就是TCP中的流量控制。如下图所示:

若发送端根据接收端发来的窗口大小判断出接收端没有能力接收数据时,之后发送端会定期向接收端发送窗口探测报文,若接收端窗口大小更新了,接收端也会向发送端发送窗口更新通知,两个策略同时使用,互不干扰。
流量控制不仅可以降低报文的发送速度,若接收端的接收能力较好,流量控制也可以加快发送端的发送速度。实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位。
滑动窗口
之前介绍过,双方通信时,发送端一次可以向接收端发送一批报文,然后接收端再对每一个报文进行应答,这个过程是通过滑动窗口实现的。
在发送缓冲区中,存在一部分区域,在这个区域内的数据可以不用暂时收应答,直接发送,这个区域就叫滑动窗口,滑动窗口是发送缓冲区的一部分区域。收发缓冲区可以当做数组来看,那么滑动窗口在逻辑上也就是一个线性结构,通过两个下标来指向缓冲区内的一部分区域,滑动窗口之后的数据为待发送数据,滑动窗口里的数据为正发送数据,滑动窗口之前的数据为已发送数据,如下图所示:

正常情况下,在最开始的时候,滑动窗口大小=min(ACK-win, 真实发送的数据量),这里我们只考虑初始为ACK-win的情况,如下图所示:

初始时滑动窗口大小为4000,一开始主机A直接将四个报文发了过去,当主机A收到确认应答,下一个是2001后,此时start_win设置为ACK对应的序号,即start_win=2001;
同时,主机A还会收到主机B的接收能力,收到后对end_win做更新,end_win=start_win+ACK_WIN。
通过以上过程,就可以实现收发数据的动态调整,当对方的接收缓冲区的剩余空间越来越小,或者上层接收数据过慢,滑动窗口就会变小,流量控制在底层就是靠滑动窗口来实现的,当滑动窗口的大小为0时,就要进行流量控制;当对方缓冲区的剩余空间变大时,滑动窗口也会变大;滑动窗口也可以不变。滑动窗口大小的变化,取决于收到的应答报文中的接收能力相比较于上一次是变大了还是变小了。
异常情况可能有两种可能,一种是数据报已抵达,但ACK被丢了,如下图所示:

这种情况下,部分ACK丢失不要紧,因为可以通过后续的ACK进行确认。
第二种情况下,就是数据包丢失了,如下图所示:

当某一段报文丢失之后,发送端只会收到1001的ACK报文,相当于主机B一直在告诉主机A,想要的是1001,如果主机A一直收到1001的ACK报文,就会将对应的数据1001-2000重新发送,这个时候接收端收到了1001之后,再次返回的ACK就是7001,因为2001-7000的数据接收端之前已经收到了,被放到了接收端操作系统内核的接收缓冲区中,这种机制也被称为"高速重发控制",也称快重传。
拥塞控制
之前的策略,都只和发送端和接收端有关,TCP能解决因为软件问题引起网络出现临时可恢复的问题。而在实际情况中,也要考虑报文在网络当中的情况,在TCP中,对网络有一个重要的控制策略,也就是拥塞控制。在网络中出现报文丢失,是非常正常的现象,但是出现了大量报文的丢失问题,那么就是网络问题了,此时就不能重传了,如果重传会加速网络的拥堵情况。若出现拥堵了,就要进行拥塞控制。如下图所示:

一旦出现拥堵,A的发送速度大大减少,如果B能正常应答,再逐渐加快发送报文的速度,这个就是TCP的慢启动机制,先发少量的的数据探探路,试探网络的拥堵状态,再决定按照多大的速度传输数据。
在拥塞控制中,还存在拥塞窗口,拥塞窗口用来衡量当前网络的拥塞情况,当发送的报文超过了拥塞窗口的大小,可能会发生网络拥塞,它是用来衡量网络接收能力的指标。滑动窗口、win窗口和拥塞窗口的关系如下所示:
滑动窗口 = min(win窗口,拥塞窗口)
当网络出现拥塞时,主机A会根据B的应答动态调整拥塞窗口大小,然后进而动态调整滑动窗口大小。
慢启动阶段,发送报文的过程前期阶段较慢,一旦到达一定程度会呈指数级增长,也就是恢复快的特点,可以让A快速恢复正常发送报文的速度,这种策略是在可靠性和效率中寻找平衡点。完整的拥塞控制算法如下:

在拥塞控制中有一个慢启动的阈值ssthresh,在这个阈值前,恢复速度是呈指数规律增长的,当拥塞窗口的大小达到阈值后,恢复速度就呈线性增长了,增长到一定程度,可能又会触发网络拥堵,此时会更新ssthresh,新的ssthresh值为最近一次引起网络拥塞时拥塞窗口大小的一半,随后继续慢启动过程。
由于网络情况是变化的,那么网络发生拥堵的上限一定是变化的,所以拥塞窗口也应该是变化的,而TCP是不知道网络的变化的,因此TCP只能通过不断地试探才能知道,拥塞控制的本质就是探索当前网络的接收能力,TCP协议想尽可能快的把数据传送给对方,但是又要避免给网络造成太大压力的折中方案。
延迟应答
在接收方收到报文时,可能不会立即应答,而是会通过时间或者个数量上晚应答,等一会儿再做应答,在等待的时间内,上层应用层可能会将接收缓冲区的数据再取走一部分,延迟等一会儿可能提高给对方的应答大小,可能给发送方一个更大的接收窗口。延迟应答的本质就是通过延迟,一定概率可以给发送方通知一个更大的接收窗口。
捎带应答
在接收方收到报文做应答时,可能还会想要发送自己的数据,这是可以将两次发送过程合并为一次,这种方式就是捎带应答,在TCP双方互发消息时,捎带应答可以减少不必要的报文发送,提高报文发送的效率。
面向字节流
在TCP的socket中,有一个发送缓冲区和接收缓冲区,由于缓冲区的存在,TCP程序的读和写不需要一一匹配,可以一次写多次读,也可以一次读多次写。
TCP异常情况
之前的讨论都是针对TCP正常情况,但TCP也可能出现如下异常:
进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别。
机器重启:机器重启会先关闭进程,也是正常关闭。
机器掉线/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset,即使没有写入操作,TCP自己也内置了⼀个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放。
