news 2026/10/23 1:03:01

MCP接入IoT Power功耗计:串口转AI工具的服务端开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP接入IoT Power功耗计:串口转AI工具的服务端开发实战

前阵子帮一个硬件团队做低功耗设备调试,桌上摆着一台 IoT Power 功耗计。这东西本来挺好用,面板上实时电压、电流、功率一目了然,可问题来了:每次要记录数据、看波动曲线,都得手动操作,或者自己写脚本轮询串口,麻烦不说,还得反复改代码。直到我把它接进了 MCP,让 AI 直接“看”功耗计读数,整个调试节奏都变了。

MCP 全称 Model Context Protocol,简单说就是给 AI 模型统一访问外部工具和数据的“插头标准”。以前你想让 AI 帮忙看功耗计,得把数据复制进对话框,或者写一堆胶水代码;现在只需要在 MCP 服务端注册几个工具,AI 就能自己调用工具、发指令、读返回值。这篇博文我会用一台支持串口通信的 IoT Power 功耗计当例子,从整体架构、协议细节、代码实现到避坑技巧,完整走一遍 MCP 服务端的开发流程。不管你是做嵌入式、硬件调试,还是对 AI Agent 接入实体设备感兴趣,这篇都值得花十分钟看完。

1. 整体设计与思路拆解

1.1 为什么是 MCP:给 AI 一双可以直接操作硬件的手

先想一个问题:AI 天生不会读串口,它只擅长理解文字。要让 AI 拿到功耗计的数据,常规路数有三种。

第一种,把数据整理成文本喂给 AI。比如你每隔一秒打印一行“5.02V 0.86A 4.32W”,然后把整段日志贴给 AI 分析。这种办法简单,但只能做离线分析,AI 没法主动获取实时数据,也无法在对话中途去“看一眼”当前功率。

第二种,自己写一个 HTTP API,把功耗计数据包一层,然后让 AI 通过函数调用访问。这条路能跑通,但你得自己定义接口规范、鉴权方式、错误处理,做完之后这套东西只能你自己用,换个 AI 客户端又得重新适配。

第三种就是 MCP。MCP 把“工具”这个概念标准化了:服务端声明自己有哪些能力(比如“读取实时功率”“读取累计电量”),每个能力带着结构化描述,AI 客户端能自动发现这些工具,并按需调用。这就像 USB-C 统一了充电口,你不需要为每个设备单独设计一套接头协议。对 AI 来说,MCP 服务端就是“一双手”,它可以通过这双手去拧螺丝、按按钮、读仪表。

当时我之所以选 MCP,还有一个现实原因:团队里不同人用不同的 AI 客户端,有人用桌面版,有人用命令行。如果走私有 API,得给每个人单独配环境。MCP 服务端一次写好,任何支持 MCP 协议的客户端都能直接接入,省掉大量重复工作。

1.2 架构设计与数据流

整体架构其实很清晰,一共四层。

最底层是硬件设备:IoT Power 功耗计通过 USB 转串口连接到电脑,一般暴露为/dev/ttyUSB0(Linux)或COM3(Windows)。功耗计内部有自己的采集电路,对外提供 Modbus RTU 协议,上位机发一条查询指令,它回一帧数据。

第二层是 MCP 服务端,这是这次项目的核心。它跑在电脑上,干三件事:管理串口通信、解析功耗计协议、把数据能力注册成 MCP 工具。这里有一点要注意:MCP 服务端本身不主动读取数据,它是等 AI 客户端发起“工具调用”时才去操作硬件,是一个“被动响应”的模式。

第三层是 MCP 协议层,负责客户端与服务端之间的消息交换。工具名、参数、返回值都以 JSON 形式传递,这一层你可以直接用官方 SDK 封装好,不用自己写协议解析。

最上层是 AI 客户端,比如你常用的桌面端或 CLI。用户输入一句“看一下当前总功率”,AI 发现相关工具存在,生成参数并调用 MCP 服务端;服务端读串口、解析数据、返回文本;AI 把文本组织成自然语言回复。

这条链路里最容易忽略的是“用户的请求并不等于工具调用”。AI 需要先判断该调用哪个工具,再决定传什么参数。所以工具的描述文案、参数说明写得清不清楚,直接影响 AI 调用的准确率。

