大部分内容源于网络加之个人理解~巨人的肩膀有多大决定你可以看得多远~
文章目录
- 1. 三次握手
- 说一下三次握手的过程
- 为什么是三次握手
- 2. 四次挥手
- 说一下四次挥手的过程
- 为什么需要四次挥手
- 有可能出现三次挥手吗,什么时候会出现呢?
- 为什么 TIME_WAIT 等待的时间是 2MSL?
- 为什么需要 TIME_WAIT 状态?
- TIME_WAIT 过多有什么危害?
- 如何优化 TIME_WAIT?
- 3. 报文丢失会怎么样
- 第一次握手报文丢失
- 第二次握手报文丢失
- 第三次握手报文丢失
- 第一次挥手报文丢失
- 第二次挥手报文丢失
- 第三次挥手报文丢失
- 第四次挥手报文丢失
- 报文丢失重传达到最大次数之后,会发生什么?
- 4. 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
- 5. 初始序列号 ISN 是如何随机产生的?
- 6. 什么是 SYN 攻击?如何避免 SYN 攻击?
- 7. 如果已经建立了连接,但是客户端突然出现故障了怎么办?
- 8. 如果已经建立了连接,但是服务端的进程崩溃会发生什么?
- 9. TCP 连接一端崩溃或者宕机会发生什么?
- 7. 使用 TCP 一定不会造成丢包吗?
- 8. TCP 中的数据结构
- 9. Socket 编程
- 针对 TCP 应该如何 Socket 编程?
- 10. TCP 可靠传输的工作原理
- 1. 为了解决数据差错问题,TCP 的校验数据部分包含首部和数据,而 ip 层只校验首部。
- 2. 为了解决数据丢失问题,TCP 数据时,会设置一个超时计时器,超时没有收到确认报文则会重传当前报文。
- 3. 为了解决数据重复问题,TCP 有确认迟到的机制,也就是说,当 TCP 收到重复报文的同时,还会发送确认报文。
- 4. 为了解决数据乱序问题, TCP 首部存在每个报文唯一的4字节序号。
- 11. TCP 可靠传输的实现
- 12. 聊聊 tcp 的流量控制
- 13. tcp 的拥塞控制的常用方法
- 14. TCP 与 UDP 的分片发生在哪里
1. 三次握手
说一下三次握手的过程
第 1 次握手建立连接时,客户端向服务器发送 SYN 报文(SEQ=x,SYN=1),并进入 SYN_SENT 状态,等待服务器确认。
第 2 次握手实际上是分两部分来完成的,即 SYN+ACK(请求和确认)报文。
● 服务器收到了客户端的请求,向客户端回复一个确认信息(ACK=x+1)。
● 服务器再向客户端发送一个 SYN 包(SEQ=y)建立连接的请求,此时服务器进入 SYN_RECV 状态。
第 3 次握手,是客户端收到服务器的回复(SYN+ACK 报文)。
此时,客户端也要向服务器发送确认包(ACK)。此包发送完毕客户端和服务器进入 ESTABLISHED 状态,完成 3 次握手。
而且值得注意的是,只有报文中的大ACK=1(确认 ACK ACKnowledgment),小ack才有效。
为什么是三次握手
这个问题的本质原因就是,信道不可靠, 因此通信双发需要就某个问题达成一致。
而要解决这个问题, 无论你在消息中包含什么信息, 三次通信是理论上的最小值。所以三次握手是为了满足 “在不可靠信道上进行可靠传输” 这一需求所导致的。
● 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
● 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
为什么不能是两次
如果只有两次,为了双方都能进入连接态,只能丢弃 SYN_RCVD中间状态。这样的话,当堵塞于网络中的旧初始化连接请求到来时,服务端会误认为旧连接是正常连接,而进入 established连接状态,导致套接字空等,造成资源浪费。
2. 四次挥手
说一下四次挥手的过程
假设存在 client 跟 server 端,挥手请求由 client发起。
第一次挥手: Client 端发起挥手请求,向 Server 端发送标志位是FIN报文段,设置序列号 seq,此时,Client 端进入FIN_WAIT_1状态。表示Client端没有数据要发送给Server端了。
第二次挥手:Server 端收到了 Client 端发送的 FIN 报文段,向 Client 端返回一个标志位是 ACK 的报文段,ack 设为 seq 加1,Client 端进入FIN_WAIT_2状态。表示 Server 端知道 Clent 端关闭连接。
第三次挥手: Server 端向 Client 端发送标志位是 FIN 的报文段,请求关闭连接,同时 Client 端进入LAST_ACK状态。表示 Server 端也没有数据要发送给 Client 了。
第四次挥手 : Client 端收到 Server 端发送的 FIN 报文段,向 Server 端发送确认报文,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。Client端等待 2MSL 的时间后依然没有收到回复,则证明Server端已正常关闭,Client 端关闭连接。
为什么需要四次挥手
因为 TCP 是一个全双工的通信协议,也就是说双方可以同时传输数据。而断开连接需要有人发起,有人确认,所以双方都断开连接就需要四次挥手了。
有可能出现三次挥手吗,什么时候会出现呢?
在 Linux 环境开发的时候,通过 tcpdump 进行抓包,可以发现经常会出现 第二次挥手跟第三次挥手合并成为一个报文发送的情况。主要是因为现在 TCP 有延迟确认的机制,且是默认开启的。
为什么 TIME_WAIT 等待的时间是 2MSL?
主要有两个原因,
第一,为了保证最后一个断开连接的报文( 包括超时重传的 ) 可以到达对端。
第二,防止已失效的连接请求报文出现在本连接。因为发送完最后一个 ACK 报文后,再经过 2MSL ,理论上就可以使得本链接所产生的所有报文都从网络中消失。
MSL(Maximum Segment Lifetime),最长报文段寿命。也就是说 MSL 时间内,报文没有到达对端,就认为报文已丢失。
2MSL 可以保证可以接收到重传报文。
为什么需要 TIME_WAIT 状态?
主动发起关闭连接的一方,才会有 TIME-WAIT 状态。
需要 TIME-WAIT 状态,主要是两个原因:
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
保证「被动关闭连接」的一方,能被正确的关闭;
TIME_WAIT 过多有什么危害?
注意:
只有先发起 Fin 报文的端点才会有 TIME_WAIT 状态。
过多的 TIME-WAIT 状态主要的危害有两种:
第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围。
客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。
如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务器发起连接了,但是被使用的端口,还是可以继续对另外一个服务器发起连接的。具体可以看我这篇文章:客户端的端口可以重复使用吗?(opens new window)
因此,客户端(发起连接方)都是和「目的 IP+ 目的 PORT 」都一样的服务器建立连接的话,当客户端的 TIME_WAIT 状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟「目的 IP+ 目的 PORT」都一样的服务器建立连接了。
不过,即使是在这种场景下,只要连接的是不同的服务器,端口是可以重复使用的,所以客户端还是可以向其他服务器发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。
如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。
如何优化 TIME_WAIT?
这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:
● 打开 net.ipv4.tcp_tw_reuse
和 net.ipv4.tcp_timestamps
选项;
● net.ipv4.tcp_max_tw_buckets
● 程序中使用 SO_LINGER
,应用强制使用 RST 关闭。
方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps
如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。
有一点需要注意的是,tcp_tw_reuse 功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。
net.ipv4.tcp_tw_reuse = 1
使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
net.ipv4.tcp_timestamps=1
(默认即为 1)
这个时间戳的字段是在 TCP 头部的「选项」里,它由一共 8 个字节表示时间戳,其中第一个 4 字节字段用来保存发送该数据包的时间,第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。
由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。
方式二:net.ipv4.tcp_max_tw_buckets
这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力。
方式三:程序中使用 SO_LINGER
我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。
struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。
但这为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。
前面介绍的方法都是试图越过 TIME_WAIT状态的,这样其实不太好。虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。
《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。
如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT。
3. 报文丢失会怎么样
源于 [小林coding]
第一次握手报文丢失
当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。
在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。
不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?
在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。
# cat /proc/sys/net/ipv4/tcp_syn_retries
centos 7.x 是6
通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。
当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。
所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。
第二次握手报文丢失
当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。
第二次握手的 SYN-ACK 报文其实有两个目的 :
● 第二次握手里的 ACK, 是对第一次握手的确认报文;
● 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。
# cat /proc/sys/net/ipv4/tcp_synack_retries
因此,当第二次握手丢失了,客户端和服务端都会重传:
● 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
● 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。
注:tcp_syn_retries 一般比 tcp_synack_retries稍微大一点。
第三次握手报文丢失
客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
第一次挥手报文丢失
当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。
正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT_2 状态。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。
# cat /proc/sys/net/ipv4/tcp_orphan_retries
当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close 状态。
第二次挥手报文丢失
当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入CLOSE_WAIT 状态。
在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
第三次挥手报文丢失
当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
第四次挥手报文丢失
当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。
在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。
报文丢失重传达到最大次数之后,会发生什么?
假设重传的最大次数是3.
如果重传三次后还未收到该报文的确认,那么就不再尝试重传,直接发送 reset 报文重置该TCP连接,但有些要求很高的业务应用系统,则会不断的重传被丢弃的报文,以尽最大可能保证业务数据的正常交互。
4. 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
主要原因有两个方面:
● 为了防止历史报文被下一个相同四元组的连接接收(主要方面);
● 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;
5. 初始序列号 ISN 是如何随机产生的?
起始 ISN 是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。
RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。
● M 是一个计时器,这个计时器每隔 4 微秒加 1。
● F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。
6. 什么是 SYN 攻击?如何避免 SYN 攻击?
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务器不能为正常用户服务。
避免 SYN 攻击方式,可以有以下四种方法:
● 调大 netdev_max_backlog;
● 增大 TCP 半连接队列;
● 开启 tcp_syncookies;
● 减少 SYN+ACK 重传次数
方式一:调大 netdev_max_backlog
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
net.core.netdev_max_backlog = 10000
方式二:增大 TCP 半连接队列
增大 TCP 半连接队列,要同时增大下面这三个参数:
● 增大 net.ipv4.tcp_max_syn_backlog
● 增大 listen() 函数中的 backlog
● 增大 net.core.somaxconn
具体为什么是三个参数决定 TCP 半连接队列的大小,可以看这篇:可以看这篇:TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?(opens new window)
方式三:开启 net.ipv4.tcp_syncookies
开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。
具体过程:
● 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不会丢弃,而是根据算法,计算出一个 cookie 值;
● 将 cookie 值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端;
● 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「 Accept 队列」。
● 最后应用程序通过调用 accpet() 接口,从「 Accept 队列」取出的连接。
可以看到,当开启了 tcp_syncookies 了,即使受到 SYN 攻击而导致 SYN 队列满时,也能保证正常的连接成功建立。
net.ipv4.tcp_syncookies 参数主要有以下三个值:
● 0 值,表示关闭该功能;
● 1 值,表示仅当 SYN 半连接队列放不下时,再启用它;
● 2 值,表示无条件开启功能;
那么在应对 SYN 攻击时,只需要设置为 1 即可。
$ echo 1 > /proc/sys/net/ipv4/tcp_syncookies
方式四:减少 SYN+ACK 重传次数
当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。
那么针对 SYN 攻击的场景,我们可以减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。
SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定(默认值是 5 次),比如将 tcp_synack_retries 减少到 2 次:
$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
7. 如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP 有一个机制是保活机制。这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
注意:
我们也可以像websocket 通信那样,自己实现心跳机制来实现。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
● tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
● tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
● tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
注意,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。
如果开启了 TCP 保活,需要考虑以下几种情况:
● 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
● 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
● 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
8. 如果已经建立了连接,但是服务端的进程崩溃会发生什么?
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程。
我自己做了个实验,使用 kill -9 来模拟进程崩溃的情况,发现在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手。
9. TCP 连接一端崩溃或者宕机会发生什么?
如果「客户端进程崩溃」,客户端的进程在发生崩溃的时候,内核会发送 FIN 报文,与服务端进行四次挥手。
但是,「客户端主机宕机」,那么是不会发生四次挥手的,具体后续会发生什么?还要看服务端会不会发送数据?
● 如果服务端会发送数据,由于客户端已经不存在,收不到数据报文的响应报文,服务端的数据报文会超时重传,当重传总间隔时长达到一定阈值(内核会根据 tcp_retries2 设置的值计算出一个阈值)后,会断开 TCP 连接;
● 如果服务端一直不会发送数据,再看服务端有没有开启 TCP keepalive 机制?
○ 如果有开启,服务端在一段时间没有进行数据交互时,会触发 TCP keepalive 机制,探测对方是否存在,如果探测到对方已经消亡,则会断开自身的 TCP 连接;
○ 如果没有开启,服务端的 TCP 连接会一直存在,并且一直保持在 ESTABLISHED 状态。
7. 使用 TCP 一定不会造成丢包吗?
● 数据从发送端到接收端,链路很长,任何一个地方都可能发生丢包,几乎可以说丢包不可避免。
● 平时没事也不用关注丢包,大部分时候TCP的重传机制保证了消息可靠性。
● 当你发现服务异常的时候,比如接口延时很高,总是失败的时候,可以用ping或者mtr命令看下是不是中间链路发生了丢包。
● TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。
8. TCP 中的数据结构
队列
半连接和全连接队列
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
● 半连接队列,也称 SYN 队列;
● 全连接队列,也称 accept 队列;
服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。
链表
双向链表
https://blog.51cto.com/u_13045346/4553142
9. Socket 编程
针对 TCP 应该如何 Socket 编程?
● 服务端和客户端初始化 socket,得到文件描述符;
● 服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口;
● 服务端调用 listen,进行监听;
● 服务端调用 accept,等待客户端连接;
● 客户端调用 connect,向服务器端的地址和端口发起连接请求;
● 服务端 accept 返回用于传输的 socket 的文件描述符;
● 客户端调用 write 写入数据;服务端调用 read 读取数据;
● 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。
这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
listen 的时候参数 backlog 的意义?
Linux内核中会维护两个队列:
● 半连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
● 全连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
int listen (int socketfd, int backlog)
● 参数一 socketfd 为 socketfd 文件描述符
● 参数二 backlog,这参数在历史版本有一定的变化
在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。
但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。
accept 发生在三次握手的哪一步?
我们先看看客户端连接服务端时,发送了什么?
● 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端进入 SYN_SENT 状态;
● 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对 SYN 包 client_isn 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务器端进入 SYN_RCVD 状态;
● 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 server_isn+1;
● ACK 应答包到达服务器端后,服务器端的 TCP 连接进入 ESTABLISHED 状态,同时服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功。至此,客户端与服务端两个方向的连接都建立成功。
从上面的描述过程,我们可以得知客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。
客户端调用 close 了,连接是断开的流程是什么?
我们看看客户端主动调用了 close,会发生什么?
● 客户端调用 close,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态;
● 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;
● 接着,当处理完数据后,自然就会读到 EOF,于是也调用 close 关闭它的套接字,这会使得服务端发出一个 FIN 包,之后处于 LAST_ACK 状态;
● 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
● 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
● 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;
10. TCP 可靠传输的工作原理
tcp 发送的报文段是交给 ip 层传送的,但是 IP 层只能提供尽最大努力服务(不可靠)。
首先,可靠传输意味着 无差错,不丢失,不重复,不乱序。那么接下来我也会从这几个方面来介绍可靠传输的工作原理。
1. 为了解决数据差错问题,TCP 的校验数据部分包含首部和数据,而 ip 层只校验首部。
2. 为了解决数据丢失问题,TCP 数据时,会设置一个超时计时器,超时没有收到确认报文则会重传当前报文。
3. 为了解决数据重复问题,TCP 有确认迟到的机制,也就是说,当 TCP 收到重复报文的同时,还会发送确认报文。
4. 为了解决数据乱序问题, TCP 首部存在每个报文唯一的4字节序号。
总的来说,就是 TCP 采用了 数据报唯一编号+确认+重传 的机制,在不可靠的信道上,利用人为的算法协议确保可靠传输。
而在这基本思想的发展上,tcp 出现了一系列的可靠传输协议,比如说:停止等待协议,连续 ARQ 协议。
停止等待”就是每发送完一个分组就停止发送,等待对方的确认。在收到确认后再发送下一个分组。
连续 ARQ 采用的是滑动窗口的算法思想,达到累积确认的目的。
11. TCP 可靠传输的实现
数据报的唯一编号是基于操作系统的时钟生成的。
以字节为单位的滑动窗口 + 超时重传的时间选择 + 选择确认(SACK)(可选的,需要双方商定)。
12. 聊聊 tcp 的流量控制
流量控制,属于点对点问题,在 tcp 主要是利用滑动窗口的思想,双方就窗口的大小进行协商,避免数据包传输过快或过慢。
13. tcp 的拥塞控制的常用方法
首先,拥塞代表着对资源的总需求量大于可用资源,而且拥塞导致丢包引起的重传会加剧拥塞程度,拥塞控制的目的就是避免过多的数据包注入到网络中。而TCP中为了减少拥塞带来的损失,采用以下四种方法。
● 1. 慢开始
每个传输轮次,加倍
● 2. 拥塞避免
传输轮次+1
● 3. 快重传
发送方连续收到三个某个报文的确认,就认为该报文没有被接收方收到,立即进行重传。
● 4. 快恢复
缩小拥塞窗口门限后立即执行拥塞避免。
14. TCP 与 UDP 的分片发生在哪里
首先,提到分片,我们需要聊一聊 MTU,它是链路层中的网络对数据帧的一个限制,以以太网为例,MTU为1500个字节。一个IP数据报在以太网中传输,如果它的长度大于该MTU值,就要进行分片传输,使得每片数据报的长度小于MTU。分片传输的IP数据报不一定按序到达,但IP首部中的信息能让这些数据报片按序组装。IP数据报的分片与重组是在网络层进完成的。
UDP数据报,由于UDP数据报不会自己进行分片,当UDP数据长度超过了MTU时,会在网络层进行IP分片。
对于TCP来说,它是尽量避免分片的,为什么?因为如果在IP层进行分片了话,如果其中的某片的数据丢失了,对于保证可靠性的TCP协议来说,会增大重传数据包的机率,而且只能重传整个TCP分组。因此当TCP数据大于MTU时,会在传输层进行分段,然后再进入网络层。
TCP 报文里面有个长度可变的选项字段,其最初只有一个 MSS(Maxiumum Segment Size),是每个 TCP 报文段里的数据字段的最大长度。