1.UDP协议
1.1端口号范围划分
端口号是16位的,范围 0 ~ 65535,共 65536 个。其中 0 -1023 是知名端口号,通常分配给 HTTP(80)、FTP(21)等常用协议。在类 Unix 系统中,只有特权用户(root)或拥有相应能力的进程才能绑定这些端口,普通用户进程不能绑定知名端口。1024-65535是操作系统动态分配的端口号,客户端程序的端口号就是由系统从这个范围分配的。一个进程可以分配多个端口号,但是一个端口号只能对应一个进程。
1.2UDP协议格式
UDP的报头固定是8字节的,分为四部分:源端口号、目标端口号、UDP长度、UDP检验,但是16位的。所以一个完整的UDP报文是8字节的报头加上有数据成的(注意可以没有数据)。如下图。

1.2.1UDP的特点
UDP传输的特点为:无连接、不可靠、面向数据报。其中无连接就是知道对端IP和端口号就可以直接进行传输,不需要建立链接。不可靠是没有确认和重传机制,也就是说如果丢包了UDP也不会做任何处理。面向数据报就是一次发送一个报、一次收一个包,不会出现数据粘包问题。UDP没有真正意义上的发送缓冲区,但是会有接收缓冲区。但是这个接收缓冲区不保证收到UDP报文的顺序一致。如果缓冲区满了,那么后续到达的UDP数据就会被丢弃。UDP的setsock即能读也能写,这个概念叫做全双工。
1.2.2UDP注意事项
注意在UDP报头里有一个长度为16位UDP长度的属性,也就是说一个UDP报文最大位64kb(包含UDP首部)。所以当我们需要传输64kb以上的数据时,要在应用层手动进行分包,并在接收方进行拼接。
1.2.3内核中的报文
在 Linux 内核中,网络报文用 struct sk_buff 表示。head 和 end 指向缓冲区起始和结束地址。data 和 tail 指向当前协议层的有效数据起始和结束位置。当上层数据(如应用数据)到达传输层:data 指针向前移动(减去协议头长度),在新空出的区域填入传输层头部,更新 tail(一般不移动,除了添加尾部选项),然后传递给网络层,重复 data 前移和填充头部的过程,直至链路层发送。
2.TCP协议
TCP全称为"传输控制协议"(Transmission Control Protocol),人如其名TCP协议会对要传输的数据进行一个详细的控制。
2.1TCP协议段格式
TCP协议报头分为以下部分:16位源端口号、16位目标端口号、32位序号、32位确认序号、4位首部长度、6个保留位、6个控制标志位、16位窗口大小、16位校验和、16位紧急指针、选项。
32位序号用来表示本报文段所发送数据的第一个字节在整个数据流中的序号,用于保证数据的顺序性和可靠性,而不是作为报文的唯一标识。
32位确认序号表示期望收到对方下一个报文段的第一个数据字节的序号,它隐含地确认了该确认号之前的所有字节都已经成功收到。
4位首部长度最大值为15,但并不是说报头最大只有15字节,而是表示TCP头部有多少个4字节。因此TCP头部最大长度为60字节。除去选项之外,固定有20字节的报头,所以TCP报头的大小范围是20到60字节。
6个控制标志位每个占1个比特位。URG表示紧急指针是否有效。ACK表示确认号是否有效。PSH表示要求接收端应用程序立即从TCP接收缓冲区中读取数据,不要等缓冲区满。RST表示要求对方重新建立连接,携带RST标识的称为复位报文段。SYN表示请求建立连接,携带SYN标识的称为同步报文段。FIN表示通知对方要断开连接。
16位窗口大小表示接收端接收缓冲区的接收能力。
16位校验和由发送端填充,使用CRC校验。接收端校验不通过则认为数据有问题。需要注意的是,此处的校验和不仅包含TCP首部,也包含TCP数据部分,此外还包括一个12字节的伪首部(包含源IP、目的IP、协议号等)。
16位紧急指针指出数据中哪一部分是紧急数据。需要特别说明的是,紧急数据的大小并没有被限制为1字节,这是一个常见误解。紧急指针指向的是紧急数据的末尾位置,紧急数据可以有多字节。
2.2确认应答(ACK)机制
在网络通信中有一个问题是,我发送的信息对方到底收到没有?其实不止网络,现实交流中依然会面临这个问题。在TCP中是通过确认应答机制来解决这个问题的,也就是说主机A给主机B发送一个消息,B在收到信息之后必须要回复一个应答。当A收到B的应答之后就可以确定之前发送的信息B收到了。这种一个信息加一个应答的方式就叫做确认应答机制。
如果每次都是发一次信息后等到应答之后再发送下一条信息的效率比较低。所以TCP通信通常采用连续发送多个报文段的方式,但接收方并不会对每一个报文段都单独回复一个应答,而是使用累积确认。接收方只回复一个确认序号,表示该序号之前的所有数据都已经收到。
例如,A发送1~1000字节的数据给B,B在完全收到这些数据之后,会发送一个确认序号为1001的报文给A,意思是1000及以前的数据已经全部收到了,之后请从1001开始发送。
然而在应答时通常不只是发送单纯的应答报文,而是在应答报文里带上要发送的数据。这种在应答报文里带上要发送的数据的方式就叫做捎带应答。

2.3超时重传机制
超时重传就是在特定时间间隔内发送方没有收到接收方发送的确认应答,就会进行重新发送。导致超时重传的情况有两种:第一种是发送方发送的数据丢包了,接收方没有收到;第二种是接收方收到数据了,但是发送回的确认应答丢包了。这两种情况不可能同时出现。如果是情况二发生,接收端就会收到重复数据,这时就可以通过序号来区分重复包来进行去重。
超时的时间不能太长,否则会影响整体重传效率,也不能太短,否则有可能频繁发送重复的包。并且随着网络波动,超时时间也应该是动态调整的。
如果重传一次之后还是没有得到应答,就会等待指数倍数的超时时间后重新发送,这个机制称为指数退避。重传间隔通常会设置一个上限值,达到上限后不再继续增长。如果多次重传后仍然得不到应答,累计一定次数后TCP会认为对端主机或者网络出现异常,会强制关闭连接。

2.4链接管理
2.4.1TCP三次握手
TCP通信是需要建立连接的,如果需要建立连接的话就需要建立一个共识,即双方都能发送并接受到对方的消息。那么建立这个共识需要几次通信呢?逻辑上应该为4次。将想要建立连接的一方叫做A,被连接的一方称为B(一般为服务端)。首先A向B发送SYN报文请求连接,B在收到之后需要回复ACK确认报文同时也要发送SYN报文,这两个报文会合并成为一个SYN+ACK报文发送给A,最后A发送ACK报文确认。
既然是四次通信那么为什么叫三次握手呢?实际上因为捎带应答机制的存在,B发送ACK和SYN会被合并成为一个报文,所以叫做三次握手。
A在发送SYN报文之后会进入SYN_SENT状态,等到A接收到B的SYN+ACK报文后再发送ACK报文之后就会进入ESTABLISHED状态,注意此时A就已经认为连接建立成功了。B在收到A的SYN报文之后就会进入到SYN_RCVD状态,再收到A的ACK报文后才进入ESTABLISHED状态,此时B才认为连接建立成功了。
在三次握手的过程中也可能出现丢包问题,一共三种情况:第一次握手丢包、第二次握手丢包、第三次握手丢包,分别对应不同的处理方法。如果是第一、二种情况直接超时重传就可以了。如果是第三种情况,因为A已经认为连接成功了,这时如果A给B发送信息而B认为连接还没建立成功,就会给A发送RST报文要求A重新建立连接。但需要注意的是,如果A的第三次握手ACK和A发送的业务数据同时到达或顺序合适,B收到后可能会正常进入ESTABLISHED状态而不会发送RST。
三次握手都是由操作系统自动完成的,所以建立连接和accept函数没有直接关系,accept只是从已完成连接队列中取出已经建立好的连接。
2.4.2四次挥手
四次挥手是通信双方想要结束通信时进行的操作,需要应用程序主动调用close或shutdown函数来触发。跟三次握手类似,想要结束通信必须建立结束的共识。四次挥手在步骤数量上类似于逻辑上的四次通信,只不过发送的报文不一样。四次挥手时主动关闭方发送FIN报文,被动关闭方收到之后发送ACK确认应答报文,被动关闭方再发送FIN报文,主动关闭方接收之后再发送ACK确认应答报文。
客户端在调用close之后开始发送FIN报文,此时客户端的状态为FIN_WAIT_1,在接收到服务端的ACK报文之后进入FIN_WAIT_2状态,之后收到服务端的FIN报文后发送ACK报文进入TIME_WAIT状态,在TIME_WAIT状态停留一段时间后再进入CLOSED状态。服务端在收到FIN报文并发送ACK报文后会进入CLOSE_WAIT状态,等到服务端调用close函数时再次发送FIN报文,进入LAST_ACK状态,在收到客户端的ACK后会直接进入CLOSED状态。
既然三次握手和四次挥手在步骤数量上不同,那为什么四次挥手不是三次挥手?因为在客户端发送FIN断开连接请求时,服务端可能还有数据要给客户端发送,所以不能捎带应答的把FIN带上。那么为什么客户端发送确认应答报文之后,不能直接进入CLOSED状态而是在TIME_WAIT状态等一会?因为发送的ACK报文可能会在网络中阻塞,这时如果客户端立即重新连接服务端,连接成功之后发送信息,上一次挥手发送的陈旧报文到达了就会干扰服务端。因此为了避免这种情况的发生,就需要TIME_WAIT状态等待一会。
TIME_WAIT等待这段时间有两个作用。第一,等待的这段时间内逻辑上客户端的连接还没有断开,如果立即重新连接服务端,客户端可以选择更换端口号,端口号更换之后陈旧报文根据四元组不匹配就不会影响新连接。第二,等待的这段时间可以让网络中的陈旧报文自然消散。
TIME_WAIT具体等待多长时间?答案是2MSL,MSL是TCP报文的最大生存时间。TIME_WAIT持续2MSL可以保证客户端发送的最后一个ACK能够到达服务端。如果ACK丢失,服务端会在超时后重传FIN,客户端在TIME_WAIT期间仍然可以收到这个重传的FIN并再次回复ACK。如果只等待1MSL,服务端重传的FIN到达时客户端可能已经关闭,会回复RST,服务端会认为连接异常关闭而不是正常完成四次挥手。