1.3 方案选型:轮询还是订阅?串口还是网络?

设计阶段我纠结过两个问题。

第一个问题,通信方式选串口还是选网络。部分 IoT Power 有以太网版本,走 Modbus TCP,好处是不占本地串口,还能远程读取;但我手头这台是 USB 串口版本,而且强调“本地调试”,串口直连更简单可靠。如果是远程部署场景,完全可以把串口层替换成 Modbus TCP 客户端,上层 MCP 工具逻辑几乎不用改。所以我最终用串口做了 MVP,但代码里把“读寄存器”这一步封装成了独立函数,方便以后替换。

第二个问题,数据获取方式选轮询还是订阅。Modbus 是典型的主从协议,功耗计是“从”,只能被动响应上位机指令,它不会主动把数据推给你。所以“订阅”模式在物理层就不成立。那是不是就要让 MCP 服务端自己定时轮询呢?我一开始确实这么想过,后来否掉了。原因有两点:

一是 AI 的上下文窗口很宝贵,如果服务端每秒往 AI 客户端推一条数据,对话马上会被数字淹没。二是一般调试场景只需要“关键时刻看一眼”,轮询不仅浪费资源,还会让串口一直在工作,反而容易出问题。

所以 MVP 阶段我采用“按需查询”模式:AI 要什么,服务端才去读什么。后面如果要做持续监控,我再加一个缓存层,让服务端定时采样、把历史数据存进内存,AI 查询时直接返回缓存数据。这个扩展后面会详细说。

2. 核心细节解析与实操要点

2.1 先把功耗计的通信协议搞清楚

写 MCP 服务端之前,必须先把手里的设备协议摸清楚。这一步偷懒,后面全是坑。

我手头这台 IoT Power 走的是 Modbus RTU,参数为 9600 波特率、8 数据位、无校验、1 停止位,设备地址默认是 1。功能码主要用 03(读保持寄存器),电压、电流、功率、电量都存放在不同的寄存器地址里。

以读取电压和电流为例,上位机发送的请求帧是:

01 03 00 00 00 02 C4 0B

拆开看:

  • 01:设备地址
  • 03:功能码,读保持寄存器
  • 00 00:起始寄存器地址(电压寄存器从 0x0000 开始)
  • 00 02:读取寄存器数量(电压、电流各占一个)
  • C4 0B:CRC16 校验,低字节在前

功耗计收到后返回类似这样的帧:

01 03 04 13 88 00 5A 9A 6B

四个数据字节分别对应两个 16 位寄存器:13 88转成十进制是 5000,按设备说明书电压分辨率为 0.001V,所以电压就是 5.000V;00 5A是 90,电流分辨率为 0.01A,就是 0.90A。功率和电量寄存器也是同样方式读取,只是地址不同。

CRC16 校验是 Modbus RTU 的老朋友,看起来吓人,其实就是查表或按位运算。我用 Python 写了一个标准实现:

def crc16_modbus(data: bytes) -> int: crc = 0xFFFF for byte in data: crc ^= byte for _ in range(8): if crc & 0x0001: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc

发送前把 CRC 低字节放前面、高字节放后面,返回帧的校验也一样。调试时最容易遇到的问题是字节序不对,明明数据是对的,校验却不过,排查好久才发现是高低位放反了。

寄存器地址这块我整理了一张表,方便对照:

数据项寄存器地址分辨率示例原始值实际值
电压0x00000.001 V50005.000 V
电流0x00010.01 A900.90 A
功率0x00020.001 W43204.320 W
累计电量0x00030.001 Wh1234512.345 Wh

不同品牌功耗计的寄存器映射可能不一样,拿到新设备第一件事是先读它的协议文档,别想当然套用。

2.2 MCP 工具定义与 Schema 设计

协议搞清楚之后,下一步就是设计 MCP 服务端暴露哪些工具。这部分看似简单,实际直接影响 AI 好不好使。

工具不能贪多。一开始我注册了 8 个工具,分别读电压、电流、功率、电量,还有各自的历史最大最小值,结果 AI 经常选错工具。后来砍到一个核心工具get_all_metrics,一次性返回所有关键数据,其他工具只做辅助,调用准确率立刻上来了。工具越少,AI 的决策越简单,这个经验其实和设计 API 时“宁可接口少而全”是一个道理。

