本节我们通过一个玩具 TCP 服务应用,来观察 TCP 协议的通信过程,进而体会它的关键知识点:
实验应用
实验应用是一个最简化的玩具应用,由服务端和客户端组成:
- 服务端 tcp-upper-server ,默认监听 9999 端口,负责将客户端连接发过来的数据转成大写后发回去;
- 客户端 tcp-upper-client ,负责将用户输入的数据发往服务端,并将服务端发回的大写数据输出到屏幕上;
实验环境
我们需要准备一台服务器和一台客户端主机,之前介绍 traceroute 的演示环境,就是一个不错的选择:
实验环境同样由 Docker 提供,只需执行这个 docker 命令即可一键启动:
1
|
docker run --name tcp-upper-lab --rm -it --privileged --cap-add=NET_ADMIN --cap-add=SYS_ADMIN -v /data -v /tmp:/data2 -h netbox fasionchan/netbox:0.8 bash /script/tcp-upper-lab.sh
|
实验环境启动后,我们将看到 4 个窗口。其中,有两个窗口属于主机 ant ,另外两个窗口属于主机 apple 。
如何在不同窗口间进行切换呢? 我们来复习一下 tmux 窗口切换操作:
- 按下 tmux 功能键
Ctrl-B
;
- 再按下
Q
,这时每个窗口会出现一个数字;
- 按下窗口上的数字,即可切到对应的窗口;
服务端
我们在主机 apple 上执行 tcp-upper-server 程序启动服务端,它默认监听 9999 端口:
1
2
|
root@apple [ ~ ] ➜ tcp-upper-server
listening at port: 0.0.0.0:9999, waiting for connections...
|
用 ss 命令查看系统套接字,我们可以看到 tcp-upper-server 创建的监听套接字:
1
2
3
|
root@apple [ ~ ] ➜ ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 100 0.0.0.0:9999 0.0.0.0:*
|
ss 命令 -t 选项表示显示 TCP 套接字,-l 选项表示只显示监听套接字。
现在,我们在服务端执行 tcpdump 命令,监听网络流量:
1
2
3
|
root@apple [ ~ ] ➜ tcpdump -Xni any port 9999
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
|
注意到,我们只关心 9999 端口上的网络流量,指定 port 9999
表达式过滤出传输层端口为 9999 的流量。
客户端
准备好服务端后,我们在 ant 上执行 tcp-uppper-client 客户端程序来连接它:
1
2
|
root@ant [ ~ ] ➜ tcp-upper-client -i 10.0.2.2
>
|
-i 指定服务端的 IP 地址,即主机 apple 的 IP 地址;服务端端口号未指定,默认连接 9999 端口。
客户端程序启动后,我们可以在服务端程序中看到连接提示:
1
|
10.0.1.2:47762 connected
|
这表明,服务端收到一个从 ant 发起的新连接:对端 IP 为 10.0.1.2 ,这是 ant 的 IP ;而端口号 47762 是 ant 系统协议栈随机分配的。
三次握手
与此同时,主机 apple 上的 tcpdump 命令嗅探到了三次握手建立 TCP 连接的通信过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
18:49:48.101543 IP 10.0.1.2.47762 > 10.0.2.2.9999: Flags [S], seq 1386804287, win 29200, options [mss 1460,sackOK,TS val 4236541223 ecr 0,nop,wscale 7], length 0
0x0000: 4500 003c e6af 4000 3d06 4009 0a00 0102 E..<..@.=.@.....
0x0010: 0a00 0202 ba92 270f 52a8 f43f 0000 0000 ......'.R..?....
0x0020: a002 7210 1732 0000 0204 05b4 0402 080a ..r..2..........
0x0030: fc84 7d27 0000 0000 0103 0307 ..}'........
18:49:48.101566 IP 10.0.2.2.9999 > 10.0.1.2.47762: Flags [S.], seq 401184951, ack 1386804288, win 28960, options [mss 1460,sackOK,TS val 398188058 ecr 4236541223,nop,wscale 7], length 0
0x0000: 4500 003c 0000 4000 4006 23b9 0a00 0202 E..<..@.@.#.....
0x0010: 0a00 0102 270f ba92 17e9 98b7 52a8 f440 ....'.......R..@
0x0020: a012 7120 1732 0000 0204 05b4 0402 080a ..q..2..........
0x0030: 17bb de1a fc84 7d27 0103 0307 ......}'....
18:49:48.101622 IP 10.0.1.2.47762 > 10.0.2.2.9999: Flags [.], ack 1, win 229, options [nop,nop,TS val 4236541223 ecr 398188058], length 0
0x0000: 4500 0034 e6b0 4000 3d06 4010 0a00 0102 E..4..@.=.@.....
0x0010: 0a00 0202 ba92 270f 52a8 f440 17e9 98b8 ......'.R..@....
0x0020: 8010 00e5 172a 0000 0101 080a fc84 7d27 .....*........}'
0x0030: 17bb de1a ....
|
- 客户端先发一个 SYN 分组;
- 服务端收到 SYN 分组,回复 SYN/ACK 分组;
- 客户端回复 ACK 分组,至此连接完全建立;
连接套接字
一个 TCP 连接可以通过由双方的地址端口对唯一确定,合起来称为 TCP 连接的 四元组 。
- 自己的地址、端口,通常称为 本端( local );
- 对方的地址、端口,通常称为 对端( peer );
操作系统通常将 TCP 连接抽象成 套接字( socket )对象,并通过它来操作 TCP 连接。除了唯一确定 TCP 连接的四元组,套接字还包含接收缓冲区、发送缓冲区以及一些状态参数(通告窗口、拥塞窗口以及各种定时器)。
TCP 连接建立后,我们可以通过 ss 命令查看它的套接字,以 ant 为例:
1
2
3
|
root@ant [ ~ ] ➜ ss -nt
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 10.0.1.2:47762 10.0.2.2:9999
|
考察唯一确定这个 TCP 连接的四元组,对 ant 而言分别如下:
本端地址 |
本端端口 |
对端地址 |
对端端口 |
10.0.1.2 |
47762 |
10.0.2.2 |
9999 |
同样,我们也可以在 apple 上观察到代表这个连接的套接字,本端和对端地址端口刚好是反过来的:
1
2
3
|
root@apple [ ~ ] ➜ ss -nt
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 10.0.2.2:9999 10.0.1.2:47762
|
数据传输
客户端启动后,显示 >
输入提示符,等待用户输入数据。用户输入数据后,按下回车,数据就会被发往服务端。现在我们尝试发送一些数据:
1
2
3
|
root@ant [ ~ ] ➜ tcp-upper-client -i 10.0.2.2
> abc
ABC
|
我们输入 abc
后按下回车,客户端将我们输入的数据发到服务端;服务端将数据转化成大写后,返回给客户端;客户端进而将数据打印在屏幕上。
在这个过程中,我们可以从 tcpdump 中观察到通信过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
18:52:19.613971 IP 10.0.1.2.47762 > 10.0.2.2.9999: Flags [P.], seq 1:5, ack 1, win 229, options [nop,nop,TS val 4236692912 ecr 398188058], length 4
0x0000: 4500 0038 e6b1 4000 3d06 400b 0a00 0102 E..8..@.=.@.....
0x0010: 0a00 0202 ba92 270f 52a8 f440 17e9 98b8 ......'.R..@....
0x0020: 8018 00e5 172e 0000 0101 080a fc86 cdb0 ................
0x0030: 17bb de1a 6162 630a ....abc.
18:52:19.613988 IP 10.0.2.2.9999 > 10.0.1.2.47762: Flags [.], ack 5, win 227, options [nop,nop,TS val 398339747 ecr 4236692912], length 0
0x0000: 4500 0034 8a28 4000 4006 9998 0a00 0202 E..4.(@.@.......
0x0010: 0a00 0102 270f ba92 17e9 98b8 52a8 f444 ....'.......R..D
0x0020: 8010 00e3 172a 0000 0101 080a 17be 2ea3 .....*..........
0x0030: fc86 cdb0 ....
18:52:19.614029 IP 10.0.2.2.9999 > 10.0.1.2.47762: Flags [P.], seq 1:5, ack 5, win 227, options [nop,nop,TS val 398339747 ecr 4236692912], length 4
0x0000: 4500 0038 8a29 4000 4006 9993 0a00 0202 E..8.)@.@.......
0x0010: 0a00 0102 270f ba92 17e9 98b8 52a8 f444 ....'.......R..D
0x0020: 8018 00e3 172e 0000 0101 080a 17be 2ea3 ................
0x0030: fc86 cdb0 4142 430a ....ABC.
18:52:19.614100 IP 10.0.1.2.47762 > 10.0.2.2.9999: Flags [.], ack 5, win 229, options [nop,nop,TS val 4236692912 ecr 398339747], length 0
0x0000: 4500 0034 e6b2 4000 3d06 400e 0a00 0102 E..4..@.=.@.....
0x0010: 0a00 0202 ba92 270f 52a8 f444 17e9 98bc ......'.R..D....
0x0020: 8010 00e5 172a 0000 0101 080a fc86 cdb0 .....*..........
0x0030: 17be 2ea3 ....
|
- 第一个分组由客户端发给服务端,搭载着用户输入的 4 字节数据(回车产生换行符);
- 第二个分组为服务端回复的 ACK 确认;
- 第三个分组由服务端发给客户端,搭载着服务端转换后的数据;
- 第四个分组为客户端回复的 ACK 确认;
可以再做一些数据交互,观察 TCP 流量,体会它的通信过程:
1
2
3
4
|
> hello world!
HELLO WORLD!
> fasionchan.com
FASIONCHAN.COM
|
四次挥手
那么,怎样才能让客户端程序断开跟服务端的连接,并退出呢?
按 shell 命令一般惯例,只要还有数据输入,程序就会不断运行。换句话讲,关闭标准输入即可通知程序主动退出。
我们可以按下 ctrl-d ,关闭客户端程序的标准输入,以此告知它我们停止输入数据了。客户端程序检测到标准输入关闭后,就会停止数据处理循环,关闭 TCP 连接并退出。
客户端一关闭 TCP 连接,服务端就会输出提示:
1
|
10.0.1.2:47762 disconnected
|
这时,我们在客户端主机 ant 上可以看到 TCP 连接进入了 TIME-WAIT 状态:
1
2
3
|
root@ant [ ~ ] ➜ ss -nat
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
TIME-WAIT 0 0 10.0.1.2:47762 10.0.2.2:9999
|
还记得 TCP 连接的状态变迁图吗?主动关闭的一方会进入 TIME-WAIT 状态。
从服务端主机 apple 上的 tcpdump 命令,可以观察到 TCP 关闭连接的四次挥手过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
18:52:52.510869 IP 10.0.1.2.47762 > 10.0.2.2.9999: Flags [F.], seq 33, ack 33, win 229, options [nop,nop,TS val 4236725844 ecr 398368775], length 0
0x0000: 4500 0034 e6b7 4000 3d06 4009 0a00 0102 E..4..@.=.@.....
0x0010: 0a00 0202 ba92 270f 52a8 f460 17e9 98d8 ......'.R..`....
0x0020: 8011 00e5 172a 0000 0101 080a fc87 4e54 .....*........NT
0x0030: 17be a007 ....
18:52:52.511005 IP 10.0.2.2.9999 > 10.0.1.2.47762: Flags [F.], seq 33, ack 34, win 227, options [nop,nop,TS val 398372679 ecr 4236725844], length 0
0x0000: 4500 0034 8a2c 4000 4006 9994 0a00 0202 E..4.,@.@.......
0x0010: 0a00 0102 270f ba92 17e9 98d8 52a8 f461 ....'.......R..a
0x0020: 8011 00e3 172a 0000 0101 080a 17be af47 .....*.........G
0x0030: fc87 4e54 ..NT
18:52:52.511074 IP 10.0.1.2.47762 > 10.0.2.2.9999: Flags [.], ack 34, win 229, options [nop,nop,TS val 4236725844 ecr 398372679], length 0
0x0000: 4500 0034 e6b8 4000 3d06 4008 0a00 0102 E..4..@.=.@.....
0x0010: 0a00 0202 ba92 270f 52a8 f461 17e9 98d9 ......'.R..a....
0x0020: 8010 00e5 172a 0000 0101 080a fc87 4e54 .....*........NT
0x0030: 17be af47 ...G
|
- 客户端发 FIN 包告诉服务端数据已发完,连接关闭;
- 服务端发 ACK 包确认连接关闭,服务端同时也发 FIN 包关闭连接,FIN 和 ACK 被合在一个分组传输;
- 客户端发 ACK 包确认连接关闭;
【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: