1. 基本概念
1.1 RST解释
在 TCP 协议中 RST 表示复位,用来异常的关闭连接,发送 RST 关闭连接时,不必等缓冲区的数据都发送出去,直接丢弃缓冲区中的数据,连接释放进入
CLOSED状态。而接收端收到 RST 段后,也不需要发送 ACK 确认。
1.2 确认号
- 不是所有的包都需要确认的
- 不是收到了数据包就立马需要确认的,可以延迟一会再确认
- ACK 包本身不需要被确认,否则就会无穷无尽死循环了
- 确认号永远是表示小于此确认号的字节都已经收到
- TCP 使用确认号(Acknowledgment number, ACK)来告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到
1.3 TCP/IP 协议族
从字面上来看,很多人会认为 TCP/IP 是 TCP、IP 这两种协议,实际上TCP/IP 协议族指的是在 IP 协议通信过程中用到的协议的统称
1.4 应用层协议还有哪些
- 域名解析协议 DNS
- 收发邮件 SMTP 和 POP3 协议
- 时钟同步协议 NTP
- 网络文件共享协议 NFS
1.5 标志位
- SYN(Synchronize):用于发起连接数据包同步双方的初始序列号
- ACK(Acknowledge):确认数据包
- RST(Reset):这个标记用来强制断开连接,通常是之前建立的连接已经不在了、包不合法、或者实在无能为力处理
- FIN(Finish):通知对方我发完了所有数据,准备断开连接,后面我不会再发数据包给你了。
- PSH(Push):告知对方这些数据包收到以后应该马上交给上层应用,不能缓存起来
1.5 MSS、MTU
MSS:最大段大小选项,是 TCP 允许的从对方接收的最大报文段 常量 TCP_MIN_MSS 的值为 88,常量 MAX_TCP_WINDOW 的值为 32768,因此不在 88~32767 直接的 MSS 值会设置失败。
SACK:选择确认选项
Window Scale:窗口缩放选项
最大传输单元(Maximum Transmission Unit, MTU)数据链路层传输的帧大小是有限制的,不能把一个太大的包直接塞给链路层,这个限制被称为最大传输单元(Maximum Transmission Unit, MTU) 不同的数据链路层的 MTU 是不同的。通过netstat -i 可以查看网卡的 mtu,比如在 我的 centos 机器上可以看到
1.6 IP 包分片
MSS = MTU - IP header头大小 - TCP头大小 IP 数据包长度在超过链路的 MTU 时在发送之前需要分片,而 TCP 层 为了 IP 层不用分片主动将包切割成 MSS 大小
1.7 端口号被划分成以下 3 种类型
熟知端口号(well-known port)
已登记的端口(registered port)
临时端口号(ephemeral port)
为了充分发挥多核 CPU 的性能,多进程的处理网络请求主要有下面两种方式
主进程 + 多个 worker 子进程监听相同的端口 多进程 + REUSEPORT
1.8 惊群问题
惊群问题指的是:多进程/多线程同时监听同一个套接字,当有网络事件发生时,所有等待的进程/线程同时被唤醒,但是只有其中一个进程/线程可以处理该网络事件,其它的进程/线程获取失败重新进入休眠。
惊群问题带来的是 CPU 资源的浪费和锁竞争的开销。根据使用方式的不同,Linux 上的网络惊群问题分为 accept 惊群和 epoll 惊群两种。
1.9 MSL
MSL(报文最大生存时间)是 TCP 报文在网络中的最大生存时间。这个值与 IP 报文头的 TTL 字段有密切的关系。
IP 报文头中有一个 8 位的存活时间字段(Time to live, TTL)如下图。 这个存活时间存储的不是具体的时间,而是一个 IP 报文最大可经过的路由数,每经过一个路由器,TTL 减 1,当 TTL 减到 0 时这个 IP 报文会被丢弃。
2. 问答总结
2.1 为什么时间是两个 MSL
- 1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端
- 1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
- 2MS = 去向 ACK 消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL)
2.2 RST 出现常见的几种情况
- 端口未监听
- 连接信息丢失,另一方并不知道继续发送数据
- SO_LINGER 设置丢弃缓冲区数据,立刻 RST
2.3 TCP/IP 协议中,MSS 和 MTU 分别工作在哪一层?
MSS->传输层,MTU:链路层
2.4 在 MTU=1500 字节的以太网中,TCP 报文的最大载荷为多少字节?
1500(MTU) - 20(IP 头大小) - 20(TCP 头大小)= 1460
2.5 TCP连接的7个定时器
- 连接建立定时器
- 重传定时器
- 延迟 ACK 定时器
- PERSIST 定时器
- KEEPALIVE 定时器
- FIN_WAIT_2 定时器
2.5.1 连接建立定时器
当发送端发送 SYN 报文想建立一条新连接时,会开启连接建立定时器,如果没有收到对端的 ACK 包将进行重传。 将重传 6 次(间隔 1s、2s、4s、8s、16s、32s),6 次重试以后放弃重试,connect 调用返回 -1,调用超时, 这个值是由/proc/sys/net/ipv4/tcp_syn_retries决定的, 在我的 Centos 机器上,这个值等于 6
2.5.2 重传定时器
如果在发送数据包的时候没有收到 ACK 呢?定时器重传定时器。重传定时器的时间是动态计算的,取决于 RTT 和重传的次数。重传时间间隔是指数级退避,直到达到 120s 为止,重传次数是15次(这个值由操作系统的
/proc/sys/net/ipv4/tcp_retries2决定),总时间将近 15 分钟。
2.5.3 延迟 ACK 定时器
在 TCP 收到数据包以后在没有数据包要回复时,不马上回复 ACK。这时开启一个定时器,等待一段时间看是否有数据需要回复。如果期间有数据要回复,则在回复的数据中捎带 ACK,如果时间到了也没有数据要发送,则也发送 ACK。在 Centos7 上这个值为 40ms
2.5.4 PERSIST 定时器
Persist 定时器是专门为零窗口探测而准备的。我们都知道 TCP 利用滑动窗口来实现流量控制,当接收端 B 接收窗口为 0 时,发送端 A 此时不能再发送数据,发送端此时开启 Persist 定时器,超时后发送一个特殊的报文给接收端看对方窗口是否已经恢复,这个特殊的报文只有一个字节。
2.5.5 KEEPALIVE 定时器
如果通信以后一段时间有再也没有传输过数据,怎么知道对方是不是已经挂掉或者重启了呢?于是 TCP 提出了一个做法就是在连接的空闲时间超过 2 小时,会发送一个探测报文,如果对方有回复则表示连接还活着,对方还在,如果经过几次探测对方都没有回复则表示连接已失效,客户端会丢弃这个连接。
2.5.6 FIN_WAIT_2 定时器
四次挥手过程中,主动关闭的一方收到 ACK 以后从 FIN_WAIT_1 进入 FIN_WAIT_2 状态等待对端的 FIN 包的到来,FIN_WAIT_2 定时器的作用是防止对方一直不发送 FIN 包,防止自己一直傻等。这个值由
/proc/sys/net/ipv4/tcp_fin_timeout决定,在我的 Centos7 机器上,这个值为 60s
2.5.7 TIME_WAIT 定时器
TIME_WAIT 定时器也称为 2MSL 定时器,可能是这七个里面名气最大的,主动关闭连接的一方在 TIME_WAIT 持续 2 个 MSL 的时间,超时后端口号可被安全的重用。
TIME_WAIT存在的意义有两个:
- 可靠的实现 TCP 全双工的连接终止(处理最后 ACK 丢失的情况)
- 避免当前关闭连接与后续连接混淆(让旧连接的包在网络中消逝)
2.6 time_wait以后两个MSL才能关闭连接,如果想立刻关闭该咋办
TCPKILL
2.7 为什么SYN段不携带数据却要消耗一个序列号
SYN 段长度为 0 却需要消耗一个序列号,原因是 SYN 段需要对端确认 ACK 段长度为 0,不消耗序列号,也不用对端确认
不占用序列号的段是不需要确认的(都没有内容确认个啥),比如 ACK 段。SYN 段需要对方的确认,需要占用一个序列号。 凡是消耗序列号的 TCP 报文段,一定需要对端确认。如果这个段没有收到确认,会一直重传直到达到指定的次数为止。
除了交换彼此的初始序列号,三次握手的另一个重要作用是交换一些辅助信息,比如最大段大小(MSS)、窗口大小(Win)、窗口缩放因子(WS)、是否支持选择确认(SACK_PERM)等,这些都会在后面的文章中重点介绍。
2.8 ISN 能设置成一个固定值呢
ISN 不能从一个固定的值开始,原因是处于安全性和避免前后连接互相干扰
答案是不能,TCP 连接四元组(源 IP、源端口号、目标 IP、目标端口号)唯一确定,所以就算所有的连接 ISN 都是一个固定的值,连接之间也是不会互相干扰的。但是会有几个严重的问题
1、出于安全性考虑。如果被知道了连接的ISN,很容易构造一个在对方窗口内的序列号,源 IP 和源端口号都很容易伪造,这样一来就可以伪造 RST 包,将连接强制关闭掉了。如果采用动态增长的 ISN,要想构造一个在对方窗口内的序列号难度就大很多了。
2、因为开启 SO_REUSEADDR 以后端口允许重用,收到一个包以后不知道新连接的还是旧连接的包因为网络的原因姗姗来迟,造成数据的混淆。如果采用动态增长的 ISN,那么可以保证两个连接的 ISN 不会相同,不会串包。
2.9 解释发送窗口
win: 向对方声明自己的接收窗口的大小, win=表示接收窗口的大小
TCP 在收到数据包回复的 ACK 包里会带上自己接收窗口的大小,接收端需要根据这个值调整自己的发送策略。
发送窗口是 TCP 滑动窗口的核心概念,它表示了在某个时刻一端能拥有的最大未确认的数据包大小(最大在途数据)
窗口的左边界表示成功发送并已经被接收方确认的最大字节序号,窗口的右边界是发送方当前可以发送的最大字节序号,滑动窗口的大小等于右边界减去左边界。
2.10 解释一下ss命令
ss -lnt | grep :9090
对于 LISTEN 状态的套接字,Recv-Q 表示 accept 队列排队的连接个数,Send-Q 表示全连接队列(也就是 accept 队列)的总大小。
2.11 如何查看对方端口是否打开
nc -v 10.211.55.12 6379
telnet 10.211.55.12 6379
如何查看端口被什么进程监听占用 比如查看 22 端口被谁占用,常见的可以使用 lsof 和 netstat 两种方法
netstat -ltpn | grep :22 lsof -n -P -i:22 其中 -n 表示不将 IP 转换为 hostname,-P 表示不将 port number 转换为 service name,-i:port 表示端口号为 22 的进程
有两种典型的使用方式会生成临时端口:
调用 bind 函数不指定端口 调用 connect 函数
2.11 为什么挥手要四次,变为三次可以吗
当然可以,因为有延迟确认的存在,把第二步的 ACK 经常会跟随第三步的 FIN 包一起捎带会对端。 在这种情况下,如果不及时发送 ACK 包,死等服务端这边发送数据,可能会造成客户端不必要的重发 FIN 包, 如果服务端确定没有什么数据需要发给客户端,那么当然是可以把 FIN 和 ACK 合并成一个包,四次挥手的过程就成了三次。
除了交换彼此的初始序列号,三次握手的另一个重要作用是交换一些辅助信息,比如最大段大小(MSS)、窗口大小(Win)、窗口缩放因子(WS)、是否支持选择确认(SACK_PERM)等
2.12 全连接队列和半连接队列
当服务端调用 listen 函数时,TCP 的状态被从 CLOSE 状态变为 LISTEN,于此同时内核创建了两个队列:
半连接队列(Incomplete connection queue),又称 SYN 队列 全连接队列(Completed connection queue),又称 Accept 队列
netstat -lnpa | grep :9090 | awk '{print $6}' | sort | uniq -c | sort -rn
全连接队列包含了服务端所有完成了三次握手,但是还未被应用调用 accept 取走的连接队列。此时的 socket 处于 ESTABLISHED 状态。每次应用调用 accept() 函数会移除队列头的连接。如果队列为空,accept() 通常会阻塞。全连接队列也被称为 Accept 队列。
listen 函数的第二个参数 backlog 用来设置全连接队列大小,但不一定就会选用这一个 backlog 值,还受限于 somaxconn,等下会有更详细的内容说明全连接队列大小的计算规则。
int listen(int sockfd, int backlog) 如果全连接队列满,内核会舍弃掉 client 发过来的 ack(应用层会认为此时连接还未完全建立)
全连接队列的大小 全连接队列的大小是 listen 传入的 backlog 和 somaxconn 中的较小值。
全连接队列大小判断是否满的函数是 /include/net/sock.h 中 的 sk_acceptq_is_full 方法。
2.12 多大的 backlog 是合适的
你如果的接口处理连接的速度要求非常高,或者在做压力测试,很有必要调高这个值 如果业务接口本身性能不好,accept 取走已建连的速度较慢,那么把 backlog 调的再大也没有用,只会增加连接失败的可能性 可以举个典型的 backlog 值供大家参考,Nginx 和 Redis 默认的 backlog 值等于 511,Linux 默认的 backlog 为 128,Java 默认的 backlog 等于 50
2.13 tcp_abort_on_overflow 参数
默认情况下,全连接队列满以后,服务端会忽略客户端的 ACK,随后会重传SYN+ACK,也可以修改这种行为,这个值由/proc/sys/net/ipv4/tcp_abort_on_overflow决定。
tcp_abort_on_overflow 为 0 表示三次握手最后一步全连接队列满以后 server 会丢掉 client 发过来的 ACK,服务端随后会进行重传 SYN+ACK。 tcp_abort_on_overflow 为 1 表示全连接队列满以后服务端直接发送 RST 给客户端。 但是回给客户端 RST 包会带来另外一个问题,客户端不知道服务端响应的 RST 包到底是因为「该端口没有进程监听」,还是「该端口有进程监听,只是它的队列满了」。
半连接队列:服务端收到客户端的 SYN 包,回复 SYN+ACK 但是还没有收到客户端 ACK 情况下,会将连接信息放入半连接队列。半连接队列又被称为 SYN 队列。 全连接队列:服务端完成了三次握手,但是还未被 accept 取走的连接队列。全连接队列又被称为 Accept 队列。 半连接队列的大小与用户 listen 传入的 backlog、net.core.somaxconn、net.core.somaxconn 都有关系,准确的计算规则见上面的源码分析 全连接队列的大小是用户 listen 传入的 backlog 与 net.core.somaxconn 的较小值
2.14 SYN flood 攻击
SYN Flood 是一种广为人知的 DoS(拒绝服务攻击) 想象一个场景:客户端大量伪造 IP 发送 SYN 包,服务端回复的 ACK+SYN 去到了一个「未知」的 IP 地址,势必会造成服务端大量的连接处于 SYN_RCVD 状态,而服务器的半连接队列大小也是有限的,如果半连接队列满,也会出现无法处理正常请求的情况。
在这种情况下,一次恶意的 SYN 包,会占用一个服务端连接 63s(1+2+4+8+16+32),如果这个时候有大量的恶意 SYN 包过来连接服务器,很快半连接队列就被占满,不能接收正常的用户请求。
如何应对 SYN Flood 攻击
增加 SYN 连接数:tcp_max_syn_backlog
减少SYN+ACK重试次数:tcp_synack_retries 重试次数由 /proc/sys/net/ipv4/tcp_synack_retries控制,默认情况下是 5 次,当收到SYN+ACK故意不回 ACK 或者回复的很慢的时候,调小这个值很有必要。
SYN Cookie 机制 SYN Cookie 技术最早是在 1996 年提出的,最早就是用来解决 SYN Flood 攻击的,现在服务器上的 tcp_syncookies 都是默认等于 1,表示连接队列满时启用,等于 0 表示禁用,等于 2 表示始终启用。由/proc/sys/net/ipv4/tcp_syncookies控制。
SYN Cookie 的原理是基于「无状态」的机制,服务端收到 SYN 包以后不马上分配为 Inbound SYN分配内存资源,而是根据这个 SYN 包计算出一个 Cookie 值,作为握手第二步的序列号回复 SYN+ACK,等对方回应 ACK 包时校验回复的 ACK 值是否合法,如果合法才三次握手成功,分配连接资源。
2.14 TFO 原理
请求Fast Open Cookie 的过程如下:
- 客户端发送一个 SYN 包,头部包含 Fast Open 选项,且该选项的Cookie 为空,这表明客户端请求 Fast Open Cookie
- 服务端收取 SYN 包以后,生成一个 cookie 值(一串字符串)
- 服务端发送 SYN + ACK 包,在 Options 的 Fast Open 选项中设置 cookie 的值
- 客户端缓存服务端的 IP 和收到的 cookie 值
TCP 快速打开(TCP Fast Open,TFO) TFO 是在原来 TCP 协议上的扩展协议,它的主要原理就在发送第一个 SYN 包的时候就开始传数据了,不过它要求当前客户端之前已经完成过「正常」的三次握手。快速打开分两个阶段:请求 Fast Open Cookie 和 真正开始 TCP Fast Open
TCP Fast Open 的优势 一个最显著的优点是可以利用握手去除一个往返 RTT
这篇文章主要用 curl 命令演示了 TCP 快速打开的详细过程和原理
客户端发送一个 SYN 包,头部包含 Fast Open 选项,且该选项的 Cookie 长度为 0 服务端根据客户端 IP 生成 cookie,放在 SYN+ACK 包中一同发回客户端 客户端收到 Cookie 以后缓存在自己的本地内存 客户端再次访问服务端时,在 SYN 包携带数据,并在头部包含 上次缓存在本地的 TCP cookie 如果服务端校验 Cookie 合法,则在客户端回复 ACK 前就可以直接发送数据。如果 Cookie 不合法则按照正常三次握手进行。
2.15 SO_LINGER 的用法
// 测试#1:
socket.setSoLinger(false, 0);
// 测试#2
// socket.setSoLinger(true, 0);
// 测试#3
//socket.setSoLinger(true, 1);
2.16 RST 出现常见的几种情况
端口未监听 连接信息丢失,另一方并不知道继续发送数据 调用 close 函数,设置了 SO_LINGER 为 true
Broken pipe 与 Connection reset by peer 错误在网络编程中非常常见,出现的前提都是连接已关闭。
Broken pipe出现的时机是:在一个 RST 的套接字继续写数据,就会出现Broken pipe。
2.17 快速重传的含义
ACK 是表示这之前的包都已经全部收到 当发送端收到 3 个或以上重复 ACK,就意识到之前发的包可能丢了,于是马上进行重传,不用傻傻的等到超时再重传
「超时重传的时间」(Retransmission TimeOut,RTO),它与 RTT 密切相关,下面我们来介绍几种计算 RTO 的方法,因此间隔多久重传就是不是一成不变的,它随着不同的网络情况需要动态的进行调整,
经典方法(适用 RTT 波动较小的情况) α 是平滑因子,建议值是0.8 ~ 0.9。假设平滑因子 α = 0.8,那么 SRTT = 80% 的原始值 + 20% 的新采样值。相当于一个低通滤波器。
标准方法(Jacobson / Karels 算法) RTT 有大的波动时,很难即时反应到 RTO 上,因为都被平滑掉了。标准方法对 RTT 的采样增加了一个新的因素,
最后的部分引入了「重传二义性」的概念,看到了计算重传情况下 RTT 的困难之处,由此引入了 Karn 算法:
重传情况下不用测量的 RTT 来更新 SRTT 和 RTTVAR 出现重传时 RTO 采用指数级退避的方式,直到后续包出现不需要重传就可以收到确认为止
2.18 第四次挥手为什么要等待2MSL
保证A发送的最后一个ACK报文段能够到达B 防止已失效的连接请求报文段出现在本连接中
2.19 TCP的粘包和拆包
TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
2.20 TCP的重传机制是什么
由于TCP的下层网络(网络层)可能出现丢失、重复或失序的情况,TCP协议提供可靠数据传输服务。为保证数据传输的正确性,TCP会重传其认为已丢失(包括报文中的比特错误)的包。TCP使用两套独立的机制来完成重传, 一是基于时间,二是基于确认信息。TCP在发送一个数据之后,就开启一个定时器,若是在这个时间内没有收到发送数据的ACK确认报文,则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号。
2.21 说下TCP的滑动窗口机制
TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 TCP会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。接收方发送的确认报文中的window字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0,则发送方不能发送数据。 TCP头包含window字段,16bit位,它代表的是窗口的字节容量,最大为65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。
2.22 详细讲一下拥塞控制?
防止过多的数据注入到网络中。 几种拥塞控制方法:
慢开始( slow-start )
拥塞避免( congestion avoidance )
快重传( fast retransmit )
快恢复( fast recovery )
2.23 拥塞处理主要涉及到下面这几个算法
慢启动(Slow Start) 拥塞避免(Congestion Avoidance) 快速重传(Fast Retransmit)和快速恢复(Fast Recovery)
为了实现上面的算法,TCP 的每条连接都有两个核心状态值:
拥塞窗口(Congestion Window,cwnd) 慢启动阈值(Slow Start Threshold,ssthresh)
拥塞窗口(Congestion Window,cwnd) 拥塞窗口指的是在收到对端 ACK 之前自己还能传输的最大 MSS 段数。
它与前面介绍的接收窗口(rwnd)有什么区别呢?
接收窗口(rwnd)是接收端的限制,是接收端还能接收的数据量大小 拥塞窗口(cwnd)是发送端的限制,是发送端在还未收到对端 ACK 之前还能发送的数据量大小 拥塞窗口初始值等于操作系统的一个变量 initcwnd,最新的 linux 系统 initcwnd 默认值等于 10。
拥塞窗口与前面介绍的发送窗口(Send Window)又有什么关系呢?
真正的发送窗口大小 = 「接收端接收窗口大小」 与 「发送端自己拥塞窗口大小」 两者的最小值
如果接收窗口比拥塞窗口小,表示接收端处理能力不够。如果拥塞窗口小于接收窗口,表示接收端处理能力 ok,但网络拥塞。
发送端和接收端不会交换 cwnd 这个值,这个值是维护在发送端本地内存中的一个值,发送端和接收端最大的在途字节数(未经确认的)数据包大小只能是 rwnd 和 cwnd 的最小值。
拥塞控制的算法的本质是控制拥塞窗口(cwnd)的变化。
慢启动阈值(Slow Start Threshold,ssthresh) 慢启动拥塞窗口(cwnd)肯定不能无止境的指数级增长下去,否则拥塞控制就变成了「拥塞失控」了,它的阈值称为「慢启动阈值」(Slow Start Threshold,ssthresh),这是文章开头介绍的拥塞控制的第二个核心状态值。ssthresh 就是一道刹车,让拥塞窗口别涨那么快。
当 cwnd < ssthresh 时,拥塞窗口按指数级增长(慢启动) 当 cwnd > ssthresh 时,拥塞窗口按线性增长(拥塞避免)
拥塞避免(Congestion Avoidance)
当 cwnd > ssthresh 时,拥塞窗口进入「拥塞避免」阶段,在这个阶段,每一个往返 RTT,拥塞窗口大约增加 1 个 MSS 大小,直到检测到拥塞为止。
2.24 TCP三次握手建立的连接,如果服务端不进行accept接受新链接,最多有多少个连接可以建立成功
考察套接字listen函数的理解
listen(sockfd, backlog)
sockfd: 套接字描述符
backlog: 已完成连接队列大小
在应用程序不进行accept的情况下,三次握手完成的连接最多是backlog + 1
2.25 TCP三次握手中可以携带应用层数据么
第一次和第二次不可以,第三次可以
第三次客户端给服务端发送ACK,客户端已经认为连接是ESTA的了,可以携带数据的 第一次和第二次连接还不是ESTA的,SYN包带有大量的数据会导致对端花费大量力气去存储这些数据,连接还不一定是有效的呢
TCP协议中是允许捎带应答的方式,将发送的数据和确认应答融合到一个数据包当中发送给服务端 三次握手以后执行的是慢启动算法,会先发送少量的数据包探测网络的转发能力和接收方的接受能力 所以通常在抓包过程当中不会遇到第三次握手携带应用层数据的情况,但理论上是可以的
2.26 TCP三次握手中,连接的序号一定要从0开始么
不管是三次握手客户端给服务端发送的初始序列号还是服务端给客户端发送的初始序列号 都是动态变化的
2.27 TCP服务端最多通过三次握手,建立多少个连接
服务端敞开了accept,最多可以创建多少个TCP连接
服务端通过accept函数,最多可以创建多少个新链接套接字
一个进程最多打开的套接字的个数
一个进程最多打开的文件描述符的个数
套接字的本质还是文件描述符
ulimit -a 可以看到操作系统对于一个进程的最大文件描述符的限制open files
TCP服务端最多通过三次握手建立TCP连接的个数取决于操作系统限制 操作系统会规定一个值,open files来规定一个进程最多有多少个文件描述符 我的机器当中看到的是1024/65535, 也就是意味着我的机器的进程最多打开文件描述符的个数是1024/65535 在进程不新创建其他描述符的情况下,得要简历TCP的连接个数减去三个文件描述符,标准输出、标准输入、标准错误 open files可以通过ulimit -n value进行修改
2.28 TCP三次握手为什么需要协商MSS
MSS:最大段的大小,单位字节,规定了TCP最多一次性发送的数据大小为多少,TCP给网络层的数据大小最大就是MSS个字节
TCP在三次握手当中协商MSS, 是为了协商双方发送的TCP数据包的最大大小
- 防止数据包过大,在网络层被IP协议分片传输,IP协议是不可靠协议,如果一个分片丢失,对于TCP而言就是整个数据包丢失,那么TCP就要重传整个数据包
- 越丢包,越重传,越重传,网络转发能力越不行,恶心循环
- TCP交给网络层IP协议的数据包符合MSS,则IP协议一定不会分片传输
2.29 数据链路层MTU和MSS的关系
MSS + TCP头部 + ip头部 <= MTU 所以MSS 一定小于 MTU
2.30 四次挥手阶段可以不可以发送应用层的数据
可以的,两次挥手以后,被动连接方可以给主动断开连接方发送数据的
2.31 主动断开连接方为什么需要等待2MSL的时间
服务端为了等待2MSL的时间,连接才能变成closed状态,那么就会存在time_wait状态对么
那么请你解释一下服务端出现大量的time_wait状态,什么原因,有什么危害,应该怎么办
MSL:报文最大生存时间,他是任何报文在网络上存在的最长时间,超过这个时间就会被抛弃
2MSL = 主动断开连接方发送的ACK + 被动断开链接方重传的MSL
多等待一个MSL,就是为了防止ACK丢失,被动断开连接会重传FIN报文
2.32 那么请你解释一下,服务端出现大量close_wait状态,什么原因,有什么危害,应该怎么办
close_wait 存在于被动断开链接方,服务端和客户端都有可能存在close_wait, 说明服务端是被动断开链接方 被动连接断开方发了FIN报文以后,状态才会从close_wait 变成last_ack
问题转化成服务端为啥不发送第三次挥手的FIN报文 被动断开方在调用close函数关闭了套接字以后,TCP底层才会发送第三次挥手的FIN报文
答案开放的
- 有可能服务端的线程阻塞了,没有办法调用到close函数
- 在close之前有大量的耗时的逻辑
2.33 SYN报文什么情况下会被丢弃
- TCP的两个队列满了造成SYN报文被丢弃
- 开启tcp_tw_recycle参数,并且在NAT环境下,造成SYN报文被丢弃
在linux操作系统中,TIME_WAIT状态的持续时间是60s,也就是2个MSL tcvp要解决当真的没有端口使用的时候,客户端还想建立连接应该怎么办,所以有了tcp_tw_recycle tcp_tw_recycle:如果开启了这个选项,允许处于TIME_WAIT状态的连接被快速回收 tcp_tw_reuse:开启选项的话,客户端在调用connect()函数的时候,如果内核选择到的端口,已经被相同的四元组占用的时候,判断该链接是否处于TIME_WAIT状态,如果处于TIME_WAIT并且状态持续超过1s,就会重用这个连接,
使用着两个参数的前提是要打开TCP时间戳net.ipv4.tcp_timestamp=1,默认就是1
开启了recycle和timestamp选项,触发PAWS机制 作用:防止TCP包中的序列号发生绕回
2.34 什么是超时重传机制
就是在发送数据的时候,设置一个定时器,当超过指定的时间后,没有收到对方的ACK确认应答报文就会重发数据, 以下情况会超时重传 数据报丢失 确认应答丢失
2.35 如何增大半连接队列
/proc/sys/net/ipv4/tcp_max_syn_backlog /proc/sys/net/core/somaxconn 开启tcp_syncookies 功能 0表示关闭, 1表示SYN半连接队列放不下,再次启用它 2表示无条件开启这个功能,默认是1 减少SYN + ACK重传次数、默认重传5次,修改成1次 检查系统代码为什么调用accept()不及时
2.36 TCP协议的序列号的值最大是多少,超过最大值以后是多少
2的32次方,达到最大值以后,序号就会回0,成为序列号回绕
2.37 什么是TCP确认应答机制
TCP的确认是对序号的确认,序号的背后就是数据
2.38 发送窗口与接收窗口
TCP 在收到数据包回复的 ACK 包里会带上自己接收窗口的大小,接收端需要根据这个值调整自己的发送策略。
2.39 wireshark 抓包中显示的 win=29312 指的是「发送窗口」的大小吗
当然不是的,其实这里的 win 表示向对方声明自己的接收窗口的大小,对方收到以后,会把自己的「发送窗口」限制在 29312 大小之内。如果自己的处理能力有限,导致自己的接收缓冲区满,接收窗口大小为 0,发送端应该停止发送数据。
发送窗口 是已发送未确认和未发送但接收端可以接受
窗口的左边界表示成功发送并已经被接收方确认的最大字节序号,窗口的右边界是发送方当前可以发送的最大字节序号,滑动窗口的大小等于右边界减去左边界。
抓包显示的 TCP Window Full不是一个 TCP 的标记,而是 wireshark 智能帮忙分析出来的,表示包的发送方已经把对方所声明的接收窗口耗尽了,三次握手中客户端声明自己的接收窗口大小为 20,这意味着发送端最多只能给它发送 20 个字节的数据而无需确认,在途字节数最多只能为 20 个字节。
TCP 包中win=表示接收窗口的大小,表示接收端还有多少缓冲区可以接收数据,当窗口变成 0 时,表示接收端不能暂时不能再接收数据了
TCP window full TCP Zero Window
现在发送端的滑动窗口变为 0 了,经过一段时间接收端从高负载中缓过来,可以处理更多的数据包,如果发送端不知道这个情况,它就会永远傻傻的等待了。于是乎,TCP 又设计了零窗口探测的机制(Zero window probe),用来向接收端探测,你的接收窗口变大了吗?我可以发数据了吗?
零窗口探测包其实就是一个 ACK 包,下面根据抓包进行详细介绍
TCP window full 与 TCP zero window 这两者都是发送速率控制的手段,
TCP Window Full 是站在发送端角度说的,表示在途字节数等于对方接收窗口的情况,此时发送端不能再发数据给对方直到发送的数据包得到 ACK。
TCP zero window 是站在接收端角度来说的,是接收端接收窗口满,告知对方不能再发送数据给自己。
2.40 说说TCP是如何确保可靠性的呢?
TCP的连接是基于三次握手,而断开则是基于四次挥手。确保连接和断开的可靠性。 TCP的可靠性,还体现在有状态。TCP会记录哪些数据发送了,哪些数据被接收了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。 确认和重传机制:建立连接时三次握手同步双方的“序列号 + 确认号 + 窗口大小信息”,是确认重传、流控的基础。 传输过程中,如果Checksum校验失败、丢包或延时,发送端重传 流量控制:窗口和计时器的使用。TCP窗口中会指明双方能够发送接收的最大数据量 拥塞控制
2.41 什么是 SYN 攻击?
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到 一个 SYN 报文,就进入 SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。
2.42 简单来讲 nagle 算法讲的是减少发送端频繁的发送小包给对方。
Nagle 算法要求,当一个 TCP 连接中有在传数据(已经发出但还未确认的数据)时,小于 MSS 的报文段就不能被发送,直到所有的在传数据都收到了 ACK。 同时收到 ACK 后,TCP 还不会马上就发送数据,会收集小包合并一起发送。网上有人想象的把 Nagle 算法说成是「hold 住哥」,我觉得特别形象。
默认情况下 Nagle 算法都是启用的,Java 可以通过 setTcpNoDelay(true);来禁用 Nagle 算法。
这篇文章主要介绍了非常经典的 Nagle 算法,这个算法可以有效的减少网络上小包的数量。Nagle 算法是应用在发送端的,简而言之就是,对发送端而言:
当第一次发送数据时不用等待,就算是 1byte 的小包也立即发送 后面发送数据时需要累积数据包直到满足下面的条件之一才会继续发送数据: 数据包达到最大段大小MSS 接收端收到之前数据包的确认 ACK 不过 Nagle 算法是时代的产物,可能会导致较多的性能问题,尤其是与我们下一篇文章要介绍的延迟确认一起使用的时候。很多组件为了高性能都默认禁用掉了这个特性。
2.43 延迟确认
首先必须明确两个观点:
不是每个数据包都对应一个 ACK 包,因为可以合并确认。 也不是接收端收到数据以后必须立刻马上回复确认包。
如果收到一个数据包以后暂时没有数据要分给对端,它可以等一段时间(Linux 上是 40ms)再确认。如果这段时间刚好有数据要传给对端,ACK 就可以随着数据一起发出去了。 如果超过时间还没有数据要发送,也发送 ACK,以免对端以为丢包了。这种方式成为「延迟确认」。
TCP 要求 ACK 延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms。 延迟确认在很多 linux 机器上是没有办法关闭的, 可以看到需要立马回复 ACK 的场景有:
如果接收到了大于一个frame 的报文,且需要调整窗口大小
处于 quickack 模式(tcp_in_quickack_mode)
收到乱序包(We have out of order data.)
这篇文章我们介绍了 TCP keepalive 机制的由来,通过定时发送探测包来探测连接的对端是否存活, 不过默认情况下需要 7200s 没有数据包交互才会发送 keepalive 探测包,往往这个时间太久了,我们熟知的很多组件都没有开启 keepalive 特性, 而是选择在应用层做心跳机制。
2.44 RST 攻击
RST 攻击也称为伪造 TCP 重置报文攻击,通过伪造 RST 报文来关闭掉一个正常的连接。
源 IP 地址伪造非常容易,不容易被伪造的是序列号,RST 攻击最重要的一点就是构造的包的序列号要落在对方的滑动窗口内,否则这个 RST 包会被忽略掉,达不到攻击的效果。
这篇文章介绍了杀掉 TCP 连接的两个工具 tcpkill 和 killcx:
tcpkill 采用了比较保守的方式,抓取流量等有新包到来的时候,获取 SEQ/ACK 号,这种方式只能杀掉有数据传输的连接 killcx 采用了更加主动的方式,主动发送 SYN 包获取 SEQ/ACK 号,这种方式活跃和非活跃的连接都可以杀掉