用 Python 的官方 MCP SDK 写工具非常方便,装饰器一挂就行:

from mcp.server.fastmcp import FastMCP mcp = FastMCP("iot-power") @mcp.tool() def get_voltage() -> str: """读取 IoT Power 实时电压,单位伏特(V)。 返回值示例:5.002V""" frame = read_register(0x0000, 1) raw = parse_uint16(frame, 0) voltage = raw * 0.001 return f"{voltage:.3f}V"

注意函数的 docstring 一定要写清楚:单位是什么、返回格式长什么样、什么场景下使用。因为 AI 看不到你的串口调试工具,它判断“该不该调用这个工具”全靠这段描述。描述写得模糊,AI 就会瞎猜。

参数设计也要克制。比如我想支持“连续采样 N 次取平均”,就在工具里加了一个可选参数samples,默认值是 1。默认值必须是稳妥、低风险的选择,因为 AI 可能不传这个参数。

返回值格式我强烈建议用纯文本描述,而不是返回一堆 JSON 让 AI 自己解析。举个例子:

好的返回:

当前电压 5.002V,电流 0.86A,功率 4.302W,累计电量 12.345Wh

差的返回:

{"voltage": 5.002, "current": 0.86, "power": 4.302, "energy": 12.345}

AI 确实能读 JSON,但需要多做一步“翻译”,出错概率更高。纯文本一步到位,还能顺带把单位写进去,让 AI 直接引用。这个细节是我被坑了几次之后总结出来的。

2.3 串口通信的时序与稳定性处理

MCP 工具函数跑得再漂亮,串口这一关过不了就是白搭。Modbus RTU 是半双工通信,一问一答,时序上比 TCP 敏感得多。

我用的串口配置是serial.Serial(port, 9600, timeout=2),timeout设成 2 秒。这个值不是随便定的:设备响应一般在 100ms 以内,但有些老设备在负载重的时候会慢一些,设大一点不易误判超时;设再大又会拖慢整个工具调用,AI 客户端普遍有超时限制,一次工具调用五六秒没返回,连接就被掐了。

发送指令后要不要延时?我看很多教程说你得sleep(0.1)等设备准备好。实测下来,只要收发都用同一个锁包着,不加延时问题也不大。但保险起见我加了一个time.sleep(0.05),成本几乎为零,换来的稳定性很值。

还有个容易踩的坑:并发调用。AI 客户端可能同时触发两个工具调用,比如一个线程读电压、一个线程读功率。Modbus 是单通道半双工协议,两个请求同时发出去,返回帧就会错乱。解决办法是给串口读写加线程锁,工具内部统一走with serial_lock:。

重试机制也必须有。串口偶尔会读到空帧或者校验失败的数据,直接抛异常太粗暴,AI 会觉得这工具“不好用”。我的做法是失败后自动重试 3 次,间隔 0.1 秒,都失败才抛错。实测下来,硬件正常时重试几乎不会触发,但在设备刚上电或线缆接触不良时能救大命。

3. 实操过程与核心环节实现

3.1 环境准备与依赖安装

先准备一个干净的环境。我用的是 Python 3.10,理论上 3.9 以上的版本都能跑。需要安装两个依赖:mcp和pyserial。

pip install mcp pyserial

如果网络环境受限装不上 mcp,可以先手动下载离线包,这个不展开。装完后建议先写个 3 行的串口探测脚本,确认设备在哪个串口、能不能打开:

import serial s = serial.Serial("/dev/ttyUSB0", 9600, timeout=2) print("串口打开成功:", s.name) s.close()

如果能打印出串口名,说明设备已经被系统识别。Linux 下如果报PermissionError,八成是当前用户不在dialout组里,用sudo usermod -aG dialout $USER加进去后重新登录即可。

然后做一次裸串口通信测试,不经过任何上层代码,直接发一帧读电压指令:

import serial cmd = bytes.fromhex("01 03 00 00 00 02 C4 0B") s = serial.Serial("/dev/ttyUSB0", 9600, timeout=2) s.write(cmd) resp = s.read(9) print(resp.hex(" ")) s.close()

