网络层核心: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/24192.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画一个转发决策的流程图,帮你理清思路:

路由器转发决策流程 收到IP报文 提取目的IP地址 遍历路由表条目 对每条计算网络地址并比较 匹配成功? 更新最佳匹配 (选最长前缀) 继续遍历 遍历完成 检查最佳匹配

避坑指南:我曾经在生产环境遇到过一个问题——路由表里同时存在/24和/32两条路由,结果/32那条死活不生效。查了半天发现是代码里比较掩码长度时用了「>=」而不是「>」,导致相同长度时后添加的覆盖了前面的。细节决定成败啊!

好了,这一章的内容就到这里。IP协议和路由是网络层的基石,理解透了才能写出高性能的网络应用。代码我已经放在上面了,建议你动手跑一跑,改改路由表看看效果。


公众号:蓝海资料掘金营,微信deep3321