基于DotNetty的JT808网关Demo:从9623端口到协议解析实战
简介这是一份面向物联网与车联网开发者的服务端源码资源基于DotNetty与Spring Boot思路实现JT/T 808部标协议解析适合想学习部标协议、Netty通信与C#服务端开发的初中级工程师参考。压缩包共82个文件约1.37MB以26个cs源码文件为核心辅以json配置、dll与pdb程序集、txt说明、sln与csproj工程文件以及NetAssist网络调试工具和JT808-2013协议PDF文档目录按数据网关、GPS、公共模块等分层组织。程序默认监听9623端口可直接运行并用调试助手联调已解析部分JT808指令应答逻辑尚待完善代码风格偏随性但结构完整。目前已有453人学习下载。读者可借此理解部标协议的报文结构与解析流程参考DotNetty搭建Tcp Server的实践方式并对照Java版jt-808-protocol实现思路快速搭建自己的物联网信息网关原型。1. 拆开这个 JT808 网关压缩包为什么我建议你先跑通 9623 端口再谈架构如果你手头正好有一批车载终端要接入或者在做物联网工程毕业设计时被 JT/T 808 协议里那一堆消息体、转义字符、校验码折腾得头大这个物联网开发信息网关DotNetty.zip值得你花一个下午拆开看看。它不是一份只讲概念的 PPT而是一个能直接跑起来的 C# 服务端 Demo基于 DotNetty 实现 TCP 监听解析了 JT808 的部分指令默认端口 9623包里还塞了一份 2013 版的协议 PDF 和一个网络调试助手。说白了它解决的是「我想用 .NET 生态快速验证一个部标网关原型但不想从零撸 Netty 线程模型」这件事。适合谁写过一点 C#、知道 Socket 大概怎么回事、想拿现成骨架改出自己网关的开发者。新手能照着把终端连上来看到心跳熟手能直接翻JT808DataServer里的解析逻辑判断这套代码值不值得作为二次开发底座。2. DotNetty 与 JT808 协议栈先搞懂数据怎么从终端流到网关2.1 为什么是 DotNetty 而不是裸 Socket很多人第一反应是「TCP 服务端我自己写个TcpListener不就行了」。单连接、低并发确实可以但 JT808 网关面对的是几十上百台车载终端长连接每台终端会不定时上报位置、心跳、报警还要处理粘包拆包。裸 Socket 你得自己管线程池、自己拼缓冲区、自己处理半包写到最后往往是一堆while(true)加byte[]拼接调试起来就是黑匣子。DotNetty 是 Netty 的 .NET 移植核心价值在于它把 Reactor 线程模型、ChannelPipeline、ByteBuf 这些成熟抽象搬了过来。你只需要往 Pipeline 里挂 Handler剩下的连接管理、读写事件分发它帮你兜住。这个 Demo 选 DotNetty 而不是原生 Socket本质上是选了一套「已经被 Java 生态验证过的网络层骨架」省掉的是最枯燥也最容易出 bug 的那部分。2.2 JT808 消息结构拆解从 7E 到 7E 之间到底装了什么JT/T 808 是部标协议终端和平台之间的每条消息都长这样7E | 消息头 | 消息体 | 校验码 | 7E消息头里包含消息 ID、消息体属性、终端手机号、消息流水号。消息体属性那 16 个 bit 里藏着消息体长度和是否分包。校验码是从消息头第一个字节到消息体最后一个字节的异或。这里有个新手最容易翻车的点转义处理。协议规定消息头、消息体、校验码里出现的0x7E要转成0x7D 0x02出现的0x7D要转成0x7D 0x01。也就是说你收到的原始字节流里真正的帧边界7E和内容里的7E是两码事必须先做反转义才能解析。这个 Demo 里JT808DataServer目录下的解析代码核心就是干这件事找到帧头帧尾、反转义、校验、再按消息 ID 分发。它参考了 Java 版 jt-808-protocol 的思路但用 C# 重写语法上确实清爽不少。2.3 环境准备与首次运行把 9623 端口拉起来动手之前确认你机器上有 .NET 运行环境。这个项目是较早期的 csproj 格式用 Visual Studio 打开JT808DataServer.sln最省事或者用dotnetCLI 也行。# 进入解决方案目录 cd JT808DataServer # 还原依赖并编译 dotnet restore dotnet build # 运行服务端默认监听 9623 dotnet run --project JT808DataServer.csproj如果你用 Visual Studio直接打开 sln把JT808DataServer设为启动项目F5 运行。程序起来后不会有什么花哨界面就是一个控制台窗口等着终端连上来。端口在哪里改作者说了在Program.cs的Main方法里。你打开会看到类似new TcpServer(9623)或者ServerBootstrap绑定端口的代码把数字改掉重新编译即可。我一般会把它抽到配置文件里但 Demo 阶段直接改代码最快。2.4 用 NetAssist 模拟终端发一条心跳看看网关反应包里Tool\NetAssist.exe就是网络调试助手双击打开选 TCP Client目标 IP 填127.0.0.1端口填9623点连接。连上之后你需要手动构造一条 JT808 消息发过去。以终端心跳消息 ID0x0002为例一条最简单的报文大致是7E 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7E当然实际消息头有固定长度手机号、流水号都得填。你可以先用这个思路在 NetAssist 里以十六进制发送观察服务端控制台有没有打印出解析日志。如果服务端能识别出消息 ID 并打印说明整条链路通了。提示NetAssist 发送时记得勾选「十六进制发送」否则你发的是 ASCII 字符网关收到的字节完全对不上。这一步的意义在于你不需要真车真终端就能验证网关的收包、反转义、解析逻辑是否正常。很多毕业设计卡在「没有硬件」其实用调试助手模拟就够了。3. 代码结构与解析流程从 Program.cs 到消息分发3.1 目录结构速览每个文件夹负责什么解压后你会看到这些内容路径作用JT808DataServer/服务端主项目核心代码都在这里JT808DataServer/Program.cs入口创建 DotNetty 服务端、绑定端口JT808DataServer/Common/公共类可能包含常量、工具方法JT808DataServer/GPS/与定位相关的处理逻辑JT808-2013.pdf部标协议原文查消息体格式必备Tool/NetAssist.exe网络调试助手模拟终端用data.png可能是数据流或界面截图README.md作者写的简要说明.vs、bin、obj是 Visual Studio 生成的不用管。.gitignore说明这原本是个 git 仓库。3.2 服务端启动流程ServerBootstrap 都配了什么DotNetty 服务端的标准套路在Program.cs里。核心代码结构大概是这样// 创建主从线程组 var bossGroup new MultithreadEventLoopGroup(1); var workerGroup new MultithreadEventLoopGroup(); var bootstrap new ServerBootstrap(); bootstrap .Group(bossGroup, workerGroup) .ChannelTcpServerSocketChannel() .Option(ChannelOption.SoBacklog, 128) .ChildHandler(new ActionChannelInitializerISocketChannel(channel { var pipeline channel.Pipeline; // 添加解码器处理粘包和转义 pipeline.AddLast(new JT808Decoder()); // 添加业务处理器 pipeline.AddLast(new JT808ServerHandler()); })); // 绑定端口默认 9623 var boundChannel await bootstrap.BindAsync(9623);bossGroup负责接受连接workerGroup负责处理 IO 读写。SoBacklog是等待队列长度128 对 Demo 足够。关键是ChildHandler里挂的两个 Handler一个解码器负责把字节流切成完整的 JT808 帧一个业务处理器负责根据消息 ID 做具体响应。参数怎么调如果你要压测可以把workerGroup的线程数显式指定比如new MultithreadEventLoopGroup(4)。SoBacklog在高并发接入时可以调到 1024。但这些是进阶操作先跑通再说。3.3 解码器里的三个关键动作找帧、反转义、验校验解码器是整个网关最核心也最容易写错的地方。JT808 的帧没有长度前缀全靠7E界定所以解码器必须处理粘包和半包。常见做法是继承ByteToMessageDecoder在Decode方法里循环查找完整的帧。protected override void Decode(IChannelHandlerContext context, IByteBuffer input, Listobject output) { while (input.ReadableBytes 2) { // 1. 找到第一个 0x7E 作为帧头 // 2. 从帧头之后找到下一个 0x7E 作为帧尾 // 3. 截取中间内容做反转义 // 4. 计算校验码与报文中的校验码比对 // 5. 校验通过则封装成 JT808Message 对象加入 output } }反转义的逻辑是遇到0x7D 0x02还原成0x7E遇到0x7D 0x01还原成0x7D。校验码是异或运算从消息头第一个字节异或到消息体最后一个字节。这里有个血泪经验反转义必须在找帧尾之前做还是之后做正确顺序是先用原始字节找到帧边界再对帧内容做反转义。如果你先反转义再找帧尾内容里原本转义过的7E会提前暴露导致帧边界判断错误。这个坑我在早期项目里踩过表现就是偶尔能解析、偶尔报校验错查了半天才发现顺序反了。3.4 消息分发与应答0x0002 心跳和 0x0200 位置上报怎么处理解码器产出完整的JT808Message后业务 Handler 的ChannelRead方法会被调用。这里根据消息 ID 做 switch 分发public override void ChannelRead(IChannelHandlerContext ctx, object msg) { var message msg as JT808Message; if (message null) return; switch (message.MessageId) { case 0x0002: // 心跳 HandleHeartbeat(ctx, message); break; case 0x0200: // 位置信息汇报 HandleLocationReport(ctx, message); break; default: // 未实现的指令打印日志 Console.WriteLine($未处理的消息ID: 0x{message.MessageId:X4}); break; } }心跳的应答很简单把终端手机号和流水号原样返回消息 ID 用0x8001平台通用应答。位置上报的应答也是0x8001但如果你要实现更复杂的交互比如下发参数设置就需要构造对应的下行消息。作者在 README 里说「应答部分暂时未弄完」所以你可能发现某些指令只打印不回复。这不影响你理解流程反而留出了二次开发的空间。我一般会先把0x8001通用应答补全这是所有交互的基础。4. 避坑与排查那些让网关「看起来通了其实没通」的细节4.1 终端连上了但服务端没日志检查 Pipeline 是否挂载成功现象NetAssist 显示 TCP 连接已建立但服务端控制台一片安静没有任何输出。原因最常见的是ChildHandler里的ActionChannelInitializer没有正确执行或者 Handler 被标记为[Sharable]但内部有状态导致异常被吞掉。另一个可能是端口被占用服务端其实没绑上但 DotNetty 的异常没抛到控制台。解决在BindAsync后面加await并捕获异常确认绑定成功。在 Handler 的ChannelActive方法里加一行Console.WriteLine(客户端已连接)确认连接事件触发了。如果连接事件有但ChannelRead没有检查解码器是否在等待更多字节——半包时解码器会缓存数据不输出这是正常的。4.2 校验码总是对不上转义顺序和异或范围搞错了现象服务端收到消息后打印「校验失败」但用其他工具算出来的校验码明明是对的。原因两个高频错误。一是反转义和校验的顺序错了应该先反转义再算校验因为校验码是基于原始内容转义前计算的。二是异或范围搞错了校验码是从消息头第一个字节到消息体最后一个字节不包括帧头和帧尾也不包括校验码本身。解决拿JT808-2013.pdf里的示例报文对照手动算一遍异或值。在代码里把校验计算单独抽成一个方法输入是byte[]输出是byte方便单元测试。4.3 粘包导致解析错乱一次收到多条消息怎么切现象终端快速连发几条消息服务端解析出来的消息 ID 是乱的或者报「消息体长度不匹配」。原因TCP 是流式协议没有消息边界。终端可能把两条 JT808 消息粘在一个 TCP 包里发过来解码器的while循环没有正确处理剩余字节。解决确保解码器在Decode方法里用while循环每次消费完一条完整消息后继续检查input里是否还有数据。如果剩余字节不足以构成一条完整消息就跳出循环等待下次数据到达。DotNetty 的ByteToMessageDecoder会自动管理input的读指针你只需要保证每次Read和SkipBytes配对正确。4.4 消息体长度字段与实际不符分包标志位没处理现象解析位置上报时消息体解析到一半就报数组越界。原因JT808 的消息体属性字段里有一个 bit 表示「是否分包」。如果终端发了分包消息而你按不分包去解析长度自然对不上。这个 Demo 可能没有完整实现分包重组。解决先确认你的终端是否开启了分包。如果开了要么在终端侧关掉要么在网关侧实现分包缓存与重组。分包重组需要按终端手机号和流水号做键把同一消息的多个包缓存起来等收齐后再合并。这是进阶功能Demo 阶段可以先不支持但要知道这个坑的存在。5. 从 Demo 到可用网关补全应答链路与压力验证5.1 补全 0x8001 通用应答让终端知道你在听Demo 里应答没写完但0x8001是所有 JT808 交互的基石。终端发心跳、位置、报警平台都要回一条通用应答否则终端可能认为平台离线触发重连或缓存补传。构造0x8001消息的步骤消息 ID 用0x8001消息体里放「应答的消息 ID2 字节 应答的消息流水号2 字节 结果1 字节0 表示成功」。消息头里的终端手机号从原消息里取流水号自己生成一个递增的。最后算校验、加帧头帧尾、转义、发送。// 构造通用应答的伪代码 var response new JT808Message { MessageId 0x8001, TerminalPhone originalMessage.TerminalPhone, SerialNumber GetNextSerialNumber(), Body new byte[] { (byte)(originalMessage.MessageId 8), (byte)(originalMessage.MessageId 0xFF), (byte)(originalMessage.SerialNumber 8), (byte)(originalMessage.SerialNumber 0xFF), 0x00 // 成功 } }; // 然后编码、转义、发送补全这一步之后你用 NetAssist 发心跳应该能收到一条7E 80 01 ...的回复。看到回复才算真正跑通了闭环。5.2 用脚本模拟多终端并发验证线程模型是否扛得住单连接跑通不代表网关能用。真实场景下几十台终端同时在线每台每隔几十秒发一次心跳和位置。你可以写个简单的 Python 脚本模拟多终端import socket import threading import time def simulate_terminal(terminal_id): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((127.0.0.1, 9623)) # 构造心跳报文这里简化处理 heartbeat bytes.fromhex(7E000200000000000000000000000000007E) while True: s.send(heartbeat) time.sleep(30) # 启动 50 个模拟终端 for i in range(50): t threading.Thread(targetsimulate_terminal, args(i,)) t.start()跑起来后观察服务端控制台的输出频率和内存占用。如果出现大量「未处理的消息ID」或者解析异常说明并发下解码器有状态竞争问题。DotNetty 的 Handler 默认不是线程安全的如果多个 Channel 共享一个 Handler 实例需要加[Sharable]并确保无状态或者每个 Channel 创建独立实例。5.3 日志与监控别让网关变成黑匣子Demo 里用Console.WriteLine打日志生产环境肯定不够。我一般会做两件事一是把原始字节流和解析结果都记下来方便事后排查二是加一个简单的统计比如当前连接数、每秒消息数、校验失败次数。原始字节流记录要注意别把日志撑爆可以只记录最近 N 条或者按终端手机号分文件。解析结果记录消息 ID、手机号、时间戳就够了。统计信息可以定时打印比如每 10 秒输出一次「当前连接数X收包Y校验失败Z」。这些改动不大但能让网关从「跑起来看看」变成「敢放在那儿跑几天」。从那以后我每次接手一个网络服务 Demo都强制先加日志和统计再谈功能不然出了问题连后悔药都没得吃。5.4 协议版本差异2013 和 2019 的消息体别混用包里附的是JT808-2013.pdf但市面上不少终端已经支持 2019 版。两版在消息体上有差异比如位置附加信息、报警标志位的定义。如果你拿 2013 的解析逻辑去解 2019 的报文可能某些字段读出来是错的但校验能过因为校验只算字节异或不管语义。常见做法是先确认终端支持的协议版本然后在网关里做版本适配。简单点可以按终端手机号段区分复杂点可以在登录消息里协商版本。Demo 阶段先用 2013 跑通等要接真实终端了再对着终端厂商的协议文档逐条核对。希望这个拆包笔记帮到你少走点我当年走过的弯路。本文还有配套的精品资源点击获取

相关新闻

可控多标签视频安全检测:Tversky策略优化实战

可控多标签视频安全检测:Tversky策略优化实战

1. 项目概述:这不是一个“打标签”的简单任务,而是一场对视频安全边界的动态校准“Controllable Multi-label Video Safety Detection via Adaptive Tversky Policy Optimization”——这个标题乍看像一串学术密码,但拆开来看,它直…

2026/10/5 8:13:01 阅读更多 →
随机森林回归气温预测实战:从特征工程到超参数调优的完整方案

随机森林回归气温预测实战:从特征工程到超参数调优的完整方案

简介:这份资源是面向高校学生与开发者的Python随机森林气温预测项目源码,适用于毕业设计、课程设计及机器学习入门实践。项目以气温预测为场景,利用随机森林处理湿度、气压、风速等多变量间的复杂关系,帮助读者理解数据预处理、特…

2026/10/5 8:13:01 阅读更多 →
PTA图遍历核心:邻接矩阵与邻接表存储及DFS/BFS实战详解

PTA图遍历核心:邻接矩阵与邻接表存储及DFS/BFS实战详解

1. 图的两大存储方案:邻接矩阵与邻接表的选择逻辑 说实话,每次在PTA上碰到图的题目,很多同学的第一反应是"直接开干",结果往往在存储方式这一步就走错了方向。我自己带过不少准备天梯赛和数据结构课程设计的同学&#x…

