一、前言
- 本文通过在 Docker 容器中执行命令,来深入了解两台主机之间的通信过程。阅读完本文,您将熟悉以下内容:
-
-
-
-
- 本文也是【网络通信与信息安全】之深入解析从输入一个URL到页面加载完成的过程 的另一个角度的回答,将解决以下两个问题:
-
-
二、准备 Docker 环境
① 下载镜像
- 关于 Docker 的基础概念 (容器、镜像等),可以参考:Docker 入门教程。
- 在启动 Docker 容器之前,需要先下载一个 Docker 镜像,这里使用 Ubuntu 系统镜像:
# docker pull <image>
docker pull ubuntu
② 首次启动
# docker run -it --name <container> <image>
docker run -it --name ubuntu ubuntu
- 参数说明:
-
- -i:让容器的标准输入保持打开,从而能够接受主机输入的命令;
-
- -t:为容器分配一个伪终端并绑定到容器的标准输入上,-i 和 -t 结合,可以在终端中和容器进行交互;
-
- –name:为容器起一个名字,方便后续操作该容器,否则每次都需要查找容器的 ContainerID。
- 需要注意的是,每次 run 都会重新创建一个新的容器。
在 Docker 的各个命令中,<container_id> 和 <container_name>、<image_id> 和 <image_name> 可以互换,本文统一使用 <container> 和 <image> 来指代这些参数。
③ 配置环境
apt-get update
apt-get install net-tools tcpdump iputils-ping
④ 提交镜像
- 如果不小心删除了容器,容器内的所有更改也将丢失,因此使用 commit 命令来保存容器中的更改:
# docker commit -m <message> --author <author_info> <container> [<repo>[:<tag>]]
docker commit -m "Install packages" --author "elonz" ubuntu ubuntu:latest
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest a5d22784e35b About a minute ago 108MB
# <image> 可以是上面的 REPOSITORY(image_name) 或 IMAGE ID
docker image rm <image>
⑤ 删除容器
# docker rm <container>
docker rm ubuntu
docker container ls --all
# docker run -it --name <container> <image>
docker run -it --name ubuntu ubuntu
⑥ 退出容器
exit
⑦ 再次启动容器
- 如果容器未启动 (Exited),执行 start 命令:
# docker start -i <container>
docker start -i ubuntu
docker exec -it <container> /bin/bash
- 容器是否启动,可以通过 docker container ls --all 查看。
三、应用层
- 当通过诸如 http.Get(“http://www.baidu.com/”) 这样的 API 向服务器发送请求时,其底层实现无非以下几个过程:
-
-
- 通过操作系统提供的系统调用创建一个 socket 连接,这实际上是完成了 TCP 的三次握手过程;
-
- 通过 socket 连接以文本形式向服务端发送请求,在代码层面实际上是在向一个 socket 文件描述符写入数据,写入的数据就是一个 HTTP 请求。
- 可以直接在终端实现这个过程,只需要以下三行命令:
exec 3<> /dev/tcp/www.baidu.com/80
printf "GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n" 1>& 3
cat <& 3
① 建立连接
cd /dev/fd && ll
total 0
dr-x------ 2 root root 0 March 18 13:06 ./
dr-xr-xr-x 9 root root 0 March 18 13:06 ../
lrwx------ 1 root root 64 March 18 13:06 0 -> /dev/pts/0
lrwx------ 1 root root 64 March 18 13:06 1 -> /dev/pts/0
lrwx------ 1 root root 64 March 18 13:06 2 -> /dev/pts/0
lrwx------ 1 root root 64 March 18 13:06 255 -> /dev/pts/0
- 系统当前只有 /bin/bash 这一个进程,上面列出了该进程的 0、1、2、255 四个文件描述符:
-
- 文件描述符(file descriptor)是一个非负整数,从 0 开始,进程使用文件描述符来标识一个打开的文件;
-
- 系统为每一个进程维护了一个文件描述符表,表示该进程打开文件的记录表,而文件描述符实际上就是这张表的索引;当进程打开(open)或者新建(create)文件时,内核会在该进程的文件列表中新增一个表项,同时返回一个文件描述符,也就是新增表项的下标;
-
- 一般来说,每个进程最多可以打开 64 个文件,fd ∈ 0~63;在不同系统上,最多允许打开的文件个数不同,Linux 2.4.22 强制规定最多不能超过 1,048,576;
-
- 每个进程默认都有 3 个文件描述符:0 (stdin)、1 (stdout)、2 (stderr)。
- 执行以下命令,建立一个连接:
exec 3<> /dev/tcp/www.baidu.com/80
- 该命令创建了一个指向 tcp://www.baidu.com:80 的可读写的 socket,绑定到当前进程的 3 号文件描述符:
-
- exec {fd}< file:以只读的方式打开文件,并绑定到当前进程的 fd 号描述符;相应的,{fd}> 是以只写的方式打开文件;
-
- 打开 /dev/tcp/ h o s t / host/ host/port 文件实际上是建立连接并返回一个 socket,Linux 中一切皆文件,所以可以对这个 socket 读写。
- 执行以下命令,可以看到已经和 www.baidu.com 成功建立了 socket 连接:
cd /dev/fd && ll # 或者:ll /proc/$$/fd
total 0
dr-x------ 2 root root 0 March 18 13:06 ./
dr-xr-xr-x 9 root root 0 March 18 13:06 ../
lrwx------ 1 root root 64 March 18 13:08 0 -> /dev/pts/0
lrwx------ 1 root root 64 March 18 13:08 1 -> /dev/pts/0
lrwx------ 1 root root 64 March 18 13:08 2 -> /dev/pts/0
lrwx------ 1 root root 64 March 18 13:11 255 -> /dev/pts/0
lrwx------ 1 root root 64 March 18 17:25 3 -> 'socket:[54134]' # 绑定在 3 号描述符
② 发送请求
- 向 www.baidu.com 发送一个 GET 请求,只需要向 3 号文件描述符写入请求报文 (格式):
printf "GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n" 1>& 3
- 说明:> 3:重定向到名为 3 的文件;>& 3:重定向到 3 号文件描述符。
③ 读取响应
cat <& 3
<!DOCTYPE html>
<!--STATUS OK--><html>
...
</html>
④ 关闭连接
# 关闭输入连接:exec {fd}<&-;关闭输出连接:exec {fd}>& -
exec 3<&- && exec 3>&-
- 这样就在 bash 中实现了 http.Get(“http://www.baidu.com/”)。
四、传输层
- 客户端使用 socket(), connect() 等系统调用来和远程主机进行通信。在底层,socket() 负责分配资源,connect() 实现了 TCP 的三次握手过程。
- Socket 通过 <源 IP、源 Port、目的 IP、目的 Port> 的四元组来区分 (实际上还有协议,TCP 或 UDP),只要有一处不同,就是不同的 socket。因此,尽管 TCP 支持的端口号最多为 65535 个,但是每台机器理论上可以建立无数个 socket 连接。比如 HTTP 服务器只消耗一个 80 端口号,但可以和不同 IP:Port 的客户端建立连接,实际受限于操作系统的内存大小。
- 使用 netstat 命令可以查看当前系统中的所有 socket:
exec 3<> /dev/tcp/www.baidu.com/80 # 在容器中手动创建一个 socket
exec 4<> /dev/tcp/www.bing.com/80 # 同上,只是为了演示
netstat -natp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 192.168.1.2:36384 110.242.68.4:80 ESTABLISHED 1/bash
tcp 0 0 192.168.1.2:44960 202.89.233.100:80 ESTABLISHED 1/bash
TCP 连接状态 |
含义 |
SYN_SENT |
发送了连接请求,等待远端的确认(三次握手第 1 步的结果) |
ESTABLISHED |
socket 已经建立了连接 |
TCP 连接状态 |
含义 |
LISTEN |
监听来自远程应用的 TCP 连接请求 |
SYN_RECEIVED |
收到了连接请求并发送了确认报文,等待最终的确认(三次握手第 2 步的结果) |
ESTABLISHED |
socket 已经建立了连接,这是数据传输阶段的状态 |
TCP 连接状态 |
含义 |
FIN_WAIT1 |
发送了连接终止请求,等待确认,通常持续时间较短 |
FIN_WAIT2 |
发送了连接终止请求并收到了远程的确认,等待远程 TCP 的连接终止请求,这个状态表明远程在收到此 socket 的连接终止请求后,没有立刻关闭它的 socket |
CLOSING |
发送了连接终止请求后,收到了远程的连接终止请求,正在等待远程对连接终止请求的确认,这个状态表明双方同时进入关闭状态 |
TIME_WAIT |
等待足够的时间,以确保远程 TCP 收到其连接终止请求的确认 |
CLOSED |
socket 已经没有连接状态 |
TCP 连接状态 |
含义 |
CLOSE_WAIT |
收到了远程的连接终止请求,正在等待本地的应用程序发出连接终止请求 |
LAST_ACK |
等待先前发送的连接终止请求的确认,只有在发送连接终止请求前先收到了远程的连接终止请求时,才会进入此状态 |
CLOSED |
socket 已经没有连接状态 |
五、网络层
- 网络层的功能是路由与寻址,数据包在网络层是一跳一跳地传输的,从源节点到下一个节点,直到目的节点,形成一个链表式的结构。
- 当数据包到达网络中的一个节点时,该节点会检查数据包的目的 IP 地址,如果不是自己的 IP 地址,就根据路由表决定将数据包发送给哪个网关。
① 路由表
- 电脑、手机、路由集等都可以视为网络层的一个节点 (或一台主机),每个节点都有一个路由表。网络层的节点通过路由表来选择下一跳地址 (next hop address)。
- Ubuntu 系统可以通过以下命令查看路由表:
route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
- 路由表由一组规则组成,每条规则包含以下字段:
-
- Destination:目的地址,可以是主机地址或网络地址,常见的是网络地址;
-
-
-
-
- 当 Destination 为 0.0.0.0 时,其 Gateway 为当前局域网的路由器 / 网关的 IP 地址。当 Gateway 为 0.0.0.0 时,表示目的机器和当前主机位于同一个局域网内,它们互相连接,任何数据包都不需要路由,可以直接通过 MAC 地址发送到目的机器上。
- 通过 Destination 和 Genmask 可以计算出目的地址集。例如对于下面的表项,其含义为“所有目的 IP 地址在 192.168.1.0 ~ 192.168.1.255 范围内的数据包,都应该发给 0.0.0.0 网关“。
Destination Gateway Genmask Flags Metric Ref Use Iface
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
- 当 Genmask 为 255.255.255.255 时,代表这条规则的 Destination 不再是一个网络地址,而是一个网络中的一台特定主机,这样的规则可能对应一条点对点信道 (Point to Point Tunnel)。
- 路由表中的规则可以手动指定,也可以通过路由协议来交换周围网络的拓扑信息、动态更新。
② 路由决策过程
- 当一个节点收到一个数据包时,会根据路由表来找到下一跳的地址。具体而言,系统会遍历路由表的每个表项,然后将目的 IP 和子网掩码 Genmask 作二进制与运算,得到网络地址,再判断这个地址和表项的 Destination 是否匹配:
-
- 如果只有一个匹配项,直接将数据包发送给该表项的网关 Gateway;
-
- 如果有多个匹配项,则选择子网掩码最长的那个规则,然后发送给对应的网关;
-
- 上面最后一种情况实际上不会出现,因为路由表中包含了下面这条规则:
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0
- 任何目的 IP 和全 0 的 Genmask 作与运算,一定会得到全 0 的 Destination,这保证所有未知的目的 IP 都会发送给当前局域网的默认网关 (比如路由器),由后者决定下一跳的地址。
③ 案例分析
- ping www.baidu.com 的路由决策过程;
- ping 局域网的另一台主机 的路由决策过程;
- 对于以下的路由表,路由器会如何转发目标 IP 为 128.75.43.16 和 192.12.17.10 的数据包?
Destination Gateway Genmask
128.75.43.0 A 255.255.255.0
128.75.43.0 B 255.255.255.128
192.12.17.5 C 255.255.255.255
default D 0.0.0.0
- 结果分析:
-
- 目标 IP 位于外部网络,默认会发给本局域网的路由器,路由器连接外部网络,知道该如何转发数据包,例如交给更高一级的运营商网关;
-
- 同局域网的主机交换数据不需要网关或路由器,直接发给交换机,交换机根据 Mac 地址发送到对应的主机;
-
- A and D,128.75.43.16 匹配了前两条规则,相应的 Destination 均为 128.75.43.0,数据包会发送给具有最长子网掩码的网关,192.12.17.10 没有匹配任何 Destination,数据包发送给默认网关。
六、数据链路层
① ARP 协议
- 当网络层选择一个特定 IP 的主机作为下一跳时,如何将数据包正确的发送给该主机?这里需要在数据包外面加上下一跳的硬件地址 (MAC),在数据包的整个传输过程中,目的 MAC 地址每一跳都在变,但目的 IP 地址不变。
- 如何根据 IP 地址找到相对应的 MAC 地址?这需要使用 ARP 协议 (Address Resolution Protocol,地址解析协议)。每台主机都设有一个 ARP 高速缓存表,记录本局域网内各主机的 IP 地址到 MAC 地址的映射。
- 执行以下命令可以查看主机的 ARP 缓存表:
arp -a
localhost (192.168.1.1) at bc:5f:f6:df:d8:19 on en0 ifscope [ethernet]
localhost (192.168.1.102) at 14:7d:da:32:8d:17 on en0 ifscope [ethernet]
- ARP 高速缓存是自动更新的,当主机 A 向本局域网的主机 B 发送数据包时,如果 ARP 高速缓存中没有主机 B 的硬件地址,就会自动运行 ARP 协议,找出 B 的硬件地址,并更新高速缓存,过程如下:
-
- 主机 A 在局域网内广播一个 ARP 请求分组,内容为:“我的 IP 是 IP_A,硬件地址是 MAC_A,我想知道 IP 地址为 IP_B 的主机的硬件地址“;
-
- 主机 B 接受到此请求分组后,如果要查询的 IP 地址和自己的 IP 地址一致,就将主机 A 的 IP 地址和 MAC 地址的对应关系记录到自己的 ARP 缓存表中,同时会发送一个 ARP 应答 (单播),内容为:“我的 IP 地址是 IP_B,硬件地址是 MAC_B”;
-
- 其他主机的 IP 地址和要查询的 IP 地址不一致,因此都丢弃此请求分组;
-
- 主机 A 收到 B 的 ARP 响应分组后,同样将主机 B 的 IP 地址和 MAC 地址的对应关系记录到自己的 ARP 缓存表中。
- ARP 协议只用于局域网中,不同局域网之间通过 IP 协议进行通信。
② ARP 协议抓包
- 首先,需要另一个终端来监听容器内的网络请求,假设已经通过 docker start 启动了一个容器,使用 docker exec 命令来创建一个新的终端会话:
docker exec -it ubuntu /bin/bash # 宿主机执行
- 为了便于表示,将一开始 docker start 创建的容器终端记为 A,docker exec 创建的记为 B,在终端 B 内执行以下命令,监听网络请求:
tcpdump -nn -i eth0 port 80 or arp
- 随后,在终端 A 中执行以下命令,触发 ARP 协议更新:
# arp -d <ip> && ping www.baidu.com
arp -d 192.168.1.1 && ping www.baidu.com
- 其中,arp -d 命令可以删除一条 ARP 映射记录,这里需要将 替换为容器的网关 IP 地址,有许多方法可以获取容器的网关地址:
-
- [容器内] 执行 route -n 命令,查看 Destination 为 0.0.0.0 时对应的 Gateway;
-
- [宿主机] 执行 docker network inspect bridge,查看 IPAM - Config - Gateway。
- 在终端 A 中有以下输出,表示 ping 命令执行成功:
PING www.a.shifen.com (110.242.68.3) 56(84) bytes of data.
64 bytes from 110.242.68.3 (110.242.68.3): icmp_seq=1 ttl=37 time=19.7 ms
64 bytes from 110.242.68.3 (110.242.68.3): icmp_seq=2 ttl=37 time=22.7 ms
64 bytes from 110.242.68.3 (110.242.68.3): icmp_seq=3 ttl=37 time=21.8 ms
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
17:04:43.847963 ARP, Request who-has 192.168.1.1 tell 192.168.1.2, length 28
17:04:43.848058 ARP, Reply 192.168.1.1 is-at 02:43:21:c4:75:58, length 28
17:04:53.009573 ARP, Request who-has 192.168.1.2 tell 192.168.1.1, length 28
17:04:53.009746 ARP, Reply 192.168.1.2 is-at 02:43:ac:11:00:02, length 28
- 可以看到,这里有两次 ARP 请求与应答,第一次是容器 (192.168.1.2) 广播查询网关 (192.168.1.1) 的 MAC 地址,第二次是网关 (192.168.1.1) 广播查询容器 (192.168.1.2) 的 MAC 地址。
七、两台主机的通信过程
① 不同局域网的两台主机
- 以主机 ping 一个域名为例,过程如下:
-
- [主机] [应用层] 通过 DNS 协议获取域名的 IP 地址;
-
- [主机] [网络层] 构造 IP 数据包,源 IP 为本机 IP,目的 IP 为域名 IP;
-
- [主机] [网络层] 根据路由表,选择下一跳的 IP 地址,即当前局域网的网关;
-
- [主机] [链路层] 根据 ARP 表,查找网关 IP 的 MAC 地址;在 IP 数据包外面包一层 MAC 地址;
-
- [局域网] 根据 MAC 地址,上一步的报文最终会发送到当前局域网的网关;
-
- [网关] [网络层] 网关查看数据包的目的 IP 地址,重复上述 2~3 步,继续发给下一跳;
-
- [互联网] 中间经过若干个下一跳主机,最终数据包发送到域名所在的网络中心的网关;
-
- [网关] [网络层] 网络中心的网关查看数据包的目的 IP 地址,根据路由表发现目的 IP 对应的 Gateway 为 0.0.0.0,这表明目的机器和自己位于同一个局域网内;
-
- [网关] [链路层] 根据 ARP 表,查找目的 IP 的 MAC 地址,构造链路层报文;
-
- [局域网] 根据 MAC 地址,上一步的报文最终会发送到目的主机;
-
- [目的主机] [网络层] 目的主机查看数据包中的目的 IP,发现是给自己的,解析其内容,过程结束。
② 同局域网内的两台主机
- 两台主机通过网线、网桥或者交换机连接,就构成了一个局域网。网桥或交换机的作用是连接多台主机,隔离不同的端口,每个端口形成单独的冲突域。当主机连接到网桥或交换机端口的时候,这些设备会要求主机上报 MAC 地址,并在设备内保存 MAC 地址与端口的对应关系。
- 同局域网内的两台主机进行通信时,只需要根据 ARP 协议获取目的主机的 MAC 地址,构造链路层报文。报文会经过网桥或交换机,后两者根据目的 MAC 地址,在 MAC 地址表里查询目的端口,然后将报文从目的端口转发给对应的主机。
- 注意:
-
- 交换机是链路层的设备,主要根据 MAC 地址进行转发、隔离冲突域;不具有路由功能,不记录路由表,这类设备也称为二层交换机,如果只使用二层交换机、不使用路由器来构建局域网,需要为交换机和每台主机分配同属于一个子网的静态 IP。
-
- 路由器工作在 OSI 的第三层网络层,记录路由表,并以此控制数据传输过程。
-
- 有些交换机也具有路由功能,记录了路由表,能够简化路由过程、实现高速转发,这类设备也称为三层交换机。
③ 同局域网内的两台主机,目的主机有多个 IP
- 问题:如果目的主机 B 为自己新增了一个 IP,同局域网的主机 A ping 主机 B 的这个 IP 能 ping 通吗?答案是不能,原因是主机 A ping 主机 B 时,根据路由表会将报文发给默认网关,但是网关的路由表里并没有主机 B 新增加的 IP 信息。
- 可以做实验验证一下:分别启动两个容器 A、B(参数 --cap-add NET_ADMIN:打开网络配置权限):
docker run -it --name ubuntu --cap-add NET_ADMIN ubuntu # 容器 A
docker run -it --name ubuntu_2 --cap-add NET_ADMIN ubuntu # 容器 B
- 在容器 B 内执行以下命令,新增一个 IP 地址 192.168.1.55:
ifconfig lo:3 192.168.1.55/16
- 查看容器 B 的 IP 配置,可以看到新增了一个 lo:3 接口:
ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 # 这里是默认 IP
inet 192.168.1.2 netmask 255.255.0.0 broadcast 192.168.255.255
ether 02:42:ac:11:00:03 txqueuelen 0 (Ethernet)
RX packets 9 bytes 726 (726.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo:3: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 # 这里是新增的 IP
inet 192.168.1.55 netmask 255.255.0.0
loop txqueuelen 1000 (Local Loopback)
- 在容器 A 内尝试 ping 192.168.1.55,发现无法 ping 通,解决办法是修改容器 A 的路由表,执行以下命令,手动新增一条规则:
# route add -host <destination> gw <gateway>
route add -host 192.168.1.55 gw 192.168.1.2
- 其中,destination 参数是容器 B 新增的 IP 地址;gateway 参数是容器 B 的默认 IP 地址,也就是上面 ifconfig 命令输出的 eth0 接口的 IP 地址,这条规则的含义是“所有目的 IP 是 192.168.1.55 的数据包都发给 192.168.1.2”。
- 容器 A 内执行 route -n,查看路由表:
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0
192.168.1.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
192.168.1.55 192.168.1.2 255.255.255.255 UGH 0 0 0 eth0
- 这个时候再从容器 A 中 ping 容器 B 的新 IP,就可以 ping 通:
ping 192.168.1.55
PING 192.168.1.55 (192.168.1.55) 56(84) bytes of data.
64 bytes from 192.168.1.55: icmp_seq=1 ttl=64 time=0.589 ms
64 bytes from 192.168.1.55: icmp_seq=2 ttl=64 time=0.291 ms
64 bytes from 192.168.1.55: icmp_seq=3 ttl=64 time=0.669 ms
...
- 如果在执行 ping 命令前,先在容器 B 执行 tcpdump,会看到如下输出:
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
08:36:28.819409 ARP, Request who-has 192.168.1.1 tell 192.168.1.3, length 28
08:36:38.987179 ARP, Request who-has 81b3a9d0f060 tell 192.168.1.3, length 28
08:36:38.987296 ARP, Reply 81b3a9d0f060 is-at 02:43:ac:11:00:03 (oui Unknown), length 28
08:36:38.987537 IP 192.168.1.3 > 192.168.1.55: ICMP echo request, id 17, seq 1, length 64
08:36:38.987585 IP 192.168.1.55 > 192.168.1.3: ICMP echo reply, id 17, seq 1, length 64
08:36:40.019291 IP 192.168.1.3 > 192.168.1.55: ICMP echo request, id 17, seq 2, length 64
08:36:40.019410 IP 192.168.1.55 > 192.168.1.3: ICMP echo reply, id 17, seq 2, length 64
...
- 其中,192.168.1.1 是网关 IP,192.168.1.3 是容器 A 的 IP,81b3a9d0f060 是容器 B 的 ContainerID。上面的输出依次表示:容器 A 通过 ARP 协议查询网关的 MAC 地址;容器 A 通过 ARP 协议查询容器 B 的 MAC 地址;容器 B 发出 ARP 应答;容器 A 发送 ICMP 请求、容器 B 应答。
七、总结
- Docker:
-
- 在 Docker 的各个命令中, 和 、 和 可以互换;
-
- docker run 会重新创建一个新的容器,docker start 可以进入已经启动的容器。
- Socket:
-
- 每个进程默认都有 0、1、2、255 四个文件描述符;
-
- 系统用 socket 来表示一个连接,socket 会绑定到进程的一个文件描述符,可以使用 open、write 系统调用来向远程主机发送请求、读取响应;
-
- Socket 通过 <源 IP、源 Port、目的 IP、目的 Port> 的四元组来区分,只要有一处不同,就是不同的 socket;
-
- netstat -natp:查看当前系统中的所有 socket。
- 路由表:
-
-
- Destination 为 0.0.0.0 时,Gateway 为默认网关;
-
- Gateway 为 0.0.0.0 时,Destination 为当前局域网的网络地址;
-
- Genmask 为 255.255.255.255 时,Destination 为一个网络中的一台特定主机。
- ARP 表:
-
-
- 通信过程:
-
- 不同局域网的主机,数据包会在网络层经过若干个下一跳 (当前局域网的默认网关 - 运营商网关 - 目的主机所在的数据中心网关),最终发送给目的主机所在的局域网:
-
- 同局域网内的主机,只需要通过 ARP 协议获取目的主机的 MAC 地址,报文在数据链路层经由网桥或交换机转发给目的主机;
-
- 交换机不具有路由功能,属于二层设备;有些交换机为了提升效率而记录路由表,属于三层设备。