2.5滑动窗口
前面曾提到TCP通信可以一次发好多个报文,之后对端再发送确认应答报文。那么这样一次发送很多报文却暂时不用等待应答是怎么实现的呢?其实就是通过滑动窗口来实现的。
滑动窗口是发送方发送缓冲区的一部分 。发送方的发送缓冲区分为四个区域:已发送且已确认、已发送但未确认、允许发送但尚未发送、不允许发送。滑动窗口覆盖的是"已发送但未确认"和"允许发送但尚未发送"这两部分区域。滑动窗口的大小由接收方通告的窗口大小和发送方的拥塞窗口共同决定,取两者的较小值。滑动窗口的左边界是已发送且已确认的下一字节,右边界是左边界加上窗口大小。窗口只能从左向右滑动,当收到确认应答时,左边界右移到下一个未确认的序号处,右边界也随之右移。由于TCP序号空间是循环使用的,当序号接近最大值时会自然回绕,但这并不影响窗口的滑动逻辑。
既然滑动窗口可以允许一次发送多个报文,那么如果其中有数据丢包了怎么办?丢包可以分为三种情况:最左侧数据丢包、中间数据丢包、最右侧数据丢包。无论哪种情况,最终都会表现为最左侧未确认的包迟迟收不到确认。如果是第一种情况,接收方返回的每一条确认应答报文都只会重复确认丢包位置之前的序号。如果客户端连续接收到3个相同的确认序号,就说明该确认序号之后的那一个序号的数据一定丢失了,这时系统会直接重传丢失的数据,这种机制叫做快重传。中间或右侧的数据丢包时,滑动窗口会卡在丢包位置无法右移,情况与第一种实质上相同。因此丢包检测的核心始终围绕最左侧未确认的包展开。
2.6流量控制
所谓的流量控制其实就是根据对端的窗口大小和网络拥塞程度共同来决定下一次发送数据的速度,可以理解为滑动窗口大小,但是滑动窗口的大小又并不完全由这个决定。
当对端窗口大小为0时,发送端主机已经不能继续发送数据了,那么这种对端窗口为0的情况怎么处理呢?实际上如果对端窗口大小为0之后,发送端也会向对端发送窗口探测报文,只不过发送的是携带1字节数据 的报文(不是纯ACK),对端收到报文后返回应答报文时就可以检测对端窗口变化了。如果返回的窗口大小依旧为0,则会继续发送窗口探测报文,如果累计一定次数后依旧为0,发送端不会发送PSH报文,而是最终因重传超时而关闭或重置连接。

