TCP协议

目录

TCP协议段格式

TCP的特点

确认应答

超时重传

连接管理

建立连接

断开连接

滑动窗口

流量控制

拥塞控制

延迟应答

捎带应答

面向字节流

粘包问题

异常情况

基于TCP的应用层协议

TCP 和 UDP 的对比

TCP小结


TCP(Transmission Control Protocol 即传输控制协议)是传输层的重要协议之一,对数据的传输进行详细的控制

TCP协议段格式

我们可以看到,相比于UDP的报头,TCP的报头要复杂得多 

16位源端口号:发送端端口号,即数据从哪个进程来

16位目的端口号:接收端端口号,即数据要到哪个进程去

32位序号:用于标识数据流中每个字节的顺序,后续在 确认应答 详讲

32位确认号:用于确认接收到的数据的下一个期望序列号,后续在 确认应答 详讲

4位TCP报头长度:表示TCP头部有多少个 32位bit(有多少个4字节,即单位是 4字节),因此,TCP头部的最大长度为 15 * 4 = 60字节

6位保留位:在UDP协议中,其报文长度受到2个字节的限制,当要传输的数据更大时,无法扩展报文长度(若自行改变报头长度,则本机发送的UDP数据报就会和其他机器不兼容,从而无法通信),因此,TCP就在设置报头时,提前设置了 6位保留位(虽然现在不使用,但以后需要使用时,可直接使用保留位),当后续需要扩展功能时,就可以使用保留位,避免TCP的扩展引起不兼容问题

6位标志位:

URG:紧急指针是否有效

ACK:确认号是否有效

PSH:提示接收端应用程序立即从TCP缓冲区把数据读走

