什么是 TCP 协议
TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制。TCP 是一个传输层的协议。
如下图:
我们接下来在讲解 TCP/IP
协议栈的下三层时都会先解决这两个问题:
- 报头与有效载荷如何分离?
- 有效载荷应该交付给上层的哪个协议?
TCP 协议段格式
数据偏移
数据偏移占 TCP
协议段的 4 个比特位!数据偏移字段是用来表示 TCP
头部的长度,以确保在 TCP
报文中正确地定位到数据的开始位置。
你可能会有疑问:数据偏移字段只有 4 个比特位,表示的范围也就是: [ 0 , 15 ] [0, 15] [0,15],连 TCP
协议段的 20 个字节的固定首部都表示不了,如何能用来表示 TCP
头部长度呢?
这是因为:数据偏移在计算的时候是有基本单位的,数据偏移的单位是 4 字节,这就意味着数据偏移字段能表示的字节数范围是: [ 0 , 60 ] [0, 60] [0,60] 字节。
数据偏移最大表示的 60 个字节,减去 20 个字节的固定首部长度,我们可以得出选项部分的长度范围是: [ 0 , 40 ] [0, 40] [0,40] 字节。
我们现在就能解决这个问题啦:TCP
报文的报头与有效载荷是如何分离的呢?
首先,获得一个
TCP
报文之后,直接读取固定长度 20 个字节,然后再读取数据偏移。由数据偏移计算出TCP
报文的头部长度。然后就可以将报头与有效载荷分离啦!
这个选项是什么东西呢?
在TCP协议报头中,选项字段用于提供额外的功能和灵活性,通常用于特定的需求或性能优化。选项字段是可选的,不是每个TCP报文都会包含选项。具体的选项有哪些你可以在网上搜搜,我们这里不做讲解,因为不是重点。
源端口号与目的端口号
我们在写 TCP
网络套接字编程的客户端代码时,不是要绑定服务器的端口号嘛。在客户端给服务端发送请求的时候,我们绑定的服务器端口号就是目的端口号。源端口号就是客户端进程绑定的端口号,客户端的端口号我们不会手动绑定,而是操作系统随机分配的。
有了源端口号,就知道数据是从哪个进程来, 到哪个进程去。那么,TCP
报文中有效载荷交付给上层的那个协议的问题也就解决了!
通过目的端口号,我们就知道
TCP
报文的有效载荷该交给上层的哪个协议!
- 如果目的端口号是 80,那么就是交给应用层的
http
协议。- 如果目的端口号是 443,那么就是交给应用层的
https
协议。
窗口
根据 TCP
协议段的图,我们可以看出窗口大小是 16 位的。
在客户端与服务端使用 TCP
协议进行通信的时候,他不能像 UDP
协议那样,一有数据就发送。TCP
内部是维护了发送缓冲区和接收缓冲区的!既然 TCP
协议不能像 UDP
协议那样一有数据就发送,那 TCP
协议是如何做的呢?
这里需要提到流量控制,现在只是提一下,等会儿会细讲的,因为
TCP
的知识点具有很强的关联性,无法一个一个地拎出来讲。流量控制:在
TCP
协议中,不能让客户端使劲儿地往服务端发数据(当然反过来也一样,CS 在互相通信嘛),如果将服务端的接收缓冲区干满了,那么服务端就会将报文丢弃,出现丢包问题(TCP
报文丢失,即服务端没有收到)。尽管说TCP
协议有丢包重传(通信一方发的报文对方没有收到,会进行重传)机制,但是这样做显然会造成传输效率下降。因此,可以看出流量控制在通信过程中的必要性!我们都知道
TCP
是可靠传输的?TCP
凭什么保证可靠性呢?最基本的特点:==确认应答机制。==客户端向服务端发送了一条消息,服务端收到客户端的消息之后,也会向客户端发送一条消息,告诉客户端,你发的消息我收到了。那么,基于这个确认应答机制,客户端被要求数据发送地慢一点,客户端的判断依据是什么呢?
显然是由服务端接收缓冲区的剩余空间大小来决定的!当客户端给服务端发送消息,服务端对客户端发来的消息做应答的时候,窗口字段填充的内容就是自身接收缓冲区的剩余空间大小。
16 位窗口大小填充的就是自身接收缓冲区的剩余空间大小。
序号与确认序号
这个世界上有 100% 可靠的网络协议吗?
答案显然是没有的!
- 假设客户端给服务端发送了一条数据,如果客户端收到了服务端的应答,那么就可以保证客户端发送给服务端的数据是可靠的。
- 因此,没有应答的数据,
TCP
协议是没有办法保证可靠性的。所以,客户端与服务端通信的最新一条数据,一定是没有应答的,即我们无法保证通信双方发出的数据是 100% 可靠的。虽然我们不能保证双方通信的数据 100% 可靠,但是只要收到应答的数据就一定是可靠的,因此
TCP
协议能保证局部数据的可靠性。这个是题外话哈!
我们现在来讲序号与确认序号的作用。由TCP
协议段格式图可以看出序号与确认序号都是 32 位的哈!客户端给服务端发送数据时,按照一定的顺序将数据发出,可是服务端收到数据的顺序会和客户端发送数据的顺序一样嘛?
显然无法保证服务端收到数据的顺序与客户端发送数据顺序的一致,这就是数据包乱序问题。你
TCP
协议不是可靠传输的协议嘛!怎么能容忍数据包乱序的问题呢?因此 32 位序号的核心作用之一:保证数据的按序到达!
什么是序号呢?
TCP
协议将每个字节的数据都进行了编号,即为序列号。如上图
TCP
发送缓冲区中的第一字节的其实位置处就是序号 1,依次类推。有了序号这个东东,接收方只需要对接收到的数据按照序号进行排序就能保证数据的顺序不会紊乱啦!我们在写
TCP
协议的网络套接字编程代码时,调用write
接口往fd
里面写入,当时我们认为调用write
就是在向对方发送数据了!其实不然,调用write
系统调用的本质是将用户层的数据拷贝到了TCP
的发送缓冲区,至于数据什么时候发送,发送多少,全是由TCP
说了算。因此TCP
才被叫做传输控制协议嘛!因此我们在自定义协议的时候,才需要将read
读到的报文做拼接,提取的过程嘛!因为read
读上来的并不一定是一个完整的报文啊!用户调用
write
拷贝到TCP
发送缓冲区的数据是有序的,并且每个字节都有自己的编号,客户端在发送数据的时候,会填充TCP
报头的 32 位序列号,填充的内容就是发送数据块的最后一个字节的序号!例如,如上图,假设客户端某一次发送数据时发送了 [ 1 , 1000 ] [1, 1000] [1,1000] 字节的数据,那么这个 32 位的序列号填充的就是 100。
服务端不是要对客户端发送的数据做应答嘛,服务端在确认应答的报文中会填充 32 位确认序号,其值为收到报文的 32 位序列号加 1。例如,假设服务端收到了客户端发来的 [1, 1000]
字节的,服务端在做应答时,32 位确认序号就会填充为 1001。
那么为什么是收到报文的 32 为序列号加 1 呢?
首先,确认序号的意义是:确认序号之前的数据,我已经全部收到了,下一次发送数据,请从确认序号指定的序号处开始发送。你品,你细品,确认序号之前的数据,我已经全部收到了哦!
我们先回到确认应答机制上来,客户端发送消息,服务端确认应答(当然反过来也是一样的,通信的过程是双方都在进行的嘛),然后再发送下一个消息。你不觉得这样串行的方式效率太低了嘛?
实际上客户端是有可能一次给服务端发送多个报文的,这个确认序号的意义在这里就能凸显出来啦。如上图:假设客户端一次性向服务端发送了三个
TCP
报文,数据分别是: [ 1 , 1000 ] [1, 1000] [1,1000] 字节, [ 1001 , 2000 ] [1001, 2000] [1001,2000] 字节, [ 2001 , 3000 ] [2001, 3000] [2001,3000] 字节,服务端收到这三个报文之后,需要对每一个报文都做应答吗?显然是不需要的,只需要一个应答,在应答报文的确认序号中填写 3001,表示 3001 序号之前的数据我都已经收到了哦,下次请从序号 3001 开始发送数据。
因此,我们允许应答有少量的缺失。
事实上,服务端在应答的时候,也是能给客户端发送数据的,因为一次应答也是发送一个完整的报文嘛,既然是一个完整的报文,为什么不能将应答与数据同时发送给客户端呢?(当然前提是服务端有数据需要向客户端发送),我们将应答与数据经由同一个报文发送给通信另一方的机制称为:捎带应答。
这里你就可能会有以疑问了:为什么要有序号和确认序号,一个序号不可以吗?
- 首先,应答也可能携带数据,即捎带应答。
- 服务端与客户端互发消息,序号(发送数据)与确认序号(应答),两个都要使用!
TCP 报头的 6 个标志位
- URG(紧急):
URG
标记位用于指示TCP
报文段中的紧急数据。当URG
标记位被设置为 1 时,表示TCP
报文段中的某些数据被标记为紧急数据,需要优先处理。- 在
TCP
报头中,紧急指针表示的就是紧急数据在TCP
有效载荷中的偏移量,单位是字节。 URG
可以在什么场景使用呢?比如服务器压力很大,现在无法处理某些客户端的请求,客户端具体也不知道服务端是什么情况,就可以通过将URG
标志位设为 1,携带紧急数据来询问服务器现在的情况。
- ACK(确认):
ACK
标记位用于确认接收方已经成功接收到了发送方发送的数据。当ACK
标记位被设置为1时,表示TCP
报文段中的确认序号有效,接收方已经成功接收到了发送方发送的数据。
- PSH(推送):
PSH
标记位用于提示接收方应该立即将收到的数据推送给应用层。当PSH
标记位被设置为 1 时,表示TCP
报文段中的数据应该被立即传输给上层应用,而不应该被缓存或等待更多数据。只有紧急情况才会将PSH
置 1。
- RST(复位):
RST
标记位用于强制中断TCP
连接。当RST
标记位被设置为 1 时,表示TCP
连接出现了异常情况,需要立即中断连接。RST
标记位通常用于处理一些异常情况,如连接超时、协议错误等。
- SYN(同步):
SYN
标记位用于发起TCP
连接的请求。当SYN
标记位被设置为 1 时,表示发送方希望建立一个新的TCP
连接。
- FIN(结束):
FIN
标记位用于结束TCP
连接。当FIN
标记位被设置为 1 时,表示发送方已经完成了数据的发送,并且希望关闭TCP连接。FIN
标记位通常用于连接的正常关闭过程中,表明发送方不再有数据发送。
保留字段
在 TCP
协议头部中,有一些字段被标记为“保留字段”。这些字段在 TCP
协议的当前版本中没有被使用,被保留供未来使用或扩展时所需。保留字段的存在有以下几个目的和作用:
- 兼容性: 保留字段的存在确保了
TCP
协议的向后兼容性。即使在当前版本中没有使用,但保留字段的存在使得未来版本能够通过修改和扩展这些字段来引入新的功能和特性,而不会破坏已有的实现和部署。 - 协议扩展: 保留字段为
TCP
协议的未来扩展提供了可能性。随着网络技术的发展和应用需求的变化,TCP
协议可能需要引入新的功能和选项。保留字段可以用于扩展协议头部,以支持新的特性或选项。 - 预留用途: 保留字段可以作为临时的占位符,用于暂时未确定具体用途的情况。在协议制定或升级过程中,有时会留出一些字段作为预留字段,以备将来可能需要引入的新功能或选项。
- 标准化: 保留字段的存在有助于标准化
TCP
协议的演进过程。通过在协议规范中明确保留字段的存在和用途,可以为未来的协议扩展和版本更新提供一个统一的框架和方向。
校验和
TCP
报头中的检验和字段(Checksum)用于检测 TCP
报文在传输过程中是否发生了损坏或错误。TCP
的检验和算法通过对 TCP
报文段中的各个字段进行计算生成一个校验和值,并将该值存储在 TCP
报头中的检验和字段中。接收端在接收到 TCP
报文时会重新计算校验和,并将计算得到的校验和值与报文中的校验和字段进行比较,以确定报文的完整性。
检验和字段的作用包括:
- 数据完整性验证: 检验和字段用于验证
TCP
报文在传输过程中是否受到了损坏或篡改。接收端会重新计算报文的校验和,并将计算得到的校验和值与发送端发送的校验和字段进行比较。如果两者不一致,则说明报文在传输过程中发生了错误,接收端会丢弃该报文并请求重传。 - 数据可靠性保证: 检验和字段可以帮助确保
TCP
报文的可靠传输。通过校验和的验证,接收端可以检测到传输过程中发生的任何错误或损坏,从而及时发现并处理错误,保证数据的可靠传输。 - 拒绝冒充攻击: 检验和字段可以防止恶意主机对
TCP
报文进行篡改或冒充攻击。由于检验和字段是通过对TCP
报文内容进行计算得到的,攻击者无法在不知道校验和算法的情况下有效地篡改TCP
报文而不被检测到。
TCP 的确认应答机制
其实这个机制在前面都讲得差不多了,这里来总结一下吧:
TCP
的确认应答机制是一种用于确保数据可靠传输的重要机制。它通过在数据传输过程中的确认应答来确认数据的成功传输,从而保证数据的可靠性。
- 序号和确认号:
TCP
协议中的每个数据段都包含一个序号字段和一个确认号字段。发送方使用序号字段对发送的数据进行编号,接收方通过确认号字段向发送方发送确认消息,确认接收到的数据。确认号字段表示接收方期望接收到的下一个序号,即已成功接收的最后一个字节的序号加 1。
TCP 的超时重传机制
首先我们来看看有哪些情况会触发 TCP
的超时重传机制:
- 主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B,如果主机A在一个特定时间间隔内没有收到 B 发来的确认应答, 就会进行重发。
- 主机 A 未收到 B 发来的确认应答, 也可能是因为
ACK
丢失了。在这种情况下接收方可能会收到重复的报文,显然报文重复也是不可靠传输的一种表现形式,因此,必须进行相同报文的去重!去重可以根据 32 位的序列号嘛!
发送方对于发出去的报文是否丢失无法进行判定!必须通过规定来确认是否需要进行报文的重传。TCP
选择使用超时重传来实现。那么这个时间是如何计算的嘞?
- 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”。
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的。
- 如果超时时间设的太长, 会影响整体的重传效率。
- 如果超时时间设的太短, 有可能会频繁发送重复的包。
TCP
为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
- Linux 中(BSD Unix和Windows也是如此), 超时以 500 m s 500ms 500ms 为一个单位进行控制, 每次判定超时重发的超时 时间都是 500 m s 500ms 500ms 的整数倍。
- 如果重发一次之后, 仍然得不到应答, 等待 2 ∗ 500 m s 2*500ms 2∗500ms 后再进行重传。
- *如果仍然得不到应答, 等待 4 ∗ 500 m s 4*500ms 4∗500ms 进行重传. 依次类推, 以指数形式递增。
- 累计到一定的重传次数,
TCP
认为网络或者对端主机出现异常, 强制关闭连接。
TCP 的连接管理机制
想必在学习 TCP
之前你就已经听过 TCP
的三次握手和四次挥手了吧!
我们之前就提到过,TCP
协议是面向连接的,这个连接的过程包含建立连接和断开连接两个部分哈!也就是我们常说的三次握手和四次挥手。
三次握手
TCP
的三次握手是建立 TCP
连接的过程,用于确保通信双方的状态同步和数据传输的可靠性。三次握手的过程如下:
- 客户端发送连接请求:
- 客户端首先向服务器发送一个连接请求报文段(SYN),其中包含了客户端的初始序列号(Seq=X)和
TCP
头部中的SYN
标志位被置为1,表示客户端希望建立连接。
- 客户端首先向服务器发送一个连接请求报文段(SYN),其中包含了客户端的初始序列号(Seq=X)和
- 服务器确认连接请求并发送响应:
- 服务器收到客户端的连接请求后,会向客户端发送一个确认报文段(SYN+ACK)。在这个确认报文段中,服务器将确认号设置为客户端的初始序列号加 1(ACK=X+1),同时在
TCP
头部中将 SYN 和 ACK 标志位都置为 1,表示服务器已经收到了客户端的连接请求,并且愿意建立连接。
- 服务器收到客户端的连接请求后,会向客户端发送一个确认报文段(SYN+ACK)。在这个确认报文段中,服务器将确认号设置为客户端的初始序列号加 1(ACK=X+1),同时在
- 客户端发送确认响应:
- 客户端收到服务器的确认响应后,会向服务器发送一个确认报文段(ACK),以确认服务器的确认响应。在这个确认报文段中,客户端将确认号设置为服务器的初始序列号加1(ACK=Y+1),同时在
TCP
头部中将 ACK 标志位置为 1,表示客户端已经收到了服务器的确认响应,连接建立成功。
- 客户端收到服务器的确认响应后,会向服务器发送一个确认报文段(ACK),以确认服务器的确认响应。在这个确认报文段中,客户端将确认号设置为服务器的初始序列号加1(ACK=Y+1),同时在
其实 3 次握手也可以看作是 4 次握手。服务端的 SYN 和 ACK 如果分开发送就是 4 次握手啦,只不过实际肯定不会这样做,捎带应答机制确保了绝大多数情况下是 3 次握手而不是 4 次握手。
一次握手行不行?
显然不行。进行三次握手,通信双方都进行了一次可靠的发送数据和接收数据,从而验证了全双工通路是没有问题的!如果只进行一次握手,即客户端向服务器发送一次连接请求,客户端就认为与服务端建立好了连接。
- 数据可靠性得不到保障,如果只进行一次握手,服务器并没有确认客户端的连接请求,那么在传输数据时可能会出现丢失或错误的情况,因为没有建立可靠的通信通道。即没有验证全双工通路是没有问题的。
- 序列号无法同步: 在
TCP
的三次握手过程中,双方会交换各自的初始序列号,以便后续的数据传输中可以正确地进行序列号的同步和确认。如果只进行一次握手,那么无法确保双方的序列号是一致的,可能会导致后续的数据传输中出现混乱或错误。- 服务器资源消耗增加:一次握手会导致服务器在接收到客户端的连接请求后立即建立连接,即使服务器并不确定客户端是否真的需要建立连接。这样会增加服务器维护连接的成本,尤其是在面对大量短暂连接的情况下,服务器可能会因为连接资源耗尽而无法处理正常的连接请求。服务器会浪费资源在维护这些无用的连接上,从而降低了服务器的性能和可用性。
- 容易受到恶意攻击: 由于一次握手会导致服务器立即建立连接,即使是来自未经身份验证的恶意客户端的连接请求,服务器也会进行响应。这样容易受到恶意攻击,如拒绝服务攻击(DoS)或洪泛攻击(SYN Flood)等,从而影响服务器的正常运行。
两次握手行不行?
显然也不行,存在和一次握手相同的问题。
- 两次握手,服务器一定是将链接先建立好的哪一方,如果客户端不管服务端的 ACK 就和一次握手完全一样了!
- 因此优先让服务器建立连接是不理智的行为,服务器本身是一对多的,如果服务端挂掉了,会影响更多的客户端,连接方面的成本不应由服务端来承受。
- 相反,三次握手如果失败,连接失败的成本是在客户端,而不是在服务端!奇数次握手,可以确保一般情况下握手失败的成本主要在客户端!
- 由上图可知,客户端收到服务端的 SYN + ACK 会建立,而服务端收到客户端的 ACK 才会建立连接。如果连接失败,即客户端 ACK 丢失等问题,那么连接失败的成本主要是在客户端!
- 因此 3 次握手是验证通信全双工的最小次数。
我们将 TCP
网络套接字编程中使用的函数与三次握手的过程串起来理解:
-
当客户端向服务端发送连接请求,即第一次握手。实际上就是调用
connect
函数向服务端发起连接请求,调用该函数的进程会阻塞在connect
函数中,等待服务器对连接请求进行应答。 -
在
TCP
的三次握手过程中,listen
函数的调用时机是在服务器调用bind
函数绑定了地址和端口之后,以及调用accept
函数之前。以下是listen
函数在三次握手过程中的角色:- 设置套接字为监听状态: 调用
listen
函数将套接字设置为监听状态,告诉操作系统该套接字将用于接受客户端的连接请求。在调用listen
函数之前,服务器必须先调用bind
函数将套接字绑定到一个特定的IP地址和端口上。 - 设置全连接队列的大小:
listen
函数还可以指定全连接队列的最大长度,即同时等待处理连接请求的最大数量。如果全连接队列已满,新的连接请求将被拒绝。这个参数通常称为backlog
。 - 开始监听连接请求: 调用
listen
函数后,服务器就开始监听连接请求了。此时,服务器处于等待状态,等待客户端发送连接请求。
listen
的第二个参数:backlog
:-
已经建立好的连接,会被放在队列之后,而
accept
函数只负责从这个连接队列中拿取一个连接,与特定的fd
关联起来返回给上层用户。因此,连接建立成功和上层的 accept 函数没有关系,三次握手是在双方操作系统内部完成的。 -
如果将全连接队列填满了,还有客户端来请求连接,服务端会处于 SYN_RECV 状态,而客户端会处于 ESTABLISHED 状态。此时客户端的连接会被放入半连接队列,半连接队列中的节点不像全链接队列中的节点那样会长时间维护,因此资源消耗并不大!
-
当客户端向服务器端发送
SYN
报文请求建立TCP
连接时,服务器端收到SYN
请求后会将该连接请求放入半连接队列中,并发送一个SYN+ACK
响应给客户端。此时连接处于半打开状态,即连接的一半已经建立(客户端到服务器端的连接),但另一半尚未建立(服务器端到客户端的连接)。一旦服务器接收到客户端的ACK
确认报文,即完成了三次握手,连接将从半连接队列中移出,进入到全连接队列中,变为完全打开状态(Established)。半连接队列的作用包括:
- 管理连接建立过程: 半连接队列用于管理正在进行的连接建立过程,即处于半打开状态的连接请求。它可以帮助服务器跟踪和处理连接建立的过程,确保连接的正确建立和管理。
- 防止SYN洪泛攻击: 半连接队列可以防止 SYN 洪泛攻击。当服务器收到大量的 SYN 请求时,如果半连接队列已满,新的连接请求将被拒绝,从而有效地阻止了恶意攻击者通过发送大量的 SYN 请求消耗服务器资源。
- 优化连接处理效率: 半连接队列可以帮助服务器在连接建立过程中进行优化处理,提高连接建立的效率和速度。通过合理设置半连接队列的大小,可以避免过多的连接请求排队等待,减少连接建立的延迟。
-
实验的现象可以看到服务器处于 SYNC_RECV 状态说明,服务器可能将客户端的 ACK 应答丢弃了,因为全连接队列已经满了嘛!无法将连接放入全链接队列。
-
-
listen
函数的第二个参数backlog
为什么不能没有呢?这就是维护全连接队列的好处啦!- 提供连接的排队和管理机制: 全连接队列提供了一个排队机制,可以让服务器按顺序处理连接请求。当服务器忙于处理其他连接或资源有限时,新的连接请求会被放置在全连接队列中,等待服务器处理。这种排队机制可以确保连接请求被有序地处理,避免过载或资源竞争。
- 控制服务器负载和资源使用: 维护全连接队列可以帮助服务器控制负载,防止过多的连接请求导致服务器资源耗尽或性能下降。通过限制全连接队列的大小,可以限制服务器同时处理的连接数量,避免过多的连接导致服务器负载过高。
- 提高系统稳定性和可靠性: 维护全连接队列可以提高服务器的稳定性和可靠性。当服务器忙于处理其他任务或遇到突发的高负载时,全连接队列可以帮助服务器缓解压力,避免因过多的连接请求导致系统崩溃或服务不可用。
- 保护系统免受拒绝服务攻击: 全连接队列可以帮助服务器抵御拒绝服务(DoS)攻击或洪泛攻击等恶意行为。通过限制全连接队列的大小或实施其他连接管理策略,可以有效地防止恶意攻击导致服务器资源耗尽或服务不可用。
- 设置套接字为监听状态: 调用
好的,理论讲了这么多,可现实是不是这个样子呢?实践才是检验真理的唯一标准:
我们使用 TCP
网络套接字编程编写一个服务端,然后分别在相同的操作系统的不同版本运行,看看是什么效果哈!服务端是单进程版本,只能处理一个客户端的连接,我们将 backlog
设置为 0,也就是说全连接队列的长度为 1。这就意味着,当第三个客户端连接服务器的时候,全连接队列已经满了!
-
centos 7 环境下测试:
我们看到,在客户端有 3 个已经建立好的连接,他们的状态都是 ESTABLISHED。在服务端有两个连接是 ESTABLISHED,还有一个连接是 SYN_RECV 状态。这跟我们上面讲的,全连接队列满了的时候,还有客户端请求连接的情况吻合!
- centos 8 环境测试:
我们看到当服务端的操作系统是 centos 8 时,情况就有所不同啦!当全连接队列满了的时候,还有客户端向服务端请求连接的时候,这个客户端会处于 SYN_SENT 的状态,并且在服务端查询不到 SYN_RECV 状态的进程!这说明在 centos 8 的环境下,当全连接队列满了的时候,服务端会拒绝客户端的连接!即第二次握手都不会发生!
如果面试官问起这个地方的细节,你就可以回答这会根据操作系统版本的区别而有所差距哈!因为不同的操作系统版本,内核的实现都是有差别的!对应的网络协议栈具体的实现也是有差别的!
四次挥手
下面是 TCP
四次挥手的过程:
- 第一步(FIN 和 ACK):
- 客户端发送一个FIN(结束)报文给服务器,表示客户端不再发送数据,并请求关闭连接。
- 服务器收到FIN后,发送一个 ACK(确认)报文给客户端,表示已经收到了客户端的关闭请求。
- 第二步(关闭发送):
- 服务器确认客户端的关闭请求后,服务器通常会继续发送尚未传输完的数据,等待数据传输完成。
- 第三步(服务器关闭):
- 当服务器的数据传输完成后,会发送一个 FIN 报文给客户端,表示服务器不再发送数据,请求关闭连接。
- 第四步(客户端确认):
- 客户端收到服务器的FIN报文后,发送一个ACK报文给服务器,表示已经收到了服务器的关闭请求,并确认关闭连接。
你可能就会问了,这里能不能将服务端的 FIN 和 ACK 合并呢?只能说是有这种情况发生的,当客户端请求关闭连接的时候,服务端收到了客户端的 FIN,同时,服务端也没有数据需要向客户端发送了,并且服务端也要和客户端断开连接。只有在这种情况下,服务端的 FIN 和 ACK 才会合并在一起发送给客户端!也就是只有在很少数的情况下,四次握手才会变成 3 次握手。
挥手的次数少于 4 次行不行?
一方想要断开连接,表示这一方已经没有数据给对方发送了!但是,发送数据是通信双方都可以进行的!这一方不发送数据了,可是架不住对方还要发送数据!因此连接的断开就必须进行两次。一次 FIN 一次 ACK,一次 FIN 一次 ACK。
挥手的次数大于 4 次显然就没必要啦!4 次已经足够啦!
在上图中,假设左侧是客户端,那么就是客户端先请求断开连接,我们发现先断开连接的一方,在收到对方的 FIN 之后会进入一个叫做 TIME_WAIT
的状态。在上图中,客户端(先断开连接的一方)进入 TIME_WAIT
状态之后,并没有立刻释放掉连接,而是等待了一段时间,才会释放连接进入 CLOSED
状态!
问题来了,为什么要进行 TIME_WAIT
?
- 确保最后的 ACK 被对方正常接收:在四次挥手的最后一步,客户端发送了 ACK,表示确认关闭连接,但这个 ACK 有可能在传输过程中丢失,导致服务器无法收到确认,从而认为连接没有被正常关闭。TIME_WAIT 状态的存在可以确保最后的 ACK 在网络中的所有副本都被正确处理,避免了这种问题。
- 避免新连接与旧连接混淆:在 TIME_WAIT 状态期间,客户端的 IP 地址和端口号仍然被保留,这样可以确保在该时间段内的任何延迟的数据包都不会被误认为是新的连接。这是因为,如果服务器收到了来自相同 IP 地址和端口号的新连接请求,而这个 IP 地址和端口号刚好是之前的连接的对方信息,那么服务器会错误地认为这是旧连接的重复数据包,导致出现数据混淆或安全问题。
学到这里,我们就能解决之前遗留的一个问题啦:为什么终止掉服务器的服务进程之后,无法绑定同一端口继续启动服务进程?
- 如上图,服务端在与客户端通信的时候,直接
ctrl + c
终止掉服务进程,然后立刻使用命令,./server 9999
重启服务进程,我们发现会报错:bind error,address already in use。- 我们使用命令
netstat -nalp
查看网络状态,发现绑定9999
端口号的进程还存在,并且处于 TIME_WAIT 状态。- 在这个例子中,服务器是主动断掉连接的一方,因此他会处于 TIME_WAIT 状态,一段时间之后才会将连接释放!既然这个连接都还没有释放如何能够绑定相同的端口号呢?
怎么解决这个问题呢?
可以等待 TIME_WAIT 状态过去了,进入 CLOSED 状态再绑定 9999 端口号,让服务器启动。显然这不是一个好办法,因为服务器是用来提供服务的,一秒钟无法提供服务,这其中的损失还是相当大的!
可不可以换端口号呢?作为一个已经上线的服务器,客户端访问你的这个服务器端口号都是固定的,换端口,怎么敢的!
使用函数解决,允许端口可以被重复绑定。
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd
:套接字描述符。
level
:选项所在的协议层,对于 TCP 可以是SOL_SOCKET
。
optname
:选项名,比如SO_REUSEADDR
。
optval
:指向存有选项值的缓冲区的指针。
optlen
:optval
的长度。
SO_REUSEADDR
是一个套接字选项,用于告诉内核允许重用处于 TIME_WAIT 状态的地址。
SO_REUSEPORT
是一个套接字选项,用于告诉内核允许重用处于 TIME_WAIT 状态的端口。所以,我们在
TCP
服务端的时候只要加上这两行代码就可以解决这个问题啦!int opt = 1; setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
还有一个问题就是这个 TIME_WAIT 持续的时间是多少呢?
TIME_WAIT 状态的等待时间通常是两倍的最大报文生存时间(Maximum Segment Lifetime,MSL)。
centos 查看 MSL 时长:
cat /procsys/net/ipv4/tcp_fin_timeout
这个不是 MSL 时间哈: M S L = t c p _ f i n _ t i m e o u t ∗ 2 MSL = tcp\_fin\_timeout * 2 MSL=tcp_fin_timeout∗2
我的机器上
cat
出来的时间是 60 秒,也就是说 TIME_WAIT 等待的时间是 240 秒
流量控制
在前面将 TCP
协议段的时候,我们就提到了流量控制的概念!流量控制用于控制数据的发送速率,确保发送方不会向接收方发送过多的数据,从而避免接收方因处理不及时而导致数据丢失或缓冲区溢出的情况。
流量控制的具体过程是怎么样的呢?
- 接收方通告窗口大小: 接收方会在
TCP
报文中的窗口字段中通告自己的接收窗口大小,表示自己当前可接收的数据量。这就是我们之前讲的 16 位窗口大小嘛! - 发送方根据接收窗口调整发送速率: 发送方根据接收方通告的窗口大小来调整自己的发送速率。如果接收窗口较小,表示接收方的缓冲区已经满了或者处理能力有限,发送方会减缓发送速率;如果接收窗口较大,表示接收方有足够的缓冲区空间或者处理能力,发送方可以增加发送速率。
- 动态调整窗口大小:
TCP
协议中的滑动窗口机制(滑动窗口等会儿就讲哈)允许发送方和接收方动态调整窗口大小,以适应网络条件的变化。发送方可以根据接收方通告的窗口大小来调整自己的发送窗口大小,从而控制发送速率;而接收方则可以根据自己的处理能力和缓冲区空间来调整自己的接收窗口大小。
不知道你会不会有疑问?第一次发送数据的时候,怎么保证发送数据的大小是合理的呢?
我们不要认为 3 次握手的过程只是单纯的 3 次握手哈!在握手的过程中,双方是交换了报文的,在交换报文的过程中也会做一些其他的事情:
- 初始化序列号(Sequence Number): 在握手过程中,客户端和服务器会交换彼此的初始序列号,以便后续的数据传输中可以正确地进行序列号的同步和确认。
- 设置初始窗口大小(Initial Window Size): 在握手过程中,客户端和服务器会协商初始窗口大小,以确定初始数据传输的窗口大小。初始窗口大小影响了数据传输的速率和效率。
- 协商TCP选项: 在握手过程中,客户端和服务器还可以协商一些
TCP
选项,如最大报文段长度(Maximum Segment Size,MSS,就是去掉TCP
报头之后的数据长度)、窗口扩大因子(Window Scaling Factor, 16 位窗口大小可能并不满足需要,可以进行加倍,例如窗口扩大因子为 1,表示窗口字段值左移 1 位)等,以提高数据传输的性能和效率。
在之前我们提到过,某些报文的应答是可以缺失的,通过 32 位确认序号减少了应答的次数!可是如果应答的次数变少了,应答报文的丢失可能会造成发送方无法得知对方接收缓冲区的大小,导致通信出现问题!因此,窗口大小字段的发送不能完全依赖于应答报文!
- 窗口探测(Window Probing):当发送方的发送窗口变得空闲时,为了确定接收方是否仍然可以接收数据,发送方可能会发送一个小的探测段(通常是一个字节),这个探测段不包含实际的数据,而只是用来触发接收方发送窗口更新。如果接收方确实可以接收数据,它将发送一个窗口更新,告知发送方可以发送更多数据。窗口探测机制有助于减少发送方在发送窗口空闲时的等待时间,从而提高数据传输的效率。
- 窗口更新(Window Update):接收方在接收缓冲区有足够空间时,会发送窗口更新通知给发送方,告知发送方可以增加发送窗口的大小,以便发送更多数据。窗口更新通常包含在确认段中发送给发送方。通过窗口更新,发送方可以动态调整发送窗口的大小,以适应接收方的接收能力,从而实现更高效的数据传输。
这两个机制是
TCP
中用于优化流量控制和提高传输效率的重要手段。窗口探测确保发送方及时得知接收方的接收能力,而窗口更新则允许接收方动态地通知发送方其接收缓冲区的状态,从而使数据传输更加顺畅和高效。通信双方都有一定策略能通知对方,能一定程度上防止因网络问题导致窗口大小更新报文的丢包问题。
滑动窗口
TCP
的滑动窗口是一种流量控制机制,用于管理发送方和接收方之间的数据传输。滑动窗口允许发送方在不等待确认的情况下连续发送多个数据段,同时确保不会超出接收方的处理能力。
都说 TCP
是可靠传输,既然 TCP
有超时重传机制,那么说明已经发出去的暂时还没有收到应答的报文,需要被 TCP
暂时保存起来,同时这种歌性质的报文显然可能存在多个,那么问题就来了,这些个报文被保存到哪里的呢?
这就不得不提到发送缓冲区的逻辑结构啦:在逻辑上发送缓冲区被分成了四个区域
- 已发送已确认:表示这个部分的数据已经发送成功,并且已经收到了接收方的应答,因此这个区域是可以被覆盖的!
- 已发送未确认:这部分包含了两种数据哈,一种是已经发送但是还没有收到应答的数据;另一种是可以立即发送但是还没有发送的数据,这部分就是我们的滑动窗口啦!
- 待发送:因为接收方缓冲区大小,网络状况等原因,这个区域里面的数据是不能直接发送的!
- 空闲区域:发送缓冲区中尚未被使用的区域!
这么看来已发送但是暂时还没有收到应答的报文还是在发送缓冲区的!完全没有必要单独拷贝一份出来哦!
如何理解发送缓冲区的区域划分?
全域划分的本质其实就是通过指针或者下标来区分发送缓冲区的不同区域,因为发送缓冲区本质也是数组嘛,通过定义相关指针或者下标就能做到区域划分啦!
滑动窗口,滑动窗口,那肯定是要滑动的呀!理解了区域划分的本质,窗口的滑动,本质上就是指针的移动嘛!因为有了滑动窗口,发送方才可以一次向对方发送大量的
TCP
报文。在目前我们认为,滑动窗口的大小,不能超过接收方的接收缓冲区的大小,实际上滑动窗口的大小会受到网络状况的限制,等我们等会儿讲拥塞控制的时候,会补全滑动窗口大小的概念。这么看来的话,滑动窗口越大,网络的吞吐率就越高呢!
滑动窗口中可发的数据为啥不可以一次给对方发送过去,而是要一段一段的发送呢?
这个问题与 IP 协议有关,现在不能完全解释原因!一次给对方发送过去,如果发生了丢包,超时重传的代价会增加!当然还要考虑到网络拥塞的问题,这个等会儿就讲。滑动窗口中一个
TCP
有效载荷的大小是在TCP
三次握手的时候就协商好了的!即 MSS,最大报文段长度!
如果网络通信的过程中丢包了,我们该如何理解滑动窗口呢?
- 应答丢失:这种情况下, 部分 ACK 丢了并不要紧, 因为可以通过后续的 ACK 进行确认;
-
数据包丢失:
- 如下图:当 [ 1 , 1000 ] [1, 1000] [1,1000] 报文段丢失之后, 发送端会一直收到 1001 这样的 ACK, 就像是在提醒发送端 “我想要的是 1001” 一样
- 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 [ 1001 , 2000 ] [1001, 2000] [1001,2000] 重新发送 (快重传机制)。
- 这个时候接收端收到了 1001 之后, 再次返回的ACK就是 7001 7001 7001 了(因为 [ 2001 , 7000 ] [2001, 7000] [2001,7000])接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中
滑动窗口怎么移动的?移动的时候大小会变化吗?怎么变化的?会为 0 吗?
- 滑动窗口原则上是不能向左移动的,只能向右移动。
- 滑动窗口在移动的时候,窗口大小会动态变化,取决于接收方接收缓冲区的大小,发送方的发送缓冲区大小,以及网络状况(拥塞控制部分详讲)。
- 滑动窗口的 start 会根据接收方发送的确认序号进行设置,滑动窗口的 e n d = m i n ( 接收方的窗口大小,发送方的有效数据,拥塞窗口 ) end = min(接收方的窗口大小,发送方的有效数据,拥塞窗口) end=min(接收方的窗口大小,发送方的有效数据,拥塞窗口) 前两个你应该现在就能理解了,至于拥塞窗口是什么,会在拥塞控制的时候详解,到时候你在回来看看就能理解了!
滑动窗口会在发送缓冲区中越界嘛?
发送缓冲区本质是一个数组嘛,而滑动窗口一直是向右移动的,这么来看好像会越界哈!其实是不会的,因为
TCP
采用了类似环形队列那种的环形算法。保证滑动窗口不会在发送缓冲区中越界!
补充一个小小的知识点:
在进行三次握手的时候,通信双方会进行起始序号的协商:一般来说通信双方会都随机出来一个起始序号,通信的起始序号就是双方随机出来的较小序号。
起始序号 + 发送缓冲区的数组下标 = 序号 ( 32 位序号中的序号 ) 起始序号 + 发送缓冲区的数组下标 = 序号(32位序号中的序号) 起始序号+发送缓冲区的数组下标=序号(32位序号中的序号)
确认序号 − 起始序号 = 下次发送数据的起始下标 确认序号 - 起始序号 = 下次发送数据的起始下标 确认序号−起始序号=下次发送数据的起始下标
快重传
当发送方收到三个连续且相同的确认序号,客户端会对报文进行立刻重发,这种机制被称为快重传(“高速重发控制”)
既然有了超时重传,为什么还要有快重传呢?
- 超时重传:
- 当发送方发送数据后,在等待一定时间内如果没有收到接收方的确认,发送方会认为数据丢失,并触发超时重传机制。
- 超时重传的主要作用是处理极端情况下的丢包或网络延迟过高的情况,确保数据的可靠传输。
- 超时重传机制可能会引入较大的延迟,因为需要等待超时时间后才能进行重传,可能会降低数据传输的效率。
- 快速重传:
- 快速重传是指当发送方连续收到 3 个重复的确认时(即收到 3 次相同的确认号),就会认为中间的数据丢失,并立即进行重传。
- 快速重传的主要作用是加速对丢失数据的检测和重传,而不必等待超时时间。
- 快速重传能够更快地响应丢失数据的情况,减少了等待超时的延迟,提高了数据传输的效率和响应速度。
快重传保证效率,超时重传用来兜底!
延迟应答
在客户端与服务端进行通信的时候,发送方如果一次能够发送更多的数据,那么发送的效率就越高,但能够发送更多数据的前提是接收方的接收缓冲区有足够大的空间!因此,如果接收方能够给发送方响应一个更大的窗口(TCP
报文中的窗口字段填入更大的值),那么数据传输的效率是不是就变高了呢?那么,如何让接收方给发送法告知一个更大的窗口呢?
接收方收到一个报文之后,不着急应答,给上层冲扽的时间将接收缓冲区中的数据取走之后在做应答,这样是不是就有更大的窗口空间了呢?
TCP
的延迟应答机制指的是TCP
接收方在接收到数据后不立即发送确认(ACK)而是延迟一段时间后再发送确认的策略。延迟应答的实现可以有多种方式,其中包括:
- 延迟应答定时器:接收方收到数据后并不立即发送确认,而是启动一个定时器。在一段延迟时间内,如果接收方没有接收到更多的数据,则发送确认。如果在延迟时间内接收到了更多的数据,接收方可能会在此数据的确认中一并确认之前接收到的数据。
- 累积确认:接收方在确认时不一定要确认每个单独的数据段,而是可以等待一段时间,然后一次性确认多个数据段。这样可以减少确认包的数量,提高网络利用率。
延迟应答机制的优点包括:
- 减少确认包的数量,降低网络负载和传输延迟。
- 提高网络的利用率,减少了确认包对网络带宽的占用。
- 可以允许接收方更有效地处理接收到的数据,而不用每次接收到数据就立即发送确认。
延迟应答效率不一定提高,因为上层可能就是不读你接收缓冲区的数据,因此,在进行
TCP
通信时, 建议每次都尽快通过recv,read
等接口将数据从接收缓冲区中读上来。延迟应答也可能会引入一定的延迟,特别是在对实时性要求较高的应用场景下,可能会影响到应用的响应时间。因此,在选择是否使用延迟应答时,需要根据具体的应用场景和需求进行权衡。
捎带应答
这个机制在前面已经讲得差不多了,这里就简单总结一下吧!
TCP
通信的捎带应答机制指的是在发送 TCP
数据包时,确认号(ACK)和数据一起发送,而不是单独发送确认数据包。这样做的目的是在尽量减少网络通信时的开销,提高通信的效率。
捎带应答机制通常会结合延迟应答机制一起使用。当接收方收到 TCP
数据包时,它可以延迟一段时间,等待接收到更多的数据包或等待定时器到期,然后一并发送确认(ACK)。
拥塞控制
如果在使用 TCP
协议发送数据时出现了问题,问题的原因不仅限于接收方的主机,还可能是网络问题!
- 如果通信的过程中,出现了少量的丢包 − − − − − − − > -------> −−−−−−−> 这是常规情况。
- 如果通信的过程中,出现了大量的丢包 − − − − − − − > -------> −−−−−−−> 可能是因为设备的硬件问题,也可能是数据量太大,引起阻塞啦!
TCP
如何判断网络出现问题了呢?
其实很简单,如果滑动窗口内有大量的数据没有收到应答,
TCP
即可判断网络出现了问题。
如果出现了大量丢包,能立即对报文进行超时重传嘛?
答案显然是不能啊!
- 如果是因为硬件异常导致的大量丢包,你进行超时重传并没有用!等着断开连接吧!
- 如果是因为网络拥塞导致的大量丢包,立即进行超时重传不就会加剧这种情况嘛!
如果出现了大量丢包发送方应该怎么办呢?
正确的做法其实就是等一会儿再发送,或者只发送少量的报文!你可能就会问了,网络拥塞这么大一个问题,仅靠我一方减少数据的发送就能解决问题吗?臣妾做不到啊!
- 网络资源是一个大家共享的资源,一台主机检测到网络出现拥塞,那么就一定会有部分其他主机也检测到网络拥塞!
- 要知道大家使用的都是
TCP/IP
协议啊!面对网络拥塞时大家的决策都是相同的啊!只要检测到网络拥塞的主机都等一等,或者少发一点数据,网络状况不就好起来了吗!- 因此,拥塞控制的具体策略是每台识别到网络拥塞的主机都要做的!
还没有说拥塞控制的概念呢!
TCP
的拥塞控制是指TCP
协议中的一种机制,用于避免网络拥塞并优化网络的性能。拥塞控制主要目的是确保网络的稳定性和公平性,防止数据包在网络中丢失或堵塞。
拥塞控制的具体原理
慢启动机制:当发生网络拥塞时(或者是
TCP
连接刚刚建立的时候),发送方会先发 1 个报文,如果收到应答再发送 2 个报文,再收到应答就发送 4 个报文,依次类推!此过程肯定不会一直这样进行下去,且听我慢慢分析。此处先引入一个概念叫做 拥塞窗口。拥塞窗口(Congestion Window) 表示了发送方在任何给定时间点上可以发送的数据包数量。因此,拥塞窗口实际上反映了发送方当前的发送能力,即在不引起网络拥塞的前提下,可以发送的最大报文数量。拥塞窗口是判断网络健康程度的指标,反映了当前网络的拥塞程度和发送方的发送能力。如果发送方发送的数据包数量超过了拥塞窗口的大小,就意味着发送方试图以超过网络承载能力的速率发送数据,从而可能导致网络拥塞的发生。
回到刚才的问题哈!发送方先发送 1 个报文,如果收到应答再发送 2 个报文,发送报文的数量会一直指数增长嘛!显然这是不可能的,当拥塞窗口的大小超过 慢启动的阈值,拥塞窗口的大小就会通过拥塞避免算法变成线性增长的。因为拥塞窗口的大小表示了发送方当前的发送能力,自然,能发送报文的数量也变成了线性增长的!
拥塞窗口的大小在先于慢启动阈值的时候是指数增长的,一旦超过满启动阈值就会变成线性增长,避免发生网络拥塞!
拥塞窗口有多大,并不代表就真的能够一次发送这么多的报文!
- 首先,发送方可能没有这么多的数据。
- 其次,接收方的接收缓冲区可能没有这么大。
- 因此,我们得到了发送方发送缓冲区滑动串口的大小公式: m i n ( 发送方数据大小,接收方接收缓冲区大小,拥塞窗口口大小 ∗ M S S ) min(发送方数据大小,接收方接收缓冲区大小, 拥塞窗口口大小 * MSS) min(发送方数据大小,接收方接收缓冲区大小,拥塞窗口口大小∗MSS)。滑动窗口表示的是报文数量嘛,对应的实际大小当然要乘上 M S S ( 最大报文段长度 ) MSS (最大报文段长度) MSS(最大报文段长度)
这个慢启动阈值是个什么东西呢?
慢启动阈值(ssthresh)的初始值一般是操作系统设定的,当拥塞窗口的大小大于慢启动阈值,拥塞窗口就会变成线性增长,如果在线性增长的过程中的某一时刻发生了网络拥塞,那么拥塞窗口的大小会立即变为 1,通信慢启动阈值也会发生改变,慢启动阈值变为发生网络拥塞时的窗口大小的二分之一。至于为什么是二分之一,这肯定是经过大量的测试得出来的!
在这个过程中,网络不一定会拥塞,并且自身发送缓冲区的数据多少,对方接收缓冲区的大小都会制约通信过程中一次性发送数据包的多少!只有当拥塞窗口的大小就是实际发送数据包的数量时,才会和上图的曲线吻合!
TCP 面向字节流的概念
创建一个 TCP
的 socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区
- 调用
write
时,数据会先写入发送缓冲区中。 - 如果发送的字节数太长,会被拆分成多个
TCP
的数据包发出。 - 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去。
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区。
- 然后应用程序可以调用
read
从接收缓冲区拿数据。 - 另一方面,
TCP
的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。这个概念叫做 全双工。
由于缓冲区的存在, TCP
程序的读和写不需要一一匹配, 例如:
- 写 100 个字节数据时,可以调用一次
write
写 100 个字节,也可以调用 100 次write
,每次写一个字节。 - 读 100 个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次
read
100 个字节,也可以一次read
一个字节,重复 100 次。
因此,我们可以这样来理解 TCP
面向字节流的概念:
-
连续的字节流:
TCP
将数据视为一系列连续的字节流,而不关心数据的边界或消息的结构。发送方将数据作为字节流发送到网络上,接收方则按照相同的顺序接收数据,并将其重新组装成原始的字节流。 -
无消息边界:与其他协议(如UDP)不同,
TCP
中的数据没有明确的消息边界。这意味着发送方可以将任意大小的数据发送到网络上,而接收方必须通过其他手段来确定消息的边界,通常是通过应用层协议或特殊的消息格式来识别消息的边界。例如:* 我们在自定义协议的时候我们就是用特殊的消息格式来识别消息的边界。* `http` 协议通过 `\r\n`,空行,以及 `Ctent-Length` 来识别消息的边界。
数据包的粘包问题
数据包的粘包问题 是指在网络通信中,发送方发送的多个数据包在传输过程中被合并成一个或多个接收方接收的数据包,或者接收方接收的一个数据包被拆分成多个数据包的现象。这种情况可能导致接收方无法正确解析数据,从而造成通信错误或数据解析错误。
- 首先要明确,粘包问题中的 “包”,是指的应用层的数据包。
- 在
TCP
的协议头中,没有如同 UDP 一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。- 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
- 站在应用层的角度,看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
那么如何避免粘包问题呢? 归根结底就是一句话,明确两个包之间的边界。
- 对于定长的数据包,保证每次都按固定大小读取即可。从缓冲区从头开始按固定大小依次读取即可。
- 对于变长的数据包,可以在报头的位置,约定一个报头总长度的字段,从而就知道了包的结束位置。自定义协议的例子哈!
- 对于变长的数据包,还可以在数据包和数据包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可)。
总结下来解决粘包问题有如下方法:
- 定长报文。
- 使用特殊分隔符。
- 自描述字段 + 定长报头。
- 自描述字段 + 特殊字符。
**对于UDP协议来说, 是否也存在 “粘包问题” 呢? **
- 对于
UDP
,如果还没有上层交付数据,UDP
的报文长度仍然在·- 同时,
UDP
是一个一个把数据交付给应用层。就有很明确的数据边界。- 站在应用层的站在应用层的角度,使用
UDP
的时候,要么收到完整的UDP
报文,要么不收。不会出现 "半个 "的情况。- 你也可能听到有人说 UDP 存在粘包问题,但严格意义上来讲真的不算是粘包问题:因为这些问题都不是在应用层上的粘包!
- 数据合并:在发送端,多个消息可能会被合并到一个
UDP
数据包中一起发送。在接收端,这些消息可能会被一次性接收到,但它们是独立的消息,没有像TCP
中那样的顺序关系。- 数据分片:在发送端,如果
UDP
数据包的大小超过了网络的MTU
(最大传输单元),则这些数据包可能会被分片传输。在接收端,接收到的数据可能是分片的形式,需要重新组装。(这是 IP 协议的内容,以后会讲的)
TCP 连接异常的问题
- 进程终止:
TCP
连接本身适合文件直接相关的,而文件的生命周期是随进程的,进程终止会释放文件描述符,仍然可以发送 FIN。和正常关闭没有什么区别。 - 机器重启:机器重启需要操作系统杀掉所有的进程,因此和进程终止的情况相同.
- 机器掉电/网线断开:这种情况下是没有机会进行 4 次挥手的。
- 发送方掉电/网络连接断开:
- 如果发送方的机器掉电或者网络连接断开,那么它将无法继续发送数据。在这种情况下,接收方将不再收到来自发送方的数据,并且可能会在一段时间后检测到连接超时或异常断开。
TCP
内部有保活机制,假设客户端出现这种情况,服务端就会向客户端发送保活信息,客户端如果没有回应,就会关闭连接! - 如果发送方重新上线并重新建立连接,通常会触发新的
TCP
连接(发送 RST 标记位为 1 的报文),而不是恢复原有的连接。这意味着之前的数据传输状态将丢失,并且需要重新发送数据或者从其他途径恢复丢失的数据。
- 如果发送方的机器掉电或者网络连接断开,那么它将无法继续发送数据。在这种情况下,接收方将不再收到来自发送方的数据,并且可能会在一段时间后检测到连接超时或异常断开。
- 接收方掉电/网络连接断开:
- 如果接收方的机器掉电或者网络连接断开,那么它将无法接收来自发送方的数据。发送方将不再收到来自接收方的确认消息,可能会触发发送方的超时重传机制,尝试重新发送数据。
- 当接收方重新上线并重新建立连接时,发送方可能会将之前未收到确认的数据重新发送,以确保数据的可靠传输。这可能会导致数据的重复发送或者乱序传输,需要接收方进行适当的处理和去重。
- 发送方掉电/网络连接断开:
TCP 总结
为什么TCP这么复杂? 因为要保证可靠性,同时又尽可能的提高性能。
- 可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重传
- 连接管理
- 流量控制
- 拥塞控制
- 提高性能:
- 滑动窗口
- 快重传
- 延迟应答
- 捎带应答
知识点总结
- 理解 TCP 协议段各个字段的含义。
- 掌握 TCP 协议可靠性策略和效率优化策略。