网络实验:观察TCP协议的通信过程

本节我们通过一个玩具 TCP 服务应用,来观察 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 窗口切换操作:

  1. 按下 tmux 功能键 Ctrl-B
  2. 再按下 Q ,这时每个窗口会出现一个数字;
  3. 按下窗口上的数字,即可切到对应的窗口;

服务端

我们在主机 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 地址,即主机 appleIP 地址;服务端端口号未指定,默认连接 9999 端口。

客户端程序启动后,我们可以在服务端程序中看到连接提示:

1
10.0.1.2:47762 connected

这表明,服务端收到一个从 ant 发起的新连接:对端 IP10.0.1.2 ,这是 antIP ;而端口号 47762ant 系统协议栈随机分配的。

三次握手

与此同时,主机 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                                ....

  1. 客户端先发一个 SYN 分组;
  2. 服务端收到 SYN 分组,回复 SYN/ACK 分组;
  3. 客户端回复 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                                ....

  1. 第一个分组由客户端发给服务端,搭载着用户输入的 4 字节数据(回车产生换行符);
  2. 第二个分组为服务端回复的 ACK 确认;
  3. 第三个分组由服务端发给客户端,搭载着服务端转换后的数据;
  4. 第四个分组为客户端回复的 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

  1. 客户端发 FIN 包告诉服务端数据已发完,连接关闭;
  2. 服务端发 ACK 包确认连接关闭,服务端同时也发 FIN 包关闭连接,FINACK 被合在一个分组传输;
  3. 客户端发 ACK 包确认连接关闭;

小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: