接着系列前几篇的 FROM scratch 套路,这次写一对真实的 TCP 服务端/客户端小程序,在默认 bridge 网络和自己建的 bridge 网络下分别测试容器间怎么通信——本机真实复现了一个几乎所有人都踩过的坑:同样的代码,换个网络,行为完全不同,而且原因直接写在容器自己的 /etc/resolv.conf 里,不用猜。
写一对真实的 TCP 程序——服务端监听 :9000 收到一行就回一句话,客户端连上去发一行、打印回复。
$ docker run -d --name srv1 netserver:scratch
$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' srv1
172.17.0.2
$ docker run --rm netclient:scratch 172.17.0.2 # 用 IP,成功 $ docker run --rm netclient:scratch srv1 # 用容器名,失败got reply: hello from 82c692f88326, you said: hi from client dial error: dial tcp: lookup srv1 on 192.168.65.7:53: no such host
同一个 srv1,用真实分配到的 IP 172.17.0.2 连接完全没问题,换成容器名就直接解析失败——这不是这套代码写错了,是默认 bridge 网络本身的设计:它不提供容器名到 IP 的自动解析。
同样的镜像、同样的代码,唯一的变量是网络。
$ docker network create mynet
$ docker network inspect mynet --format '{{json .IPAM.Config}}'
[{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]
$ docker run -d --name srv2 --network mynet netserver:scratch $ docker run --rm --network mynet netclient:scratch srv2 # 直接用容器名got reply: hello from c52e2400efe5, you said: hi from client
自定义网络先拿到了一个完全独立的子网(172.18.0.0/16,跟默认 bridge 的 172.17.0.0/16 是两个不相交的段),而且这次直接用容器名 srv2 就真实连通了——同一套代码,换个网络就从"必须硬编码 IP"变成"可以用名字互相找到对方"。
直接对比两种网络下容器自己看到的 /etc/resolv.conf。
$ docker run --rm resolvcheck:scratch # 默认 bridgenameserver 192.168.65.7 # Based on host file: '/etc/resolv.conf' (legacy)
$ docker network create mynet2 $ docker run --rm --network mynet2 resolvcheck:scratch # 自定义网络nameserver 127.0.0.11 options ndots:0 # Based on host file: '/etc/resolv.conf' (internal resolver) # ExtServers: [host(192.168.65.7)]
默认 bridge 网络下,容器的 DNS 直接指向宿主机的真实 DNS(192.168.65.7),查询 srv1 这种名字自然只会当成一个不存在的域名去查外部 DNS,查不到;自定义网络下,nameserver 变成了 127.0.0.11——Docker 在每个自定义网络里都跑了一个内嵌 DNS 服务器,认识这个网络里所有容器的名字,查不到的名字才会转发给真实的外部 DNS(ExtServers 那一行就是转发目标)。这也是 Docker 官方文档里明确写的行为:
Docker 官方文档 · Networking overview · docs.docker.com/engine/network With the default configuration, containers attached to the default bridge network have unrestricted network access to each other using container IP addresses. They cannot refer to each other by name. Once connected to a user-defined network, containers can communicate with each other using container IP addresses or container names.
前面几组实验都是容器之间互相访问,这次反过来——从运行 docker 命令的这台 Mac,直接连容器内部的服务。
$ docker run -d --name srv3 -p 19000:9000 netserver:scratch $ printf 'hi from host\n' | nc 127.0.0.1 19000 $ docker port srv3hello from 10d1a7fe75cc, you said: hi from host 9000/tcp -> 0.0.0.0:19000 9000/tcp -> [::]:19000
容器内部这个进程自己完全不知道"19000"这个端口的存在——它只认识自己监听的 :9000。-p 19000:9000 在宿主机这一侧开了个监听,把发到 19000 的流量转发进容器网络命名空间里的 9000 端口,对容器里的程序完全透明。宿主机这边发起的 nc 请求,真实穿过这层转发拿到了容器里程序的真实回复。
对应上面的真实实验,切换"默认 bridge"和"自定义网络"两种场景,看同一次"用容器名连接"走到哪一步。
| 步骤 | 结果 |
|---|
数据直接取自上面真实实测的 resolv.conf 差异和真实的连接结果(默认 bridge 下 no such host,自定义网络下真实拿到回复),演示动画只是把"查 DNS → 有没有内嵌解析器 → 连不连得上"这条链路逐步呈现。