看到类似01 03 04 13 88 00 5A 9A 6B的返回值,说明整条物理链路是通的。这一步做扎实,后面所有问题都能聚焦到代码上。我记得第一次焊完线,怎么读都是空帧,排查半天发现是串口没打开就 send 了,低级错误。

3.2 服务端代码实现

核心服务端我拆成四个文件管理:serial_device.py负责串口封装和协议解析,power_meter.py负责寄存器映射和业务操作,server.py负责注册 MCP 工具,config.py放串口参数和设备地址。小项目也建议分层,后面要加新设备、新功能会轻松很多。

先看串口封装层,这层最重要的功能是“原子性的读寄存器操作”。我把整个请求加锁、写帧、读帧、校验、重试的逻辑放在一个函数里:

import serial import threading import time from config import SERIAL_PORT, BAUDRATE, DEVICE_ADDR from crc import crc16_modbus _serial_lock = threading.Lock() _device = serial.Serial(SERIAL_PORT, BAUDRATE, timeout=2) def read_register(addr: int, count: int = 1, retries: int = 3) -> bytes: """读取一个或多个保持寄存器,返回包括地址、功能码、字节数的完整响应帧。""" request = bytearray([DEVICE_ADDR, 0x03, addr >> 8, addr & 0xFF, 0x00, count]) crc = crc16_modbus(request) request += bytes([crc & 0xFF, crc >> 8]) with _serial_lock: for attempt in range(retries): _device.reset_input_buffer() _device.write(request) time.sleep(0.05) resp = _device.read(3 + count * 2 + 2) if len(resp) >= 5 and crc16_modbus(resp[:-2]) == (resp[-2] | resp[-1] << 8): return resp time.sleep(0.1) raise IOError("串口读取超时或校验失败")

这里最值得注意的两点:第一,每次写前先reset_input_buffer(),把上一次可能残留的字节清掉,这一步能避免不少脏数据;第二,read()的字节数是按帧长精确计算的,比如读两个寄存器就是 3 + 2*2 + 2 = 9 字节,这样天然解决了大部分粘包问题。

业务层的寄存器映射可以写成字典,方便后面加传感器:

REGISTERS = { "voltage": {"addr": 0x0000, "factor": 0.001, "unit": "V", "fmt": ".3f"}, "current": {"addr": 0x0001, "factor": 0.01, "unit": "A", "fmt": ".2f"}, "power": {"addr": 0x0002, "factor": 0.001, "unit": "W", "fmt": ".3f"}, "energy": {"addr": 0x0003, "factor": 0.001, "unit": "Wh", "fmt": ".3f"}, } def get_metric(name: str) -> str: reg = REGISTERS[name] resp = read_register(reg["addr"], 1) raw = int.from_bytes(resp[3:5], byteorder="big") value = raw * reg["factor"] return f"{value:{reg['fmt']}}{reg['unit']}"

int.from_bytes(resp[3:5], byteorder="big")是把返回帧里的两个数据字节按大端解析成整数,Modbus 数据默认大端,别用小端去读,不然电压会变成一个莫名其妙的巨大数值。

接下来是 MCP 服务端主体,用 FastMCP 的高层封装非常简洁:

from mcp.server.fastmcp import FastMCP mcp = FastMCP("iot-power") @mcp.tool() def get_all_metrics() -> str: """读取功耗计全部关键指标并返回格式化文本。 包含:电压(V)、电流(A)、功率(W)、累计电量(Wh)。""" parts = [get_metric(k) for k in ("voltage", "current", "power", "energy")] return "当前" + ",".join(parts) @mcp.tool() def get_voltage_history(samples: int = 10) -> str: """连续采样读取电压并返回列表,用于观察短时波动。samples 为采样次数,默认 10,最大 50。""" samples = max(1, min(50, samples)) values = [] for _ in range(samples): values.append(get_metric("voltage")) time.sleep(0.2) return "电压采样: " + ", ".join(values) if __name__ == "__main__": mcp.run()

最后运行:

python server.py

看到控制台输出MCP server running之类的信息,服务端就起来了。正常情况下它会保持后台运行,等着 AI 客户端连接。

3.3 客户端接入与联调

