Linux 可以用 tcpdump 抓包,保存为 .pcap 格式 然后使用 WireShark 可视化和分析(WireShark 可直接抓包 Windows)
# 客户端 终端一 执行 tcpdump 抓包 $ tcpdump -i any tcp and host 192.168.3.200 and port 80 -w http.pcap # 客户端 终端二 执行 curl 命令 $ curl http://192.168.3.200 # 客户端 终端一 停止 tcpdump 抓包 Ctrl+C 并导出 http.pcapHTTP 是基于 TCP 协议进行传输的:
最开始的 3 个包就是 TCP 三次握手建立连接的包中间是 HTTP 请求和响应的包最后的 3 个包则是 TCP 断开连接的挥手包 (有时服务器在收到客户端的FIN后也直接关闭连接,所以 ACK + FIN 是同时发送的,但通常是分开的标准四次挥手)当客户端发起的 TCP 第一次握手 SYN 包,在超时时间内没收到服务端的 ACK,就会超时重传 SYN 数据包,每次超时重传的间隔RTO是翻倍上涨的(比如1s,2s,4s,8s,16s…),直到 SYN 包的重传次数到达 tcp_syn_retries 值后 (默认为5),客户端不再发送 SYN 包。
当第二次握手的 SYN、ACK 丢包时,客户端会超时重发 SYN 包,服务端也会超时重传 SYN、ACK 包。客户端 SYN 包超时重传的最大次数,是由 tcp_syn_retries 决定的,默认值是 5 次;服务端 SYN、ACK 包时重传的最大次数,是由 tcp_synack_retries 决定的,默认值是 5 次。
具体过程如图: 服务端收到客户的 SYN 包后,就会回 SYN、ACK 包,但是客户端一直没有回 ACK,服务端在超时后,重传了 SYN、ACK 包,接着一会,客户端超时重传的 SYN 包又抵达了服务端,服务端收到后,正常返回了 SYN、ACK 包,超时定时器重新计时,超时后又重传了 SYN + ACK,一会儿又受到了客户端重传的 SYN…… 所以相当于服务端的超时定时器只触发了一次,就因为收到客户端重传的 SYN 而被重置了。 最后,客户端 SYN 超时重传次数达到了 5 次(tcp_syn_retries 默认值 5 次,可修改),就不再继续发送SYN 包了。 在客户端最后一次重传 SYN 后,服务端继续重传 SYN+ ACK 达到 5 次(tcp_synack_retries 默认值 5 次,可修改),也不再继续发送了。
在建立 TCP 连接时,如果第三次握手的 ACK,服务端无法收到,则服务端就会短暂处于 SYN_RECV 状态,而客户端会处于 ESTABLISHED 状态。
由于服务端一直收不到 TCP 第三次握手的 ACK,则会一直重传 SYN、ACK 包,直到重传次数超过tcp_synack_retries 值(默认值 5 次)后,服务端就会断开 TCP 连接。
而客户端则会有两种情况: 如果客户端没发送数据包,一直处于 ESTABLISHED 状态,然后经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接,于是客户端连接就会断开连接。(TCP 的 保活机制) 如果客户端发送了数据包,一直没有收到服务端对该数据包的确认报文,则会一直重传该数据包,直到重传次数超过 tcp_retries2 值(默认值 15 次)后,客户端就会断开 TCP 连接。
TCP 定义了一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个「探测报文」,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
客户端发送 SYN 后状态变为 SYN_SENT ,正常情况下,服务器会在几毫秒内返回 SYN+ACK ,但如果客户端长时间没有收到 SYN+ACK 报文,则会重发 SYN 包,重发次数由参数 tcp_syn_retries 决定,默认为 5,一般第一次超时重发是 1 秒后,超时间隔呈2倍增长,第5次重发 SYN 后等待 32 秒,如果仍然没有得到回应 ACK,客户端就会终止三次握手。(总耗时 1+2+4+8+16+32=63 秒,大约 1 分钟)
可以根据网络的稳定性和目标服务器的繁忙程度修改 SYN 的重传次数,调整客户端的三次握手时间上限。比如内网通讯时,可以适当调低重试次数,尽快把错误暴露给应用程序。
当服务端收到 SYN 包后,会马上回复 SYN+ACK 包,表明确认收到了客户端的序列号,同时也把自己的序列号发给对方。 此时,服务端出现了新连接,状态是 SYN_RCV 。Linux 内核建立了一个半连接队列(未完成连接队列 / SYN 队列)来存放未完成三次握手的新连接,如果该队列溢出,就无法继续建立新连接。SYN 攻击就是这种情况。
应对 SYN 队列溢出的方法:1. 调整 SYN 队列大小,不能只修改 tcp_max_syn_backlog,也要修改 somaxconn 和 backlog (accept 队列的大小) ,否则只单纯增大 syn 队列是无效的 2. 控制溢出后的操作,比如直接丢弃(继续保持连接,但丢弃队满之后的包),或是返回 RST(告诉客户端废掉这个握手过程和这个连接);3. 启动 SYN cookie,收到新连接的 SYN 不再放入 SYN 队列,计算出一个 cookie 之后,放入返回给客户端的 SYN+ ACK 当中,当收到客户端的 ACK 之后,取出该cookie进行校验,连接确认合法后直接进入 accept 队列。
如果客户端没有收到 ACK+SYN,服务端无法收到客户端的 ACK 应答,则会重发 ACK+SYN,重发次数由 tcp_synack_retries 决定,默认为 5 。不过,由于客户端没收到 SYN+ACK 也会重发 SYN,所以服务端每次重发的计数计时都会被客户端的 SYN 所中断和重置。直到客户端最后一次重发 SYN,服务端正常回应 ACK+SYN后,继续重发 5 次,如果仍然没有收到客户端回应,就会终止连接。
当网络繁忙、不稳定时,报文丢失就会变严重,此时应该调整 tcp_synack_retries 参数,调大重发次数,反之则可以调小重发次数。
服务器收到 ACK 后连接建立成功,此时,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。如果进程不能及时地调用 accept 函数,就会造成 accept 队列(也称全连接队列)溢出,最终导致建立好的 TCP 连接被丢弃。
accept 队列溢出的应对方法:1. 调整 accept 队列大小,min(somaxconn, backlog),前者是Linux内核参数 默认128,后者一般是511,两种都可以调整, 2.控制溢出后的操作,比如直接丢弃(继续保持连接),或者发送 RST 给客户端(表示废掉这个握手过程和这个连接)
如果判断是短期溢出(突发流量),一般选择丢弃后面的包。客户端的连接状态仍然是 ESTABLISHED,只要服务器没有回复 ACK,客户端的请求就会被多次「重发」。如果服务器上的进程只是短暂的繁忙造成 accept 队列满,那么当 accept队列有空位时,再次接收到的请求报文由于含有 ACK,仍然会触发服务器端成功建立连接。
丢弃可以提高连接建立的成功率,只有你非常肯定 TCP 全连接队列会长期溢出时,才需要将 tcp_abort_on_overflow 参数设置为 1 向客户端发送 RST 复位报文,告诉客户端连接已经失败。
标准的 TCP 三次连接是上面这张图,只有第三次握手才可以携带数据。HTTP 请求必须在一个 RTT(从客户端到服务器一个往返的时间) 后才能发送。
在 Linux 3.7 内核版本之后,提供了 TCP Fast Open 功能,这个功能可以减少 TCP 连接建立的时延。