前言

计算机网络基础,是一名计算机学生必须牢牢掌握的基本功。

这里安利下斯坦福大学开设的CS144课程,给想进一步了解TCP协议具体实现的小伙伴。

课程🔗:https://cs144.github.io/

这门课将带你从0到1实现一个属于自己的能在网络上通信的 TCP/IP 协议栈。

好了,开始基本的知识点梳理~

TCP和UDP的区别

UDP头部

image-20240427173840722

字段含义

  • 端口:长度16位,指定发送方所使用的端口号,若不需要对方回发消息,则可全置为0。

  • 目的端口:长度16位,指定接收方所使用的端口号。

  • UDP总长度:长度16位,指定了UDP数据报的总长度。

  • 校验和:长度16位,用于UDP的差错检测,防止UDP报文出错,同时伪首部参与计算,避免UDP用户数据报传送到错误的目的地。UDP的首部,数据部分,伪首部都会参与检验和的计算,各字段是按照16比特为单位进行计算的,因此数据部分是要保证是16比特的倍数,不够用0填充。

TCP头部

image-20240427173903612

字段含义

  • 16位端口号:源端口号,主机该报文段是来自哪里;目标端口号,要传给哪个上层协议或应用程序。

  • 32位序号:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。

  • 32位确认号:用作对另一方发送的tcp报文段的响应。其值是收到的TCP报文段的序号值加1。

  • 4位头部长度:表示tcp头部有多少个32bit字(4字节)。因为4位最大能标识15,所以TCP头部最长是60字节。

  • 6位标志位:URG(紧急指针是否有效),ACk(表示确认号是否有效),PSH(缓冲区尚未填满),RST(表示要求对方重新建立连接),SYN(建立连接消息标志接),FIN(表示告知对方本端要关闭连接了)。

  • 16位窗口大小:是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。

  • 16位校验和:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。

  • 16位紧急指针:一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

区别

  • TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接。
  • TCP提供可靠的服务;UDP不保证可靠交付。
  • TCP面向字节流,把数据看成一连串无结构的字节流;UDP是面向报文的。
  • TCP有拥塞控制;UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如实时视频会议等)。
  • 每一条TCP连接只能是点到点的;UDP支持一对一、一对多、多对一和多对多的通信方式。
  • TCP首部开销20字节;UDP的首部开销小,只有8个字节。

常见的应用协议

基于TCP的应用层协议有:HTTP、FTP、SMTP、TELNET、SSH

  • HTTP:HyperText Transfer Protocol(超文本传输协议),默认端口80
  • FTP: File Transfer Protocol (文件传输协议), 默认端口(20用于传输数据,21用于传输控制信息)
  • SMTP: Simple Mail Transfer Protocol (简单邮件传输协议) ,默认端口25
  • TELNET: Teletype over the Network (网络电传), 默认端口23
  • SSH:Secure Shell(安全外壳协议),默认端口 22

基于UDP的应用层协议:DNS、TFTP、SNMP

  • DNS : Domain Name Service (域名服务),默认端口 53
  • TFTP: Trivial File Transfer Protocol (简单文件传输协议),默认端口69
  • SNMP:Simple Network Management Protocol(简单网络管理协议),通过UDP端口161接收,只有Trap信息采用UDP端口162。

TCP是如何确保可靠性的呢?

  • 首先,TCP的连接是基于三次握手,而断开则是基于四次挥手。确保连接和断开的可靠性。
  • 其次,TCP的可靠性,还体现在有状态;TCP会记录哪些数据发送了,哪些数据被接收了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。
  • 再次,TCP的可靠性,还体现在可控制。它有数据包校验、ACK应答、超时重传(发送方)、失序数据重传(接收方)、丢弃重复数据、流量控制(滑动窗口)和拥塞控制等机制。

TCP的三次握手

假设发送端为客户端,接收端为服务端。

开始时客户端和服务端的状态都是CLOSED

image-20240427171700576

  • 第一次握手:客户端向服务端发起建立连接请求,客户端会随机生成一个起始序列号x,客户端向服务端发送的字段中包含标志位SYN=1,序列号seq=x。第一次握手前客户端的状态为CLOSE,第一次握手后客户端的状态为SYN-SENT。此时服务端的状态为LISTEN
  • 第二次握手:服务端在收到客户端发来的报文后,会随机生成一个服务端的起始序列号y,然后给客户端回复一段报文,其中包括标志位SYN=1ACK=1,序列号seq=y,确认号ack=x+1。第二次握手前服务端的状态为LISTEN,第二次握手后服务端的状态为SYN-RCVD,此时客户端的状态为SYN-SENT。(其中SYN=1表示要和客户端建立一个连接,ACK=1表示确认序号有效)。
  • 第三次握手:客户端收到服务端发来的报文后,会再向服务端发送报文,其中包含标志位ACK=1,序列号seq=x+1,确认号ack=y+1。第三次握手前客户端的状态为SYN-SENT,第三次握手后客户端和服务端的状态都为ESTABLISHED此时连接建立完成。

两次握手可以吗?

防止重复连接

  • 三次握手的主要原因是为了防止旧的重复连接引起连接混乱问题。
  • 比如在网络状况比较复杂或者网络状况比较差的情况下,发送方可能会连续发送多次建立连接的请求。
  • 如果 TCP 握手的次数只有两次,那么接收方只能选择接受请求或者拒绝接受请求,但它并不清楚这次的请求是正常的请求,还是由于网络环境问题而导致的过期请求,如果是过期请求的话就会造成错误的连接。
  • 所以如果 TCP 是三次握手的话,那么客户端在接收到服务器端 SEQ+1 的消息之后,就可以判断当前的连接是否为历史连接,如果判断为历史连接的话就会发送终止报文(RST)给服务器端终止连接;如果判断当前连接不是历史连接的话就会发送指令给服务器端来建立连接。

image-20240427172057128

「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号,且容易收到攻击。
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

个人理解:

先说前提:我认为2次握手对于客户端来说是在收到服务端发送的信息的时候算是建立成功。

解释:如果发送网络阻塞,由于TCP/IP协议定时重传机制,B向A发送了两次SYN请求,分别是x1和x2,且因为阻塞原因,导致x1连接请求和x2连接请求的TCP窗口大小和数据报文长度不一致,如果最终x1达到A,x2丢失,此时A同B建立了x1的连接,这个时候,因为AB已经连接,B无法知道是请求x1还是请求x2同B连接,如果B默认是最近的请求x2同A建立了连接,此时B开始向A发送数据,数据报文长度为x2定义的长度,窗口大小为x2定义的大小,而A建立的连接是x1,其数据包长度大小为x1,TCP窗口大小为x1定义,这就会导致A处理数据时出错。

问题

  1. 数据包长度不一致:由于A和B使用的TCP连接参数不同,数据包长度不一致,A无法正确处理B发送的数据。
  2. TCP窗口大小不一致:由于TCP窗口大小不一致,A和B之间的数据流控制会出现问题,可能导致数据丢失或重复。
  3. 2次握手的原因是混乱的连接导致客户端收到服务端的seq是错误的,导致之后会存在很多无用的网络信息。

补充问题:三次握手过程中可以携带数据吗?

其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不**可以携带数据**

答案:假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。

TCP的四次挥手

客户端A发完数据,要关闭连接。

