TCP/IP协议¶
《Linux高性能服务器编程》 - 游双 的第1、2、3章,《UNIX网络编程卷1》 - 第三版 的第2章,以及《小林coding - TCP篇》的读书笔记,本文中的所有代码可在GitHub仓库中找到
TCP基础¶
- 序列号(SYN)
- 用来解决网络包乱序的问题,初始值为一个随机数,随后每发送一次,加一
- 确认应答号(ACK)
- 用来解决丢包问题,指下一次期望收到的数据的序列号,可确认此序列号前面的数据都已经被正常接收
- 控制位
- ACK - 确认应答,除了最初建立连接的
SYN
包以外,此控制位都必须置1 - RST - 强制断开连接
- SYN - 希望建立连接
- FIN - 希望断开连接
- ACK - 确认应答,除了最初建立连接的
- 窗口大小
- 用来做流量控制,表示接收方还能容纳多少字节的数据
三次握手¶
如上图所示,通过三个TCP报文,就能建立一个TCP连接,常称为三次握手:
- SYN报文
- 客户端发起连接请求,并带客户端序列号
client_isn
- 客户端发起连接请求,并带客户端序列号
- SYN+ACK报文
- 服务端应答客户端序列
client_isn+1
,并带上服务端序列号server_isn
- 服务端应答客户端序列
- ACK报文
- 客户端应答服务端序列
server_isn+1
,并带上数据(可选)
- 客户端应答服务端序列
四次挥手¶
如上图所示,需要四个TCP报文,才能完全断开一个TCP连接,常称为四次挥手。客户端和服务端都可以先发起断开请求,每个方向都需要一个FIN
报文和ACK
报文,并且只有主动关闭的连接,才有TIME_WAIT
状态。
- FIN报文
- 主动方发送断开请求,并带上主动方的
SEQ
和期望的被动方ACK
- 主动方发送断开请求,并带上主动方的
- ACK报文
- 被动方应答主动方的断开请求,并带上被动方的
SEQ
和期望的主动方ACK
- 被动方应答主动方的断开请求,并带上被动方的
- FIN报文
- 被动方处理完本地事务后,向主动方发送断开请求,并带上正确的
SEQ
和ACK
- 被动方处理完本地事务后,向主动方发送断开请求,并带上正确的
- ACK报文
- 主动方应答被动方的断开请求,并带上主动方的
SEQ
- 如果
SEQ
是被动方期望的,被动方进入CLOSE
状态 - 主动方还需等待两倍的MSL(最大生成时间),才能进入
CLOSE
状态,以保证收到来自被动方的ACK报文丢失请求的时候,能重发
- 主动方应答被动方的断开请求,并带上主动方的
头部选项¶
上面的TCP报文头部中,有一个可选的部分,是“TCP头部选项”,其结构如下:
通过kind
和length
的不同组合,TCP报文可携带各种不同的配置选项。这里只介绍“最大报文段长度”选项,其他选项信息可参考《Linux高性能服务器编程》的第3.2.2节。
当kind=2
时,TCP头部选项为“最大报文段长度”选项。TCP连接初始化(同步报文)时,通信双方使用该选项来协商最大报文段长度(Max Segment Size, MSS)。为了避免发生IP分片,TCP模块通常将MSS设置为(MTU - 40)
字节(20字节TCP头部和20字节IP头部)。MTU和MSS的概念如下:
- MTU (Max Transmit Unit)
- 当IP数据报长度超过MTU时,它会被分片,可通过
ifconfig
查看
- 当IP数据报长度超过MTU时,它会被分片,可通过
- MSS (Max Segment Size)
- 除去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度
tcpdump实战¶
tcpdump选项 | 示例 | 说明 |
---|---|---|
-i | tcpdump -i eth0 |
指定网卡,any表示所有网卡 |
-nn | tcpdump -nn |
不解析IP地址和端口号的名称 |
-c | tcpdump -c 5 |
限制要抓取的网络包的个数 |
-w | tcpdump -w file.pcap |
保存到文件中,可用wireshark 打开 |
host, src host, dst host | tcpdump -nn host 192.168.1.100 |
主机过滤 |
port, src port, dst port | tcpdump -nn port 80 |
端口过滤 |
ip, ip6, arp, tcp, udp, icmp | tcpdump -nn tcp |
协议过滤 |
and, or, not | tcpdump -nn host 192.168.1.100 and port 80 |
逻辑表达式 |
tcp[tcpflags] | tcpdump -nn "tcp[tcpflags] & (tcp-syn|tcp-ack) != 0" |
特定状态的TCP包 |
正常连接/断开实验¶
例子"tcpdump/normal"模拟了三次握手和四次挥手,其中包含了实验脚本Makefile
和实验结果normal.pcap
:
# 在远端的机器上通过`nc`命令启动一个TCP服务,假设其IP是10.207.83.17,端口是1234
> nc -lk -p 1234
# 在近端的机器上启动tcpdump监控,假设网卡名称是:ens33
> sudo tcpdump -i ens33 tcp and host 10.207.83.17 and port 1234 -w normal.pcap
# 在近端的机器上通过`nc`命令连接服务器,并通过`Ctrl+D`直接退出
> nc -q 1 10.207.83.17 1234
客户端连接到服务器后,直接通过Ctrl+D
退出。tcpdump
工具在客户端抓取三次握手和四次挥手的TCP包,并记录在normal.pcap
文件中。用wireshark
软件直接打开tcpdump
的输出结果normal.pcap
,其中显示的Seq
和Ack
是wireshark
做过优化后的相对值:
数据传输实验¶
例子"tcpdump/normal"的方法同样可以获取客户端和服务端之间的数据传输包。transfer.pcap
记录的数据包括了,客户端向服务端发送"hello"字符串,服务端向客户端发送"world"字符串,因此下图中除了正常的连接同步包以外,还多了两个PSH+ACK
包:
PSH+ACK
包中长度Len
取决于需要传递的数据的大小。例如,客户端向服务端传递的"hello"字符串需要6个字节,其中包括最后一个换行符\n
。下图是客户端向服务端发送的PSH+ACK
包的二进制码:
第一次握手失败¶
为了模拟TCP第一次握手SYN
丢包的情况,可利用iptables
的INPUT
规则在服务端过滤掉客户端输入的包,iptables
相关命令可参考文档。网络包进入主机后的顺序如下:
- 进来的顺序: Wire -> NIC -> tcpdump -> iptables(INPUT规则)
- 出去的顺序: iptables(OUTPUT规则) -> tcpdump -> NIC -> Wire
例子"tcpdump/1st_handshake"模拟了第一次握手失败,中包含了实验脚本Makefile
和实验结果1st_syn_fail.pcap
:
# 在远端的机器上利用`iptables`工具过滤掉端口1234的包
> sudo iptables -A INPUT -p tcp --destination-port 1234 -j DROP
# 在远端的机器上通过`nc`命令启动一个TCP服务,假设其IP是10.207.83.17,端口是1234
> nc -lk -p 1234
# 在近端的机器上启动tcpdump监控,假设网卡名称是:ens33
> sudo tcpdump -i ens33 tcp and host 10.207.83.17 and port 1234 -w 1st_syn_fail.pcap
# 在近端的机器上通过`nc`命令连接服务器,由于无法连接服务器,一段时间后会自行退出
> nc -q 1 10.207.83.17 1234
由于服务端过滤了客户端发来的SYN
包,客户端发起了SYN
包后,一直没有收到服务端的ACK
包,所以一直会超时重传,并且每次RTO超时时间是不同的(如下图),同时客户端重传次数由/proc/sys/net/ipv4/tcp_syn_retries
指定:
第二次握手失败¶
例子"tcpdump/2nd_handshake"模拟了第二次握手失败,其中包含了实验脚本Makefile
和实验结果2nd_syn_fail.pcap
:
# 在远端的机器上通过`nc`命令启动一个TCP服务,假设其IP是10.207.83.17,端口是1234
> nc -lk -p 1234
# 在近端的机器上启动tcpdump监控,假设网卡名称是:ens33
> sudo tcpdump -i ens33 tcp and host 10.207.83.17 and port 1234 -w 2nd_syn_fail.pcap
# 在近端的机器上利用`iptables`工具过滤掉来自服务器的包
> sudo iptables -I INPUT -s 10.207.83.17 -j DROP
# 在近端的机器上通过`nc`命令连接服务器,由于无法收到服务器的SYN+ACK包,服务器会一直重传SYN+ACK包
> nc -q 1 10.207.83.17 1234
由于客户端过滤了服务端发来的SYN+ACK
包,服务端由于没有收到对SYN+ACK
包的回复,会认为SYN+ACK
包丢失,然后超时重传SYN+ACK
包,同时服务端重传SYN+ACK
包的次数由/proc/sys/net/ipv4/tcp_synack_retries
指定: