news 2026/9/13 7:11:36

C#上位机与松下PLC串口通讯实战:Mewtocol协议解析与机器视觉集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#上位机与松下PLC串口通讯实战:Mewtocol协议解析与机器视觉集成

做机器视觉上位机开发的,早晚要面对一件事:视觉结果怎么交给产线去执行。相机拍完照、算法出结果,OK还是NG如果不送到PLC里,后面的剔除机构、气缸、报警灯全都不会动。我在实际项目里最常用的方案,就是用C#写上位机,通过串口跟Panasonic松下PLC做通讯,把视觉判定结果写进PLC的寄存器,同时读取产线当前状态。这篇文章把我自己在松下FP系列PLC串口通讯上的完整经验整理出来,从Mewtocol协议、报文格式、C#代码实现,再到现场踩过的坑,一次性讲透。适合正在做机器视觉集成、或者手里有松下PLC想接上位机的朋友,也适合第一次接触工控通讯、对着协议手册一头雾水的C#开发者。内容偏实战,看完能直接照着写。

1. 项目背景与整体设计思路

1.1 为什么是“串口通讯”而不是网口

先聊一个方向问题。现在很多PLC都带以太网口,松下FP系列里也有一部分型号支持TCP/IP通讯,那为什么还要用串口?答案很简单:老设备存量太大,串口方案便宜又稳定。

我接触过的不少产线,PLC型号是FP0R、FP-X这类老款,默认配置就是COM口,没有网口,或者网口被厂家程序占用了。就算有新设备,一个RS232/RS485串口模块也就几十块钱,而上位机这边随便一个USB转串口就能解决,布线和配置都简单。对于视觉结果这种“每次就传几字节”的场景,串口的速度完全够用——哪怕9600波特率,一秒也能传几百个数据帧,视觉检测的节拍一般在几十毫秒到几百毫秒,串口根本不是瓶颈。

所以方向选择上我的建议是:能用串口解决的问题,优先串口;等到需要大数据量、多设备组网的时候,再考虑以太网。这跟有些人一上来就追求高大上不一样,工控项目里稳定和够用永远是第一位的。

1.2 机器视觉与PLC的典型交互流程

机器视觉上位机跟PLC的交互,说白了就是三件事:收指令、给结果、查状态。

视觉相机拍照的触发信号,往往就是PLC通过串口发给上位机的。比如PLC检测到产品到位,发送“请拍照”的命令,上位机收到之后控制相机拍照、跑算法,得出OK/NG结果,再写回PLC。PLC拿到结果之后驱动气缸把不合格品剔除。整个过程像一条流水线,两边的配合完全靠通讯协议维持。

这类交互里,有几个关键点要注意:命令要带超时判断,因为相机拍照偶尔会失败;结果写入要确认,不能发出去就不管了;循环采集状态下UI不能卡顿,不然操作员看界面会很难受。这些我在第3章和第4章都会详细展开。

1.3 松下PLC串口通讯的协议选择:Mewtocol

松下PLC的串口通讯有它自己的一套协议,叫Mewtocol,官方手册全称是《松下电工 可编程控制器FP系列 通信命令手册》。ASCII格式,报文清晰,调试起来比Modbus RTU那堆十六进制字符直观多了。

注意,Mewtocol跟Modbus不是一回事。C#社区里常用的NModbus4库,那是走Modbus协议用的。松下FP系列老型号的串口默认走Mewtocol,如果你想用Modbus,得看具体型号支不支持,而且还要在PLC系统寄存器里改协议类型。所以做松下串口,先老老实实把Mewtocol用熟练,这是基本功。

还有一点,Mewtocol不仅支持读取写入数据寄存器(D区),还支持直接读线圈触点(X、Y、R区),甚至可以远程操作PLC的程序状态。这意味着上位机能做的操作非常灵活,不只是传数据,还可以控制PLC启停、读取运行状态,这对产线上位机来说非常实用。

2. Mewtocol报文协议核心拆解

2.1 帧格式与BCC校验原理