image-20240427172134551

  1. A的应用进程先向其TCP发出连接释放报文段(FIN=1,seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认。
  2. B收到连接释放报文段后即发出确认报文段(ACK=1,ack=u+1,seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。
  3. A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。
  4. B发送完数据,就会发出连接释放报文段(FIN=1,ACK=1,seq=w,ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认。
  5. A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL(最大报文段生存时间)后,A才进入CLOSED状态。B收到A发出的确认报文段后关闭连接,若没收到A发出的确认报文段,B就会重传连接释放报文段。

什么TIME_WAIT状态?

TIME-WAIT状态指的是第四次挥手后,主动中断连接方所处的状态,这个状态下,主动方尚未完全关闭TCP连接,端口不可复用。

为什么TIME-WAIT状态需要等待2MSL?

2MSL(Maximum Segment Lifetime,最大报文段生存时间)

  • 保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在2MSL时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到CLOSED状态,若A在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到CLOSED状态。
  • 防止已失效的连接请求报文段出现在本连接中。A在发送完最后一个ACK报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段

TCP的状态转换图

状态转换图几乎出现在每一本有关TCP的教材中,可谓是经典中的经典。

TCP通信过程包括三个步骤:建立TCP连接通道(三次握手)、数据传输、断开TCP连接通道(四次挥手)

image-20240427171057056

而整个过程可以表示成TCP状态状态转换图

image-20240427171502114

详细学习这篇内容:TCP/IP 教程

TCP的滑动窗口机制

TCP 利用滑动窗口实现流量控制。

流量控制是为了控制发送方发送速率,保证接收方来得及接收。 TCP会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。接收方发送的确认报文中的window字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0,则发送方不能发送数据。

image-20240427172810673

TCP头包含window字段,16bit位,它代表的是窗口的字节容量,最大为65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。

TCP拥塞控制

什么是拥塞窗口

【拥塞窗口在发送方!】 min(接收窗口,拥塞窗口)

image-20240626231201675

防止过多的数据注入到网络中。【防止网络拥堵和丢包】 几种拥塞控制方法:慢开始( slow-start )、拥塞避免( congestion avoidance )、快重传( fast retransmit )和快恢复( fast recovery )。

image-20240427173017396

version 1

慢开始

把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值【一般MSS值1460】。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。每经过一个传输轮次,拥塞窗口 cwnd 就加倍。 为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。

  • 当 cwnd < ssthresh 时,使用慢开始算法。

  • 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。

  • 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。

拥塞避免

让拥塞窗口cwnd缓慢地增大,每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长。

无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。

快重传

有时个别报文段会在网络中丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,就会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又设置为1,因而降低了传输效率。

快重传算法可以避免这个问题。快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认,使发送方及早知道有报文段没有到达对方。

发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%。

快恢复

当发送方连续收到三个重复确认,就会把慢开始门限ssthresh减半,接着把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大。

在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。 采用这样的拥塞控制方法使得TCP的性能有明显的改进。

version 2

网络丢包的情况补充

1.缓冲区溢出(Buffer Overflow)

解释:

网络设备(如路由器、交换机)有有限的缓冲区来存储传输中的数据包。如果数据包到达的速度超过了设备处理和转发的速度,缓冲区会迅速填满。一旦缓冲区满了,新的数据包无法进入,就会被丢弃。这种情况通常发生在网络流量过高或网络设备过载时。

2.带宽限制

每条网络链路都有固定的带宽(传输速率),即每秒可以传输的数据量。如果数据流量超过了链路的带宽限制,超出的部分无法立即传输,就会导致数据包丢失。例如,在一个1Gbps的链路上,如果流量超过1Gbps,超过部分的数据包将无法通过链路并被丢弃。

3.网络拥塞

解释

网络拥塞是指网络中的流量接近或超过其处理能力,导致传输延迟增加和数据包丢失。当多个设备和应用同时使用网络资源时,可能会导致网络节点和链路的负载过重,引发拥塞。一旦发生拥塞,网络设备会丢弃部分数据包以减轻负载。

4.QoS(服务质量)策略

解释

一些网络设备和配置会应用QoS策略,根据不同的流量类型来优先处理重要的数据流。例如,视频会议和语音通话可能会被优先处理,而文件传输可能会被延后或丢弃。在网络流量过高时,低优先级的数据包可能会被丢弃以保证高优先级流量的传输。

拥塞控制和流量控制的区别

(1)拥塞控制就是为了防止过多的数据注入到网络中

(2)流量控制往往是点对点通信量的控制

【※✳※】

TCP慢启动(Slow Start)

原理:在连接开始或重传超时后,TCP初始发送的报文段数量从一个开始,逐渐增加。【慢指的是发送的数据包少】
机制:每次收到一个ACK,拥塞窗口(Congestion Window, cwnd)加倍增长,直到达到慢启动阈值(ssthresh)。
目的:逐步探测网络容量,防止突然发送大量数据导致网络拥塞。

拥塞避免(Congestion Avoidance)

原理:当cwnd达到慢启动阈值(ssthresh)后,进入拥塞避免阶段,cwmd增长速度减慢。
机制:每个RTT(Round-Trip Time)增加一个MSS(最大报文段大小),即线性增长。
目的:在网络接饱和时,防止拥塞的发生,保持网络稳定。

快速重传(Fast Retransmit)

原理:在收到三个重复的ACK后,立即重传丢失的报文段,而不等待重传计时器超时。
机制:当收到第三个重复ACK时,认为该报文段已经丢失,立即重传。
目的:快速恢复丢失的数据,提高数据传输效率。

快速恢复(Fast Recovery)

原理:在快速重传后,不进入慢启动,而是直接进入拥塞避免。
机制:将ssthresh设为当前cwnd的一半,cwnd设为ssthresh加上3个MSS(为了补偿被认为丢失的报文段),然后线性增长。
目的:避免传输速率过度降低,迅速恢复到网络负载的合理水平。

其他

粘包

粘包指TCP协议中,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

Note:只有UDP协议不存在粘包。这是由于UDP有消息保护边界,不会发生粘包拆包问题。

因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。

  • 如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。

  • 如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。

粘包和拆包示意图:

image-20240427173217720

上图中演示了以下几种情况:

  • 正常的理想情况,两个包恰好满足TCP缓冲区的大小或达到TCP等待时长,分别发送两个包;
  • 粘包:两个包较小,间隔时间短,发生粘包,合并成一个包发送;
  • 拆包:一个包过大,超过缓存区大小,拆分成两个或多个包发送;
  • 拆包和粘包:Packet1过大,进行了拆包处理,而拆出去的一部分又与Packet2进行粘包处理。

解决办法

  • 发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
  • 发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
  • 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
  • 通过自定义协议进行粘包和拆包的处理。

Nagle算法

Nagle算法(Nagle’s Algorithm)是一种用于网络传输控制的算法,旨在减少小数据包的数量,从而提高网络的效率。这个算法主要应用在TCP协议中。

背景

在计算机网络中,每个数据包的传输都会消耗一定的网络资源。如果我们发送许多小的数据包,这些资源会被大量浪费,导致网络效率降低。因此,Nagle算法的核心思想是:在发送小数据包时,先将它们暂时积累起来,直到满足一定条件后再一次性发送出去。

Nagle算法的规则

Nagle算法的工作规则如下:

  1. 当发送方有新的数据要发送时,如果当前已经有未确认(未收到确认应答)的数据包,则将新的数据暂时缓存起来,等待这些未确认数据的确认应答。
  2. 只有当所有的未确认数据包都得到了确认,或者积累的数据量达到一定大小(例如一个MSS,最大报文段大小)时,才会将缓存中的数据一起发送出去。

详细可以看这篇博客:TCP-IP详解:Nagle算法

SYN-flood攻击

SYN 洪泛攻击 (SYN flood attack)

原理

  • 利用三次握手的过程漏洞
  • 大量发送第一次握手(假IP)的报文
  • 攻击方忽略第二次握手的报文
  • 被攻击方多个TCP连接处于(SYNC-RCVD)阶段,耗费大量资源
  • 最终因为资源耗尽,拒绝服务(DoS)

解释:耗费大量资源具体指什么?

内存耗尽:大量的半连接记录占用了服务器的大量内存,可能导致内存溢出或服务器无法分配足够的内存给其他进程。

CPU过载:处理大量伪造的SYN请求使得CPU使用率飙升,导致服务器响应变慢或无法响应合法请求。

带宽耗尽:虽然单个SYN请求和响应占用的带宽很小,但在大规模攻击下,这些小数据包的总量可能占满可用带宽,影响其他正常流量。

连接表溢出:TCB表中的条目达到上限,服务器无法再为新的连接分配资源,导致拒绝服务。

应对

1. 增加 SYN 连接,也就是增加半连接队列的容量

解释:

在TCP三次握手过程中,服务器在接收到客户端的SYN请求后,会在内存中分配一个半连接(half-open connection)记录,这个记录会保存在半连接队列中。SYN Flood攻击通过发送大量的SYN请求,使得半连接队列被占满,从而导致新连接请求被拒绝。

解决方法: 增加半连接队列的容量可以缓解SYN Flood攻击的影响。通过扩展队列大小,服务器可以容纳更多的半连接请求,从而减少被攻击时拒绝合法连接请求的概率。然而,这只是增加了服务器的抗压能力,并不能从根本上解决问题。

2. 减少 SYN + ACK 重试次数,避免大量的超时重发

解释:

在TCP三次握手中,服务器在收到客户端的SYN请求后,会发送一个SYN+ACK包给客户端,并等待客户端的ACK确认。如果没有收到ACK确认,服务器会重试发送SYN+ACK包多次,直到达到重试次数上限。

解决方法: 减少SYN+ACK重试次数可以降低服务器在遭受SYN Flood攻击时的资源消耗。因为每次重试都需要服务器的计算和网络资源,减少重试次数可以减少这些资源的浪费。同时,这也意味着在正常情况下,服务器对网络问题的容忍度会降低,需要在平衡性能和安全之间找到适当的参数。

3. 利用 SYN Cookie 技术

解释:

SYN Cookie是一种在服务器受到SYN Flood攻击时保护自身资源的技术。在传统的TCP三次握手过程中,服务器在接收到SYN请求时,会分配资源并在内存中记录半连接状态。而SYN Cookie技术则改变了这一流程。

解决方法: 当服务器接收到SYN请求时,不立即分配资源,而是计算出一个特殊的加密值(Cookie),并将这个Cookie与SYN+ACK包一起返回给客户端。客户端在发送第三个握手包(ACK)时,会携带这个Cookie值。服务器在接收到这个ACK包时,可以根据Cookie验证这个请求是否合法。如果验证通过,服务器才会分配资源并建立连接。

这种方法的优点在于,服务器不需要在接收到初始的SYN请求时分配资源,从而大大减少了在遭受SYN Flood攻击时的资源消耗。只有在验证通过的情况下,才会分配资源,从而有效抵御SYN Flood攻击。

总结

  1. 增加SYN连接(增加半连接队列容量): 通过增加半连接队列的容量,提升服务器在面对大量SYN请求时的抗压能力。
  2. 减少SYN+ACK重试次数: 通过减少SYN+ACK包的重试次数,降低服务器在遭受SYN Flood攻击时的资源消耗。
  3. 利用SYN Cookie技术: 通过在初始SYN请求阶段不分配资源,而是使用Cookie验证的方式,有效防御SYN Flood攻击。

参考资料