2.7拥塞控制
刚刚我们说的流量控制只是考虑了对端的窗口大小,实际上影响发送端发送速率的还有网络的拥塞情况。也就是说滑动窗口的右端大小是对端窗口大小和拥塞窗口的较小值。
那么什么时候需要拥塞控制呢?网络拥塞的具体表现主要是丢包率。TCP通信整体丢包率而言,小于0.1%是正常情况,0.1%~1%是轻微拥塞,1%~5%是一般拥塞,5%及以上就是非常拥塞了。
在拥塞情况下继续重传就很可能会导致拥塞更加严重,那么TCP是如何解决的呢?TCP引入慢启动机制来解决。慢启动就是在发生拥塞丢包后,将拥塞窗口设为1,并以指数方式增长(每收到一个ACK增加1)。但也不会一直指数增长,当拥塞窗口达到或超过慢启动阈值(ssthresh,通常设为丢包发生时窗口大小的一半)之后,就改为线性增长。当再次遇到网络拥塞时,再更新ssthresh的值,重复以上步骤。
不论是拥塞控制还是流量控制,都是通过滑动窗口这个机制来实现的。反过来,滑动窗口的大小变化也完全由这两种控制策略驱动。所以从实际运作来看**,滑动窗口与拥塞控制、流量控制可以视为相互依存、互为因果的关系**------没有控制目的就不需要滑动窗口,没有滑动窗口控制策略也无法落地。

2.8延迟应答
既然应答报文的确认序号是确认收到该序号之前的所有报文,那么是不是不用每条报文都回复应答报文、效率更高?答案是肯定的,但要考虑延迟副作用。
那么具体怎么回复呢?一般采用延迟确认(Delayed ACK) 策略:可以每隔 N 个报文段回复一次 ACK,也可以超过一定时间后强制回复一次。N 通常为 2(每收到 2 个完整报文段后回复一个 ACK),超时时间在不同操作系统中不同,常见范围是 40ms ~ 200ms,例如 Linux 约为 40ms 或 200ms,Windows 约为 200ms。
2.9TCP异常情况
TCP 异常情况大致分三类:进程终止、机器重启、机器断电/网线断开。进程终止 :操作系统会释放该进程的文件描述符,socket 被关闭后会主动发送 FIN,发起 四次挥手。但能否完成挥手取决于对端是否正常以及网络是否通畅。机器重启 :操作系统会先终止所有进程,进程关闭 socket 并尝试发送 FIN。但重启过程中协议栈会关闭,FIN 不一定能发出去或送达,所以不等同于普通进程终止 。机器断电/网线断开:发送端突然掉线,无法发送 FIN。接收端并不知道对端已断开,会认为连接仍然正常。此时接收端发送数据不会收到应答。
TCP 提供了一个保活机制(Keep-Alive,需通过 SO_KEEPALIVE 开启),开启后会定期询问对方是否还在,经过一定次数无响应后就会释放连接。但 TCP 保活的时间参数(空闲时间、探测间隔、探测次数)在系统中可以调整,并非完全固定。
TCP 保活机制需要开启 SO_KEEPALIVE 才能生效,而且默认空闲时间很长(Linux 为 2 小时),参数调整也不够灵活。更重要的是,TCP 保活只能检测连接是否存活,无法知道对端应用是否正常。因此在实际开发中,通常使用应用层保活(如自定义心跳),可以根据业务场景灵活设置心跳间隔(比如 30 秒一次,连续 3 次无响应就断开),并且心跳报文还可以携带业务数据,实现更精准的健康检查。