RST:对方要求重新建立连接(我们将携带RST表示的报文称为 复位报文段

SYN:请求建立连接(我们将携带SYN标识的报文称为 同步报文段

FIN:通知对方,本端要关闭了(我们将携带FIN标识的报文称为 结束报文段

16位窗口大小:表示接收窗口大小,用于流量控制,后续在 流量控制 详讲

16位校验和:发送方通过 CRC 计算校验和,若接收方校验不通过,则认为数据有误(校验和不光包含TCP首部,还包含TCP数据部分)

16位紧急指针:标识哪部分数据是紧急数据,仅URG 标志位被设置时才有效

40字节选项:可变长度字段,用于指定各种控制选项。选项长度可变化,但一般以4字节为单位

数据:实际传输的数据部分

TCP的特点

有连接:双方在通信之前需要先建立连接

可靠传输:TCP通过序列号、确认应答、重传机制等手段来确保数据的可靠传输。接收方会确认已成功接收到的数据,并且发送方会在没有收到确认时进行重传,以确保数据不会丢失或损坏。

面向字节流:以字节流为单位进行传输

全双工通信:TCP允许通信双方同时发送和接收数据,实现了全双工的通信能力。

其中 可靠传输 是TCP协议 的重要特性,TCP的初心就是为了解决 可靠传输 问题

如何实现可靠传输?

网络通信的过程是复杂的,我们无法确保发送方发送的数据一定到达接收方,因此,我们只能尽可能发送,当发送方能够知道对方是否接收到,就认为是可靠传输了 

其中,确保可靠传输的重要机制是 确认应答

接下来,我们就先来学习确认应答 

确认应答

确认应答:接收方 接收到数据后向 发送方 发送的一个确认消息,用于通知发送方数据已经成功接收。即 当 发送方 发送数据后,接收方 接收到数据后会发送一个 确认消息 给 发送方,告知 发送方 数据已被正确接收。

TCP会对每个字节的数据进行编号,即为序列号

TCP的序列号和确认序号都是以字节来进行编号的。由于序号是连续的,因此发送时只需要在报头中保存第一个字节的序列号,后续字节的序列号都能够通过计算得出(例如:接下来要发送 序列号为 2001 - 3000 的数据,发送方只需在报头保存 序列号2001 ,后续的序列号都可以通过计算得出)

当 接收方 接收到数据后,会给 发送方 反馈一个 应答报文(也称为 ACK报文),标志位中的ACK置为1时,标志着当前报文是应答报文。

每个应答报文都带有对应的确认序列号,意思是告诉发送方:我已经接收了哪些数据,下一次你应该从哪里开始发送数据(例如,接收方收到 序列号为1 -1000的数据后,应答报文的确认序号值就为 1001,表示:1001之前的数据已全部接收,接下来应发送 序列号为1001及以后的数据了

例如:

注意:确认序列号表示的是 当前序列号之前的数据已全部接收

发送方 发送数据给 接收方,接收方 发送应答报文告诉发送方当前数据是否成功接收,但在网络通信过程中可能会出现 丢包 的情况,若数据包丢了,未到达对方,对方也就不会返回 应答报文;若应答报文丢了,发送方 未收到 应答报文,也就不知道对方是否 收到数据

因此,仅仅靠确认应答无法保证可靠传输,还需要 超时重传

超时重传

超时重传:当 发送方 发送数据后,在一定时间内没有收到 接收方 的确认消息时,发送方 会认为数据丢失或损坏,然后触发超时重传机制,重新发送数据。即,当 发送方 发送数据后,会启动一个定时器来计时。如果在定时器结束之前没有收到接收方的确认消息,发送方就会认为数据丢失,然后会重新发送相同的数据。这样可以确保数据在传输过程中不会丢失。

发送方未接收到确认消息可能有两种情况:

(1)数据丢失:

(2)应答报文丢失: 

当出现应答报文丢失的情况时,发送方重传数据,则接收方收到两份相同的数据,接收方会如何处理呢?

TCP socket 在内核中存在 接收缓冲区,发送方发来的数据,首先要放入接收缓冲区中,数据到达接收缓冲区时,接收方首先会判断当前缓冲区是否已经有该数据(或该数据曾在接收缓冲区中存在过),若存在或存在过,则会丢弃重复的数据

那接收方如何判断 该数据 存在或存在过呢?

判断的核心依据是 数据的序列号

若数据还在接收缓冲区中,未被读取,则 将接收到的数据序号与缓冲区中数据序号对比,若存在数据序号与新数据序号相同,则数据重复了

若数据已经被读取走了,由于应用程序读取数据时,是按照序号的先后顺序进行读取的(先读取序号小的,再读取序号大的),此时 socket API 中会记录上次读取的最后一个字节的序号,若新数据的序号小于 最后一个字节序号,则新数据是重复数据

超时重传时间的设置对于TCP性能和效率是至关重要的,如何确定超时时间?

设置过长的超时时间会导致数据传输速度变慢,而设置过短的超时时间可能会导致不必要的数据重传。最理想的情况下,找到一个最小的时间,保证"确认应答⼀定能在这个时间内返回",但这个时间的长短,随着网络环境的变化是有差异的。因此,TCP为了保证无论在任何环境下都能比较高性能的通信,会动态计算这个最大超时时间

出现丢包后会超时重传,若一直丢包会无限重传吗?

 重传的次数是有上限的,当重传到一定程度,还未接收到应答报文,发送方就会尝试重置连接,若重置失败,就会直接放弃连接

重传的重传时间不是固定不变的,其会随着重传次数的增加而增大(即重传的频率会越来越低)

当重传之后,还是出现了丢包,可能是网络出现问题,此时再多次重传也不会有应答,可以多等一会儿,等恢复后再进行重传,若再进行重传还丢包,说明此时丢包概率太大,网络可能出现严重问题,此时就会尝试重置连接

例如,在 Linux中,超时时间是以 500ms 为单位进行控制的,每次超时重传的超时时间都是 500ms 的整数倍

若一次重传之后,仍然得不到应答,则会等待 2 * 500 ms 后再进行重传

若仍得不到应答,等待 4 * 500ms 后进行重传,以此类推,以指数形式递增

累计到一定的重传次数,TCP 会认为网络或对端主机出现异常,就会强制关闭连接

连接管理

正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接

建立连接

TCP是有连接的,因此,在进行通信之前要先建立连接

当客户端执行 socket = new Socket(serverIp, serverPort) 时,这个操作就是在建立连接,但仅仅只是调用了socket API,其真正连接建立的过程是在操作系统内核完成的

那么内核是如何实现 建立连接 的呢?

通过 “三次握手” 来建立连接(此处所建立的连接 是 抽象的 连接,目的是为了让通信双方保存对方相关信息)

在 连接建立 的过程中,客户端是主动的一方,第一次交互,一定是客户端发起的

建立连接的过程:

首先,客户端向服务器发送SYN(同步)报文段(其中标志位中的 SYN 置为 1),告诉 服务器:我想和你建立连接

接着,服务器收到后回复一个SYN+ACK(同步+确认)报文段,表示:好的,接受连接请求且准备好建立连接。

最后,客户端回复一个ACK(确认)报文段,表示:连接建立成功。

 

上述建立连接的过程,本质上就是 通信双方各自给对方发起一个 SYN,再各自给对方回复一个 ACK

在第一次握手时,客户端就已经将信息告诉服务器了,但服务器并不会立即存储这些信息,等握手环节完成,服务器才会最终保存客户端的信息

为什么要进行三次握手呢?

1. 三次握手可以针对通信路径,进行 投石问路,初步确认一下通信链路是否通畅

2. 三次握手也是在验证通信双方的发送能力和接收能力是否正常

客户端 发送 SYN报文段,服务端接收后,就能够知道:客户端发送能力正常,服务器接收能力正常

接着 服务端 回复,客户端接收后,就能知道:SYN报文段到达服务器(客户端发送能力正常,服务器接收能力正常),且 服务器发送能力正常,客户端接收能力正常),

最后客户端发送 ACK报文段,告诉服务器 : 服务器发送能力正常,客户端接收能力正常

3. 三次握手过程中通信双方也会协商一些必要的参数

而协商的参数往往放在 选项 中,其中一个重要的参数是 TCP 通信的序号起始值。序号的起始值一般不是从 0 或 1 开始的,而是选择一个 较大的数字 作为起始值,这样,即使是同一个客户端和服务器,每次连接,开始的序号都是不同的

(例如:通信双方在第一次连接过程中,有一个传输的数据包一直未到达对面,等到达对面时,第一次连接已经断开,建立第二次连接了,此时,这个数据包应该丢弃,如何判断这个数据包是否应该丢弃?可以通过序列号来确定,因此,每次连接选择不同的开始序号,以便判断数据包是否是本次通信的数据包)

断开连接

连接的本质就是让通信双方保存对方的信息,而每个客户端(或服务器)都需要保存很多的对端信息,而断开连接的目的,就是为了将对端信息删除掉

如何断开连接呢?

通过 “四次挥手” 来断开连接, 在断开连接的过程中,第一次 “挥手”可以由客户端发起,也可以由服务器发起。

四次挥手的过程:

当数据传输完成或者需要关闭连接时,就可以进行连接的释放。

此时,一方(A)发送一个FIN(结束)报文段,表示不再发送数据

对方(B)收到后回复一个ACK报文段,确认收到FIN报文段,

然后对方(B)也发送一个FIN报文段,也表示不再发送数据

另一方(A)回复ACK报文段,最终完成连接的释放

 

上述断开连接的过程,本质上也是 通信双方各自给对方发起一个 FIN,再各自给对方回复一个 ACK

在 四次挥手中,接收到 对端 发送的FIN报文段后,会先发送一个 ACK报文段,再发送一个FIN报文段,为什么不像三次握手那样,将 ACK 和 FIN 合并到一起发送呢?

 在三次握手时,服务器接收到客户端发送的 SYN报文段后,需要发送 ACK 和 SYN 报文段给客户端,这两个报文段的触发时机是一致的,都是内核收到 SYN 报文段后立即触发,且这两个报文段的触发都和应用程序无关,因此,这两个报文段发送的时间间隔很小,因此可以合并到一起发送

而四次挥手中,在接收到 对方的 FIN 报文段后,内核收到 FIN报文段,立即触发 ACK,但 FIN 是在调用 socket.close() 时触发,ACK 和 FIN报文段时间间隔可能较长,此时就会无法进行合并,就只能分开发送了。

在学习了三次握手和四次挥手之后,我们来总结两者的相似之处和不同之处:

相似之处:

都是通信双方各自给对方发起一个SYN(或FIN)报文段,各自给对方返回一个ACK报文段

数据的传输顺序: SYN(或FIN) -> (ACK -> SYN(或FIN))(同一个机器)-> ACK

不同之处:

三次握手中间两次一定能合并,但四次挥手则不一定

三次握手必须是客户端主动发起;四次挥手,客户端和服务器都可以主动发起

滑动窗口

在确认应答策略中,对于每发送的一个数据,接收方都要返回一个ACK确认应答,发送方在收到AKC后再发送下一个数据,发送方大量时间消耗在等待ACK上,传输效率较低

而滑动窗口能够在保证可靠传输的前提下,尽可能提高效率,让消耗的时间成本尽可能少(通过滑动窗口机制,能够提高效率,但效率不可能高于UDP)

发送方维护一个发送窗口,表示可以发送的数据范围,窗口中的数据无需等待确认应答,即发送方将窗口中的所有数据都发送出去,然后等待ACK;当收到ACK后,滑动窗口向后移动,继续发送后续数据

操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区中删掉,窗口越大,网络的吞吐率就越高

若此时出现了丢包,然后进行重传?

(1)ACK丢了

数据发送方(A)发送的数据成功到达接收方(B),但接收方(B)发送的ACK丢了:以上图为例,若 确认消息(下一个是 1001)丢了,但 确认消息(下一个是 2001)成功到达数据发送方(A),由于确认序号表示的是::已经接收了哪些数据,下一次你应该从哪里开始发送数据,因此 2001表示的是:已经接收 2001之前所有数据,下一次应该从 2001 开始发送

因此,若ACK丢了,可以通过后续ACK进行确认

(2)数据包丢了

发送方如何知道哪个数据包丢了?

 TCP有 接收缓冲区,当接收方接收到数据后,先将这些数据放到接收缓冲区里排序,此时,就很容易发现中间缺失哪个数据包

假设1001 - 2000 的数据丢了,此时接收方会一直发送 确认消息(下一个是 1001),提示发送方 1001 - 2000 的数据需要重传

若发送方连续三次收到了同样的 确认消息(下一个是1001),就会对数据 1001 - 2000进行重新发送

此时发送方接收到1001之后,就会返回 ACK(下一个是4001),这是因为1 - 1000 及 2001 - 4000接收方之前已经收到了,放在接收方的接收缓冲区

上述重传过程中,整体的效率是很高的。这里的重传做到了“针对性”的重传,即哪个丢了就重传哪个,已经收到的数据不必重复发送,这种机制被称为 “高速重发控制”,也叫 “快重传”

流量控制

接收端处理数据的速度是有限的,若发送端发的太快,就会导致接收端的缓冲区被打满,此时若发送端继续发送,就会造成丢包,从而引起丢包重传等一系列连锁反应

因此,TCP 支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做 流量控制(Flow Control)

接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过 ACK 通知发送端

窗口大小字段越大,说明网络的吞吐量越高,传输效率也就越高

接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端

发送端接收到这个窗口后,就会减慢自己的发送速度

若接收端缓冲区满了,就会将窗口大小置为0,此时发送端不再发送数据,但需要定期发送一个 窗口探测数据段,使接收端将窗口大小告诉发送端

接收端通过 16位窗口字段 告诉发送端窗口大小信息,那么,16位数字最大表示 65535,TCP 窗口最大就是 65535 字节吗?

实际上,TCP 首部 40 字节选项中还包含了一个 窗口扩大因子 M,实际的窗口大小是窗口字段的值左移M位

拥塞控制

虽然 TCP 通过滑动窗口机制能够高效可靠的发送大量数据,但是若在刚开始阶段就发送大量数据,仍然可能会引发问题

这是因为网络上有很多计算机,当前网络状态可能已经比较拥堵,在不清楚当前网络状态下,贸然发送大量数据,可能会让网络状态 “雪上加霜”

因此,TCP 引入了 慢启动 机制,先发送少量数据去探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据

在这里,使用到了 拥塞窗口:

 在开始发送时,定义拥塞窗口的大小为1

每次收到一个 ACK 应答,拥塞窗口 + 1

每次发送数据包时,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口

以上的拥塞窗口增长速度,是指数级别的。慢启动只是指初始时慢,但其增长速度非常快

为了不增长那么快(不能使拥塞窗口单纯的加倍)

在此处引入了一个 慢启动的阈值

当拥塞窗口超过这个阈值时,不再按照指数方式增长,而是按照线性方式增长

当TCP 开始启动时,慢启动阈值等于窗口最大值

每次 超时重发 时,慢启动的阈值都会变成原理的一半,同时拥塞窗口置为1

当少量丢包时,仅仅是触发超时重传;当大量丢包时,就认为是网络拥塞

当 TCP 开始通信后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降

拥塞控制,归根结底是 TCP 协议想尽可能快的把数据传输给对方,但是又要避免网络造成太大压力的折中方案

延迟应答

若接收端接收到数据后立即返回ACK应答,此时返回的窗口可能比较小

假设接收端缓冲区为 1M,一次接收到了 500K 数据,若立即应答,返回的窗口大小就是 500K

但实际上接收端处理的速度很快,10ms 之内就能将 500K 数据从缓冲区消费掉

在这种情况下,接收端处理还没有达到自己的极限,即使将窗口再放大一些,也能够处理过来

若接收端稍等一会再应答,如再等 20ms 再应答,此时返回的窗口大小就是 1M

 

那么,所有的包都可以延迟应答吗?

当然不是,延迟应答也有一定的限制条件:

数量限制:每隔 N 个包就应答一次

时间限制:超过最大延迟时间就应答一次

具体的数量和时间,不同的操作系统也有差异,一般 N 取 2,超时时间取 200ms

捎带应答

 在延迟应答的基础上,我们可以发现,在很多情况下,客户端和服务器在应用层是 “一发一收”的,例如,客户端发送 “你好”,服务器会给客户端回复一个 “你好”

此时,ACK 就能够“搭上顺风车”,与服务器回应的 “你好” 一起回给客户端,就能够将能够合并的数据包进行合并,从而起到提高效率的效果

面向字节流

 当创建一个 TCP 的 socket 时,同时在内核中创建了一个 发送缓冲区 和一个 接收缓冲区

调用 write 时,数据会先写入发送缓冲区

若发送的字节太长,会被拆分成多个 TCP 的数据包发出

若发送的字节数太短,就会先在缓冲区等待,等到缓冲区长度差不多,或其他合适的时机发出去

接收数据时,数据也是从网卡驱动程序到达内核的接收缓冲区

应用程序可以调用 read 从接收缓冲区拿数据

TCP 的一个连接,既有发送缓冲区,也有接收缓冲区,那么,对于这一个连接,既可以读数据,也可以写数据,这个概念叫做 全双工

由于缓冲区的存在,TCP的读和写不需要一一匹配,如:

写 100 个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节

读 100 个字节数据时,也不需要考虑写的时候是如何写入的,可以一次 read 100个字节,也可调用100次read,一次 read 1 个字节

粘包问题

这里的 包,指的是 应用层数据包

在 TCP 的协议头中,没有如 UDP 一样的 “报文长度” 这样的字段,但有一个序号这样的字段

站在传输层的角度,TCP 是一个一个报文传输过来的,按照序号排序好放在缓冲区中

站在应用层的角度,看到的是一串连续的字节数据

此时,应用程序看到一连串的字节数据,就不知道一个完整的应用层数据包是从哪里开始,哪里结束的

如何解决 粘包问题 呢?

要解决 粘包问题,就是要 明确两个包之间的边界

对于定长的包,只需要保证每次都按固定大小读取即可

对于变长的包,可以在包头的位置约定一个包总长度的字段,就可以知道包的结束位置

对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议是由我们自己来定,因此,只需要保证分隔符不与正文冲突即可)

那么,对于 UDP 协议来说,是否存在 粘包问题 呢?

对于 UDP,若还没有向上层交付数据,UDP 的报文长度仍然存在,同时,UDP 是一个一个把数据交付给应用层的,此时就会有很明确的数据边界

站在应用层的角度,使用 UDP 时,要么收到的是完整的UDP报文,要么不收,不会出现 “半个”的情况

异常情况

在数据传输的过程中,除了会出现丢包问题,网络也可能出现故障,此时该如何处理呢?

进程终止:进程终止时会释放文件描述符表,仍然可以发送FIN,与正常关闭没有区别

机器重启:与进程终止情况相同

机器断电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端就会发现连接已经不在了,就会进行 reset,即使没有写入操作,TCP 自己也内置了一个 保活定时器,会定期询问对方是否还在,若对方不在,也会把连接释放

此外,应用层的某些协议,也会有一些这样的检测机制,如 HTTP 长连接中,也会定期检测对方的状态

基于TCP的应用层协议

HTTP

HTTPS

SSH

Telnet

FTP

SMTP

...

TCP 和 UDP 

        TCPUDP
有连接无连接
可靠传输不可靠传输
面向字节流面向数据报
有接收缓冲区,也有发送缓冲区有接收缓冲区,但无发送缓冲区
大小不受限大小受限(一次最多传输64K)

TCP 用于可靠传输的情况,如文件传输

UDP 用于高速传输和实时性要求较高的通信,如视频传输

归根结底,TCP 和 UDP 都是我们使用的工具,什么时候用,什么时机用,具体怎么用,都需要根据具体的需求场景判定

TCP小结

相比于 UDP,TCP要复杂许多,这是因为TCP既要保证可靠性,同时又要尽可能的提高性能

可靠性:

校验和

序列号

确认应答

超时重传

连接管理

流量控制

拥塞控制

提高性能:

滑动窗口

快速重传

延迟应答

捎带应答

其他:

定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)

评论 56
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

楠枬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值