3. 数据链路层实现:帧结构、MAC地址、以太网协议、CSMA/CD机制
数据链路层,说白了就是给物理层传输的比特流「化妆」的。它把一堆0和1打包成帧,加上地址,再确保对方能收到。我刚开始学网络时,总觉得这层很鸡肋——不就是加个头部吗?后来在项目中调试丢包问题,才发现这层要是没做好,上层再牛也白搭。
3.1 帧结构:数据链路层的「信封」
帧,就是数据链路层传输的基本单位。你可以把它想象成一个信封:里面装着网络层下来的IP数据包,外面写上源MAC、目的MAC,再贴上类型标签和校验码。
以太网帧的标准格式长这样:
+----------------+----------------+----------------+----------------+----------------+----------------+
| 前导码(7字节) | 定界符(1字节) | 目的MAC(6字节) | 源MAC(6字节) | 类型/长度(2字节)| 数据(46-1500字节)|
+----------------+----------------+----------------+----------------+----------------+----------------+
| |
| FCS(4字节) |
+------------------------------------------------------------------------------------------------+
嗯,这里要注意几个关键字段:
- 前导码和定界符:用来同步时钟的。说白了就是告诉接收端「我要开始发数据了,你准备好」。
- MAC地址:6字节,全球唯一。我见过有人把MAC地址写错一位,结果整个局域网都找不到那台设备,排查了半天。
- 类型/长度:0x0800表示上层是IP协议,0x0806是ARP。这个字段决定了接收端把数据交给谁处理。
- 数据:最少46字节,最多1500字节。如果IP包太小,要填充到46字节;太大就要分片。
- FCS:循环冗余校验,4字节。用来检测帧在传输过程中有没有出错。
3.2 MAC地址:设备的「身份证」
MAC地址,也叫物理地址,是烧录在网卡里的。48位,前24位是厂商代码(OUI),后24位是厂商分配的序列号。
你想想看,全球几十亿台设备,怎么保证MAC地址不重复?其实并不能完全保证。有些小厂商会乱用OUI,或者虚拟机可以随意修改MAC地址。我在项目中就遇到过两台设备MAC地址一样,结果交换机不知道把数据发给谁,网络直接瘫痪。
MAC地址的格式:
# 常见的表示方式
00:1A:2B:3C:4D:5E # 冒号分隔
00-1A-2B-3C-4D-5E # 减号分隔
001A.2B3C.4D5E # 点分格式(Cisco设备常用)
这里有个重要的概念:单播、广播、组播。
| 类型 | MAC地址特征 | 用途 |
|---|---|---|
| 单播 | 第1字节最低位为0 | 点对点通信 |
| 广播 | FF:FF:FF:FF:FF:FF | 发送给所有设备 |
| 组播 | 第1字节最低位为1 | 发送给特定组 |
3.3 以太网协议:最常用的数据链路层协议
以太网协议,说白了就是一套规则,告诉设备怎么在共享介质上通信。它定义了帧格式、访问控制、寻址方式等。
以太网的发展经历了几个阶段:
- 10BASE5/10BASE2:同轴电缆,总线型拓扑。我入行时已经很少见了,只在老机房见过。
- 10BASE-T:双绞线,星型拓扑。这是以太网普及的开始。
- 100BASE-TX:快速以太网,100Mbps。
- 1000BASE-T:千兆以太网,现在的主流。
- 10GBASE-T:万兆以太网,数据中心常用。
以太网帧的封装过程,我习惯用Python来模拟:
class EthernetFrame:
def __init__(self, dst_mac, src_mac, ether_type, payload):
self.preamble = b'\x55' * 7 # 前导码
self.sfd = b'\xD5' # 定界符
self.dst_mac = dst_mac # 目的MAC
self.src_mac = src_mac # 源MAC
self.ether_type = ether_type # 类型
self.payload = payload # 数据
self.fcs = self._calc_fcs() # 校验码
def _calc_fcs(self):
# 简化的CRC32计算
data = self.dst_mac + self.src_mac + self.ether_type + self.payload
return binascii.crc32(data).to_bytes(4, 'big')
def pack(self):
return self.preamble + self.sfd + self.dst_mac + \
self.src_mac + self.ether_type + self.payload + self.fcs
3.4 CSMA/CD机制:共享介质上的「礼貌规则」
CSMA/CD,全称是载波监听多点接入/碰撞检测。名字很绕口,但原理很简单——就像一群人开会,谁想发言先听听有没有人在说,没人说就开讲,如果两个人同时开口就都停下来等一会再试。
具体流程是这样的:
- 监听:发送前先听一下信道是否空闲。
- 发送:如果空闲,就开始发送数据。
- 检测:发送过程中持续监听,看有没有冲突。
- 冲突处理:如果检测到冲突,立即停止发送,发送一个阻塞信号通知所有设备。
- 退避:等待一个随机时间后重新尝试。
退避算法用的是二进制指数退避:
import random
def backoff_time(collision_count):
# collision_count: 冲突次数(0-15)
if collision_count > 10:
collision_count = 10 # 最多退避到2^10
# 随机选择0到(2^collision_count - 1)之间的一个数
slot = random.randint(0, 2**collision_count - 1)
# 每个时隙是512比特时间(对于10Mbps以太网,约51.2微秒)
return slot * 51.2 # 单位:微秒
为什么会这样设计?你想想看,如果冲突次数少,说明网络负载轻,退避时间短一点没关系。如果冲突次数多,说明网络很忙,大家多等一会再试,避免再次冲突。
3.5 实践:用Python模拟CSMA/CD
下面是一个简单的模拟程序,帮你理解CSMA/CD的工作过程:
import time
import random
class CSMACD_Node:
def __init__(self, name):
self.name = name
self.collision_count = 0
self.max_attempts = 16
def send(self, channel):
print(f"{self.name}: 开始发送")
# 载波监听
if channel.busy:
print(f"{self.name}: 信道忙,等待")
time.sleep(0.1)
return False
# 发送数据
channel.busy = True
print(f"{self.name}: 发送中...")
time.sleep(0.05) # 模拟发送时间
# 碰撞检测
if channel.collision:
print(f"{self.name}: 检测到碰撞!")
channel.busy = False
self.collision_count += 1
if self.collision_count > self.max_attempts:
print(f"{self.name}: 重试次数过多,放弃发送")
return False
# 退避
wait_time = random.randint(0, 2**min(self.collision_count, 10) - 1) * 0.01
print(f"{self.name}: 等待 {wait_time:.3f} 秒后重试")
time.sleep(wait_time)
return self.send(channel)
print(f"{self.name}: 发送成功")
channel.busy = False
self.collision_count = 0
return True
# 模拟两个节点同时发送
channel = type('Channel', (), {'busy': False, 'collision': True})()
node1 = CSMACD_Node("节点A")
node2 = CSMACD_Node("节点B")
# 同时发送,触发碰撞
node1.send(channel)
node2.send(channel)
好了,数据链路层的内容就讲到这里。下一章我们会进入网络层,看看IP协议是怎么把数据包送到千里之外的。如果你在项目中遇到帧结构或MAC地址相关的问题,欢迎随时交流。