网络层核心:IP协议与路由
各位同学,今天我们来聊聊网络层最核心的东西——IP协议和路由。说实话,我在做交易系统的时候,网络层这块踩过不少坑。有一次因为路由表配置错误,导致订单数据绕了大半个地球才到达交易所,延迟直接飙到200毫秒。嗯,从那以后我就特别重视这一块。
IPv4报文结构:别小看这20字节
IPv4报文头部固定20字节,后面可以跟选项。我习惯把报文头分成几个关键字段来记:
| 字段 | 长度(bit) | 说明 |
|---|---|---|
| 版本 | 4 | IPv4就是4 |
| 首部长度 | 4 | 单位是4字节,最小5 |
| 总长度 | 16 | 整个IP报文长度 |
| 标识+标志+片偏移 | 16+3+13 | 分片相关 |
| 生存时间(TTL) | 8 | 每跳减1,为0丢弃 |
| 协议 | 8 | TCP=6, UDP=17 |
| 首部校验和 | 16 | 只校验头部 |
| 源IP地址 | 32 | 发送方IP |
| 目的IP地址 | 32 | 接收方IP |
这里有个坑——首部校验和只覆盖头部,不校验数据部分。为什么?因为数据部分有上层协议(比如TCP)自己校验,IP层没必要重复劳动。我在项目中见过有人自己实现IP协议栈,结果把整个报文都算了校验和,白白浪费CPU。
子网划分:把大网络切成小段
子网划分说白了就是借主机位当网络位。我记得刚入行时,有个老工程师跟我说:「子网掩码就是一把尺子,1的部分是网络号,0的部分是主机号。」这句话我一直记到现在。
举个例子:192.168.1.0/24,掩码是255.255.255.0。如果你想切成4个子网,需要借2位主机位:
- 子网1:192.168.1.0/26(掩码255.255.255.192)
- 子网2:192.168.1.64/26
- 子网3:192.168.1.128/26
- 子网4:192.168.1.192/26
每个子网有62个可用主机地址(2^6 - 2)。为什么要减2?网络地址和广播地址不能用。我曾经在配置服务器时忘了这茬,结果分配了192.168.1.63给一台机器,网络直接不通……
路由表与最长前缀匹配
路由表就是一张「去哪走哪」的表格。每条记录包含:目的网络、掩码、下一跳、出接口。转发时,路由器会匹配所有条目,选前缀最长的那个。
为什么会这样?你想想看,192.168.1.0/24和192.168.1.0/26都能匹配192.168.1.10,但/26更精确,所以优先用/26的路由。这就是最长前缀匹配的核心思想。
核心原则:匹配越多位,优先级越高。如果完全匹配不上,就走默认路由(0.0.0.0/0)。
用Python实现简易路由器转发逻辑
下面我写一个简化版的路由器转发逻辑。这个代码我在内部培训时经常用,能帮你理解路由转发的本质。
class Router:
def __init__(self):
# 路由表:[(网络地址, 掩码长度, 下一跳, 出接口)]
self.routing_table = []
def add_route(self, network, mask_len, next_hop, interface):
self.routing_table.append((network, mask_len, next_hop, interface))
def _ip_to_int(self, ip_str):
"""把IP字符串转成整数,方便位运算"""
parts = ip_str.split('.')
return (int(parts[0]) << 24) | (int(parts[1]) << 16) | \
(int(parts[2]) << 8) | int(parts[3])
def _int_to_ip(self, ip_int):
"""整数转回IP字符串"""
return f"{(ip_int >> 24) & 0xFF}.{(ip_int >> 16) & 0xFF}." \
f"{(ip_int >> 8) & 0xFF}.{ip_int & 0xFF}"
def _apply_mask(self, ip_int, mask_len):
"""对IP应用掩码,得到网络地址"""
mask = (0xFFFFFFFF << (32 - mask_len)) & 0xFFFFFFFF
return ip_int & mask
def forward(self, packet):
"""
转发一个IP报文
packet: 字典,包含 'dst_ip' 和 'data'
返回: (下一跳, 出接口) 或 None
"""
dst_ip = packet['dst_ip']
dst_int = self._ip_to_int(dst_ip)
best_match = None
best_len = -1
for network, mask_len, next_hop, interface in self.routing_table:
net_int = self._ip_to_int(network)
# 计算目的IP的网络地址
dst_net = self._apply_mask(dst_int, mask_len)
# 比较是否匹配
if dst_net == net_int:
# 选最长前缀
if mask_len > best_len:
best_len = mask_len
best_match = (next_hop, interface)
return best_match
def show_table(self):
print("路由表:")
print(f"{'目的网络':<20} {'掩码长度':<10} {'下一跳':<16} {'出接口'}")
print("-" * 60)
for network, mask_len, next_hop, interface in self.routing_table:
print(f"{network}/{mask_len:<18} {mask_len:<10} {next_hop:<16} {interface}")
# 测试一下
if __name__ == '__main__':
r = Router()
r.add_route('192.168.1.0', 24, '10.0.0.1', 'eth0')
r.add_route('192.168.1.0', 26, '10.0.0.2', 'eth1')
r.add_route('0.0.0.0', 0, '192.168.0.1', 'eth2')
r.show_table()
# 测试转发
test_packets = [
{'dst_ip': '192.168.1.10', 'data': 'hello'},
{'dst_ip': '192.168.1.66', 'data': 'world'},
{'dst_ip': '8.8.8.8', 'data': 'dns'},
]
for pkt in test_packets:
result = r.forward(pkt)
if result:
print(f"转发 {pkt['dst_ip']} -> 下一跳: {result[0]}, 出接口: {result[1]}")
else:
print(f"转发 {pkt['dst_ip']} -> 无匹配,丢弃")
个人经验:实际生产环境的路由表可能有几十万条,用线性查找肯定不行。工业级实现会用Trie树或哈希表来加速。我这个代码只是为了教学,别直接拿去用哈。
核心逻辑流程图
下面我用SVG画一个转发决策的流程图,帮你理清思路:
避坑指南:我曾经在生产环境遇到过一个问题——路由表里同时存在/24和/32两条路由,结果/32那条死活不生效。查了半天发现是代码里比较掩码长度时用了「>=」而不是「>」,导致相同长度时后添加的覆盖了前面的。细节决定成败啊!
好了,这一章的内容就到这里。IP协议和路由是网络层的基石,理解透了才能写出高性能的网络应用。代码我已经放在上面了,建议你动手跑一跑,改改路由表看看效果。
公众号:蓝海资料掘金营,微信deep3321