Mewtocol的报文格式非常固定,读懂了就一通百通。一帧完整的命令由这几部分组成:

  • 起始符:%(ASCII 0x25),表示这是一条上位机发出的命令;如果返回的响应帧起始符是%,表示正常响应,也可能是!,那代表出错了,后面会说。
  • 站号:两位十进制数,范围00到99。PLC侧有对应的系统寄存器设置,默认是01,可以改。如果你的产线上挂了多台松下PLC,靠站号区分。
  • 命令类型:两个大写字母,比如RD读数据、WD写数据、RCS读触点、WCS写触点。
  • 文本区:地址、数量、数据等具体信息。
  • BCC校验:这是松下Mewtocol特别有意思的地方,它不像Modbus RTU用CRC16,而是用非常简单的异或(XOR)。计算方法:从“%”之后、BCC之前的所有字符,按ASCII码逐字节异或,得到一个8位字节,再转成两位大写十六进制ASCII字符,拼到帧尾部。
  • 终止符:回车符\r(0x0D)。

我举个例子,读Y0触点状态的命令,完整帧是这样的:

%01#RCS050\r

拆开来看:%起始符,01站号,#固定分隔符,RCS读触点命令,0是触点编号,50是BCC校验。BCC怎么来的?把“01#RCS0”这7个字符的ASCII码逐个异或,正好算出0x50,转换成ASCII字符就是“50”。

C#里计算BCC就一行代码的事,后面代码部分会写。但如果用串口调试助手手工发命令验证,这个手算过程还是得会,不然现场临时没电脑、没法跑程序的时候就抓瞎了。

2.2 常用命令详解:读数据、写数据、读写触点

Mewtocol的命令不少,但我实际项目中90%以上只用四个:RD、WD、RCS、WCS。

RD(读数据寄存器):格式是%站号#RD起始地址 数据个数。比如读D100开始的一字数据,地址用四位十六进制表示,命令帧就是%01#RD01000001**\r,其中0100是D100的地址,0001是读取1个字。正常响应格式是%01$RD0100数据**\r,比如返回%01$RD01001234**\r,那D100的当前值就是0x1234。

WD(写数据寄存器):格式是%站号#WD起始地址 数据个数 数据内容。比如要把D100写成1,命令帧就是%01#WD010000010001**\r,写完之后PLC会返回一个不带数据区的响应帧%01$WD**\r,收到它就知道写入成功了。

RCS/WCS(读/写单个触点):触点包括输入继电器X、输出继电器Y、内部继电器R。比如读Y0输出状态,发%01#RCS0**\r,返回%01$RC0状态 校验,状态是0就是OFF,1就是ON。写Y0同理,%01#WCS01**\r表示把Y0置ON。

这里有个细节,D区地址从0000开始映射,R区地址规则跟D区不完全一样,不同型号也略有差异。我一般直接参考对应型号的手册里的地址映射表,别靠记忆,容易踩坑。比如老款FP0的D区和FP-X的D区,在某些特殊地址上就不同。

2.3 寄存器地址映射与数据类型

Mewtocol里,寄存器地址映射这件事,新手最容易懵。其实逻辑不复杂:数据寄存器D、链接寄存器L、文件寄存器FL等,都有各自的编号规则。以最常见的D区为例,D100在协议里对应的地址就是0100,直接用十进制转十六进制就行。而W(写入)指令发送的数据区,前面说的四个十六进制字符表示一个字,它是一个无符号十六进制数。

做视觉上位机的时候,我通常会把“检测结果”“产品编号”“测试时间戳”这些放到固定的D区地址。比如约定D100 = 当前产品结果(1表示OK,2表示NG),D101 = 产品计数,D102-D110 = 根据实际需要扩展。这样PLC工程师那边也好写,上位机这边也好维护。

另外,数据类型转换方面要留意:从PLC读回来的原始数据是字符串形式的十六进制,C#里要转成ushort、int或者float,要根据PLC里面存的是什么类型来决定。如果PLC那边是32位浮点,那就得连续读两个寄存器,再手动拼成float,这个我在代码部分会给出示例。

3. C#串口通讯模块的完整实现

3.1 搭建一个松下PLC通讯类

我用C#写松下串口通讯,一般会封装成一个独立的类库,比如叫PanasonicMewtocolClient。这样视觉算法、业务逻辑、UI层都可以调用它,而且以后换PLC型号,只要改这个类就行。

先看最基础的结构,包括串口初始化、打开关闭、BCC计算、报文拼接这几个功能:

