首页/新闻资讯/正文详情

Erlang/OTP 端口(Ports)与端口驱动(Port Drivers)完全指南:从消息协议到 C 语言实战

发布时间:2026/9/25 6:01:16 来源:云帆数科 栏目:资讯中心
Erlang/OTP 端口(Ports)与端口驱动(Port Drivers)完全指南:从消息协议到 C 语言实战
编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载Ports 是 Erlang 与外部世界通信的基础机制它为 Erlang 进程提供了一条字节导向byte-oriented的通道用于与运行在独立 OS 进程中的外部程序交互而 Port Drivers 则是将用 C 编写的驱动动态链接进运行时系统从 Erlang 侧看起来与普通端口无异。本文以 Erlang/OTP 参考手册中 ports.md 为骨架完整梳理端口的创建、消息协议、BIF 调用方式并引入 Interoperability Tutorial 中的完整可运行示例结合 ERTS 的erl_driver、driver_entry接口与erl_ddll内核模块源码深入讲解端口驱动的工作原理。读完本文你将能够独立编写 Erlang 侧端口封装模块并实现一个用 C 编写的外部程序或链接式端口驱动。PortsErlang 视角下的外部世界接口从 Erlang 的角度看Ports端口提供了与外部世界通信的基本机制。一个端口向外部程序提供一个字节导向的接口当端口被创建后Erlang 可以通过发送和接收字节列表list of bytes以及二进制数据binaries来与它通信。端口有几个关键特性端口所有者Port Owner创建端口的 Erlang 进程被称为该端口的port owner或称connected process连接进程。所有进出端口的通信必须经过端口所有者。生命周期绑定如果端口所有者终止端口也随之终止外部程序若编写正确也会随之终止。独立 OS 进程外部程序运行在另一个操作系统进程中。默认情况下它从标准输入文件描述符 0读取数据向标准输出文件描述符 1写入数据。外部程序应在端口关闭时终止自身。这一模型在 c_port.md 中用一个 mermaid 流程图做了直观示意Erlang 进程Connected process与 Port 双向通信Port 再与外部 OS 进程双向通信外部程序始终位于 ERTS 之外。创建端口open_port/2与端口选项创建端口的入口是 BIFopen_port(PortName, PortSettings)它返回一个端口标识符Port。端口标识符的使用方式与 PID 类似可以像对 PID 一样向端口标识符发送和接收消息可以用link/1与端口建立链接可以用register/2为端口注册名字。PortName外部程序名PortName通常是一个元组{spawn, Command}其中字符串Command是外部程序的名字可以包含命令行参数。外部程序运行在 Erlang 工作区workspace之外除非运行时系统中恰好存在一个名为Command的端口驱动——如果找到同名驱动则启动的是该驱动而非外部 OS 进程。这正是普通端口与端口驱动在创建方式上统一的原因二者都用{spawn, Name}。PortSettings端口选项列表PortSettings是端口的选项settings列表典型配置至少包含一个{packet, N}元组选项含义{packet, N}指定端口与外部程序之间传输的数据前附加一个 N 字节的长度指示符length indicatorN 的合法取值为1、2、4。Erlang 侧会自动添加长度指示符但外部 C 程序需要自行显式处理它binary若希望以二进制binary而非字节列表来收发数据必须包含该选项从源码角度看{packet, N}这类选项直接影响运行时系统对端口数据流的封帧处理在教程示例 complex1.erl 中采用{packet, 2}即用 2 字节大端序长度指示符简化 C 与 Erlang 之间的消息切分。端口消息协议发送与接收端口所有者进程Pid通过发送和接收消息与端口Port通信。事实上任何进程都可以向端口发送消息但消息中必须标明端口所有者的身份即消息必须形如{Pid, ...}。发送给端口的消息在下列示例中Data必须是 I/O 列表I/O list即一个二进制或一个可能深度嵌套的由二进制或 0~255 范围内整数组成的列表。可以向端口发送的消息有{Pid, {command, Data}}—— 向端口发送Data。{Pid, close}—— 关闭端口。除非端口已经关闭否则端口会在所有缓冲区排空、端口真正关闭后回复{Port, closed}。{Pid, {connect, NewPid}}—— 将端口Port的所有者设置为NewPid。除非端口已经关闭否则端口会向旧所有者回复{Port, connected}。注意旧端口所有者仍然与端口保持链接linked而新的端口所有者不会被自动链接到端口。从端口接收的消息端口将以下消息发送给拥有它的进程{Port, {data, Data}}—— 从外部程序接收到Data。{Port, closed}—— 对Port ! {Pid, close}的应答。{Port, connected}—— 对Port ! {Pid, {connect, NewPid}}的应答。{EXIT, Port, Reason}—— 端口因某种原因终止时发送。投递语义异步OTP 16 起的变更发送给端口的消息是异步投递的。参考手册特别标注了一处历史变更在 Erlang/OTP 16 之前发送给端口的消息是同步投递的。理解这一点对排查遗留代码的时序问题很重要——现代 OTP 中Port ! {self(), {command, Data}}之后不能假设数据已经到达驱动侧。用 BIF 而非消息与端口通信除了消息传递Erlang 还提供一组 BIF 完成同样的操作性能上通常更受青睐BIF功能port_command(Port, Data)向端口发送Data等价于消息{Pid, {command, Data}}port_close(Port)关闭端口等价于消息{Pid, close}port_connect(Port, NewPid)将端口Port的所有者设置为NewPid。旧所有者Pid仍与端口保持链接如不希望如此必须自行调用unlink(Port)erlang:port_info(Port, Item)按Item返回端口的指定信息erlang:ports()返回当前节点上所有端口的列表此外还有两个仅适用于端口驱动的 BIFport_control/3—— 向端口驱动发送一个控制命令erlang:port_call/3—— 同步调用端口驱动中的回调与port_control类似但是同步调用。Port Drivers链接进虚拟机的 C 驱动除了启动外部 OS 进程Erlang 还允许按照特定规则用 C 编写驱动并动态链接到 Erlang 运行时系统。这种链接式驱动linked-in driver从 Erlang 程序员的角度看就是一个端口因此被称为port driver端口驱动。警告一个错误的端口驱动会导致整个 Erlang 运行时系统内存泄漏、挂起或崩溃。原因在于驱动回调是作为 VM 原生代码的直接扩展执行的运行环境并不安全——没有抢占式调度、没有内存保护。驱动代码一旦崩溃整个 VM 都会崩溃。端口驱动的核心文档位于 ERTS 中erl_driver —— Erlang 驱动的 API 函数说明driver_entry —— 驱动入口结构定义erl_ddll —— Kernel 中的动态加载/卸载驱动模块。驱动的加载erl_ddll端口驱动是一个共享库UNIX 下为.soWindows 下为.dll带有特殊入口点。在创建端口之前必须先用erl_ddll:load_driver/2加载驱动。从 erl_ddll.erl 源码看该模块提供了两组语义不同的接口load_driver/2与unload_driver/1以及reload_driver/2当驱动仍有端口打开时卸载会被视为错误——最后持有驱动的用户卸载驱动或最后一个持有驱动的进程消亡时仍然打开的端口会以driver_unloaded原因被杀死load/2与reload/2用于等最后一个打开端口关闭后再进行代码替换配合驱动监视器使用更安全。需要驱动代码热替换时推荐由单个进程专职管理驱动生命周期进程启动时加载需要替换时 reload进程退出时卸载。由于一个驱动不能同时以 old 和 new 两份代码存在所有用户打开的端口必须先关闭旧代码才能卸载并加载新代码且整个卸载/加载过程是原子操作期间会阻塞系统内其他进程对该驱动的使用。实战示例一外部 OS 进程端口complex1 C下面完整复现 c_port.md 中的示例演示 Erlang 与 C 程序通过端口通信。Erlang 侧封装所有 Erlang 与 C 的通信都从创建端口开始。创建端口的进程即为connected process示例同时设置了trap_exit以便检测外部程序的失败-module(complex1). -export([start/1, stop/0, init/1]). -export([foo/1, bar/1]). start(ExtPrg) - spawn(?MODULE, init, [ExtPrg]). stop() - complex ! stop. foo(X) - call_port({foo, X}). bar(Y) - call_port({bar, Y}). call_port(Msg) - complex ! {call, self(), Msg}, receive {complex, Result} - Result end. init(ExtPrg) - register(complex, self()), process_flag(trap_exit, true), Port open_port({spawn, ExtPrg}, [{packet, 2}]), loop(Port). loop(Port) - receive {call, Caller, Msg} - Port ! {self(), {command, encode(Msg)}}, receive {Port, {data, Data}} - Caller ! {complex, decode(Data)} end, loop(Port); stop - Port ! {self(), close}, receive {Port, closed} - exit(normal) end; {EXIT, Port, Reason} - exit(port_terminated) end. encode({foo, X}) - [1, X]; encode({bar, Y}) - [2, Y]. decode([Int]) - Int.这里的编解码方案非常简单假定参数和结果都小于 256foo用字节 1 表示bar用字节 2 表示参数/结果各用一个字节。open_port的{packet, 2}选项让 Erlang 侧自动附加 2 字节长度指示符而 C 侧必须显式处理它。C 侧通信函数erl_comm.cC 侧需要自己实现带 2 字节长度指示符的读写函数。默认情况下C 程序从标准输入fd 0读取、向标准输出fd 1写入完整实现见 erl_comm.c/* erl_comm.c */ #include stdio.h #include unistd.h typedef unsigned char byte; int read_exact(byte *buf, int len) { int i, got0; do { if ((i read(0, bufgot, len-got)) 0){ return(i); } got i; } while (gotlen); return(len); } int write_exact(byte *buf, int len) { int i, wrote 0; do { if ((i write(1, bufwrote, len-wrote)) 0) return (i); wrote i; } while (wrotelen); return (len); } int read_cmd(byte *buf) { int len; if (read_exact(buf, 2) ! 2) return(-1); len (buf[0] 8) | buf[1]; return read_exact(buf, len); } int write_cmd(byte *buf, int len) { byte li; li (len 8) 0xff; write_exact(li, 1); li len 0xff; write_exact(li, 1); return write_exact(buf, len); }注意C 侧的stdin/stdout是带缓冲的绝不能用于与 Erlang 通信必须直接使用底层read/write系统调用。C 侧主循环port.cmain函数持续监听来自 Erlang 的消息首个字节决定调用哪个函数第二个字节作为函数参数结果再发回 Erlang。完整代码见 port.c/* port.c */ typedef unsigned char byte; int main() { int fn, arg, res; byte buf[100]; while (read_cmd(buf) 0) { fn buf[0]; arg buf[1]; if (fn 1) { res foo(arg); } else if (fn 2) { res bar(arg); } buf[0] res; write_cmd(buf, 1); } }C 程序必须处于while循环并检查read_cmd/1的返回值——这正是它检测端口关闭并自行终止的方式。编译与运行Step 1.编译 C 代码complex.c定义foo/bar函数见 complex.c$ gcc -o extprg complex.c erl_comm.c port.cStep 2.启动 Erlang 并编译 Erlang 代码$ erl Erlang/OTP 26 [erts-14.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] Eshell V14.2 (press CtrlG to abort, type help(). for help) 1 c(complex1). {ok,complex1}Step 3.运行示例2 complex1:start(./extprg). 0.34.0 3 complex1:foo(3). 4 4 complex1:bar(5). 10 5 complex1:stop(). stop实战示例二链接式端口驱动complex5 port_driver.c下面复现 c_portdriver.md 中的示例。端口驱动是动态链接进仿真器进程的共享库调用 C 函数无需上下文切换因此是从 Erlang 调用 C 代码最快的方式但也是最不安全的方式——驱动中的崩溃会直接带崩整个仿真器。Erlang 侧封装与普通端口不同创建端口前必须先加载驱动-module(complex5). -export([start/1, stop/0, init/1]). -export([foo/1, bar/1]). start(SharedLib) - case erl_ddll:load_driver(., SharedLib) of ok - ok; {error, already_loaded} - ok; _ - exit({error, could_not_load_driver}) end, spawn(?MODULE, init, [SharedLib]). init(SharedLib) - register(complex, self()), Port open_port({spawn, SharedLib}, []), loop(Port). stop() - complex ! stop. foo(X) - call_port({foo, X}). bar(Y) - call_port({bar, Y}). call_port(Msg) - complex ! {call, self(), Msg}, receive {complex, Result} - Result end. loop(Port) - receive {call, Caller, Msg} - Port ! {self(), {command, encode(Msg)}}, receive {Port, {data, Data}} - Caller ! {complex, decode(Data)} end, loop(Port); stop - Port ! {self(), close}, receive {Port, closed} - exit(normal) end; {EXIT, Port, Reason} - io:format(~p ~n, [Reason]), exit(port_terminated) end. encode({foo, X}) - [1, X]; encode({bar, Y}) - [2, Y]. decode([Int]) - Int.关键点open_port({spawn, SharedLib}, [])中的SharedLib是驱动名即驱动入口结构中声明的driver_name而不是文件路径文件路径在erl_ddll:load_driver/2中给出。由于同名驱动可能被多个进程创建加载时需容忍already_loaded。C 驱动port_driver.c驱动是一个编译成共享库的模块包含头文件erl_driver.h通过DRIVER_INIT(driver_name)宏声明的特殊入口点返回驱动结构。完整实现见 port_driver.c/* port_driver.c */ #include stdio.h #include erl_driver.h typedef struct { ErlDrvPort port; } example_data; static ErlDrvData example_drv_start(ErlDrvPort port, char *buff) { example_data* d (example_data*)driver_alloc(sizeof(example_data)); d-port port; return (ErlDrvData)d; } static void example_drv_stop(ErlDrvData handle) { driver_free((char*)handle); } static void example_drv_output(ErlDrvData handle, char *buff, ErlDrvSizeT bufflen) { example_data* d (example_data*)handle; char fn buff[0], arg buff[1], res; if (fn 1) { res foo(arg); } else if (fn 2) { res bar(arg); } driver_output(d-port, res, 1); } ErlDrvEntry example_driver_entry { NULL, /* F_PTR init, called when driver is loaded */ example_drv_start, /* L_PTR start, called when port is opened */ example_drv_stop, /* F_PTR stop, called when port is closed */ example_drv_output, /* F_PTR output, called when erlang has sent */ NULL, /* F_PTR ready_input, called when input descriptor ready */ NULL, /* F_PTR ready_output, called when output descriptor ready */ example_drv, /* char *driver_name, the argument to open_port */ NULL, /* F_PTR finish, called when unloaded */ NULL, /* void *handle, Reserved by VM */ NULL, /* F_PTR control, port_command callback */ NULL, /* F_PTR timeout, reserved */ NULL, /* F_PTR outputv, reserved */ NULL, /* F_PTR ready_async, only for async drivers */ NULL, /* F_PTR flush, called when port is about to be closed, but there is data in driver queue */ NULL, /* F_PTR call, much like control, sync call to driver */ NULL, /* unused */ ERL_DRV_EXTENDED_MARKER, /* int extended marker, Should always be set to indicate driver versioning */ ERL_DRV_EXTENDED_MAJOR_VERSION, /* int major_version, should always be set to this value */ ERL_DRV_EXTENDED_MINOR_VERSION, /* int minor_version, should always be set to this value */ 0, /* int driver_flags, see documentation */ NULL, /* void *handle2, reserved for VM use */ NULL, /* F_PTR process_exit, called when a monitored process dies */ NULL /* F_PTR stop_select, called to close an event object */ }; DRIVER_INIT(example_drv) /* must match name in driver_entry */ { return example_driver_entry; }几个值得注意的实现细节驱动实例状态example_drv_start是唯一收到端口实例句柄ErlDrvPort的回调必须把它保存下来。示例用driver_alloc分配一个驱动自定义结构并回传指针不要用全局变量保存实例状态——因为驱动可能被多个 Erlang 进程同时 spawn驱动结构需要实例化多份数据回发example_drv_output收到 Erlang 发来的数据后用driver_output把结果发回 Erlang版本管理ErlDrvEntry结构中必须设置ERL_DRV_EXTENDED_MARKER与主/次扩展版本宏。依据 erl_driver.mdERTS 5.5.3 起引入扩展接口与版本管理ERTS 5.9 起旧驱动必须重新编译并改用扩展接口且需适配 64 位驱动接口未用回调置 NULL驱动是共享模块而非程序没有main函数本例未用到的函数指针全部设为 NULL。编译与运行Step 1.编译 C 代码不同平台命令不同unix gcc -o example_drv.so -fpic -shared complex.c port_driver.c windows cl -LD -MD -Fe example_drv.dll complex.c port_driver.cStep 2.启动 Erlang 并编译 Erlang 代码 erl Erlang/OTP 26 [erts-14.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] Eshell V14.2 (press CtrlG to abort, type help(). for help) 1 c(complex5). {ok,complex5}Step 3.运行示例2 complex5:start(example_drv). 0.34.0 3 complex5:foo(3). 4 4 complex5:bar(5). 10 5 complex5:stop(). stop深入原理erl_driver回调模型与并发安全结合 erl_driver.md可以从源码级理解驱动为何危险且强大回调即 VM 扩展驱动回调是 VM 原生代码的直接扩展执行时不处于安全环境。驱动回调崩溃会崩溃整个 VM实现不当的回调还可能造成 VM 内部状态不一致导致调用之后任意时刻的崩溃或异常行为回调内长时间工作lengthy work会降低 VM 响应性可能引发内存使用极端化和调度器负载不均。锁模型驱动在驱动级driver level或端口级port level上加锁。默认使用驱动级锁即同一时刻只有一个仿真器线程执行驱动代码若在driver_entry中设置ERL_DRV_FLAG_USE_PORT_LOCKING标志则可启用端口级锁允许多个仿真器线程同时执行驱动代码但同一端口对应的回调同一时刻仍只有一个线程调用此时驱动编写者必须自行同步各端口共享的数据。SMP 支持出现之前编写的大多数驱动在使用驱动级锁时无需改写即可在 SMP 运行时中运行。减少拷贝驱动 API 中ErlDrvBinary类型的驱动二进制由调用者分配和释放直接使用二进制可避免一次额外的数据拷贝多数输出函数带 header bufferhbuf/hlen参数在二进制或列表之前先发送一个头部列表便于在接收端匹配消息与二进制语法配合可以高效地匹配消息头部。小结端口与端口驱动构成了 Erlang 与外部世界互操作的两条主线Ports外部进程隔离性最好外部程序运行在独立 OS 进程中崩溃不会波及 VM通信基于字节流配合{packet, N}长度帧或自行设计协议通过消息或port_command/port_close等 BIF 交互Port Drivers链接式驱动性能最高无上下文切换但安全风险最大需要严格遵循erl_driver.h的 API 约定、正确处理实例状态与版本标记并通过erl_ddll管理加载/卸载与代码替换。无论选择哪种方式都需牢记端口所有权模型所有通信经由端口所有者进程所有者消亡则端口消亡。更多进阶示例NIF、C Nodes、erl_interface 等互操作手段可继续阅读 Interoperability Tutorial 中的 c_port.md、c_portdriver.md、nif.md 等章节BIF 的完整语义以 ERTS 参考文档 及erlang模块文档为准。赞分享编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载相关推荐Erlang/OTP 端口Ports互操作实战用 Port 从 Erlang 调用 C 程序完整教程Erlang/OTP 端口Ports互操作实战用 Port 从 Erlang 调用 C 程序完整教程 Port端口是 Erlang/OTP 内置的三大编程语言语言运行时标准库编译器并发编程Erlang/OTP SNMP Agent 网络接口Net If进程完全指南消息协议与自定义实现Erlang/OTP SNMP Agent 网络接口Net If进程完全指南消息协议与自定义实现 Net IfNetwork Interface进程是编程语言语言运行时标准库编译器并发编程终极音乐下载解决方案Musicdl 3分钟快速上手指南终极音乐下载解决方案Musicdl 3分钟快速上手指南 还在为不同音乐平台的歌曲分散在各个APP中而烦恼吗想要一键下载全网音乐却找不到合适的工具Music网页爬虫音频处理上一篇Android Advanced RecyclerView 教程下一篇5步构建企业级远程监控管理平台Tactical RMM深度解析与实践指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

