题目:ARP 缓存与报文转发模拟
背景介绍
在计算机网络中,当一台设备(如路由器)需要向另一个在同一局域网内的设备发送数据包时,它需要知道对方的物理地址(MAC 地址)。地址解析协议 (ARP) 就是用来根据目标设备的 IP 地址查询其 MAC 地址的。
为了提高效率,设备会维护一张 ARP 表(也称 ARP 缓存),记录近期通信过的 IP 与 MAC 地址的映射关系。当需要发送数据包时:
- 首先查找 ARP 表。如果找到,就能直接封装并发送数据包。
- 如果找不到,设备就不能立即发送,需要先通过 ARP 请求广播来获取 MAC 地址。在此期间,待发送的数据包会被缓存起来。
- 一旦通过后续操作(如手动配置)学习到了目标 MAC 地址,设备就会立即将缓存的报文补发出去。
你的任务就是实现这套包含 LRU 缓存淘汰策略 和报文缓存机制的系统。
核心组件
-
ARP 表 (ARP Table):
-
功能: 存储 IP 地址到 MAC 地址的映射。
-
容量: 最大条目数为
arpCap
。 -
淘汰策略: 当表满时,采用 LRU (Least Recently Used, 最久未被命中) 策略。即,当需要添加新条目而表已满时,移除那个最久没有被"命中"过的条目。
-
命中 (Access): 以下操作均视为一次"命中",会将被操作的 IP 地址更新为"最近使用"的:
update
一个已存在的 IP。update
一个新添加的 IP。forward
时成功查找到一个 IP。
-
-
报文缓存 (Packet Cache):
-
功能: 暂存因 ARP 表中没有对应条目而无法立即发送的报文。
-
容量限制:
- 全局上限
cachedPktCap
: 设备最多能缓存的报文总数。 - 单 IP 上限
perIpCap
: 同一个目的 IP 最多能缓存的报文数。
- 全局上限
-
功能要求
你需要实现一个 ArpSys
类,支持以下操作:
ArpSys(int arpCap, int cachedPktCap, int perIpCap)
- 功能: 初始化系统,设置三个容量上限。
update(int ip, String macAddr)
-
功能: 配置或更新一条 ARP 表项。
-
规则:
-
如果 IP 已存在: 直接用新的
macAddr
覆盖旧的。此操作会更新该 IP 为最近使用。 -
如果 IP 不存在:
- 若 ARP 表未满: 直接添加新表项。
- 若 ARP 表已满: 淘汰最久未被使用的条目,然后添加新表项。
- 添加或替换成功后,此操作也会更新该 IP 为最近使用。
-
补发报文:
update
操作完成后,检查报文缓存中是否有发往该ip
的报文。如果有,将这些报文按pktId
升序 返回,并从缓存中清空。
-
-
返回值: 返回被补发的报文 ID 列表
List<Integer>
;如果没有报文需要补发,则返回空列表[]
。
forward(int ip, int pktId)
-
功能: 模拟一次报文转发请求。
-
规则:
-
ARP 命中: 如果在 ARP 表中找到 了
ip
对应的macAddr
:- 将该表项更新为最近使用。
- 返回对应的
macAddr
字符串。
-
ARP 未命中: 如果在 ARP 表中找不到
ip
:- 尝试将报文
pktId
存入报文缓存。 - 缓存检查: 只有在全局缓存数量 和该 IP 的缓存数量都未达到上限时,才能缓存成功。
- 如果任一缓存已满,则缓存失败,该报文被丢弃。
- 无论缓存成功还是失败,都返回空字符串
""
。
- 尝试将报文
-
解答要求与输入输出格式
-
时间/内存限制: 1000ms / 256MB。
-
输入:
arpCap
:1 <= arpCap <= 100
cachedPktCap
:1 <= perIpCap <= cachedPktCap <= 100
ip
:1 <= ip <= 2^31 - 1
pktId
:0 <= pktId <= 10000
macAddr
: 固定长度为 17 的十六进制字符串。
-
输出:
- 根据每个函数的原型要求返回相应的值。评测框架会自动处理
null
(针对构造函数) 和其他返回值的输出。
- 根据每个函数的原型要求返回相应的值。评测框架会自动处理
样例 1 解读
初始化: ArpSys(3, 3, 2)
-> ARP 容量=3, 全局缓存=3, 单IP缓存=2。
# | 调用 | 解释与系统状态变化 | 返回值 |
---|---|---|---|
1 | update(123, "...") |
ARP 表为空,添加 123 。ARP (LRU: head->tail): [123] 。 |
[] |
2 | forward(456, 23) |
ARP 未命中。缓存报文 23。Cache: {456: [23]} 。Total Cached: 1。 |
"" |
3 | forward(456, 0) |
ARP 未命中。缓存报文 0。Cache: {456: [23, 0]} 。Total Cached: 2。 |
"" |
4 | forward(456, 8) |
ARP 未命中。IP 456 的缓存已达上限(2),缓存失败。Cache: {456: [23, 0]} 。Total Cached: 2。 |
"" |
5 | update(456, "...") |
ARP 表未满,添加 456 。ARP: [123, 456] 。补发 IP 456 的报文 [23, 0] ,排序后为 [0, 23] 。Cache: {} 。Total Cached: 0。 |
[0, 23] |
6 | update(123, "...") |
IP 123 已存在,覆盖 MAC 地址。123 变为最近使用。ARP: [456, 123] 。 |
[] |
7 | forward(123, 5) |
ARP 命中。123 变为最近使用。ARP: [456, 123] 。 |
"CD-EF-AB-AA-AB-AB" |
java
import java.util.*;
/**
* 模拟 ARP 表和报文缓存的系统。
*
* 核心数据结构:
* - arpTable: 使用 LinkedHashMap 实现,自带 LRU 淘汰策略。
* - packetCache: 使用 HashMap 存储 IP 到其待发报文列表的映射。
* - totalCachedPacketCount: 整数,用于 O(1) 检查全局缓存是否已满。
*/
public class ArpSys {
// --- 成员变量 ---
private final int arpCapacity; // ARP 表的最大容量
private final int globalCacheCapacity; // 总报文缓存的上限
private final int perIpCacheCapacity; // 单个 IP 缓存报文的上限
// ARP 表: Key=IP地址, Value=MAC地址.
// 使用 LinkedHashMap 实现 LRU (最久未使用) 淘汰策略。
private final Map<Integer, String> arpTable;
// 报文缓存: Key=IP地址, Value=等待补发的报文ID列表
private final Map<Integer, List<Integer>> packetCache;
// 当前已缓存的报文总数,用于快速检查全局缓存容量
private int totalCachedPacketCount;
/**
* 构造函数 - 初始化 ARP 系统
* @param arpCap ARP 表的最大容量
* @param cachedPktCap 设备能缓存的总报文数上限
* @param perIpCap 单个 IP 能缓存的报文数上限
*/
public ArpSys(int arpCap, int cachedPktCap, int perIpCap) {
this.arpCapacity = arpCap;
this.globalCacheCapacity = cachedPktCap;
this.perIpCacheCapacity = perIpCap;
this.totalCachedPacketCount = 0;
this.packetCache = new HashMap<>();
// 初始化支持 LRU 的 LinkedHashMap。
// 构造函数的第三个参数 accessOrder=true 是关键,它使得 get/put 操作
// 都会将被访问的条目移动到内部链表的末尾(表示最近使用)。
this.arpTable = new LinkedHashMap<Integer, String>(arpCap, 0.75f, true) {
/**
* 重写此方法,使得在 put 操作后,如果 Map 的大小超过了设定的容量,
* LinkedHashMap 会自动移除最旧的条目(链表头部的条目)。
*/
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
return this.size() > arpCapacity;
}
};
}
/**
* 配置或更新一条 ARP 表项。
* @param ip IP 地址
* @param macAddr MAC 地址
* @return 如果此 IP 有缓存的报文,返回补发的报文 ID 列表(升序);否则返回空列表。
*/
public List<Integer> update(int ip, String macAddr) {
// 1. 更新或插入 ARP 表项。
// 由于 accessOrder=true,此 put 操作会将 ip 标记为最近使用。
// 如果 put 后 size() > arpCapacity,重写的 removeEldestEntry 会自动触发,移除最旧条目。
arpTable.put(ip, macAddr);
// 2. 检查此 IP 是否有缓存的报文需要补发。
// 使用 getOrDefault 返回一个空列表,避免 null 检查。
List<Integer> packetsToSend = packetCache.getOrDefault(ip, Collections.emptyList());
if (!packetsToSend.isEmpty()) {
// 如果有报文需要补发:
List<Integer> sortedPackets = new ArrayList<>(packetsToSend);
// a. 按 pktId 升序排序
Collections.sort(sortedPackets);
// b. 更新缓存的总数
this.totalCachedPacketCount -= packetsToSend.size();
// c. 从缓存中移除此 IP 的条目
packetCache.remove(ip);
// d. 返回补发的报文列表
return sortedPackets;
} else {
// 此 IP 没有缓存的报文,返回空列表
return Collections.emptyList();
}
}
/**
* 模拟报文转发。
* @param ip 目的 IP 地址
* @param pktId 报文 ID
* @return 如果 ARP 表中有对应表项,返回 MAC 地址;否则返回空字符串 ""。
*/
public String forward(int ip, int pktId) {
// 1. 在 ARP 表中查找 IP
if (arpTable.containsKey(ip)) {
// ARP 命中!
// get 操作会自动将此条目更新为最近使用,满足 LRU 要求
return arpTable.get(ip);
} else {
// 2. ARP 未命中,尝试缓存报文
// a. 检查全局缓存是否已满
if (this.totalCachedPacketCount >= this.globalCacheCapacity) {
// 全局缓存满,缓存失败,丢弃报文
return "";
}
// b. 获取或创建该 IP 的缓存列表
List<Integer> ipPacketList = this.packetCache.computeIfAbsent(ip, k -> new ArrayList<>());
// c. 检查单个 IP 的缓存是否已满
if (ipPacketList.size() >= this.perIpCacheCapacity) {
// 单个 IP 的缓存满,缓存失败,丢弃报文
return "";
}
// d. 缓存成功
ipPacketList.add(pktId);
this.totalCachedPacketCount++;
return "";
}
}
}