【计算基础|网络14】网络层:IP、ICMP 与 ping
你能 ping 通 www.baidu.com,但你说得出这个"通"字背后发生了什么吗?数据包为什么知道下一跳往哪走?TTL 是干嘛的?为什么断网了 ping 127.0.0.1 照样通?为什么面试爱问"ICMP 封装在哪个协议里"?
这篇把网络层的主干一次讲透:IP 负责寻址和路由,ICMP 负责报错和探测,ping/traceroute 是它们的具体应用。不讲虚的,每个字段对着真实报文讲,每个原理都有本机实测输出。
环境:macOS(BSD 用户态),
ping、traceroute、netstat -rn、ifconfig均为系统自带;Python 3.10+ 标准库ipaddress、socket。Linux 对应命令为ip addr、ip route、route -n。
一、IP 协议:只负责"把信送到",不保证送到
1.1 IP 的三个性格
结论先给:IP 是不可靠、无连接、尽最大努力交付的协议。
| 性格 | 含义 | 推论 |
|---|---|---|
| 无连接 | 收发双方不先建立连接,每个数据报独立路由 | 后发的包可能比先发的先到 |
| 不可靠 | 不确认、不重传、不保证顺序 | 丢了就是丢了,可靠性靠上层(TCP)补 |
| 尽最大努力 | 路由器不随便丢包,但拥堵/出错就丢 | 丢包是常态,不是 bug |
说白了,IP 就像普通邮递:信封上写着收发件人地址(IP 首部),邮局尽力投递,丢了不赔,包裹顺序不管。保证可靠、按序、不丢,是 TCP 的活,不是 IP 的活------这也是为什么 UDP 直接跑在 IP 上就是不可靠的。
IP 层干两件事:寻址 (靠 IP 地址标识每台主机)和路由(决定每个包下一跳交给谁)。
1.2 IPv4 首部逐字段
一个 IPv4 数据报 = 首部 + 数据(payload)。普通首部 20 字节,最长 60 字节(带选项)。逐字段拆解:
| 字段 | 长度 | 作用 | 你需要知道的 |
|---|---|---|---|
| 版本 Version | 4 bit | 协议版本 | IPv4 = 4,IPv6 = 6 |
| 首部长度 IHL | 4 bit | 首部长度,单位 4 字节 | 普通=5(20字节),最大=15(60字节) |
| 服务类型 TOS/DSCP | 8 bit | QoS 标记,区分服务 | 现代主要用 DSCP 做流量分级,开发很少碰 |
| 总长度 Total Length | 16 bit | 整个 IP 数据报长度(首部+数据) | 最大 65535 字节 |
| 标识 Identification | 16 bit | 同一个数据报的分片共用同一标识 | 重组时靠它认亲 |
| 标志 Flags | 3 bit | DF / MF | DF=1 禁止分片;MF=1 后面还有分片 |
| 片偏移 Fragment Offset | 13 bit | 分片在原数据报中的位置,单位 8 字节 | 所以分片长度必须是 8 的倍数 |
| 生存时间 TTL | 8 bit | 每过一个路由器减 1,到 0 丢弃 | 防止包在环路里无限打转;traceroute 靠它 |
| 协议 Protocol | 8 bit | 上层协议号 | ICMP=1,TCP=6,UDP=17 |
| 首部校验和 Header Checksum | 16 bit | 只校验首部 | 每跳重算(因为 TTL 变了) |
| 源 IP 地址 | 32 bit | 发送方地址 | |
| 目的 IP 地址 | 32 bit | 接收方地址 | |
| 选项 Options | 可变 | 时间戳、记录路由等 | 很少用,安全上还常被过滤 |
🔴 必须背下来的三个协议号:ICMP=1、TCP=6、UDP=17。这是 IP 把 payload 交给谁的"投递标签"------网卡驱动收到 IP 包后,看这一字段决定交给 ICMP 模块、TCP 模块还是 UDP 模块。
1.3 IPv4 地址与特殊地址
IPv4 地址 32 位,点分十进制表示(如 192.168.1.130),由网络号 + 主机号两部分组成。开发中你会反复遇到下面这张特殊地址表:
| 地址段 | 名称 | 用途 |
|---|---|---|
0.0.0.0 |
本网络/任意地址 | 服务端绑定它表示监听本机所有网卡 |
127.0.0.0/8 |
环回地址 | 本机协议栈内部回环,不经过网卡 ,常见 127.0.0.1 |
10.0.0.0/8 |
私有地址 | 内网,不可在公网路由 |
172.16.0.0/12 |
私有地址 | 内网 |
192.168.0.0/16 |
私有地址 | 家里路由器最常见 |
169.254.0.0/16 |
链路本地 | DHCP 失败时自动分配,插网线却没网时常见 |
224.0.0.0/4 |
组播 | 一对多,如 mDNS、视频直播 |
255.255.255.255 |
广播 | 局域网内所有人收 |
私有地址三段(10/8、172.16/12、192.168/16)合称 RFC 1918 地址,需要 NAT 才能上公网------这也是为什么你家手机电脑看到的都是 192.168.x.x,而不是公网 IP。
二、子网划分与 CIDR:把大网切成小网
2.1 为什么要划分子网
一个大 /24(254 台主机)全在一个广播域里,问题有三:广播包到处飞、IP 浪费、安全和管理难隔离。划分子网就是把主机位借几位当网络位,把一个大网切成多个小网。
2.2 子网掩码与 CIDR 记法
- 子网掩码 :网络位全 1、主机位全 0。
/24=255.255.255.0。 - CIDR(无类别域间路由) :直接写
IP/前缀长度,如192.168.1.0/24,比以前按类(A/B/C 类)灵活得多。
2.3 手工计算:给定 IP 和掩码,求四件套
口诀:IP 和掩码做按位与,得到网络地址;主机位全 1,得到广播地址;中间是可用主机;可用主机数 = 2^(主机位数) - 2(减网络地址和广播地址)。
拿题目要求的例子手算 192.168.1.130/26:
/26= 掩码255.255.255.192,即前 26 位是网络位,后 6 位是主机位。- 第四段
130=1000 0010,与192(1100 0000)按位与 =1000 0000=128→ 网络地址192.168.1.128。 - 主机位(后 6 位)全 1:第四段变成
1011 1111=191→ 广播地址192.168.1.191。 - 可用主机:
192.168.1.129~192.168.1.190。 - 主机数:2^6 - 2 = 62 台。
2.4 用 Python 验证(别手算了,用库)
工程上别手算,标准库 ipaddress 直接出答案。我本机实跑:
python
import ipaddress
net = ipaddress.ip_network("192.168.1.130/26", strict=False)
print("网络地址:", net.network_address)
print("广播地址:", net.broadcast_address)
print("掩码:", net.netmask, " 前缀长度:", net.prefixlen)
hosts = list(net.hosts())
print("可用主机范围:", hosts[0], "-", hosts[-1])
print("可用主机数:", net.num_addresses - 2)
n24 = ipaddress.ip_network("10.0.0.0/24")
print("10.0.0.0/24 ->", "网络:", n24.network_address, "广播:", n24.broadcast_address, "可用主机数:", n24.num_addresses - 2)
print("192.168.1.200 in 192.168.1.128/26 ?", ipaddress.ip_address("192.168.1.200") in net)
print("192.168.1.150 in 192.168.1.128/26 ?", ipaddress.ip_address("192.168.1.150") in net)
实测输出:
text
网络地址: 192.168.1.128
广播地址: 192.168.1.191
掩码: 255.255.255.192 前缀长度: 26
可用主机范围: 192.168.1.129 - 192.168.1.190
可用主机数: 62
10.0.0.0/24 -> 网络: 10.0.0.0 广播: 10.0.0.255 可用主机数: 254
192.168.1.200 in 192.168.1.128/26 ? False
192.168.1.150 in 192.168.1.128/26 ? True
⚠️ strict=False 表示"这个 IP 不一定是网络地址也接受",否则 ip_network("192.168.1.130/26") 会直接报错------130 不是该网段的网络地址。
三、IP 分片与重组:包太大怎么办
3.1 MTU 与分片
MTU(最大传输单元) 是一条链路层帧能装下的最大 IP 数据报,以太网默认 1500 字节。当 IP 数据报长度超过下一跳 MTU 时:
DF=0(允许分片):路由器把它切成几个小片,每个片独立走路由,在目的主机重组。DF=1(禁止分片):路由器直接丢弃,并回一个 ICMP"需要分片但 DF 置位"(类型 3 代码 4)。
分片靠三个字段协同:
| 字段 | 作用 |
|---|---|
| 标识 Identification | 同一数据报的所有分片标识相同,目的端靠它拼回去 |
| MF(More Fragment) | MF=1 表示后面还有分片;最后一个分片 MF=0 |
| 片偏移 | 该分片数据在原报文中的位置,单位 8 字节 |
#mermaid-svg-Jn7CmL1LLSAOhwuL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Jn7CmL1LLSAOhwuL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Jn7CmL1LLSAOhwuL .error-icon{fill:#552222;}#mermaid-svg-Jn7CmL1LLSAOhwuL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Jn7CmL1LLSAOhwuL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Jn7CmL1LLSAOhwuL .marker.cross{stroke:#333333;}#mermaid-svg-Jn7CmL1LLSAOhwuL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Jn7CmL1LLSAOhwuL p{margin:0;}#mermaid-svg-Jn7CmL1LLSAOhwuL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Jn7CmL1LLSAOhwuL .cluster-label text{fill:#333;}#mermaid-svg-Jn7CmL1LLSAOhwuL .cluster-label span{color:#333;}#mermaid-svg-Jn7CmL1LLSAOhwuL .cluster-label span p{background-color:transparent;}#mermaid-svg-Jn7CmL1LLSAOhwuL .label text,#mermaid-svg-Jn7CmL1LLSAOhwuL span{fill:#333;color:#333;}#mermaid-svg-Jn7CmL1LLSAOhwuL .node rect,#mermaid-svg-Jn7CmL1LLSAOhwuL .node circle,#mermaid-svg-Jn7CmL1LLSAOhwuL .node ellipse,#mermaid-svg-Jn7CmL1LLSAOhwuL .node polygon,#mermaid-svg-Jn7CmL1LLSAOhwuL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Jn7CmL1LLSAOhwuL .rough-node .label text,#mermaid-svg-Jn7CmL1LLSAOhwuL .node .label text,#mermaid-svg-Jn7CmL1LLSAOhwuL .image-shape .label,#mermaid-svg-Jn7CmL1LLSAOhwuL .icon-shape .label{text-anchor:middle;}#mermaid-svg-Jn7CmL1LLSAOhwuL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Jn7CmL1LLSAOhwuL .rough-node .label,#mermaid-svg-Jn7CmL1LLSAOhwuL .node .label,#mermaid-svg-Jn7CmL1LLSAOhwuL .image-shape .label,#mermaid-svg-Jn7CmL1LLSAOhwuL .icon-shape .label{text-align:center;}#mermaid-svg-Jn7CmL1LLSAOhwuL .node.clickable{cursor:pointer;}#mermaid-svg-Jn7CmL1LLSAOhwuL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Jn7CmL1LLSAOhwuL .arrowheadPath{fill:#333333;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Jn7CmL1LLSAOhwuL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jn7CmL1LLSAOhwuL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Jn7CmL1LLSAOhwuL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jn7CmL1LLSAOhwuL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Jn7CmL1LLSAOhwuL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Jn7CmL1LLSAOhwuL .cluster text{fill:#333;}#mermaid-svg-Jn7CmL1LLSAOhwuL .cluster span{color:#333;}#mermaid-svg-Jn7CmL1LLSAOhwuL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Jn7CmL1LLSAOhwuL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Jn7CmL1LLSAOhwuL rect.text{fill:none;stroke-width:0;}#mermaid-svg-Jn7CmL1LLSAOhwuL .icon-shape,#mermaid-svg-Jn7CmL1LLSAOhwuL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jn7CmL1LLSAOhwuL .icon-shape p,#mermaid-svg-Jn7CmL1LLSAOhwuL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Jn7CmL1LLSAOhwuL .icon-shape .label rect,#mermaid-svg-Jn7CmL1LLSAOhwuL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jn7CmL1LLSAOhwuL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Jn7CmL1LLSAOhwuL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Jn7CmL1LLSAOhwuL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始数据报 4000 字节
DF=0, ID=777
下一跳 MTU=1500
每片数据≤1480
片1: 数据1480, MF=1, 偏移=0
ID=777
片2: 数据1480, MF=1, 偏移=185
(1480/8)
片3: 数据1040, MF=0, 偏移=370
(2960/8)
目的主机按 ID 收集
按偏移重组
3.2 分片的三个坑
🔴 任何一个分片丢了,整个数据报都得重传。目的端收不齐所有片就扔掉全部,TCP 只好重传整个大报文------这就是为什么分片多了性能掉得厉害。
- 分片攻击:伪造大量重叠分片,让目的端重组时耗尽资源(古老的 Teardrop 攻击)。
- PMTU 发现:发送方先按路径 MTU 发 DF=1 的包,收到"需要分片"的 ICMP 就把包调小,逐步探到路径上的最小 MTU,避免中间路由器被动分片。
四、路由:每个包怎么找下一跳
4.1 路由表与最长前缀匹配
路由器/主机查转发决策靠路由表 ,每条路由记录:目的网络、子网掩码、下一跳、出接口、度量值。查路由的核心规则是最长前缀匹配 ------多条路由都匹配时,选掩码最长(前缀最长)的那条。
我本机 netstat -rn(macOS)实测片段:
text
Routing tables
Internet:
Destination Gateway Flags Netif Expire
default 10.170.255.254 UGScg en0
10.170/16 link#15 UCS en0
10.170.0.41 9c:7b:ef:4e:9e:b2 UHLWI en0
...
解读:
default 10.170.255.254 UGScg en0:默认路由 (0.0.0.0/0),所有没被更精确匹配的包都扔给网关10.170.255.254,从en0网卡出去。10.170/16:本机所在网段,直连,走en0。- 最长前缀匹配举例:去
10.170.0.41的包,既匹配default(/0)又匹配10.170/16(/16)还匹配主机路由(/32),选最长的 /32 主机路由。
本机 ifconfig en0:inet 10.170.2.152 netmask 0xffff0000 broadcast 10.170.255.255------0xffff0000 就是 /16,和路由表对上了。
Linux 看路由用
ip route或route -n;-n表示不做域名反解,直接显示数字 IP,快。
4.2 traceroute:路径上每一跳是谁
traceroute 靠 TTL 把戏把路径上的路由器一个个"钓"出来,下一节和 ICMP 一起讲。
五、ICMP:网络层的报错与探测信令
5.1 ICMP 是什么
ICMP(互联网控制报文协议)是 IP 层的辅助协议,用来传递差错报告和网络探测信息 。它不传输应用数据 ,纯粹是网络层的"控制信令"。封装上,ICMP 报文装进 IP 数据报,IP 首部协议号 = 1。
注意一个常被绕晕的点:ICMP 虽然是网络层协议,但它是封装在 IP 里的------就像 TCP/UDP 封装在 IP 里一样。IP 是信封,ICMP/TCP/UDP 是信的内容,靠协议号区分。
5.2 ICMP 报文类型对照表
| 类型 | 名称 | 方向 | 代码举例 | 用途 |
|---|---|---|---|---|
| 0 | 回显应答 Echo Reply | 应答 | 0 | ping 的 pong |
| 3 | 目的不可达 Destination Unreachable | 差错 | 0=网络不可达,1=主机,3=端口,4=需要分片但DF置位 | 报错 |
| 5 | 重定向 Redirect | 差错 | 0/1/2/3 | 告诉发送方换个更好的网关 |
| 8 | 回显请求 Echo Request | 请求 | 0 | ping 的 ping |
| 11 | 超时 Time Exceeded | 差错 | 0=TTL=0 | traceroute 的核心 |
| 12 | 参数问题 | 差错 | 0/1 | IP 首部字段错了 |
| 13/14 | 时间戳请求/应答 | 请求/应答 | 0 | 古老的对时,基本不用 |
两大类:差错报告 (3/5/11/12)和询问/应答(8/0、13/14)。ping 用的就是 8(请求)和 0(应答)这一对。
六、ping 原理:一次"你好,在吗"
6.1 工作流程
目标 本机 目标 本机 #mermaid-svg-LqjEmDsXyrhoiRme{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LqjEmDsXyrhoiRme .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LqjEmDsXyrhoiRme .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LqjEmDsXyrhoiRme .error-icon{fill:#552222;}#mermaid-svg-LqjEmDsXyrhoiRme .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LqjEmDsXyrhoiRme .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LqjEmDsXyrhoiRme .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LqjEmDsXyrhoiRme .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LqjEmDsXyrhoiRme .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LqjEmDsXyrhoiRme .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LqjEmDsXyrhoiRme .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LqjEmDsXyrhoiRme .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LqjEmDsXyrhoiRme .marker.cross{stroke:#333333;}#mermaid-svg-LqjEmDsXyrhoiRme svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LqjEmDsXyrhoiRme p{margin:0;}#mermaid-svg-LqjEmDsXyrhoiRme .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LqjEmDsXyrhoiRme text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-LqjEmDsXyrhoiRme .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LqjEmDsXyrhoiRme .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-LqjEmDsXyrhoiRme .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-LqjEmDsXyrhoiRme .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-LqjEmDsXyrhoiRme #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-LqjEmDsXyrhoiRme .sequenceNumber{fill:white;}#mermaid-svg-LqjEmDsXyrhoiRme #sequencenumber{fill:#333;}#mermaid-svg-LqjEmDsXyrhoiRme #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-LqjEmDsXyrhoiRme .messageText{fill:#333;stroke:none;}#mermaid-svg-LqjEmDsXyrhoiRme .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LqjEmDsXyrhoiRme .labelText,#mermaid-svg-LqjEmDsXyrhoiRme .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-LqjEmDsXyrhoiRme .loopText,#mermaid-svg-LqjEmDsXyrhoiRme .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-LqjEmDsXyrhoiRme .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LqjEmDsXyrhoiRme .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-LqjEmDsXyrhoiRme .noteText,#mermaid-svg-LqjEmDsXyrhoiRme .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-LqjEmDsXyrhoiRme .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LqjEmDsXyrhoiRme .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LqjEmDsXyrhoiRme .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LqjEmDsXyrhoiRme .actorPopupMenu{position:absolute;}#mermaid-svg-LqjEmDsXyrhoiRme .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-LqjEmDsXyrhoiRme .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LqjEmDsXyrhoiRme .actor-man circle,#mermaid-svg-LqjEmDsXyrhoiRme line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-LqjEmDsXyrhoiRme :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 收到后原样回显 收到应答, 用"现在-时间戳"算出 RTT ICMP 回显请求 (类型8, 标识PID, 序号seq, 带时间戳)ICMP 回显应答 (类型0, 同样的标识和序号)
ping 做的事:发一个 ICMP 类型 8 的回显请求,带标识符和递增序号,payload 里塞发送时刻;对方收到后原样回一个类型 0。本机用收到时刻减去发送时刻得到 RTT(往返时延)。
6.2 对着真实输出逐行读
本机 ping -c 4 www.baidu.com 实测:
text
PING www.baidu.com (198.18.0.37): 56 data bytes
64 bytes from 198.18.0.37: icmp_seq=0 ttl=64 time=0.693 ms
64 bytes from 198.18.0.37: icmp_seq=1 ttl=64 time=0.367 ms
64 bytes from 198.18.0.37: icmp_seq=2 ttl=64 time=0.455 ms
64 bytes from 198.18.0.37: icmp_seq=3 ttl=64 time=0.492 ms
--- www.baidu.com ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.367/0.502/0.693/0.119 ms
| 输出片段 | 含义 |
|---|---|
56 data bytes / 64 bytes from |
请求带 56 字节数据,加上 8 字节 ICMP 头 = 64 字节 |
icmp_seq=0 |
序号,丢包/乱序靠它判断 |
ttl=64 |
应答包里的剩余 TTL(不是本机发出去的初值) |
time=0.693 ms |
RTT 往返时延 |
0.0% packet loss |
丢包率,0% 表示全通 |
min/avg/max/stddev |
最小/平均/最大/抖动(mdev,标准差) |
我这里 RTT 不到 1ms、TTL=64,是因为本机走了本地代理隧道(解析到
198.18.x.x),不是真实公网延迟;真实跨运营商 RTT 通常几十毫秒。
6.3 自己用 Python 写一个 ping
ping 的核心就是:构造 ICMP 报文 → 算校验和 → 发出去 → 等应答 → 算 RTT。下面这个版本在 macOS 上免 sudo 就能跑------关键是用了 BSD/macOS 支持的非特权 ICMP datagram socket (SOCK_DGRAM + IPPROTO_ICMP);Linux 传统上需要 SOCK_RAW + root。
python
import socket, struct, time, select
def ones_comp_sum(data):
"""ICMP 校验和:16 位反码求和"""
s = 0
for i in range(0, len(data), 2):
w = (data[i] << 8) + (data[i+1] if i+1 < len(data) else 0)
s += w
s = (s & 0xffff) + (s >> 16)
return ~s & 0xffff
def ping(host="127.0.0.1", count=3):
# macOS/BSD: SOCK_DGRAM + IPPROTO_ICMP 免 root;Linux 需 SOCK_RAW + sudo
soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_ICMP)
dest = socket.gethostbyname(host)
pid = 1234
for seq in range(1, count + 1):
payload = b"blog-ping-0123456789"
header = struct.pack("!BBHHH", 8, 0, 0, pid, seq) # 类型8 代码0 校验和0 标识 序号
chksum = ones_comp_sum(header + payload)
packet = struct.pack("!BBHHH", 8, 0, chksum, pid, seq) + payload
soc.sendto(packet, (dest, 0))
t0 = time.time()
if select.select([soc], [], [], 1)[0]:
buf, _ = soc.recvfrom(1024)
rtt = (time.time() - t0) * 1000
icmp = buf[20:] # 这种 socket 会把 20 字节 IPv4 首部一起返回,要剥掉
print(f"64 bytes from {dest}: icmp_seq={seq} type={icmp[0]} code={icmp[1]} time={rtt:.3f} ms")
else:
print(f"icmp_seq={seq} request timed out")
soc.close()
ping("127.0.0.1", 3)
本机实测输出(type=0 code=0 正是回显应答):
text
64 bytes from 127.0.0.1: icmp_seq=1 type=0 code=0 time=0.074 ms
64 bytes from 127.0.0.1: icmp_seq=2 type=0 code=0 time=0.049 ms
64 bytes from 127.0.0.1: icmp_seq=3 type=0 code=0 time=0.042 ms
🔴 这段代码的两个细节很值钱:一是校验和必须先填 0 算一遍再填回去;二是这种 ICMP socket 收到的包带 IP 首部 ,要 buf[20:] 剥掉才读得到 ICMP 类型。
七、经典面试题:断网能 ping 通 127.0.0.1 吗?
能! 这是网络层最经典的送分题,也是最容易答错的题。
7.1 为什么断网也通
127.0.0.0/8 是环回地址 ,发往它的数据包根本不走出网卡------协议栈在内部就把它绕回来了。不管你拔网线、关 Wi-Fi、断外网,环回始终通,因为它压根没碰物理链路。
#mermaid-svg-Qq3XzbmLy6LHT8AQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .error-icon{fill:#552222;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .marker.cross{stroke:#333333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Qq3XzbmLy6LHT8AQ p{margin:0;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .cluster-label text{fill:#333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .cluster-label span{color:#333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .cluster-label span p{background-color:transparent;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .label text,#mermaid-svg-Qq3XzbmLy6LHT8AQ span{fill:#333;color:#333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .node rect,#mermaid-svg-Qq3XzbmLy6LHT8AQ .node circle,#mermaid-svg-Qq3XzbmLy6LHT8AQ .node ellipse,#mermaid-svg-Qq3XzbmLy6LHT8AQ .node polygon,#mermaid-svg-Qq3XzbmLy6LHT8AQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .rough-node .label text,#mermaid-svg-Qq3XzbmLy6LHT8AQ .node .label text,#mermaid-svg-Qq3XzbmLy6LHT8AQ .image-shape .label,#mermaid-svg-Qq3XzbmLy6LHT8AQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .rough-node .label,#mermaid-svg-Qq3XzbmLy6LHT8AQ .node .label,#mermaid-svg-Qq3XzbmLy6LHT8AQ .image-shape .label,#mermaid-svg-Qq3XzbmLy6LHT8AQ .icon-shape .label{text-align:center;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .node.clickable{cursor:pointer;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .arrowheadPath{fill:#333333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Qq3XzbmLy6LHT8AQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qq3XzbmLy6LHT8AQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Qq3XzbmLy6LHT8AQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .cluster text{fill:#333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .cluster span{color:#333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Qq3XzbmLy6LHT8AQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .icon-shape,#mermaid-svg-Qq3XzbmLy6LHT8AQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .icon-shape p,#mermaid-svg-Qq3XzbmLy6LHT8AQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .icon-shape .label rect,#mermaid-svg-Qq3XzbmLy6LHT8AQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qq3XzbmLy6LHT8AQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Qq3XzbmLy6LHT8AQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Qq3XzbmLy6LHT8AQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
应用进程
socket API
TCP/IP 协议栈
目的地址=127.0.0.1
目的是环回?
协议栈内部直接回环
不经过网卡/驱动
7.2 环回接口的三个实际作用
- 本机进程间通信 :Nginx 转发给本机
127.0.0.1:8000的后端,走环回,快且安全。 - 测试协议栈:ping 127.0.0.1 通,说明本机 TCP/IP 协议栈没坏,问题在网卡或外部网络。
- 只允许本机访问 :服务绑定
127.0.0.1而不是0.0.0.0,外网就访问不到------这是最常用的"内网服务不暴露"手段。
我本机 ping -c 3 127.0.0.1 实测,RTT 只有 0.1ms 左右:
text
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.102 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.097 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.123 ms
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.097/0.107/0.123/0.011 ms
八、TTL 与 traceroute:把路径上的路由器钓出来
8.1 原理
前面说 TTL 每过一个路由器减 1,减到 0 就丢弃并回 ICMP 超时(类型 11)。traceroute 利用这点:
- 先发 TTL=1 的探测包,第一跳路由器把 TTL 减到 0,丢弃,回 ICMP 超时 → 这就知道了第一跳是谁。
- 再发 TTL=2,第二跳回超时 → 知道第二跳。
- 依次递增 TTL,直到包到达目的端(目的端收到后不回超时,而是根据包类型回别的------UDP 端口不可达,或 ICMP 回显应答),路径就完整了。
8.2 实测
本机 traceroute -m 3 -w 2 -q 1 10.170.255.254(网关):
text
traceroute to 10.170.255.254 (10.170.255.254), 3 hops max, 40 byte packets
1 10.170.255.254 (10.170.255.254) 15.661 ms
第一跳就是网关,和路由表对上了。
⚠️ 我 traceroute 外网(如 223.5.5.5)每跳都是 *:
text
traceroute to 223.5.5.5 (223.5.5.5), 4 hops max, 40 byte packets
1 *
2 *
3 *
4 *
这不代表网络不通 (ping 是通的),而是中间路由器/网关禁止或过滤了 ICMP 超时报文 ------安全上运营商和企业网常这么干,traceroute 就只能显示星号。看到 * 要会判断:多半是中间设备不回 ICMP,而不是断网。
九、IPv6 简介:为什么要换
| 维度 | IPv4 | IPv6 |
|---|---|---|
| 地址长度 | 32 位(约 43 亿个) | 128 位,地址近乎无限 |
| 首部 | 20~60 字节,字段多 | 固定 40 字节,首部简化,去掉校验和 |
| 广播 | 有(255.255.255.255) | 无广播,用组播 |
| 配置 | 手动 / DHCP | 支持自动配置(SLAAC) |
| 安全性 | IPsec 可选 | IPsec 原生内置 |
| 分片 | 路由器和源都能分片 | 只在源端分片,路由器不分片 |
IPv6 地址用冒号分十六进制,如 2001:0db8::1,:: 表示一串连续的 0。开发上你现在要做的主要是:服务监听地址别写死 0.0.0.0,用 ::(IPv6 全地址)或双栈兼容;AF_INET 替换成 AF_INET6。
总结:你真正需要记住的 10 件事
- IP = 无连接、不可靠、尽最大努力;可靠性靠 TCP,不靠 IP。
- IP 首部三个必背协议号:ICMP=1、TCP=6、UDP=17。
- 分片三要素:标识认亲、MF 标记后续、片偏移按 8 字节 ,且在目的端重组。
- 子网计算口诀:IP 与掩码得网络地址,主机位全 1 得广播地址,可用主机 = 2^主机位 - 2;别手算,用
ipaddress。 - 路由查找遵循最长前缀匹配 ;
default/0.0.0.0/0是兜底默认路由。 - ICMP 是网络层的控制信令,封装在 IP 里(协议号 1);差错报告 + 询问应答两大类。
- ping = ICMP 类型 8 请求 / 类型 0 应答,RTT = 收到时刻 - 发送时刻。
- 断网也能 ping 通 127.0.0.1------环回包不经过网卡,协议栈内部回环。
- traceroute 靠 TTL 递增 + ICMP 超时(类型 11) 钓出每一跳;全星号通常是中间设备过滤 ICMP,不是断网。
- IPv6 = 128 位、首部简化、无广播用组播、支持自动配置、IPsec 内置。
验证清单
- 能默写 IPv4 首部关键字段,并说出 TTL、协议号、标识/标志/片偏移的作用
- 能手算
192.168.1.130/26的网络地址、广播地址、可用主机范围和主机数,并用ipaddress验证 - 能说出私有地址三段和环回地址,并解释为什么
127.0.0.1断网也通 - 能用
netstat -rn(或ip route)找到默认路由和本机网段 - 能逐行解释
ping输出里的 icmp_seq、ttl、time、丢包率和 min/avg/max/mdev - 能说清 traceroute 的 TTL 原理,并解释为什么会看到
* - 能用 Python 标准库写出一个发送 ICMP 回显请求的小脚本
- 能说出 IPv4 和 IPv6 在地址长度、广播、分片、安全性上的主要区别
参考资源
- RFC 791(IPv4 协议)https://www.rfc-editor.org/rfc/rfc791
- RFC 792(ICMP 协议)https://www.rfc-editor.org/rfc/rfc792
- RFC 1918(私有地址分配)https://www.rfc-editor.org/rfc/rfc1918
- Python
ipaddress官方文档 https://docs.python.org/3/library/ipaddress.html - IANA 协议号注册表 https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
- TCP/IP 入门(Cloudflare Learning Center)https://www.cloudflare.com/learning/network-layer/what-is-the-network-layer/
下一篇《【上手】Socket 网络编程:亲手写 TCP/UDP 程序》:网络层和传输层讲完了,这篇直接上手写代码------用 Python socket 标准库从零实现 TCP echo 服务端/客户端、UDP 通信,重点复现面试必考的 TCP 粘包/分包问题 并给出"长度头"解决方案,最后用 selectors 写出第一个并发服务器,为后面操作系统篇的 epoll 埋下伏笔。