什么是TCP
- TCP是位于传输层的,面向连接的、可靠的、基于字节流的传输层通信协议
TCP/UDP报文格式
**TCP首部:**前20个字节是固定的,后面有4n个字节是根据需而增加的选项,所以TCP首部最小长度为20字节。
- ①源端口/目的端口:指数据发送方的应用进程端口号及接收方的进程端口号。
- ②序号:
TCP
为了保证数据的可靠传输,会对分段数据标注序号,勇于组装和确认数据的正确性。 - ③确认序号:当接收方收到接收到本次数据时,下次需要发送的数据段序号。
- ④首部:表示
TCP
报文头的长度,因为TCP
头长度可变性,因此需要在头信息中声明每个头的长度。 - ⑤保留位:预留一些空间给未来拓展时使用。
- ⑥
URG
:表示本次发送的报文数据中是否紧急数据。 - ⑦
ACK
:确认信号,当报文中ACK=1
的时候表示正确或同意。 - ⑨
RST
:如果收到RST=1
的报文,说明与主机的连接出现严重错误(如主机崩溃),必须释放连接,然后重新建立连接。而接收端收到 RST 包后,也不必发送 ACK 包来确认。 - ⑩
SYN
:建立一个新连接,SYN=1
表示这是一个请求建立连接的报文段。 - ⑪
FIN
:断开一个连接,FIN=1
表示通知告知对方本段要关闭连接了。 - ⑫窗口大小:表示现在允许发送的数据量,当到达此值,需要
ACK
确认后才能继续发送数据。 - ⑬校验和: 通过
CRC
算法提供额外的可靠性,用于效验数据正确性。 - ⑭紧急指针:标记紧急数据在数据字节流中的位置。
- ⑮选项:这块属于动态的可选择参数。主要选项如下:
- 最大报文段长度、窗口扩大、时间戳。
- 数据:报文段传输的数据内容(不属于
TCP
报文头的范畴内)。
UDP首部:UDP的首部只有8个字节,源端口号、目的端口号、长度和校验和各两个字节。
端口号16比特,在0~65535之间, 0~1023为周知端口号,分配给常见协议,使用受限。
发送 RST 报文的几种可能的情况:
- 端口未被监听:客户端尝试与服务器未对外提供服务的端口建立TCP连接,服务器将会直接向客户 端发送 RST 报文。
- 程序崩溃:客户端和服务器的某一方在交互的过程中发生异常(如程序崩溃等),该方系统将向对 端发送 RST 报文,告之对方释放相关的 TCP 连接。
- 尚未建立连接:接收端收到 TCP 报文,但是发现该 TCP 的报文,并不在其已建立的 TCP 连接列表 内,则其直接向对端发送 RST 报文。
- 长期未收到来自对方的确认报文:在交互的双方中的某一方长期未收到来自对方的确认报文,则其 在超出一定的重传次数或时间后,会主动向对端发送 RST 报文释放该 TCP 连接。
TCP/UDP区别
- UDP 面向报文:发送方的 UDP 对应用程序交下来的报文,在添加了首部之后就向下交付,即不合并也不 拆分,而是保留这些报文的边界,即应用层交给 UDP 多长的报文,UDP 就照样发送,一次发送一个报 文,接收方 UDP 对下方交上来的 UDP 用户数据报,在去除首部之后就原封不动的交付给上层的应用程 序,一次交付一个完整报文,所以是 UDP 是面向报文的。
- TCP 面向字节流:发送方 TCP 对应用程序交下来的报文,视为无结构的字节流(无边界约束,可拆分/ 合并),但维持各字节流顺序(相对顺序没有变),TCP 发送方有一个发送缓冲区,当应用程序传输的 数据太长,TCP 可以把它划分端一些再传输,当应用程序传输的数据太短, TCP 可以等待积累足够多的 字节后再构成报文端发送出去,所以 TCP 是面向字节流的。为什么 TCP 要对报文进行拆分呢,主要是为了超时重传时只需要重传一小部分数据,而不是整个报文。
- TCP 用于在传输层有必要实现可靠传输的情况(如 HTTP、FTP),UDP 用于对高速传输和实时性有较高 要求的通信(如 DNS、视频通话)。
TCP最大连接数
理论上:对 IPv4,客户端的 IP 数最多为 2
的 32
次方,客户端的端口数最多为 2
的 16
次方,也就是服务端单机最大 TCP 连接数,约为 2
的 48
次方。
实际上:
- 文件描述符,Socket 实际上是一个文件,也就会对应一个文件描述符。在 Linux 下,单个进程打开的文件描述符数是有限制的,没有经过修改的值一般都是 1024,不过我们可以通过 ulimit 增大文件描述符的数目;
- 系统内存,每个 TCP 连接在内核中都有对应的数据结构,意味着每个连接都是会占用一定内存的;
TCP keep-alive,一个TCP连接可以理论上无上限发HTTP请求。
同一时刻浏览器针对同一域名最多可以建立6个TCP连接,每个TCP连接可以发送多次http请求,但也是阻塞式的。所以一些网站选择将资源放在不同域名下以提高并发请求量,代价是多了DNS解析的消耗。而http2中由于有多路复用,效率大大提升,因此不需要建立这么多TCP连接也能很快完成资源加载。(Chrome最多允许同一个Host可建立6个TCP连接,不同的浏览器有一些区别)
TCP 和 UDP 可以使用同一个端口吗?
可以。当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理。
TCP可靠传输
-
校验和:数据接收完成后进行效验。
-
序列号:数据按顺序传输和接收。
-
确认应答:收到数据之后返回
ACK
应答。 -
超时重传:超出规定时间后,发送方未收到
ACK
应答,则会重新再次发送数据。超时重传定时器
-
连接管理:建立和管理连接时,都会经历三次握手、四次挥手确保数据可靠传递。
-
流量控制:
TCP
根据接收端的处理能力, 来决定发送端的发送速度,避免丢包如果发送端发送的数据太快,接收端来不及接收就会出现丢包问题。为了解决这个问 题,TCP协议利用了滑动窗口进行了流量控制。在TCP首部有一个16位字段大小的窗口,窗口的大 小就是接收端接收数据缓冲区的剩余大小。接收端会在收到数据包后发送ACK报文时,将自己的窗 口大小填入ACK中,发送方会根据ACK报文中的窗口大小进而控制发送速度。如果窗口大小为零, 发送方会停止发送数据。 -
拥塞控制:当一个网络面对的负载超过了它了处理能力时,拥塞就会发生。若出现拥塞而不进行控制,整个网络的吞吐量将随输入负荷的增大而下降。
这里的发送方会维护一个拥塞窗口的状态变量,它和流量控制的滑动窗口是不一样的,滑动窗口是根据接收方数据缓冲区大小确定的,而拥塞窗口是根据网络的拥塞情况动态确定的,一般来说发送 方真实的发送窗口为滑动窗口和拥塞窗口中的最小值。
- 慢开始:当cwnd < ssthresh(慢开始门限值)由小到大逐渐增加拥塞窗口的大小。初始化cwnd为1,每个RTT,cwnd为2,以此类推成,指数增长。
- 拥塞避免:当cwnd > ssthresh时, 线性增长,把发送方拥塞窗口cwnd加1,而不是加倍。
- 快重传:发送方收到三个重复确认就立即重传对方未收到的报文段,不必等到重传计时器到期
- 快恢复:发生快重传时,拥塞窗口减半,慢开始门限值ssthresh = 拥塞窗口cwnd。
三次握手
- 客户向80端口发起TCP连接,主机端口处不接受TCP连接:该主机向源发送RST重置报文,RST=1。
- 当主机接收到一个UDP分组,目的端口同正在进行的UDP套接字不匹配:该主机会发送一个ICMP报文。
- 第一次握手:客户端请求建立连接,向服务端发送一个同步报文(SYN=1),同时选择一个随机 数 seq = x 作为初始序列号,并进入SYN_SENT状态,等待服务器确认。
- 第二次握手::服务端收到连接请求报文后,如果同意建立连接,则向客户端发送同步确认报文 (SYN=1,ACK=1),确认号为 ack = x + 1,同时选择一个随机数 seq = y 作为初始序列号
- 第三次握手:客户端收到服务端的确认后,向服务端发送一个确认报文(ACK=1),确认号为 ack = y + 1,序列号为 seq = x + 1。服务器收到 ACK 报文之后,双方以建立起了链接。
状态变化:
- 一开始,客户端和服务端都处于
CLOSED
状态。先是服务端创建监听套接字,主动监听某个端口,处于LISTEN
状态。 - 然后客户发起TCP连接,发送SYN,之后处于
SYN-SENT
状态。 - 服务端接收SYN,发送SYN&ACK,之后处于
SYN-RCVD
状态。 - 客户端接收SYN&ACK,发送ACK,之后处于
ESTABLISHED
状态,因为它一发一收成功了。 - 服务端接收ACK,处于
ESTABLISHED
状态,因为它也一发一收了。
两次握手行不行?
-
假设建立TCP连接仅需要两次握手,那么如果第二次握手时,服务端返回给客户端的确认报文丢失 了,客户端这边认为服务端没有和他建立连接,而服务端却以为已经和客户端建立了连接,并且可能向服务端已经开始向客户端发送数据,但客户端并不会接收这些数据,浪费了资源。如果是三次 握手,不会出现双方连接还未完全建立成功就开始发送数据的情况。
-
如果服务端接收到了一个早已失效的来自客户端的连接请求报文,会向客户端发送确认报文同意建立TCP连接。如果是两次握手连接,就无法阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
第三次握手,客户端发现自己期望收到的ACK不正确,可以发起RST报文中止连接。
三次握手可以携带数据吗?
-
第三次握手的时候,可以携带。前两次握手不能携带数据。
-
如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据,那么服务器势必会消耗更多的时间和内存空间去处理这些数据,增大了服务器被攻击的风险。
-
第三次握手的时候,客户端已经处于
ESTABLISHED
状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。
SYN Flood 攻击原理
-
出现于三次握手时,前两次握手服务器为客户分配连接变量和缓存。
-
SYN攻击:客户端在短时间内伪造大量不存在的 IP 地址,并向服务端发送
SYN
,而不完成第三次握手。 -
后果:当服务端接收到一个Syn请求时,服务端必须使用一个监听队列将该连接保存一定时间。因此,通过向服务端不停发送Syn请求,但不响应Syn+Ack报文,从而消耗服务端的资源。当监听队列被占满时,服务端将无法响应正常用户的请求,达到拒绝服务攻击的目的。
- 处理大量的
SYN
包并返回对应ACK
, 势必有大量连接处于SYN_RCVD
状态,从而占满整个半连接队列,当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃。 - 由于是不存在的 IP,服务端长时间收不到客户端的
ACK
,会导致服务端不断重发数据,直到耗尽服务端的资源。
- 处理大量的
-
解决方法:
-
增加 SYN 连接,也就是增加半连接队列的容量。
-
减少 SYN + ACK 重试次数,避免大量的超时重发。
-
SYN Cookie:
当半连接队列「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃,而是根据算法,计算出一个
cookie
值;在服务端接收到SYN
后不立即分配连接资源,而是根据这个SYN
计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK
的时候带上这个Cookie
值,服务端验证 Cookie 合法之后才分配连接资源。
-
半连接队列和全连接队列
-
半连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
-
全连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
-
Linux 内核的 「 SYN 队列」和 「 Accept 队列」工作流程:
-
当服务端接收到客户端的 SYN 报文时,会将其加入到内核的「 SYN 队列」;
-
接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
-
服务端接收到 ACK 报文后,从「 SYN 队列」移除放入到「 Accept 队列」;
-
应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接。
-
-
当超过了 TCP 最大全连接队列,服务端则会丢掉后续进来的 TCP 连接请求
-
如果半连接队列满了,并且没有开启 tcp_syncookies,则会丢弃;
seq为什么随机?
- 为了防止历史报文被下一个相同四元组的连接接收(主要方面);
- 服务器断电重启,建立了和上一个连接相同的四元组的连接。
- 如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。
- 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;
三次握手信息丢失
- 第一次握手丢失:如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。
- 第二次握手丢失:客户端和服务端都会重传:客户端重传SYN报文,服务端重传SYN+ACK。
- 第三次握手丢失:当服务端超时重传 2 次 SYN-ACK 报文后,达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
四次挥手
-
第一次挥手:客户端向服务端发送连接释放报文(FIN=1,ACK=1),主动关闭连接,同时等待服 务端的确认。此时客户端处于CLOSED_WAIT1状态。
- 序列号 seq = x,即客户端上次发送的报文的最后一个字节的序号 + 1
- 确认号 ack = y, 即服务端上次发送的报文的最后一个字节的序号 + 1
-
第二次挥手:服务端收到连接释放报文后,立即发出确认报文(ACK=1),序列号 seq = y,确认 号 ack = x + 1。此时服务端处于 CLOSE_WAIT2状态。
这时 TCP 连接处于半关闭状态,即客户端到服务端的连接已经释放了,但是服务端到客户端的连接 还未释放。这表示客户端已经没有数据发送了,但是服务端可能还要给客户端发送数据。
-
第三次挥手:服务端向客户端发送连接释放报文(FIN=1,ACK=1),主动关闭连接,同时等待 A 的确认。此时服务端处于 LAST_ACK 的状态。
- 序列号 seq = z,即服务端上次发送的报文的最后一个字节的序号 + 1。
- 确认号 ack = x + 1,与第二次挥手相同,因为这段时间客户端没有发送数据
-
第四次挥手:客户端收到服务端的连接释放报文后,立即发出确认报文(ACK=1),序列号 seq = x + 1,确认号为 ack = z + 1。此时客户端处于 TIME_WAIT 状态,等待2msl之后进入进入 CLOSED 状态
-
客户端:
- 处于ESTABLISHED,客户端关闭TCP连接(服务器也可以选择关闭),客户端发送FIN=1的报文,客户端进入FIN_WAIT_1
- 接收到ACK,进入FIN_WAIT_2
- 客户端接收FIN=1的报文段,发送ACK,进入TIME_WAIT状态
- 等待一段2MSL,进入CLOSED状态。
-
服务端:
- 处于ESTABLISHED,接收FIN,发送ACK,进CLOSE_WAITT
- 发送FIN,进入LAST_ACK
- 接收ACK,进入CLOSED状态。
四次挥手信息丢失
- 第一次挥手丢失:客户端触发超时重传机制,重传 FIN 报文。
- 第二次挥手丢失:客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
- 第三次挥手丢失:服务端进程主动调用 close 函数来触发服务端重新发送 FIN 报文
- 第四次挥手丢失:服务端重传第三次挥手报文。
可以三次挥手吗?
-
因为服务器端收到客户端的
FIN
后,服务器端同时也要关闭连接,这样就可以把ACK
和FIN
合并到一起发送,节省了一个包,变成了“三次挥手”。 -
但是非要三次挥手(等数据传输完发送FIN+ACK),如果不及时发送 ACK 包,死等服务端这边发送数据,可能会造成客户端不必要的重发 FIN 包
为什么客户端发出第四次挥手的确认报文后要等2MSL的时间
MSL的意思是报文的最长寿命,可以从两方面考虑:
-
客户端发送第四次挥手中的报文后,再经过2MSL报文最大生存时间,可使本次TCP连接中的所有报文全部消失,不会出现在下一个TCP连接中。
-
考虑丢包问题,如果第四挥手发送的报文在传输过程中丢失了,那么服务端没收到确认ack报文就 会重发第三次挥手的报文。如果客户端发送完第四次挥手的确认报文后直接关闭,而这次报文又恰好丢失,则会造成服务端无法正常关闭。
-
假设客户端没有 TIME_WAIT 状态,客户端已经进入到关闭状态,在收到服务端重传的 FIN 报文后,就会回 RST 报文。而服务端收到这个 RST 并将其解释为一个错误。
-
TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的 时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。
-
close wait和time wait
-
作用:
- close wait:
- 等待应用进程调用 close 函数关闭连接。
- time wait:
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收。
- 保证「被动关闭连接」的一方,能被正确的关闭。
- close wait:
-
服务端大量出现close wait/time wait场景/原因:
-
close wait:
- 服务端处理请求太慢,或者没有及时释放连接资源,没有执行 close() 方法。
- 服务器的父进程派生出子进程,子进程继承了 socket,收到 FIN 的时候子进程处理但父进程没有处 理该信号,导致 socket 的引用不为 0 无法回收
-
time wait:
-
HTTP 没有使用长连接:服务端有大量的短连接存在。因为任意一方没有开启 HTTP Keep-Alive,都会导致服务端在处理完一个 HTTP 请求后,就主动关闭连接,此时服务端上就会出现大量的 TIME_WAIT 状态的连接。
解决:让客户端和服务端都开启 HTTP Keep-Alive 机制。
-
HTTP 长连接超时:导致服务端主动关闭连接,产生大量处于 TIME_WAIT 状态的连接。
-
HTTP 长连接的请求数量达到上限:服务端主动会关闭长连接。
-
服务器的进程挂掉了,内核会发起 FIN 报文,这时候相当于服务器会主动挂断这个连接,于是在服务器上就会出现大量的 TIME_WAIT 状态。
-
-
-
危害:
- close wait:服务端的文件描述符耗尽,无法建立新的连接,影响服务质量和性能。
- time wait:客户端端口资源不足,无法创建新的套接字,或者造成旧的连接数据和新的连接数据混淆,引起通信错误。
-
优化:
- close wait:
- 优化服务端的处理逻辑,及时关闭和释放连接资源,避免占用过多的文件描述符。
- time wait:
- 开启socket复用:客户端(连接发起方)调用 connect() 函数时,内核会随机找一个 time_wait 状态的连接给新的连接复用。
- 开启socket的快速回收:当系统中处于 TIME_WAIT 的连接一旦某个值时,系统就会将后面的 TIME_WAIT 连接状态重置
- close wait:
TCP粘包和拆包
TCP 是面向流,没有界限的一串数据。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲 区的实际情况进行包的划分,所以一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个 小的包封装成一个大的数据包发送。发生粘包与拆包现象的本质是因为 TCP 是流式协议,消息无边界。
- TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,多个数据包在一块儿,无法确定每个数据包之间的分割边界。
- 粘包原因:
- TCP默认使用Nagle算法,将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包,减少网络中报文段的数量
- TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
- 拆包原因:
- 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包;
- 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。即 TCP 报文长度-TCP 头部长度 > MSS。
- 解决方法:
- 应用层发送数据时定长发送。将每个数据包封装成固定长度,不够的用
0
补齐,接收方每次按照固定大小读取数据即可。 - 尾部标记序列。通过特殊标识符表示数据包的边界,例如\n\r,\t,或者一些隐藏字符。
- 头部标记分步接收。在TCP报文的头部加上表示数据长度。
- 应用层发送数据时定长发送。将每个数据包封装成固定长度,不够的用
UDP 不会出现粘包与拆包问题(因为其是面向报文的协议,有固定的边界,不会对包进行合并与 拆分)。
UDP为什么比TCP快?
- TCP为了可靠性保证,增加了3次握手4次挥手,以及流量控制、拥塞控制,增加网络延迟。
- TCP重传机制。采用TCP,一旦发生丢包,TCP会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,延时会越来越大,基于UDP对实时性要求较为严格的情况下,采用自定义重传机制,有包就发,能够把丢包产生的延迟降到最低,尽量减少网络延迟。
- .基于滑动窗口,发送发被限制只能发送那些序号在指定范围内的分组,且必须等到分组被确认,才能移动滑动窗口。接收方接收到分组必须是连续的,才能交付给上层。
- TCP头部的大小,进一步增加了,传输的数据量。
UDP比TCP慢的情况:
对于关键数据,UDP依旧会重传,比如视频的关键帧。
TCP对于特别大的数据包,会划分MSS大小的段,到达IP层,每个包大小不超过MTU,这时候发生丢包只需要重传MSS分段就可以。但对于UDP,数据数据过大,IP层会进行分片,再次重传整个大的数据包。
Socket
让你说下对socket的理解,如果你的回答是封装tcp\ip协议的调用接口,绑定了一组IP地址和端口号,这个回答当然没有问题,但是可能只有60分。但是如果你再说一下Linux环境下一切皆文件的思想socket也是文件,讲一下tcp和udp协议下不同的编程模型,比如tcp下如何得到文件描述符的,如何其实监听和传送数据其实是两个socket,内核如何维护这俩socket队列的·······我相信细心说完这些这个问题在面试官心中的评分会在90分以上。
- 运行在不同机器上的进程彼此通过向套接字发送报文来进行通信。
- 每个进程具有一个或多个套接字。
- 每当生成一个套接字,就为它分配一个端口号标识。
- socket虽然是长链接,但是由于网络等原因也有可能会断开链接。所以往往会提供一个心跳包,每隔一段时间向服务器发送一个规定好的心跳,如果某一个时间发送没有收到心跳,那代表已经断开链接
- 封装tcp/ip协议的调用接口,绑定了一组IP地址和端口号,包括源IP,源端口,目的IP,目的端口,即标识了一条可用的连接。
110.122.144.166:45678
,110.88.92.104:80
- Linux 内核中会为每个 Socket 维护两个队列:
- 半连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
- 全连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
TCP套接字编程:
- 客户端和服务端创建套接字,指定类型
- 服务端调用
bind
绑定IP+端口 - 服务端调用
listen
,监听端口。(可以通过netstat
命令查看对应的端口号是否有被监听) - 服务端调用
accept
阻塞,等待客户端连接 - 客户端
connect
服务端欢迎套接字,服务accept
阻塞调用返回创建连接套接字(三次握手) - 客户端调用
write
写入数据;服务端调用read
读取数据 - 客户端会调用
close
断开连接,服务端read
读取数据的时候,就会读取到了文件结束符EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
IO多路复用epoll模式:
- 创建服务端 socket,bind 绑定端口、listen 监听端口
- 将服务端 socket 注册到 epoll
- epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket
- 将已连接的 socket 注册到 epoll
- epoll_wait 等待事件发生
- 对方连接关闭时,我方调用 close
Accept():客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。客户端协议栈收到 ACK 之后,使得应用程序从
connect
调用返回,表示客户端到服务端端的单向连接建立成功,客户端的状态为 ESTABLISHED。第三次握手ACK 应答包到达服务端端后,服务端端的 TCP 连接进入 ESTABLISHED 状态,同时服务端端协议栈使得accept
阻塞调用返回,这个时候服务端端到客户端的单向连接也建立成功。
Accept
同步从侦听套接字的连接请求队列中提取第一个挂起的连接请求,然后创建并返回一个新 Socket请求。accept函数的作用是接受客户端的连接请求,并返回一个新的套接字用于服务器和客户端之间的数据交换。
Close():客户端调用
close
,表明客户端没有数据需要发送了;服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符EOF
到接收缓冲区中,应用程序可以通过read
调用来感知这个 FIN 包。服务端处理完数据后,读到EOF
,于是也调用close
关闭它的套接字。
TCP长连接/短连接
- 长连接:多用于操作频繁,点对点的通讯,而且连接数不能太多情况。数据库连接用长连接
- 短连接:并发量大,但每个用户无需频繁操作情况。web网站的http服务一般用短连接。
TCP保活机制
如果一个给定的连接在两小时内没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:
- 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。
- 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
- 客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。
- 客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探查的响应。
拔掉网线,TCP连接还存在吗?
有数据传输的情况:
- 在客户端拔掉网线后,如果服务端发送了数据报文,那么在服务端重传次数没有达到最大值之前,客户端就插回了网线,那么双方原本的 TCP 连接还是能正常存在,就好像什么事情都没有发生。
- 在客户端拔掉网线后,如果服务端发送了数据报文,在客户端插回网线之前,服务端重传次数达到了最大值时,服务端就会断开 TCP 连接。等到客户端插回网线后,向服务端发送了数据,因为服务端已经断开了与客户端相同四元组的 TCP 连接,所以就会回 RST 报文,客户端收到后就会断开 TCP 连接。至此, 双方的 TCP 连接都断开了。
没有数据传输的情况:
-
如果双方都没有开启 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,那么客户端和服务端的 TCP 连接状态将会一直保持存在,并且一直保持在 ESTABLISHED 状态。
-
如果有开启,服务端在一段时间没有进行数据交互时,会触发 TCP keepalive 机制,探测对方是否存在,如果探测到对方已经消亡,则会断开自身的 TCP 连接:
-
保活机制的弊端:
- 在出现短暂的网络错误的时候,keepalive参数设置不合理(探测时间过于短暂)时有可能会使一个健康的连接断开;
- 保活机制会占用不必要的流量;
-
为什么有了保活机制,应用层还需要实现心跳机制呢?
保活机制可以感应到对端网络的异常,可以回收异常的连接,那么应用层之所以还需要实现心跳机制的原因:
TCP keepalive
只能判断连接是否存活,无法判断连接是否正常可用,当服务端出现异常的时候,客户端无法通过该机制感知到服务端进程是否阻塞或其他应用内死锁等异常。TCP keepalive
是存在于传输层的,客户端与服务端之间可能会存在传输层之上的代理,如haproxy
中的四层代理,这样只有传输层以上的流量才会被转发
-
TCP Keepalive HTTP Keep-Alive 的关系
HTTP
的Keep-Alive
是为了让TCP连接
活得更久一点,在发起多个http请求时能复用同一个连接,提高通信效率;TCP
的KeepAlive
机制意图在于探测连接的对端是否存活,是一种检测TCP连接状况
的机制。- tcp具备双端连续收发报文的能力,开启了
keep-alive
的HTTP连接,由于协议本身的限制,服务端无法主动发起应用报文。
一端断电和进程崩溃对TCP连接的影响
- 如果「客户端进程崩溃」,客户端的进程在发生崩溃的时候,内核会发送 FIN 报文,与服务端进行四次挥手。
- 但是,「客户端主机宕机」,那么是不会发生四次挥手的。
服务端establish状态,客户端崩溃重启后发送SYN
- 处于 establish 状态的服务端如果收到了客户端的 SYN 报文(注意此时的 SYN 报文其实是乱序的,因为 SYN 报文的初始化序列号其实是一个随机数),会回复一个携带了正确序列号和确认号的 ACK 报文,这个 ACK 被称之为 Challenge ACK。
- 客户端收到这个 Challenge ACK,发现确认号(ack num)并不是自己期望收到的,于是就会回 RST 报文,服务端收到后,就会释放掉该连接。