服务端写好了,接下来要和 AI 客户端做联调。现在主流支持 MCP 的客户端基本都在配置文件里统一管理服务端,一段 JSON 就能搞定,例如:

{ "mcpServers": { "iot-power": { "command": "python", "args": ["/path/to/your/server.py"] } } }

把路径换成实际的位置,重启客户端,然后在对话里输入一句“看看当前功耗计的功率是多少”。正常情况下,AI 会在内部调用get_all_metrics工具,然后回复类似“当前功率 4.302W”的结果。

如果发现 AI 直接说他不知道,或者答非所问,优先检查两件事:

一是服务端的日志。MCP SDK 默认会打印工具调用的记录,如果你看到Calling tool get_all_metrics,说明链路已经通了,只是 AI 的表述问题;如果没有调用记录,说明客户端没连上服务端,检查 JSON 配置里的 command 和 args 是否正确。

二是工具返回内容。直接在 Python 里手动执行get_all_metrics()看看输出是否正确,排除硬件问题。我自己调试时习惯单独开一个终端跑python server.py,同时在客户端里发命令,两边对照看日志,问题出在哪一层一眼就能定位。

联调阶段还有一个技巧:先用最简单的工具测通,再上复杂的。我一般会先在客户端里让 AI 调用一个“返回 hello”的测试工具,确认 MCP 链路本身没问题,再接入真实串口。这样分层排查,不会一把抓瞎。

3.4 数据缓存与历史记录扩展

MVP 跑通后,我开始琢磨另一个需求:“过去一分钟的平均电压是多少?”这个问题靠按需查询很难回答,因为每分钟需要密集采样。如果每次让 AI 调用 60 次工具,既不现实又会把对话刷屏。

我的解决方案是给服务端加一个后台缓存线程。服务端启动时,每秒自动读一次全量数据,放进一个长度为 60 的循环队列,这样永远保存最近一分钟的数据。AI 想查历史统计时,工具直接读队列,不需要碰硬件。

核心代码:

from collections import deque import threading history = deque(maxlen=60) def background_sample(): while True: try: history.append(get_metric("voltage")) except Exception: pass time.sleep(1.0) threading.Thread(target=background_sample, daemon=True).start()

然后加一个统计工具:

@mcp.tool() def get_voltage_average() -> str: """返回最近一分钟电压平均值和波动范围,可用于观察电源稳定性。""" values = [float(v.strip("V")) for v in history] if not values: return "暂无历史数据" avg = sum(values) / len(values) ext = (min(values), max(values)) return f"最近{len(values)}秒平均电压 {avg:.3f}V,最低 {ext[0]:.3f}V,最高 {ext[1]:.3f}V"

有了缓存层,AI 回答这类“趋势类”问题就不再需要现场采样,响应速度快很多。这里也变相解决了一个多线程访问串口的隐患:后台采样和工具调用都用同一个_serial_lock,读线程和主线程不会撞车,实测下来很稳。

4. 常见问题与排查技巧实录

4.1 串口权限与占用问题

Linux 下最常见的是PermissionError: [Errno 13]。这个基本是权限问题,把当前用户加入dialout组就行。注意改完组之后要重新登录终端,当前会话不一定立刻生效。Windows 下则可能是串口被某个串口调试工具占用了,关掉调试工具再试。

还有一个隐藏问题:插拔 USB 之后,设备节点可能从/dev/ttyUSB0变成/dev/ttyUSB1。我一开始把串口名写死在配置里,结果每次重插都要手动改代码。后来学乖了,启动时用serial.tools.list_ports自动扫描,按 VID/PID 过滤出功耗计对应的串口,代码就稳定多了。

4.2 数据粘包与半包处理

Modbus RTU 是字节流传输,虽然串口不像 TCP 网络那样有明确的粘包概念,但操作系统和驱动层有可能一次返回多帧,或者一帧分两次返回。如果直接read(9),你会读到错位的数据,CRC 自然校验不过。

我的经验是两层防护:首先要按帧长精确读取,读 9 字节就传 9 字节,不要read(1024)然后再去截取,那样很容易把第二帧开头也吞进来。其次要校验帧头和 CRC,不通过就丢弃重试。只要这两条抓实,实际使用中基本碰不到粘包问题。

4.3 AI 工具调用失败与超时排查

