纯Java实现DHCP协议栈:客户端/服务端/中继全角色支持

发布时间:2026/11/6 3:54:08

纯Java实现DHCP协议栈:客户端/服务端/中继全角色支持 简介本资源是一份基于Java实现DHCP协议核心功能的完整开源项目面向网络协议学习者、Java网络编程初学者及高校计算机网络课程实践者旨在帮助读者深入理解DHCP四步交互机制Discover/Offer/Request/Ack并掌握UDP通信、BOOTP报文编解码、IP地址池管理、多线程服务端设计等关键技术。压缩包共84个文件含16个核心Java源码覆盖服务器监听、客户端广播、报文构造与解析等模块、64个Javadoc生成页含overview-summary.html、allclasses-frame.html等便于快速查阅API结构与类关系以及CSS样式、properties配置和GIF示意图等辅助文件整体仅186KB轻量易读。目前已有634人学习下载代码结构清晰、注释充分配套文档完整可直接导入IDE调试运行是开展协议分析、网络工具开发或课程实验的理想参考实现。1. 一个纯 Java 实现的 DHCP 协议栈不依赖操作系统网络栈能直接发 Offer、Ack、Decline甚至模拟租约过期重协商全过程你有没有试过在嵌入式设备、容器隔离环境或自定义网络命名空间里让 Java 进程自己“当 DHCP 客户端 服务端”不是调dhclient命令也不是靠 Linux 内核自动分配——而是从以太网帧开始手撕 UDP 封包、解析 DHCP Option、校验 checksum、处理事务 IDxid匹配、维护 IP 租约状态机。这个项目就是干这个的它用标准 JDKJDK 8纯 Java 实现了 DHCPv4 的完整协议交互逻辑包含客户端DHCPClient、服务端DHCPServer、中继代理DHCPRelay三类角色所有报文构造/解析、定时重传、超时清理、Option 12/51/54/58/59 支持全部落在源码里。适合做网络协议教学实验、IoT 设备轻量级网络配置模块、SDN 控制面 DHCP 模拟器或者你想彻底搞懂为什么 Wireshark 里看到的 DHCP Discover 总是带 0x01000000 的 flags 字段——它就藏在这份代码的DHCPPacketBuilder.java第 217 行。2. 架构总览与核心模块拆解三层结构 状态驱动 零反射设计这个项目没用 Spring、Netty 或任何网络框架它走的是“协议即对象”的极简路径所有 DHCP 报文类型Discover / Offer / Request / Decline / Ack / Nak / Release / Inform都继承自统一基类DHCPPacket所有 Option53 Message Type、54 Server Identifier、51 Lease Time…都实现DHCPOption接口整个生命周期由DHCPStateMachine驱动状态流转完全基于收到的报文类型和当前租约状态。这种设计让调试变得极其透明——你不需要猜 Netty ChannelHandler 在哪一层丢了包只要打断点进DHCPServer.handleDiscover()就能看到它怎么查 IP 池、怎么选 Offer 地址、怎么填yiaddr和siaddr。2.1 目录结构与关键文件职责说明项目采用 Maven 标准布局核心逻辑集中在src/main/java/com/example/dhcp/下├── core/ # 协议基础报文基类、Option 解析器、校验工具 │ ├── DHCPPacket.java # 所有 DHCP 报文的抽象父类含 op、htype、hlen、hops、xid、secs、flags... │ ├── DHCPOption.java # Option 接口定义 getType()、getLength()、getData() │ ├── DHCPChecksum.java # RFC 1071 校验和计算支持 IPv4 UDP 伪头部 │ └── ByteUtils.java # 字节序转换BE/LE、MAC/IP 地址字节数组互转 ├── client/ # DHCP 客户端实现状态机 重传控制 租约持久化 │ ├── DHCPClient.java # 入口类启动监听、触发 Discover、处理 Offer/Ack │ ├── DHCPClientState.java # 枚举INIT、SELECTING、REQUESTING、BOUND、RENEWING、REBINDING... │ └── LeaseStore.java # 内存/文件租约存储默认用 Properties 存到 dhcp.lease ├── server/ # DHCP 服务端IP 池管理、Offer 分配策略、租约超时调度 │ ├── DHCPServer.java # 主服务类绑定 67/68 端口分发报文到对应 handler │ ├── IPPool.java # 可配置 IP 段192.168.1.100-192.168.1.200、排除列表、租期默认值 │ └── DHCPLeaseManager.java # 维护 Lease 对象ip、mac、expires、state支持定时扫描过期 ├── relay/ # 中继代理修改 giaddr、转发至远端服务器、回填 hops │ └── DHCPRelay.java # 监听 67 端口收到 client 报文后改 giaddr 并 UDP 转发 └── util/ # 工具类命令行参数解析Apache Commons CLI、日志封装SLF4J提示core/是协议层client/和server/是角色层relay/是网络层扩展。没有 Controller/Service/DAO 分层因为 DHCP 本质是状态机驱动的事件流强行分层反而增加理解成本。2.2 DHCP 报文构造核心从DHCPPacketBuilder到真实以太网帧所有报文生成都通过DHCPPacketBuilder完成。它不拼字符串而是操作字节数组严格对齐 RFC 2131 的字段偏移。比如构造一个 Discover 报文// src/main/java/com/example/dhcp/client/DHCPClient.java#L132 DHCPPacket discover new DHCPPacketBuilder() .setOp(DHCPConstants.OP_BOOTREQUEST) // 0x01 .setHtype(DHCPConstants.HTYPE_ETHERNET) // 0x01 .setHlen((byte) 6) // MAC 长度 .setHops((byte) 0) .setXid(0x1a2b3c4d) // 事务 ID后续 Offer/Ack 必须匹配 .setSecs((short) 0) // 秒数从开始尝试获取 IP 起计 .setFlags((short) 0x8000) // 0x8000 broadcast flag告诉 server 用广播回复 .setCiaddr(IPv4Address.ZERO) // client ip此时为 0.0.0.0 .setYiaddr(IPv4Address.ZERO) // your ipserver 填 Offer 地址 .setSiaddr(IPv4Address.ZERO) // next server ip通常为 0 .setGiaddr(IPv4Address.ZERO) // relay agent ip直连时为 0 .setChaddr(clientMacBytes) // client hardware address (6 bytes) .addOption(new DHCPOptionMessageType(DHCPConstants.MSG_TYPE_DISCOVER)) .addOption(new DHCPOptionParameterRequestList( DHCPConstants.OPTION_SUBNET_MASK, DHCPConstants.OPTION_ROUTER, DHCPConstants.OPTION_DOMAIN_NAME_SERVERS)) .addOption(new DHCPOptionClientIdentifier( DHCPConstants.HTYPE_ETHERNET, clientMacBytes)) .build();这段代码生成的discover对象其toByteArray()输出就是完整的 300 字节 DHCP 报文含 192 字节固定头 可变 Options。关键点在于setFlags((short) 0x8000)是必须的很多初学者忽略此标志导致服务端单播回复而客户端收不到因为 client 还没 IP无法收单播addOption(...)顺序不影响解析但DHCPOptionParameterRequestList必须显式声明想要哪些 Option否则 server 可能不返回 DNS 地址clientMacBytes必须是 6 字节不足补 0超过截断——这是htype1, hlen6的硬约束。2.3 租约状态机DHCPClientState如何驱动整个生命周期客户端不是线性执行而是状态驱动。DHCPClientState枚举定义了 7 个状态每个状态对应一组可接收报文类型和超时行为状态触发条件可接收报文超时动作关键行为INIT启动—1s 后发 Discover清空旧租约生成新 xidSELECTING收到 OfferOffer未设等待 Request记录 Offer准备 RequestREQUESTING发送 Request 后Ack/Nak2s/4s/8s 指数退避重发 Request若 Nak 则回 INIT若 Ack 则写租约BOUND收到 Ack—leaseTime * 0.5后进入 RENEWING启动续约定时器保存 lease 到磁盘RENEWING租约过半Ack/Nak若超时则发新 Request直连 server直接单播给 server ipREBINDINGRENEWING 失败Ack/Nak若超时则回 INIT广播 Request找任意 serverDECLINING收到 Nak 或 IP 冲突—无发送 Decline清空本机租约这个状态机实现在DHCPClient.processPacket()中它根据当前 state 和收到的报文 type 查表跳转// src/main/java/com/example/dhcp/client/DHCPClient.java#L288 private void processPacket(DHCPPacket packet) { switch (currentState) { case INIT: if (packet.getMessageType() DHCPConstants.MSG_TYPE_OFFER) { // 不处理INIT 状态只发 Discover不收 Offer return; } break; case SELECTING: if (packet.getMessageType() DHCPConstants.MSG_TYPE_OFFER packet.getXid() currentXid) { handleOffer(packet); currentState DHCPClientState.REQUESTING; sendRequest(); // 构造 Request 报文并发送 } break; case REQUESTING: if (packet.getMessageType() DHCPConstants.MSG_TYPE_ACK packet.getXid() currentXid) { handleAck(packet); currentState DHCPClientState.BOUND; persistLease(); // 写入 dhcp.lease 文件 startRenewTimer(); // 启动续约定时器 } else if (packet.getMessageType() DHCPConstants.MSG_TYPE_NAK) { currentState DHCPClientState.INIT; // 重来 log.warn(Server NAKed our request, restarting DHCP flow); } break; // ... 其他状态分支 } }注意currentXid是每次 INIT 时随机生成的用于严格匹配同一事务的请求/响应。这是 DHCP 协议防乱序的核心机制也是很多 DIY 实现翻车的第一关——如果忘了校验 xid就会把上一个事务的 Offer 当成本次的导致 IP 分配错乱。3. 运行指南从零编译、抓包验证到服务端部署项目使用 Maven 构建无需额外安装 C 依赖或系统服务。整个流程可在普通 Linux/macOS/WindowsWSL上完成不需要 root 权限除非你要监听 67 端口此时需 sudo。3.1 编译与本地测试无需 root先确认 JDK 版本java -version # 输出应为 java version 1.8.0_301 或更高 mvn -v # 需要 Maven 3.5克隆并编译假设已下载源码到dhcp-java目录cd dhcp-java mvn clean compile # 编译成功后class 文件在 target/classes/运行内置客户端模拟一台新设备接入# 使用默认配置监听 0.0.0.0:68向 255.255.255.255:67 发送 Discover mvn exec:java -Dexec.mainClasscom.example.dhcp.client.DHCPClient \ -Dexec.args--interface eth0 --debug注意--interface eth0指定网卡名Linux 上可用ip link show查看macOS 用en0Windows 用Ethernet。不指定则默认用第一个非 loopback 网卡。此时你会看到日志输出类似[INFO] Sending DHCPDISCOVER (xid0x1a2b3c4d) from 0.0.0.0:68 to 255.255.255.255:67 [DEBUG] Received DHCPOFFER (xid0x1a2b3c4d) from 192.168.1.1:67 [INFO] Selected offer: yiaddr192.168.1.105, lease3600s, server192.168.1.1 [INFO] Sending DHCPREQUEST (xid0x1a2b3c4d) to 192.168.1.1:67 [DEBUG] Received DHCPACK (xid0x1a2b3c4d) from 192.168.1.1:67 [INFO] Lease acquired: 192.168.1.105/24, expires in 3600s3.2 用 Wireshark 抓包验证协议细节这是理解本项目最有效的方式。启动 Wireshark过滤bootp然后运行客户端第一帧DHCP DiscoverSource: 0.0.0.0:68Destination: 255.255.255.255:67Flags0x8000BroadcastOptions 包含Message Type1,Parameter Request List[1,3,6]子网掩码、路由器、DNS第二帧DHCP OfferSource: 192.168.1.1:67Destination: 255.255.255.255:68yiaddr192.168.1.105siaddr192.168.1.1Options 包含Subnet Mask255.255.255.0,Router192.168.1.1第三帧DHCP RequestSource: 0.0.0.0:68Destination: 255.255.255.255:67Options 包含Requested IP Address192.168.1.105,Server Identifier192.168.1.1第四帧DHCP AckSource: 192.168.1.1:67Destination: 192.168.1.105:68注意此时目的 IP 已知可单播Options 同 Offer。你会发现所有报文的chaddrclient hardware address字段都是你本机 MAC且xid四帧一致。这就是协议握手的铁证。3.3 启动内置 DHCP 服务端需 root服务端默认监听0.0.0.0:67Linux/macOS 需要 root# 启动服务端分配 192.168.50.100-192.168.50.200租期 7200 秒 sudo mvn exec:java -Dexec.mainClasscom.example.dhcp.server.DHCPServer \ -Dexec.args--pool 192.168.50.100-192.168.50.200 --lease 7200 --debug参数说明--poolIP 地址池支持单个 IP192.168.50.100或范围192.168.50.100-192.168.50.200--lease默认租期秒数7200 2 小时--exclude排除地址如--exclude 192.168.50.100,192.168.50.101--server-ip指定siaddr和Option 54 Server Identifier默认为本机第一个非 loopback IP。启动后用另一台机器或虚拟机运行客户端即可获得该服务端分配的 IP。服务端日志会显示[INFO] DHCPServer started on 0.0.0.0:67, pool192.168.50.100-192.168.50.200, lease7200s [DEBUG] Received DHCPDISCOVER from 00:11:22:33:44:55 (xid0xabcdef01) [INFO] Offering 192.168.50.100 to 00:11:22:33:44:55 [DEBUG] Sent DHCPOFFER to 255.255.255.255:684. 常见问题排查5 个血泪经验总结出的真实翻车点开发和调试这个项目时我和某高校网络实验室的 A 同学一起踩过不少坑。以下是高频、隐蔽、且文档几乎不提的 5 个问题按「现象 → 原因 → 解决」整理每一条都来自真实抓包日志。4.1 现象客户端一直收不到 OfferWireshark 显示只发了 Discover没回包原因服务端DHCPServer绑定的端口是67但 Linux 默认防火墙ufw/firewalld或云主机安全组拦截了 UDP 67 入站。更隐蔽的是某些发行版如 Ubuntu 22.04的systemd-resolved会占用 53 端口并干扰 67 端口绑定。解决先停掉systemd-resolvedsudo systemctl stop systemd-resolved再检查防火墙sudo ufw status verbose添加规则sudo ufw allow 67/udp云主机务必检查安全组是否放行 UDP 67。4.2 现象客户端收到 Offer但立刻发 Decline日志显示 “IP conflict detected”原因DHCPClient启动时会主动发 ARP 探测yiaddr是否已被占用RFC 2131 3.7 节要求。如果局域网内已有设备用了该 IP比如静态配置ARP reply 会触发 Decline。这不是 bug是协议强制行为。解决确认 IP 池不与现有网络冲突或临时关闭探测修改DHCPClient.java#L198注释掉sendArpProbe()调用仅用于测试。4.3 现象服务端日志显示 “Received DHCPREQUEST but no pending offer for xid0x…”原因客户端发 Request 时Server IdentifierOption54填的是错误 IP比如填了0.0.0.0或其他 server 的 IP导致服务端无法匹配之前发的 OfferOffer 里siaddr和 Request 里 Option 54 必须一致。解决检查客户端DHCPClient.java#L245的addOption(new DHCPOptionServerIdentifier(serverIp))确保serverIp是你服务端的实际 IP不能是127.0.0.1服务端启动时用--server-ip显式指定。4.4 现象租约到期后不自动 Renew客户端卡在 BOUND 状态不动原因startRenewTimer()启动的ScheduledExecutorService被 JVM GC 或异常中断且代码中未设置setRemoveOnCancelPolicy(true)导致定时任务泄漏。更常见的是LeaseStore写磁盘失败目录无权限导致persistLease()抛异常后续startRenewTimer()被跳过。解决在persistLease()周围加 try-catch 并打 error 日志检查dhcp.lease所在目录权限chmod 755在startRenewTimer()后加一行log.info(Renew timer scheduled for {}s, renewTime)确认执行。4.5 现象中继模式下客户端收到 Ack但giaddr字段为空0.0.0.0原因DHCPRelay收到客户端报文后必须将giaddr设为中继自身接口 IP并将hops再转发给远端 server。但如果中继机器有多个 IPDHCPRelay.java默认取第一个非 loopback IP可能取错比如取了 docker0 的 172.17.0.1。解决启动中继时用--relay-ip 192.168.10.1显式指定中继 IP或修改DHCPRelay.getRelayIp()方法用NetworkInterface.getByInetAddress()精确查找目标网卡。5. 二次开发实战3 个高价值改造方向与落地代码这个项目最大的价值不是“能跑”而是“好改”。它的模块边界清晰、无隐藏状态、所有协议逻辑裸露在 Java 代码里。我把它用在某跨平台 IoT 网关项目中做了三个关键改造全部已上线稳定运行 18 个月。下面给你可直接抄的方案。5.1 改造一支持 DHCP Option 125Vendor-Specific Information解析与注入工业设备常通过 Option 125 传递厂商信息如设备型号、固件版本。原项目不支持需手动解析 TLV 结构。步骤在core/下新建DHCPOption125.java// src/main/java/com/example/dhcp/core/DHCPOption125.java public class DHCPOption125 implements DHCPOption { private final byte[] enterpriseNumber; // 3 bytes, big-endian private final byte[] subOptions; // raw sub-option data public DHCPOption125(byte[] enterpriseNumber, byte[] subOptions) { this.enterpriseNumber Arrays.copyOf(enterpriseNumber, 3); this.subOptions subOptions.clone(); } Override public byte getType() { return (byte) 125; } Override public byte getLength() { return (byte) (3 subOptions.length); // 3-byte EN sub-options } Override public byte[] getData() { byte[] data new byte[3 subOptions.length]; System.arraycopy(enterpriseNumber, 0, data, 0, 3); System.arraycopy(subOptions, 0, data, 3, subOptions.length); return data; } // 解析静态方法供服务端读取 public static DHCPOption125 parse(byte[] data) { if (data.length 3) return null; byte[] en new byte[3]; System.arraycopy(data, 0, en, 0, 3); byte[] subOpts new byte[data.length - 3]; System.arraycopy(data, 3, subOpts, 0, subOpts.length); return new DHCPOption125(en, subOpts); } }在DHCPServer.handleRequest()中解析// src/main/java/com/example/dhcp/server/DHCPServer.java#L188 DHCPOption125 opt125 null; for (DHCPOption opt : packet.getOptions()) { if (opt.getType() 125) { opt125 DHCPOption125.parse(opt.getData()); break; } } if (opt125 ! null) { String vendorInfo parseVendorInfo(opt125.getSubOptions()); // 自定义解析逻辑 log.info(Device vendor info: {}, vendorInfo); // 可据此分配不同 IP 段或下发定制 Option }提示Option 125 的 sub-options 是嵌套 TLV需递归解析。我一般用parseVendorInfo()提取前 4 字节作为设备类型码再查表返回策略。5.2 改造二租约存储对接 Redis实现多实例服务端共享 IP 池单机IPPool无法支撑集群部署。我们用 Redis 的SETNXEXPIRE实现分布式锁和租约过期。步骤添加 Redis 依赖pom.xmldependency groupIdredis.clients/groupId artifactIdjedis/artifactId version4.4.3/version /dependency实现RedisLeaseStore替换默认LeaseStore// src/main/java/com/example/dhcp/server/RedisLeaseStore.java public class RedisLeaseStore implements LeaseStore { private final JedisPool jedisPool; private final String leaseKeyPrefix dhcp:lease:; public RedisLeaseStore(String redisHost, int redisPort) { this.jedisPool new JedisPool(redisHost, redisPort); } Override public boolean storeLease(String mac, String ip, long leaseTimeSeconds) { try (Jedis jedis jedisPool.getResource()) { String key leaseKeyPrefix mac; // SETNX EXPIRE 原子操作避免竞态 String result jedis.set(key, ip, SetParams.setParams().nx().ex(leaseTimeSeconds)); return OK.equals(result); } } Override public String getLease(String mac) { try (Jedis jedis jedisPool.getResource()) { return jedis.get(leaseKeyPrefix mac); } } Override public void releaseLease(String mac) { try (Jedis jedis jedisPool.getResource()) { jedis.del(leaseKeyPrefix mac); } } }修改DHCPServer初始化逻辑注入RedisLeaseStore。注意Redis 的SETNX是原子的但EXPIRE不是。所以必须用SET key value NX EX seconds一条命令完成jedis 4.4 支持SetParams。老版本需用 Lua 脚本。5.3 改造三客户端增加 “静默模式”不配置系统网络只返回 IP 信息供上层应用使用某些场景如 Android TV App、车载中控不允许 Java 进程直接调ifconfig只需知道分配到的 IP 和网关由系统层配置。步骤在DHCPClient新增SilentCallback接口// src/main/java/com/example/dhcp/client/DHCPClient.java public interface SilentCallback { void onLeaseAcquired(String ip, String netmask, String gateway, String dns); void onError(String errorMsg); }修改handleAck()不调用configureNetworkInterface()而是回调private void handleAck(DHCPPacket ack) { String ip ack.getYiaddr().toString(); String netmask getOptionValue(ack, DHCPConstants.OPTION_SUBNET_MASK); String gateway getOptionValue(ack, DHCPConstants.OPTION_ROUTER); String dns getOptionValue(ack, DHCPConstants.OPTION_DOMAIN_NAME_SERVERS); if (silentCallback ! null) { silentCallback.onLeaseAcquired(ip, netmask, gateway, dns); } }启动时传入回调DHCPClient client new DHCPClient(eth0); client.setSilentCallback(new SilentCallback() { Override public void onLeaseAcquired(String ip, String netmask, String gateway, String dns) { System.out.printf(Got IP: %s/%s, GW: %s, DNS: %s%n, ip, netmask, gateway, dns); // 此处可调用 Android ConnectivityManager 或 iOS NetworkExtension API } }); client.start();从那以后我每次做网络协议模块都强制走一遍 Wireshark 抓包 手动校验 xid 和 flags。这项目教会我的不是 DHCP 本身而是“协议栈的每一字节都值得怀疑”——当你亲手把0x8000写进 flags 字段才真正读懂了广播的含义。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/6 3:54:08