using System; using System.IO.Ports; using System.Text; public class PanasonicMewtocolClient : IDisposable { private SerialPort _port; private byte _stationNo = 0x01; // 站号,默认1 private readonly object _lockObj = new object(); public PanasonicMewtocolClient(string portName, int baudRate = 9600) { _port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _port.ReadTimeout = 500; _port.WriteTimeout = 500; } public void Open() { if (_port.IsOpen) return; _port.Open(); } public void Close() { if (_port != null && _port.IsOpen) _port.Close(); } /// <summary> /// 计算BCC校验:从%之后到BCC之前的字符做异或 /// </summary> private byte CalculateBcc(string framePart) { byte result = 0; byte[] bytes = Encoding.ASCII.GetBytes(framePart); foreach (byte b in bytes) { result ^= b; } return result; } /// <summary> /// 拼装完整命令帧:起始符 % + 内容 + BCC + \r /// </summary> private string BuildFrame(string bodyWithoutPercent) { byte bcc = CalculateBcc(bodyWithoutPercent); return "%" + bodyWithoutPercent + bcc.ToString("X2") + "\r"; } }

这里有几个细节需要注意:串口参数波特率不一定是9600,得跟PLC系统寄存器里设置的一致,否则发出去全是乱码,这个我在第4章展开。还有,站号默认是1,如果现场改了PLC站号,这里也要对应改。

3.2 读寄存器与写寄存器的完整方法

有了基础类,接下来实现最常用的两个方法:读数据寄存器ReadRegister和写数据寄存器WriteRegister

/// <summary> /// 读取数据寄存器,例如读取D100一个寄存器 /// </summary> public ushort ReadRegister(int address) { lock (_lockObj) { string body = $"{_stationNo:D2}#RD{address:X4}0001"; string frame = BuildFrame(body); _port.DiscardInBuffer(); _port.Write(frame); string response = ReadResponse(); if (string.IsNullOrEmpty(response) || response[0] == '!') throw new Exception($"读寄存器失败,响应:{response}"); // 响应格式示例:%01$RD01001234**\r // 截取数据区:地址4位 + 数据4位 string dataPart = response.Substring(9, 4); return Convert.ToUInt16(dataPart, 16); } } /// <summary> /// 写入数据寄存器,例如把D100写为1 /// </summary> public void WriteRegister(int address, ushort value) { lock (_lockObj) { string data = value.ToString("X4"); string body = $"{_stationNo:D2}#WD{address:X4}0001{data}"; string frame = BuildFrame(body); _port.DiscardInBuffer(); _port.Write(frame); string response = ReadResponse(); if (string.IsNullOrEmpty(response) || response[0] == '!') throw new Exception($"写寄存器失败,响应:{response}"); } }

这里我用了lock _lockObj,原因很现实:产线上位机往往是多线程的,视觉线程在写结果,UI线程在轮询状态,如果不加锁,两个线程同时往串口写数据,报文就会互相穿插,PLC那边收到的就是乱帧。串口操作必须串行化,这是工控通讯的基本素养。

接收响应的ReadResponse这里,我用了一个简单但可靠的循环读取方式:读直到遇到\r结尾为止:

private string ReadResponse() { StringBuilder sb = new StringBuilder(); int start = Environment.TickCount; while (Environment.TickCount - start < 500) { int count = _port.BytesToRead; if (count > 0) { byte[] buffer = new byte[count]; _port.Read(buffer, 0, count); sb.Append(Encoding.ASCII.GetString(buffer)); if (sb.Length > 0 && sb[sb.Length - 1] == '\r') break; } else { Thread.Sleep(10); } } return sb.ToString(); }

注意,这个方法是阻塞等待响应的,适合命令/响应模式的场景。如果你的上位机需要全双工、PLC会主动上报数据,那得换事件驱动的方式,用DataReceived事件加队列,后面会讲。

3.3 循环采集数据与UI刷新卡顿的解决思路

做上位机的人基本都遇到过这个热搜问题:C#循环采集串口数据时,UI界面卡成PPT。原因很简单,串口数据接收是在后台线程(或者说DataReceived事件线程)里触发的,如果你在这个线程里直接去改文本框、图表控件,UI线程忙不过来,自然卡顿。再加上有些人图省事用Thread.Sleep死循环去轮询,那更卡。

我常用的解决方案是“生产者-消费者”模式:串口接收线程只管把原始数据塞进队列,UI线程用定时器按固定频率去队列里取数据刷新界面。

private ConcurrentQueue<string> _receiveQueue = new ConcurrentQueue<string>(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { SerialPort sp = (SerialPort)sender; int count = sp.BytesToRead; byte[] buffer = new byte[count]; sp.Read(buffer, 0, count); string chunk = Encoding.ASCII.GetString(buffer); _receiveQueue.Enqueue(chunk); }

UI这边,在窗体加载时启动一个System.Windows.Forms.Timer,间隔50ms,在Tick事件里批量取出队列数据并解析、刷新控件:

private void TimerRefresh_Tick(object sender, EventArgs e) { while (_receiveQueue.TryDequeue(out string chunk)) { _rawBuffer.Append(chunk); // 尝试从缓冲区里取出完整的一帧(以\r结尾) int endIndex; while ((endIndex = _rawBuffer.ToString().IndexOf("\r")) >= 0) { string frame = _rawBuffer.ToString().Substring(0, endIndex + 1); _rawBuffer.Remove(0, endIndex + 1); ProcessFrame(frame); } } }

这样做的核心思想是:UI永远只做轻量级的事情,重活(解析、校验、业务处理)都放到后台线程,或者至少不要在控件里堆积大量数据。实测下来,就算每秒几百帧数据,界面也稳如老狗。

3.4 读取32位浮点数据与批量地址封装

机器视觉项目里,经常会遇到需要读取PLC里Float数据的情况,比如相机曝光值、位置坐标、温度值等。PLC侧如果存的是32位浮点数,那它实际占用了两个连续寄存器,比如D200和D201。Mewtocol读回的是两个16位的十六进制字符串,我们需要拼起来再转成C#的float。

public float ReadFloat(int startAddress) { // 连续读两个寄存器 string body = $"{_stationNo:D2}#RD{startAddress:X4}0002"; string frame = BuildFrame(body); _port.DiscardInBuffer(); _port.Write(frame); string response = ReadResponse(); if (string.IsNullOrEmpty(response) || response[0] == '!') throw new Exception($"读浮点失败,响应:{response}"); // 假设响应数据区是 8 个十六进制字符,如 0100 0200 string dataPart = response.Substring(9, 4) + response.Substring(13, 4); uint raw = Convert.ToUInt32(dataPart, 16); return BitConverter.ToSingle(BitConverter.GetBytes(raw), 0); }

这里有个很容易出错的点:大小端顺序。松下PLC和C#的内存字节序不一定一致,如果直接转出来的浮点数完全不对,多半就是寄存器高低字顺序反了。我一般会先把读到的两个寄存器值打印出来,跟PLC侧比对,确认顺序后再封装,避免现场瞎试。

4. 工程上的坑与排查实录

4.1 PLC串口参数不匹配,数据全乱码

这是最开始最常见的坑。上位机串口默认参数一般设置成9600、8、N、1,但PLC侧的系统寄存器里,通信格式可能是19200,甚至有校验位、停止位不同。两边参数只要有一个不一致,收到的就是乱码或者根本没响应。

排查方法很简单:先在PLC编程软件(FPWIN GR)里查看系统寄存器No.20~No.26,确认协议类型是不是“计算机链接”,确认波特率、数据位、校验位、停止位,然后让上位机的SerialPort参数跟它严格一致。

特别提醒:松下PLC的串口协议一定要设置成“计算机链接”,如果还停在编程口模式,你发的Mewtocol命令它会当没看见。我遇到过两次这种问题,都以为代码写错了,最后发现是系统寄存器没改。

4.2 通信模式没切换,命令石沉大海

接上文,很多人配好了波特率,发送数据还是没反应,那就要检查PLC系统寄存器里有没有把通信模式设成“计算机链接”。松下PLC的编程口默认是用于编程软件上传下载程序的,如果不切换模式,上位机发过去的数据PLC根本不会按Mewtocol解析。

这问题最坑的地方在于:串口参数全对、硬件接线也没问题、代码看着也没问题,但它就是不回你。所以这块我建议在项目启动前就先确认好,别等联调的时候浪费时间。改完系统寄存器之后,PLC需要重新上电生效,这一点也要跟现场电工打好招呼。

4.3 粘包、半包与数据校验失败

串口通讯天生没有“消息边界”的概念,它只是一串字节流。PLC返回的数据可能会两帧粘在一起,也可能一帧被拆成两半,这是串口编程里永远躲不开的问题。好在Mewtocol有\r作为帧结束符,所以解析时以\r为边界把数据切分成完整帧,就能解决90%的问题。

我在第3章代码里用的_rawBuffer字符串缓冲区就是这个思路:不断追加新收到的数据,然后循环找\r,找到一个就切走一帧。这里有个小技巧,收到的数据不要直接当字符串处理,最好先按ASCII解码,然后统一用\r分割。响应帧结尾的\r就是天然的帧分隔符,不需要自己去猜帧长。

另外,如果BCC校验失败,PLC会返回!开头的错误响应帧,常见错误码有这几个(具体以手册为准):

错误码含义常见原因
21BCC校验错误帧尾校验值算错或通讯干扰
22格式错误报文格式不对,缺字符或多字符
32超出范围地址或数据数量超出PLC可访问范围
41密码保护PLC设置了密码,禁止远程操作

调试时看到!响应,别慌,对照错误码就能定位到问题。

4.4 现场串口频繁断开,USB转串口不稳定的处理

产线上用的上位机,很多是普通工控机,不带原生COM口,几乎全靠USB转串口。这种方案便宜,但偶尔会遇到串口假死、无法打开、拔插之后端口号变了这类问题。我的处理经验是:

  • 选用FTDI芯片或CH340芯片的USB转串口线,兼容性好,驱动稳定;国内有些几块钱的转接线芯片太杂,现场干扰一大就容易掉。
  • 上位机代码里要处理SerialPort打开失败、读写异常等情况,不要一崩溃就退出,最好加自动重连机制。
  • 固定USB端口号:在设备管理器里,给USB转串口设置固定COM号,避免每次插到不同USB口导致COM号变化,上位机配的端口对不上。
  • 程序启动时做一次端口检测,不依赖固定COM号最好,可以自动枚举可用串口并匹配。

自动重连的逻辑不复杂,定时检测串口是否IsOpen,如果关闭了就尝试重新打开。我一般用一个后台线程来做这个检查,间隔2秒,实时性好,也不影响主逻辑。

4.5 机器视觉联调中的典型问题

视觉程序和PLC联调,最典型的一个问题就是通讯时序。比如PLC这边发了一个“拍照”命令,结果视觉程序因为上电启动慢、算法初始化没完成,没有及时响应,PLC那边就超时报错了。解决办法是上位机启动完成后主动向PLC发送一个“上位机就绪”状态,PLC收到之后才允许产线启动,双方约定好握手流程。

还有一个常见问题,就是“结果写进去PLC没反应”。这时候先别怀疑通讯,先用串口调试助手手动发一帧写寄存器命令,比如把D100写成1,看PLC那边对应的寄存器值变没变。如果变了,说明通讯链路和协议都没问题,问题出在PLC程序逻辑里没有去读这个地址,或者地址映射错了。这种排查思路能帮你快速缩小范围,而不是反复改上位机代码又看不到效果。

5. 调试环境搭建与辅助工具

5.1 使用串口调试助手手动验证协议

写代码之前,我强烈建议先用串口调试助手把Mewtocol命令完整跑通一遍。新建一个连接,选择对应的COM口和波特率,然后手动发送一帧,比如:

%01#RCS050\r

如果PLC正常响应,应该会返回类似%01$RC0**\r的内容,这就说明物理链路、协议设置都OK了。然后你再把同样的命令拿到C#代码里发,一切水到渠成。

手动验证的价值在于:它把“硬件问题”“协议问题”“代码问题”这三层一次切干净。串口助手发命令PLC有反应,那问题一定在代码侧;串口助手发命令都没反应,那就要查接线、查PLC参数、查站号了。

5.2 虚拟串口工具与本地调试

有时候PLC不在身边,或者实验室里只有一台电脑,怎么写代码?用虚拟串口工具。我现在常用的方案是com0com这类虚拟串口软件,创建一对绑定端口,比如COM3和COM4,然后让C#程序打开COM3,另一个串口调试助手打开COM4,两边就能互发数据。这样开发阶段就可以把协议解析、界面刷新这些逻辑先跑通,拿到现场再对接真PLC。

不过要注意,虚拟串口只能模拟字节流的收发,PLC的响应逻辑、错误码返回这些模拟不了。所以本地调试只能验证上位机的“发送-接收-解析”链路,真正的BCC计算对不对、帧格式对不对,还是得拿真实PLC过一遍。

5.3 日志系统:工控上位机必备的调试手段

做上位机通讯,日志系统真的不能省。我见过太多项目,现场出了问题,上位机界面又不能随便停,最后一堆人围在那里猜。所以我在所有通讯模块里都加了一套简单的文本日志:记录每一帧发出的命令、每一帧收到的响应、异常信息、耗时。

日志级别大概分3类:

  • 调试级:记录全部收发帧原文,开发阶段开,现场关闭,避免日志文件疯狂膨胀。
  • 信息级:记录读写寄存器成功、结果写入OK这类关键动作。
  • 错误级:记录超时、BCC校验失败、串口异常等。

日志文件按天切割,保留最近30天。现场出了问题,翻日志几秒钟就能定位到是哪一帧通讯异常、是PLC没响应还是数据校验不对。这套习惯帮我排掉过无数莫名其妙的现场问题。

6. 从通讯到项目落地的几点体会

代码写完之后,最后聊点我的个人经验。

第一个体会:通讯协议要尽早定下来。视觉程序、PLC程序的开发往往是并行的,如果等到联调那天才坐下来对地址表,两边都会很痛苦。我一般会牵头出一份简单的通讯地址表,列出每个寄存器的地址、读写方向、数据类型、含义,发给PLC工程师确认,双方照着实现,联调效率能提升一大截。

第二个体会:代码里要有容错,但不要过度重试。串口通讯偶尔丢一帧很正常,重试机制可以做,但比如写结果这种操作,重试3次都失败就该停下来报错,而不是无限重试,否则产品已经流到下一工位了,结果还没写进去,产线上会出现漏剔。

第三个体会:松下的Mewtocol协议看着简单,但现场工控环境千奇百怪,地线干扰、USB供电不足、线缆过长都会导致偶发通讯异常。如果产线通讯不稳定,先查硬件、换线、换USB口,别急着怀疑协议。我见过一个项目,最后问题是出在一条用了五年、内部已经折断的串口线上,换线立马就好了,这种事情见得多了,你也会把“硬件优先”放在第一位。

第四个体会:如果有人问我,做机器视觉上位机,最值得提前储备的通讯技能是什么,我的答案不是某个具体协议,而是串口通讯这一整套思维:帧格式、校验、缓冲、超时、重试、多线程访问。这套东西学会了,Mewtocol、Modbus、三菱、西门子,无非就是换一套报文规则而已,底层逻辑完全相通。最后再说一个小技巧:调试串口命令的时候,串口助手上如果开的是“文本模式 + 发送新行(\r\n)”,记得把波特率、停止位这些挨个跟代码里的配置核对一遍,再开始怀疑协议。松下这个系列的PLC,我踩过的坑一半都在参数不匹配上。把这个问题先排掉,很多“疑难杂症”其实都不是问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 7:11:25

网络热词传播机制与文化内涵解析

1. 项目背景与核心概念解析"91、馒头老师的爱人"这个看似简单的标题&#xff0c;实际上蕴含着丰富的网络文化内涵。作为近年来在特定圈层中流行的网络梗&#xff0c;它已经发展成为一个具有多重解读可能性的文化符号。这个梗最初源于某网络社区中用户"馒头老师&…

作者头像 李华
网站建设 2026/9/13 7:10:50

Loki 背后的熵编码利器:klauspost/compress huff0 包原理与实战指南

Loki 背后的熵编码利器&#xff1a;klauspost/compress huff0 包原理与实战指南 【免费下载链接】loki Like Prometheus, but for logs. 项目地址: https://gitcode.com/GitHub_Trending/lok/loki 本篇技术指南深入剖析当前仓库 vendor/github.com/klauspost/compress/h…

作者头像 李华
网站建设 2026/9/13 7:10:38

Java ThreadLocal内存管理与弱引用机制解析

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

作者头像 李华
网站建设 2026/9/13 7:10:28

Java 17 安装实战:跨平台干净部署与环境配置

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

作者头像 李华