AI 明明连上了服务端,但它就是不调用工具,或者调用了却返回“数据获取失败”,这种问题我遇到好多次。

先说工具描述。AI 是靠自然语言理解工具用途的,你的描述越是“具体 + 带示例”,它越不容易选错。一个检验办法:把自己当成一个完全不懂硬件的 AI,只看 docstring,看能不能判断出该调哪个工具。如果不能,那描述就得改。

再说超时。AI 客户端对工具调用的超时上限通常很严格。如果你的串口读取timeout=5,加上重试 3 次,最坏情况一次工具调用可能要 15 秒,这时候 AI 客户端早就超时断连了。解决办法是把单次串口超时降到 1 秒,重试次数降到 2 次,宁可偶尔失败,也不能让调用卡死。

最后是异常消息。工具抛出IOError("串口读取超时")这种英文原始异常,AI 虽然能看懂,但不够友好。我建议在工具内部捕获底层异常,返回统一格式的中文错误说明,比如“功耗计无响应,请检查串口连接”。AI 拿到这样的提示,能给出更有帮助的下一步建议。

4.4 功耗计长时间运行的稳定性

服务端跑时间久了,偶尔会遇到串口假死:控制台没有任何报错,但工具调用一直超时。这种一般发生在设备热插拔、USB 休眠唤醒之后。我的解决方案是监听串口异常,发现后自动关闭串口、延迟 2 秒、重新打开,并把这次重连记录到日志里。

代码思路:

def ensure_serial(): global _device if not _device.is_open: _device = serial.Serial(SERIAL_PORT, BAUDRATE, timeout=2)

每次工具调用前都调一下ensure_serial()。成本极低,却能把“假死”产生的故障降到最小。实际用下来,两周的持续运行中只发生过一次 USB 掉线,自动重连后一切正常。

如果跑更长时间,还可以加一层系统级的 watchdog,比如让服务端每 5 分钟自测一次串口读写,失败就重启进程。我没有做到这一步,因为目前算力环境还不需要,但真到了 7x24 小时无人值守的阶段,这层还是不能省。

我在实际项目中还有个心得:日志一定要带时间戳。刚开始我只打印工具调用结果,出问题想复盘时发现根本不知道哪个数据是什么时候读到的。后来把每次串口收发帧都记录到独立的日志文件,设备异常和 AI 误调用的排查效率提高了很多。别嫌日志啰嗦,关键时刻能救命。

如果你也想做类似的事情,我的建议是从最简版本开始:先手工跑通裸串口指令,再写 MCP 服务端,最后接客户端。工具数量一开始控制在 3 个以内,等链路稳定了再扩展缓存、历史统计这些高级功能。硬件接入 AI 这件事,难点从来不在 AI 那一侧,而是你能不能先把设备本身“驯服”。

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

【ROS】ROS(九):坐标系统tf

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

作者头像 李华
网站建设 2026/10/23 1:01:46

【opencv】OpenCV一些函数的简单笔记

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

作者头像 李华
网站建设 2026/10/23 1:01:36

【c++】C++:boost::bind的一些用法

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

作者头像 李华
网站建设 2026/10/23 1:01:20

【android】Application.mk的文件梳理

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

作者头像 李华
网站建设 2026/10/22 23:59:19

SSRF服务端请求伪造:原理、利用与防御实践

先说一个我自己的经历。某次线上排查&#xff0c;发现一台业务服务器在半夜不断向内网发起非常规的请求&#xff0c;抓包一看&#xff0c;目标端口从 3306 扫到 6379&#xff0c;再扫到 11211。顺着调用链往上追&#xff0c;问题落在了我们自己做的一个“URL预览”功能上——前…

作者头像 李华
网站建设 2026/10/22 23:56:32

SQL注入原理与防御实战:从数据库安全到参数化查询的完整指南

1. SQL注入到底是什么&#xff1a;从一道“不该通过”的登录说起聊SQL注入之前&#xff0c;先讲一个我早年做项目时遇到的真实案例。某公司内部系统上线没多久&#xff0c;运维反馈后台登录接口响应偶尔很慢&#xff0c;偶尔又很快&#xff0c;而且数据库负载波动明显。一开始怀…

作者头像 李华