Ariakit ComboboxDisclosure:用按钮开关 Combobox 下拉列表的完整实现解析
Ariakit ComboboxDisclosure:用按钮开关 Combobox 下拉列表的完整实现解析

UI组件前端 【免费下载链接】ariakit Toolkit with accessible components, styles, and examples for your next web app 项目地址: https://gitcode.com/gh_mirrors/ar/ariakit 点击查看 免费下载 本文基于 Ariakit 仓库的官方示例 combobox-disclosure&#xff… · 2026/9/25 6:01:10

Java+微信小程序宠物医院预约源码:并发扣减与状态流转实战
Java+微信小程序宠物医院预约源码:并发扣减与状态流转实战

简介:这是一套面向计算机相关专业在校学生与教师的宠物医院预约微信小程序项目源码,采用Java后端开发,配套完整数据库脚本,可作为课程设计、毕业设计、期末大作业或项目初期立项演示的参考方案。资源包共49个文件,以35… · 2026/9/25 6:01:10

用 Apache Ossie 互操作 Fixture 守护语义模型格式:knowledge-catalog 的 osi-schema 校验实践
用 Apache Ossie 互操作 Fixture 守护语义模型格式:knowledge-catalog 的 osi-schema 校验实践

数据目录AI Agent人工智能知识管理示例工程 【免费下载链接】knowledge-catalog Google Cloud Knowledge Catalog Tools and Samples 项目地址: https://gitcode.com/gh_mirrors/kn/knowledge-catalog 点击查看 免费下载 导读 本文围绕 knowledge-catalog 仓库中 … · 2026/9/25 6:01:10