Windows全平台离线更新下载工具:构建内网可信补丁仓库

简介:这是一款面向Windows系统管理员、IT运维工程师及批量部署技术人员的离线补丁管理工具,专为解决新装系统后需耗费数小时手动下载安装更新的痛点而设计。工具支持全系列Windows桌面与服务器操作系统(XP至8.1、Server 2003至2012 R2&#x…

2026/11/6 3:49:08

区块链网络访问控制系统:身份角色、权限策略与审计追溯

商品编号:000055区块链在网络访问控制中的应用本项目围绕身份、角色、访问策略与审计日志组织管理功能,采用链上链下协同方案。后端使用Java与Spring Boot,前端使用Vue 3、Element Plus,配套MySQL和FISCO BCOS联盟链。【功能展示】…

2026/11/6 6:19:25

Spyglass:CDC官方Hands-on Training(三)

相关阅读 Spyglasshttps://blog.csdn.net/weixin_45791458/category_12828934.html?spm1001.2014.3001.5482 本文是对Spyglass CDC Hands-on Training中第一个实验的翻译(有删改),Lab文件可以从以下链接获取。 Spyglass CDC Hands-on Trai…

2026/11/6 6:19:25

什么是升压ic

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 6:19:25

Java中关于HashSet常见问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 6:19:25

Docker + IDEA 一键部署!

Docker IDEA 一键部署! 作为 3 Year全栈开发者相比对Docker一定很熟悉,镜像容器化技术,就不过多介绍了! 很久之前做的 Docker 入门学习Blog ,现在回头看发现有点过于臃肿,后期会总结一套常用命令&#x…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 1:03:39

MCU晶振电路设计全解析:从负载电容计算到PCB布局调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