目录
1.理解TCP的部分字段
2.TCP的策略以及其它报头
确认应答(ACK)机制编辑
超时重传机制
连接管理机制
建立连接为什么要三次握手?
为什么要四次挥手?
验证两种状态,CLOSE_WAIT(不关闭文件fd即可)和TIME_WAIT
3.流量控制
4.滑动窗口
理解滑动窗口
丢包问题
其它问题
5.拥塞控制
6.延迟应答
7.捎带应答
8.面试问题
8.1面向字节流
8.2粘包问题
9.TCP异常问题
10.TCP小结
1.理解TCP的部分字段
TCP协议传输的数据单元是报文段,一个报文段由TCP首部(报文头)和TCP数据两部分组成,其中TCP首部尤其重要,首部用于控制(新建、断开)连接、流量和拥塞等。TCP首部的固定长度是20B,最大长度是60B,其中可变选项长度最长为40B(4B×10)。
- 单位:首部长度字段以32位字(即4字节)为单位进行表示。这意味着,如果首部长度为5,则实际表示的是20字节(5 * 4 = 20)。
- 范围:TCP首部的最小长度是20字节(没有选项字段时),最大长度是60字节(包含最大长度的选项字段时)。因此,首部长度字段的值范围从5(表示20字节的首部)到15(表示60字节的首部)。
如何我们需要进行解包,那么我们就可以使用首部长度-20,看是否有余数,没有就表示没有选项,有就表示携带选项,那么我们就可按照这种方式将首部,选项,数据进行解包。那么进行解包后如何向上交付呢?
我们把报文发送给远端的时候,得知道目的端口,那么它就可以被对方交给指定的进程,由指定的进程读到报文的数据,所以目的端口可以帮我们对报文进行分用
2.TCP的策略以及其它报头
确认应答(ACK)机制
想象你正在和一个朋友通过邮寄信件来交流。你写了一封信,并在信封上标上了序号,比如“1”,然后寄给了朋友。朋友收到信后,会知道你已经成功地把这封信发送给了他,于是他会给你回一封简短的信,上面写着“我已经收到你的第1封信了”。这封简短的回信,就相当于TCP中的ACK报文。
在这个比喻中:
- 你写信并标上序号,就相当于TCP发送方发送数据并附上序列号。
- 朋友收到信并给你回信,就相当于TCP接收方接收数据并发送ACK报文。
- 朋友回信中提到的“第1封信”,就相当于ACK报文中的确认序号,它告诉发送方接收方已经成功接收到了哪个序列号之前的所有数据。
通过这个机制,你可以确保你写的每一封信都被朋友成功接收了。如果有一封信朋友没有收到,他就不会给你回那封简短的回信。这样,你就知道那封信可能丢失了,你可以重新写一封并再次寄出。
在TCP中,确认应答机制也是同样的道理。它确保了发送方发送的每一个数据包都被接收方成功接收了。如果某个数据包没有收到ACK报文,发送方就会知道这个数据包可能丢失了,然后它会重新发送这个数据包,直到收到ACK报文为止。
TCP 将每个字节的数据都进行了编号. 即为序列号(存在于报头中,每一个序号都代表的是一个完整的 报文 ,TCP通信双发是以报文的形式进行交互数据)
每一个 ACK 都带有对应的确认序列号(如1001,表示1001之前的数据全部都收到了), 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.为什么非要有两个序号?
由于TCP支持全双工通信,因此在一个TCP连接中,同时存在两个方向的数据流。序列号用于标识发送方发送的数据顺序,而确认号用于标识接收方期望收到的数据顺序。这两个序号分别服务于发送方和接收方,共同确保数据的可靠传输。
两个序号需要同时使用的场景
TCP协议通过发送确认报文(ACK)来确认数据的成功接收。通常情况下,每当接收端收到发送端的数据后,会立即返回一个ACK报文。然而,在某些情况下,如果接收端恰好也需要向发送端发送数据(例如,对请求的响应),那么TCP协议允许将ACK报文与这些数据合并发送,此时一个序号,一个确认序号,这个过程就称为捎带应答。(提高IO效率)
因此只要发送方对接收方发消息,接收方应答了才表示数据收到了,没应答就表示数据丢失了,确认应答(ACK)机制在TCP中保证了发送方对接收方的可靠性。因此只要客户端和服务端都采用确认应答机制,那么就能保证两个朝向上的可靠性。该机制由OS自动发生。TCP的报文是有不同类型的
一定存在多个客户端同时向服务器发送不同类型的报文,因此TCP协议要有处理不同类型报文的能力。
为了区分自己报文的种类,TCP报头中存在6个标记为
fin
、syn
、rst
、psh
、ack
、urg
:TCP控制标志位,分别用于表示连接结束、同步序列号、重置连接、推送功能、确认收到、紧急指针有效等。其它都为保留字段如何理解序号
我们把缓冲区再逻辑上理解成一个char类型的数组,所以我们从应用层把数据字节流式拷贝到发送缓冲区,就相当于把数据放在了该数组当中,那么这些数据就天然的有序号了,第一字节,第二字节.....,如果要从第一个字节序号开始发,要发送100个字节,那么它的序号就是100,对方应答101,那么我后就从101开始发送数据。那么我们就可以理解,序号就是该数组缓冲区的下标!确认序号就是以对方的发送数据序号+1。当然真实情况会更复杂,但就是这么一个逻辑
超时重传机制
• 主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B;
• 如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答, 判定为丢包,就会进行重发;但是, 主机 A 未收到 B 发来的确认应答, 也可能是因为 ACK 丢失了;
因此主机 B 会收到很多重复数据. 那么 TCP 协议需要能够识别出那些包是重复的包, 并
且把重复的丢弃掉.
这时候我们可以利用前面提到的序列号, 因为序号就是数组缓冲区的下标,如果是同一段报文,那么它的序号肯定是一样的,就很容易做到去重了.序号不仅有上面的去重效果,还有排序效果,因为网路是很复杂的,我们即使先发送,但也不一定是先到达,对方的缓冲区会按照序号进行排序,在发给上层,做到按序到达!
小结:序号:1.构建确认序号 2.做去重操作 3.保证TCP的按序到达,所以TCP可靠!
如果超时的时间如何确定?
• 最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
• 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
• 如果超时时间设的太长, 会影响整体的重传效率;
• 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP 为了保证无论在任何环境(比如:网速的变化)下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
• Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍.
• 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
• 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
• 累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.
连接管理机制
在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接
服务端状态转化:
• [CLOSED -> LISTEN] 服务器端调用 listen 后进入 LISTEN 状态, 等待客户端连接;
• [LISTEN -> SYN_RCVD]客户端调用connect, SYN被置为1,表示一个连接请求的报文,一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送 SYN 确认报文.
• [SYN_RCVD -> ESTABLISHED] 客户端会给服务端的ACK做应答,服务端一旦收到客户端的确认报文, 就进入ESTABLISHED 状态, 可以进行读写数据了.connect只是发起了三次握手,三次握手的过程由TCP协议层自主完成;accept不参与三次握手,只是在调用后,把建立好的连接,给我们一个文件描述符交到应用层给我们操作
建立连接的本质:最后一个ACK对方收到了没有(建立成功/失败),那么如果客户端最后一个ACK发给服务端了,服务端没有收到怎么办?
- 如果客户端在发送最后一个ACK包后崩溃或网络故障导致包无法到达服务端,而服务端又未能在超时重传期间收到ACK包,那么服务端最终会关闭这个连接。
- 如果客户端之后恢复并尝试继续发送数据,但由于连接已被服务端关闭,客户端将收到一个RST重置包(服务端发送报文时将RST置为1),指示连接已不存在。此时,客户端需要重新发起连接请求。
以上过程就是三次握手
• [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用 close), 服务器会收到结束报文段(FIN), 服务器返回确认报文段并进入 CLOSE_WAIT,完成一次挥手;
• [CLOSE_WAIT -> LAST_ACK] 在这个状态下,它回复一个ACK报文段给客户端,确认收到客户端的断开连接请求。服务端仍然可以继续向客户端发送数据,直到所有待发送的数据都处理完毕。(完成二次挥手)•当服务端完成所有数据的发送后,它会向客户端发送一个FIN报文段,表示服务端也将停止数据发送。发送FIN报文段后,服务端进入LAST_ACK状态,等待客户端的ACK确认报文段。(完成三次挥手)
• [LAST_ACK -> CLOSED] 服务器收到了对 FIN 的 ACK, 彻底关闭连接.(完成四次挥手)断开连接是一个双发协商的过程:
客户端发送FIN,服务端同意ACK(客户端要给你发送的数据已经发送完了,我后面没有数据可以发送了,我断开连接了)。
服务端:我还有数据要发给你,我还没断开连接,而且你必须给我回ACK。
为什么客户端还能回ACK?
TCP连接是一个全双工连接,即数据可以在两个方向上同时传输。当客户端发送FIN报文段时,它只是关闭了从客户端到服务端的传输方向,但服务端到客户端的传输方向仍然是打开的。因此,客户端仍然可以接收来自服务端的数据,并回复ACK报文段。
虽然我可以把消息发给客户端,客户端都close(fd)了,文件描述符都关了,它上层怎么能读到数据呢?
如果在这个时候,服务端发送了数据给客户端,客户端的TCP协议栈仍然会接收这些数据,并且会发送ACK报文段来确认收到。这是因为TCP协议栈的职责是确保数据的可靠传输,即使应用程序已经关闭了socket。
但是,这里有一个重要的点需要注意:虽然TCP协议栈会接收这些数据并发送ACK,但这些数据并不会被传递给应用程序。因为应用程序已经关闭了socket,所以它没有机会读取这些数据。这些数据只是被TCP协议栈处理,用于确保连接的可靠关闭。
所以如果你想更精细的控制fd,就使用接口:shutdown,指定关闭文件描述符的读端/写端。在此状态下客户端只是被关闭了写端,读端没被关闭,还可以将数据读到上层
服务端发送FIN,客户端同意ACK(服务器也发完了,断开连接了,此时才彻底断开连接了)四次挥手,以最小的通信成本,建立了断开连接的共识
以上过程就是四次挥手
建立连接为什么要三次握手?
一次握手/两次握手,可以吗?
现实情况下,会有很多的客户端和服务端建立连接,那么哪些是正在连接,哪些是要断开连接。此时,连接肯定是需要一个内核数据结构进行管理的,那么就需要开辟内核数据结构的空间,也要花时间对结构中的变量做初始化。所以维护连接,不管时客户端和服务器,都是有时间和空间成本的。
洪水攻击问题
如果说有人把建立连接设置成一次握手,那么如果有一台机器给服务器发送大量的SYN报文(一个客户端给服务器发送大量的SYN,我们称为SYN洪水),对于客户端来说几乎没什么成本,此时服务器就被挂满了大量的连接,而且这些连接不会被使用,一次连接占一点资源,多次下来,导致服务器的资源越来越少,这就会导致资源的浪费。
三次握手就没这个问题吗?当然也存在上面洪水攻击的问题,在三次握手过程中,服务器在收到客户端的SYN报文后,会回复一个SYN+ACK报文。但是,此时服务器并不会立即分配资源给这个连接,而是会等待客户端的ACK报文。只有收到客户端的ACK报文后,服务器才会确认连接并建立相应的资源,这会让客户端和服务端消耗同等的资源 ,增加了客户端的成本。虽然没有彻底解决洪水攻击的问题,但比(一次/二次)好一些(SYN缓存、SYN Cookie会解决洪水攻击问题)。
正式回答为什么要三次握手
1.验证全双工---验证网络的连通性(用最小次数验证网络畅通,而且确实能发消息,收消息,对方也一样可以)一次握手只说明客户端能发,二次只能验证服务器能发能收,三次验证客户端也能收
2.建立双方通信的共识意愿:(一次)客户端:我想给你建立连接(SYN);(二次)服务端:好的(ACK)我也想和你建立连接(SYN)(只是服务端对于客户端的请求是来者不拒的,所以ACK+SYN,是做捎带应答);(三次)客户端:好的,我也同意和你建立连接(ACK)
为什么要四次挥手?
其实四次挥手本质和三次握手没有本质区别,客户端向服务端发送连接请求,服务器都是无脑同意的,所以SYN和ACK都是同时发出的,打包做捎带应答;如果客户端和服务器 同时都想断开连接,实际上服务也是可以把ACK和FIN打包做捎带应答,变成三次挥手,但通常客户端断开连接后,服务器还会有数据要给客户端,所以服务器的ACK和FIN不会同时发出,因此是四次挥手。
验证两种状态,CLOSE_WAIT(不关闭文件fd即可)和TIME_WAIT
客户端是浏览器,通过TCP连接我自己的服务器
代码:Linux--应用层协议HTTP协议(http服务器构建)-CSDN博客
CLOSE_WAIT:
服务器不关闭文件描述符:那么服务器就不会进行第三次和第四次挥手
当我使用浏览器访问服务器时,服务器没有关闭fd,此时就处于CLOSE_WAIT状态:
当我手动关闭服务器时,服务器状态才变成LAST_ACK:
如果我们服务器卡顿就可以考虑查看一下是否存在大量的CLOSE_WAIT状态,这就是不关闭文件fd造成的,导致fd泄漏。
TIME_WAIT:
我先把文件描述符正常关了:
在TCP四次挥手关闭连接的过程中,主动关闭方(通常是客户端)在发送最后一个ACK报文段后,会进入TIME_WAIT状态。这个状态会持续一段时间(通常是2倍的MSL,即最大报文段生存时间),以确保被动关闭方(通常是服务器)收到了ACK报文段。如果ACK报文段丢失,被动关闭方会重发FIN报文段,此时主动关闭方需要能够重发ACK报文段进行确认。
此次我们让服务器作为主动断开连接的一方,果然服务器处在了TIME_WAIT状态
然后我们立即重启服务器,发现bind失败:
在TCP连接中,主动关闭连接的一方(这里是服务器)在发送完最后一个ACK包后,会进入TIME_WAIT状态。这个状态会持续一段时间(通常是2倍的MSL,即最大报文段生存时间,通常为30秒到2分钟不等).
TIME_WAIT状态的主要目的是确保网络中所有的TCP报文段都已经消逝,这样做是为了避免旧的数据包干扰到新的连接。在TCP协议中,数据包可能会在网络中延迟或重复发送,如果一个新的连接在旧的数据包完全消逝之前就开始使用相同的四元组(源IP地址、源端口、目的IP地址、目的端口),那么这些旧的数据包可能会被错误地接收并处理,从而导致数据混乱或连接失败。(等待历史报文消散,2*MSL,即等待双发两个朝向的历史报文都消散)
MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, 在 Centos7 上默认配置的值是 60s;
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值:
在TIME_WAIT状态下,TCP连接的四元组(源IP地址、源端口、目的IP地址、目的端口)仍然被占用,因此服务器无法立即在同一端口上绑定新的监听套接字。这就是为什么重启服务器后立即尝试
bind
到同一端口会失败的原因。为了解决这个问题,我们要引入函数setsockopt:
在创建套接字(socket)之后,但在绑定(bind)之前,调用
setsockopt
函数设置SO_REUSEADDR
套接字选项。这个选项允许在同一个本地地址和端口上启动一个新的监听服务器,即使之前的连接仍处于TIME_WAIT状态。
sockfd
:套接字描述符,即你要设置的套接字的标识。level
:选项所在的协议层,通常设置为SOL_SOCKET
以访问套接字级别的选项。optname
:需要设置的选项名。optval
:指向包含新选项值的缓冲区的指针。optlen
:optval
缓冲区的大小。
SO_REUSEADDR
是一个常用的套接字选项,它允许在同一端口上启动一个新的监听服务器,即使之前的连接仍处于TIME_WAIT
状态。这可以通过在bind
调用之前设置此选项来实现。这个函数应该在套接字绑定之前被调用,以确保即使之前的连接仍处于
TIME_WAIT
状态,也可以在同一端口上启动新的监听服务器。void ReUseAddr() override//允许复用地址 { int opt=1; ::setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); }
3.流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这
个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此 TCP 支持根据接收端的处理能力(取决于接收端缓冲区的大小), 来决定发送端的发送速度. 这个机制就叫做流量控制。在报头中有16位的窗口大小(默认大小2的16次方),可以告知对方自己缓冲区剩余空间大小,便于对方动态的调整自己的发送速度(当然流量控制是双方都要做的)
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过 ACK 端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 在三次握手期间,就已经协商了双方的接收能力
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为 0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端。同时如果接收端好了,接收端也会主动的给发送端发送窗口更新通知。
如果作为接收端,缓冲区满了但是一直不更新,这是发送端的报文就会填写PSH标志位。对于接收方来说,当收到带有PSH标志的报文时,它会立即把接收缓冲区中的数据推送给进程,而不是按照通常的TCP流控机制等待缓冲区填满或超时事件发生。如果我们想让对方尽快处理数据,都可以设置PSH。
16位紧急指针
URG表示启用16位紧急指针。紧急数据是一种需要被优先处理的数据。当接收方收到URG标志位为1的TCP报文段时,它会意识到这部分数据需要被立即处理,而不是像普通数据那样先放入接收缓冲区等待后续处理。紧急数据通常会绕过接收缓冲区,直接交付给上层应用程序。tcp中紧急数据只有一个字节!
比如我们要向云服务器上传100G的数据,当下载到70个G的时候我想取消/暂停下载,这是紧急数据就可以插队优先交给上层,执行取消/暂停下载操作。
当我们要发送/接收紧急数据的时候,可以将send/recv的flag参数设置成MSG_OOB,我们将这些数据称之为:带外数据(于迅速通告对方本端发生的重要事件。带外数据比普通数据(也称为带内数据)有更高的优先级,应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。)
当然你可以不使用URG的操作,你可以给服务器绑定两个端口号,一个用于正常的接收数据,一个用于接收控制命令。
4.滑动窗口
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个 ACK 确认应答. 收
到 ACK 后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数
据往返的时间较长的时候既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性
能(其实是将多个段的等待时间重叠在一起了)
虽然我们可以并发的发很多消息,但前提是对方有能力接受。这里针对于发送方,要引出滑动窗口的概念了,在滑动窗口以内的数据,可以直接发送,暂时不用收到应答。
滑动窗口是发送缓冲区中的一部分,窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是 4000 个字节(四个段)。这里我们可以给出一个结论:滑动窗口的大小=对方同步给我的窗口大小,即对方的接收能力(暂时这么定)
滑动窗口的左侧区域是已发送已确认的数据,右侧则是待发送的数据。滑动窗口中发送的数据只要收到应答,则继续整体向右边滑动,发送待发送的数据。
问题:
互动窗口只能向右滑动吗?能不能向左滑动?
从目前来看,是不能的,因为左边是已经发送且被应答的数据。
滑动窗口可以变大吗?可以变小吗?滑动窗口可以为0吗?
当然可以,滑动窗口的大小是根据对方的接收能力动态调整的。
理解滑动窗口
上面我们在理解序号的时候,我们把缓冲区理解成了一个char类型的数组(逻辑上是环形的,不用担心越界),所以我们从应用层把数据字节流式拷贝到发送缓冲区,就相当于把数据放在了该数组当中,那么这些数据就天然的有序号了。
因此我们就可以理解,滑动窗口的本质就是发送缓冲区的下标,滑动窗口滑动的本质就是让指针做++。这样就让窗口向右边移动了。滑动窗口的动态变化,本质就是下标的范围在进行变化
滑动窗口如何更新?
当我们收到来自接收方的ACK报文,报文中有确认序号seq,告诉发送方下次消息从哪个地方开始发送,此时 win_start=ack_seq ;报文中还有窗口大小,也就接收方的接收能力,win_end=win_start+win(接收方窗口大小)
丢包问题
在滑动窗口中并发的发送了这么多数据,如何丢包了怎么办?
1.最左侧报文丢失
比如2000的报文丢失了,但是后面3000,4000,.....,7000都收到了。那么接收方ACK的确认序号怎么填?确认序号是指,确认序号之前的报文,接收方都收到了。由于接收方2001的报文并没有收到,但是后面的报文都收到,确认报文也只能填写1001,此时滑动窗口不向右进行滑动。当接收方收到了三个同样的确认序号1001,此时主机A就只能意识到我的2000报文丢了(即使其它报文也丢了,主机A暂时也不知道,因为它是根据重复三次的确认序号来判断的),主机A就会对2000的报文进行重发(这就是高速重发控制,也叫 "快重传"),这个时候接收端收到了 1001 之后, 再次返回的 ACK 就是 7001 了(因为 2001 -7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中,此时滑动窗口再向右边滑动。
既然有超时重传 ,为什么还要快重传呢?
如果数据出现了丢包,但没有达到三次重复的ACK报文,那么就会触发超时重传。尽管超时重传机制有效,但它存在一个问题:当网络状况不佳时,超时时间可能较长,导致数据重传延迟较大,进而影响数据传输的实时性和效率。为了解决这个问题,TCP引入了快速重传机制。
超时重传机制为数据传输提供了基本的可靠性保障,而快速重传机制则进一步提高了数据传输的实时性和效率。在实际应用中,TCP会根据网络状况动态调整这两种机制的使用,以达到最佳的传输效果。
数据包已经抵达, ACK 被丢了
这种情况下, 部分 ACK 丢了并不要紧, 因为可以通过后续的 ACK 进行确认;
2.中间报文丢失
比如是3000报文丢失,此时ACK的确认序号就应该填写2001,win_start此时会跟新到2001,此时又变成了新窗口的最左侧丢包问题,那么重复最左侧报文丢失的解决方案就行了 3.最右侧报文丢失
如果5000的报文丢失,此时ACK的确认序号就应该填写4001,win_start此时会跟新到4001,此时又变成了新窗口的最左侧丢包问题,那么重复最左侧报文丢失的解决方案就行了
也就是说,滑动窗口的报文丢失最终都会转化为最左侧报文丢失(这是序号机制决定的)。
其它问题
流量控制是如何根据对方的接收能力,发送数据?
滑动窗口的范围就等于对方接收能力,就是能给服务器一次发送数据的最大值,流量控制是通过滑动窗口机制来实现的,它根据对方的接收能力来动态调整发送方的发送速率,从而确保数据的可靠传输和网络资源的有效利用。
1.初始化窗口大小
- 在建立TCP连接时(如三次握手过程中),接收方会根据自身的处理能力和接收缓冲区大小来通告初始的窗口大小。
- 发送方则根据接收方通告的窗口大小来设置自己的初始发送窗口大小。
2.数据确认发送
- 发送方按照发送窗口的大小发送数据,并等待接收方的确认。
- 接收方在收到数据后,会返回确认报文(ACK),并通告当前的窗口大小。
3.窗口的滑动与调整
- 发送方根据接收方的确认信息和窗口大小来更新发送窗口,并继续发送数据。
- 如果发送窗口内的数据全部被确认,且接收方的窗口大小允许,发送方会继续发送新的数据。
- 如果接收方的窗口大小减小到零,发送方会停止发送数据,并等待接收方发送新的窗口大小通告。
超时重传:超时时间以内,已经发送的报文不能被丢弃,而是需要保存起来,保存在那里?
保存在滑动窗口中,已发送但未确认的数据:这部分数据已经发送出去,但还没有收到接收方的确认信息,因此必须保留以便在需要时重传。
已经发送,已经确认的数据是否要被清理?
不用清除,当滑动窗口重新从开始进行拷贝的时候,直接覆盖就行了,因此左侧的报文就是废弃报文。
5.拥塞控制
虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开
始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络
状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.如果发送了1000个报文只接受了1个报文,丢了999个报文,因为TCP有滑动窗口的机制,他知道服务器能接收多少报文,所以不可能是服务器的问题,一定是网络出现了很大的问题。此时网络出现了严重的拥塞,是不能立即快重传/超时重传的,这只会加剧网络的拥堵问题,这时发送方就要发生慢启动机制避免网络拥塞加剧。当然不是你一台主机这么做,而是多个使用同一个网络进行通信的主机都这么做,有拥塞避免的共识!这才是解决网络拥塞问题的,最大价值。
TCP 引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按
照多大的速度传输数据;
- • 此处引入一个概念称为拥塞窗口,上面我们说:滑动窗口= 应答窗口(接收方接收能力),现在我们要考虑网络了->拥塞窗口,指网络发送方可以发送的数据量。(拥塞窗口的大小会根据网络的拥塞程度动态调整,以避免网络拥塞的发生。)因此滑动窗口=min(应答窗口,拥塞窗口).
- 发送开始的时候, 定义拥塞窗口大小为 1;
- 每次收到一个 ACK 应答, 拥塞窗口加 1;
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 减少网络发送,让网络恢复,但是增长速度非常快,中后期让通信恢复起来
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
- 此处引入一个叫做慢启动的阈值
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长,此时可能已经超越了滑动窗口的大小,但拥塞窗口还在增大,因为要测出新的拥塞窗口的大小,以判断网络情况,确定出新的网络拥塞的临界值。(线性探测)
- 当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;
- 如果再发生网络拥塞, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回 1(乘法减小),重新开始;少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;当 TCP 通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是 TCP 协议想尽可能快的把数据传输给对方, 但是又要避免给网络
造成太大压力的折中方
6.延迟应答
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小
- 假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;
- 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是 1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络
不拥塞的情况下尽量提高传输效率;那么所有的包都可以延迟应答么? 肯定也不是;
- 数量限制: 每隔 N 个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般 N 取 2, 超时时间取 200ms;
7.捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收"
的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine,
thank you";
那么这个时候 ACK 就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户
端假设客户端向服务器发送了一个HTTP GET请求来获取网页内容。服务器在接收到请求后,会处理该请求并生成相应的HTML响应。在准备发送这个HTML响应时,如果服务器发现还有未发送的ACK消息(用于确认客户端的请求已被接收),那么服务器可以将这个ACK消息与HTML响应一起发送给客户端。这样,客户端在接收到响应数据的同时,也收到了对之前请求的确认。
特殊情况:在我们三次握手建立连接的时候,会发生SYN报文,客户端也会回ACK同时发送SYN报文,这就是一次捎带应答;第一次握手的时候能携带数据吗?是不允许的,还没建立号连接,不能发送数据,第二次也是不可以的,但第三次的时候客户端已经认为连接建立好了,是可以携带数据的
8.面试问题
8.1面向字节流
创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;TCP将来自应用层的数据视为连续的字节流,而不是独立的报文或消息。这意味着TCP不关心数据的具体格式或结构,只负责将这些字节流以可靠的方式传输到对端。
- 调用 write 时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用 read 从接收缓冲区拿数据;
- 另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工
由于缓冲区的存在, TCP 程序的读和写不需要一一匹配, 而是按照字节的去读取何发送,例如:
- 写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100 次write, 每次写一个字节;
- 读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100 个字节, 也可以一次 read 一个字节, 重复 100 次;
8.2粘包问题
- 首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
- 在 TCP 的协议头中, 没有如同 UDP 一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.
- 站在传输层的角度, TCP 是一个一个报文过来的. 按照序号排好序放在缓冲区中.
- 站在应用层的角度, 看到的只是一串连续的字节数据.
- 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界
- 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的 Request 结构, 是固定大小的, 那么就从缓冲区从头开始按 sizeof(Request)依次读取即可;
- 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
- 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);
对于 UDP 协议来说, 是否也存在 "粘包问题" 呢?
- 对于 UDP, 如果还没有上层交付数据, UDP 的报文长度仍然在. 同时, UDP 是一个一个把数据交付给应用层. 就有很明确的数据边界.
- 站在应用层的站在应用层的角度, 使用 UDP 的时候, 要么收到完整的 UDP 报文, 要么不收. 不会出现"半个"的情况.
9.TCP异常问题
进程终止: 进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已
经不在了, 就会进行 reset. 即使没有写入操作, TCP 自己也内置了一个保活定时器, 会
定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如 HTTP 长连接中, 也会定期检测对方的状态. 例如 QQ, 在 QQ 断线之后, 也会定期尝试重新连接
10.TCP小结
为什么 TCP 这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:
• 校验和
• 序列号(按序到达)
• 确认应答
• 超时重发
• 连接管理
• 流量控制
• 拥塞控制
提高性能:
• 滑动窗口
• 快速重传
• 延迟应答
• 捎带应答
其他:
• 定时器(超时重传定时器, 保活定时器, TIME_WAIT 定时器等)
基于 TCP 应用层协议
• HTTP
• HTTPS
• SSH
• Telnet
• FTP
• SMTP
当然, 也包括你自己写 TCP 程序时自定义的应用层协议;
TCP/UDP 对比
我们说了 TCP 是可靠连接, 那么是不是 TCP 一定就优于 UDP 呢? TCP 和 UDP 之间的
优点和缺点, 不能简单, 绝对的进行比较
• TCP 用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
• UDP 用于对高速传输和实时性要求较高的通信领域, 例如, 早期的 QQ, 视频传
输等. 另外 UDP 可以用于广播;
归根结底, TCP 和 UDP 都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体
的需求场景去判定.
用 UDP 实现可靠传输(经典面试题)
参考 TCP 的可靠性机制, 在应用层实现类似的逻辑;
例如:
• 引入序列号, 保证数据顺序;
• 引入确认应答, 确保对端收到了数据;
• 引入超时重传, 如果隔一段时间没有应答, 就重发数据;
• ......