Windows11本地部署OpenClaw:从WSL2环境到飞书接入的完整实践
Windows11本地部署OpenClaw:从WSL2环境到飞书接入的完整实践

最近我在 Windows11 上折腾 OpenClaw,前前后后花了两天,把一个“装不上、跑不通”的状态调到了稳定运行,现在它每天定时抓资讯、生成摘要、发到飞书,基本替代了我早上刷新闻的习惯。OpenClaw 本质上不是又一个聊天框,而… · 2026/9/25 7:33:50

Java+JSP+MySQL学生宿舍管理系统:从环境搭建到部署避坑全解析
Java+JSP+MySQL学生宿舍管理系统:从环境搭建到部署避坑全解析

简介:Java Web开发中,JSP/Servlet与MySQL的组合是理解服务端动态网页技术的基础。其核心原理在于:浏览器请求经Tomcat容器路由至Servlet,业务逻辑通过JDBC访问MySQL,最终由JSP渲染响应页面。这种分层架构虽然“传统”&… · 2026/9/25 7:33:50

从异步导出到Redis连接池:第三十二周技术复盘与思考
从异步导出到Redis连接池:第三十二周技术复盘与思考

第三十二周的周报我拖到周四深夜才动笔。不是没东西写,恰恰相反,这周经历了订单模块重构收尾、报表导出功能发布、还有一次线上接口超时的排查,随便挑一件都够写两千字。但真正坐下来打开文档的时候,我反而反复删了好几版——原因… · 2026/9/25 7:33:44

ESXi将USB硬盘映射为本地磁盘并创建VMFS的完整实操指南
ESXi将USB硬盘映射为本地磁盘并创建VMFS的完整实操指南

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

开源LLM代码审查工作流:CLI+Git原生集成实践
开源LLM代码审查工作流:CLI+Git原生集成实践

1. 项目概述:这不是一个“工具”,而是一套可落地的开源代码审查工作流open-code-review 这个名字乍看像某个具体软件,但实际它代表的是一类正在快速成型的新型开发实践——用开源、透明、可审计的方式,把大语言模型(LL… · 2026/9/25 7:33:38

用Trellis驯服AI编码代理:规范文件如何让代码不再失控
用Trellis驯服AI编码代理:规范文件如何让代码不再失控

1. AI编码代理的失控时刻:为什么没人敢放手让它写代码如果你这段时间用过Cursor、Windsurf这类AI编程工具,八成已经体会过那种"又爽又怕"的感觉。爽的是,一个前端页面、一个后台接口、一段脚本,敲几行提示词就出来了&am… · 2026/9/25 7:33:38

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

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

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

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

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

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

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码