2026/10/5 8:12:01 阅读更多 →

最新新闻

Java零基础入门:周末大总结

Java零基础入门:周末大总结

import java.util.Scanner; public class SummaryTry { public static void main(String[] args) { 综合编程题 11:简易学生成绩管理系统(控制台版)】 结合本周所有知识点,完成以下需求: 使用 Scanner 输入3名学生的姓…

2026/10/5 9:23:55 阅读更多 →
AI辅助芯片选型:从痛点拆解到实战工作流

AI辅助芯片选型:从痛点拆解到实战工作流

芯片选型的AI工具,现在其实是个“看着热闹、用着别扭”的领域。真干过硬件的人都知道,上午还在为选一颗合适的LDO翻三个分销商网站,下午就可能因为某颗MCU的交期变成52周而推翻整版方案。最近AI工具的声量很大,但能正经回答“帮我…

2026/10/5 9:23:55 阅读更多 →
自注意力+对抗生成网络:深度子空间聚类实战解析

自注意力+对抗生成网络:深度子空间聚类实战解析

简介:一份关于基于自注意力对抗的深度子空间聚类研究的Word文档,面向机器学习、计算机视觉等领域的研究者和学习者,系统阐述如何融合自注意力机制与对抗网络来提升高维数据聚类的鲁棒性。文档从聚类基础与经典算法讲起,涵盖稀疏子…

2026/10/5 9:23:55 阅读更多 →
分数阶时滞神经网络稳定性分析:Caputo导数、LMI判据与数值验证

分数阶时滞神经网络稳定性分析:Caputo导数、LMI判据与数值验证

简介:这份PDF文献《含有离散时滞及分布时滞分数阶神经网络的渐近稳定性分析》面向从事神经网络、深度学习与数据建模方向的研究生、教师及工程技术人员,聚焦分数阶神经网络在同时含离散时滞与分布时滞情形下的渐近稳定性难题。文章在Caputo导数意义下构造…

2026/10/5 9:23:55 阅读更多 →
从爬虫到可视化:豆瓣读书Top250全栈数据项目实战

从爬虫到可视化:豆瓣读书Top250全栈数据项目实战

做这个豆瓣读书 Top250 全栈数据项目,起因其实特别朴素,就是想给简历里添一个能从头讲到尾的实战项目。python爬虫负责采数据,pandas做数据清洗,MySQL落库,Java后端提供接口,前端再用ECharts把图书数据渲染…

2026/10/5 9:23:55 阅读更多 →
Google AX 开源:用声明式 YAML 编排十亿级 AI Agent 任务

Google AX 开源:用声明式 YAML 编排十亿级 AI Agent 任务

1. 从“一天一个开源项目”聊起:为什么 AX 值得单独写一篇做 Agent 开发这两年,我最大的感受就是:写一个能跑的 Agent 不难,难的是让一千个、一万个甚至更多 Agent 稳定地跑起来、跑对、跑完还能查账。单机跑个 ReAct 循环&#x…

2026/10/5 9:22:54 阅读更多 →

日新闻

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

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

2026/10/5 0:00:22 阅读更多 →
AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

1. 从“plugins”这个词说起:它到底在解决什么问题如果你最近在折腾 AI 编程工具,尤其是 Cursor、Codex CLI、Claude Code 这类带 CLI 的编辑器或命令行助手,那你大概率绕不开一个词——plugins。这个词本身不新鲜,从浏览器到 IDE…

2026/10/5 0:00:23 阅读更多 →
第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

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

2026/10/5 0:00:23 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/5 5:06:42 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/5 1:10:22 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/5 3:06:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/4 11:40:45 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/4 20:14:29 阅读更多 →