尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
深入理解Linux Netlink:内核与用户态通信的协议与实战
1. Netlink是什么为什么它值得花时间学1.1 从一次配置IP的经历说起我第一次认真折腾Netlink是在做一个网络配置工具的时候。当时的需求很简单让一个嵌入式设备开机后自动给某个接口配置IP地址。最粗暴的方案我当然知道——在代码里用system(ip addr add 192.168.1.10/24 dev eth0)三秒钟搞定。可问题在于每次调用都会fork出一个shell进程不仅慢而且错误处理特别别扭。你想拿到“添加失败”的具体原因得去解析字符串输出一旦目标平台的ip命令版本不同输出格式变了你的解析逻辑就全废了。后来我换成了ioctl又是另一番折腾。ioctl能设置地址但内核返回值非常简单很多事情做不了。比如我想监听“接口被拔插网线”的事件ioctl根本无能为力它只能一问一答没有主动通知的机制。真正把这些问题一并解决的就是Netlink——Linux内核专门用来和用户态进程通信的一套socket接口。Netlink既不是普通的TCP/UDP网络通信也不是读写procfs那种文件系统方式。它是一套设计的很“内聚”的消息传递机制内核可以主动给用户态发消息用户态也能给内核发请求双方传递的是结构化的二进制消息不是一行行文本。如果你做的是网络管理、路由配置、防火墙规则同步这类事情Netlink几乎是绕不开的一层。1.2 Netlink在Linux内核里的位置先说清楚一个容易混淆的点Netlink是“内核模块”和“用户态进程”之间的通道但它不是某个内核网络协议的驱动程序。它底层是一个socket域domain名字叫AF_NETLINK。你创建socket的时候这样写fd socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);拿到这个fd之后用户态进程可以通过sendto和recv收发消息。另一端在内核态由相应的Netlink协议处理函数响应。它和普通socket最大的区别是普通socket的通信双方通常都是用户态进程而Netlink的使用场景是用户态和内核态对话。那为什么不直接读写procfs呢procfs本质是一个伪文件系统每次读写都要经过文件系统层像“查询路由表”这种高频操作解析文本的效率很低。Netlink传递的是紧致的二进制结构体内核parse起来快用户态parse起来也方便而且它天然支持多播、请求应答、异步通知这些特性让它在网络管理领域几乎成为事实标准。你现在在命令行里敲ip、route、iptables它们背后走的都是Netlink不是直接写procfs。1.3 学Netlink前需要哪些基本功说实话Netlink不是那种“鼠标点一点就能上手”的东西。它的API层级相对底层适合有一定基础的开发者学习。如果你满足下面几个条件学起来会顺畅很多熟悉C语言尤其是结构体、指针和内存对齐的基本概念。有socket编程基础理解bind、sendto、recv这些系统调用的语义。对Linux网络栈的常见术语不陌生比如route、neighbor、link、rule这些概念。即使你是Java、Go、Python为主的开发者我也建议至少动手写一遍C语言的Netlink客户端。原因只有一个Netlink消息本身就是二进制结构体你用C能最直观地看到“消息是怎么被构造、填充、发送、解析的”换成高层语言反而容易被封装库遮住关键细节。当然如果只是做应用开发用现成的libnl3或者语言库也行但第一遍最好还是亲手把底层流程走通。2. 核心原理从socket到消息解析2.1 socket()参数背后的真实含义很多资料喜欢把Netlink形容成“内核和用户态之间的桥梁”这个说法没问题但对写代码的帮助有限。我更愿意把它理解成一个“政务大厅”AF_NETLINK就是大厅入口NETLINK_ROUTE就是你要去的窗口SOCK_RAW表示你打算直接使用内核定义的原始消息格式不去套TCP/UDP那一层东西。常见的Netlink协议族有这么几个协议族常量主要用途常见消息类型NETLINK_ROUTE路由、链路、邻居、地址等网络管理RTM_GETLINK、RTM_NEWROUTE等NETLINK_NETFILTER防火墙/网络过滤规则NFNL_MSG_BATCH_BEGIN等NETLINK_GENERIC通用Netlink适合自定义协议CTRL_CMD_GETFAMILY等NETLINK_INET_DIAGsocket诊断ss工具的后端SOCK_DIAG_BY_FAMILY第一眼看到这么多族可能会有点慌。其实日常学习和实践大部分时间都耗在NETLINK_ROUTE上因为它直接对应ip命令的绝大多数功能。等你把NETLINK_ROUTE的消息交互流程搞清楚再去看NETLINK_GENERIC会非常轻松因为两者的收发机制是完全一致的区别只是消息头里多了一个genlmsghdr。2.2 一次完整通信的三个核心地址要素Netlink通信的发起方是用户态进程但在这之前有一个微妙的步骤自己先bind一个地址。很多人不理解为什么要bind后面还会因此踩坑。这里用的地址结构是sockaddr_nlstruct sockaddr_nl { sa_family_t nl_family; // AF_NETLINK unsigned short nl_pad; // 0 __u32 nl_pid; // 通信标识符 __u32 nl_groups; // 多播组掩码 };nl_pid这名字很有迷惑性它不是进程PID。它的真实含义是“本地地址标识”如果你填0内核会为你这个socket自动分配一个唯一的标识通常是当前线程的PID。官方推荐是用户态进程自己填0让内核自动分配或者填一个能够唯一标识进程的值。这意味着你不能胡写成某个随机数否则收到的消息可能和你无关。nl_groups用来控制订阅哪些多播组。比如你想监听路由变化就把RTNLGRP_IPV4_ROUTE对应的位掩码置上。相当于你在政务大厅里对工作人员说某个广播频道有更新时记得也通知我一声。这个机制让内核在接口up/down、路由变化时主动往你的socket里塞一条消息不需要你反复轮询。struct sockaddr_nl sa {0}; sa.nl_family AF_NETLINK; sa.nl_pid 0; // 让内核自动分配 sa.nl_groups RTNLGRP_LINK; // 订阅链路事件 bind(fd, (struct sockaddr *)sa, sizeof(sa));注意bind之后你用一个有本地标识的socket去和内核通信内核会认出你。如果不bind直接sendto内核虽然也能收到请求但你的地址是匿名的回包送不到你手上。这是Netlink和传统socket编程的第一个分水岭。2.3 nlmsghdr一个16字节的信封Netlink消息最外层是一个统一的“信封”struct nlmsghdr。无论是你发的请求还是内核回复都套着这个头。它的定义躺在linux/netlink.h里struct nlmsghdr { __u32 nlmsg_len; // 整个消息长度包括这个头部自身 __u16 nlmsg_type; // 消息类型比如 RTM_GETLINK __u16 nlmsg_flags; // 标志位比如 NLM_F_REQUEST __u32 nlmsg_seq; // 序列号 __u32 nlmsg_pid; // 发送方标识 };这个结构体正好16字节条件是4字节对齐。你构造消息的时候nlmsg_len是第一个要填的字段后续所有字段都跟着它走。如果你漏写了nlmsg_len内核可能在解析时直接拒绝因为内核不知道消息边界在哪里——如果你在同一个buffer里塞了几条Netlink消息边界全靠nlmsg_len来划分。nlmsg_type告诉内核你要做什么操作是查询RTM_GETLINK、新建RTM_NEWLINK还是删除RTM_DELLINK。nlmsg_flags则携带请求标志比如NLM_F_REQUEST必须置上表示这是一条请求NLM_F_ACK表示希望内核在处理完成后回一条确认消息NLM_F_DUMP配合GET类消息使用表示要批量导出所有对象比如把所有网卡名字都拉出来。数据部分紧跟在nlmsghdr之后对NETLINK_ROUTE来说通常是rtattr数组属性。属性采用TLVType-Length-Value的变体struct rtattr { unsigned short rta_len; // 属性自身长度包含头部 unsigned short rta_type; // 属性类型 // 后面是数据 };这里有个经典陷阱rta_len必须是4字节对齐后的真实长度。比如你传递一个“接口名”字符串名字长度可能是7对齐后就是8。如果你在解析时没有做RTA_ALIGN读下一个属性时会错位导致数据全乱。3. 第一次实践用Netlink查询接口信息3.1 代码骨架与关键步骤理论讲再多不如写一个能跑的例子。下面这个例子做的事情和ip link show eth0差不多查询某个网络接口的名字和MTU。第一步创建socket并bind#include stdio.h #include string.h #include unistd.h #include sys/socket.h #include linux/netlink.h #include linux/rtnetlink.h #include net/if.h int main(void) { int fd socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd 0) { perror(socket); return 1; } struct sockaddr_nl sa {0}; sa.nl_family AF_NETLINK; sa.nl_pid 0; // 让内核自动分配 if (bind(fd, (struct sockaddr *)sa, sizeof(sa)) 0) { perror(bind); close(fd); return 1; } // 后续代码... }这步走完你已经有了一个合法的Netlink通道。bind在这里不是传统C/S架构里的“监听”更像是在发布者那里注册了一个回信地址。第二步构造请求消息。这里要发送的数据有两层外层是nlmsghdr内层是ifinfomsg再跟着若干属性本例其实不用额外属性空属性也行char buf[512] {0}; struct nlmsghdr *nlh (struct nlmsghdr *)buf; nlh-nlmsg_len NLMSG_LENGTH(sizeof(struct ifinfomsg)); nlh-nlmsg_type RTM_GETLINK; nlh-nlmsg_flags NLM_F_REQUEST | NLM_F_DUMP; nlh-nlmsg_seq 1; nlh-nlmsg_pid 0; struct ifinfomsg *ifi (struct ifinfomsg *)NLMSG_DATA(nlh); ifi-ifi_family AF_UNSPEC; // 查询所有接口NLM_F_DUMP这个标志很有用它告诉内核“把当前所有接口的信息都发给我”内核会分多条消息返回。如果你不写DUMP只指定了某个接口索引内核可能只返回一条。新手学Netlink最容易犯的错就是照抄示例代码但漏了NLM_F_DUMP然后发现解析出来的数据和自己预期完全对不上。第三步发送并接收struct sockaddr_nl kernel_addr {0}; kernel_addr.nl_family AF_NETLINK; sendto(fd, buf, nlh-nlmsg_len, 0, (struct sockaddr *)kernel_addr, sizeof(kernel_addr)); char recvbuf[4096]; int len recv(fd, recvbuf, sizeof(recvbuf), 0);注意sendto的目标地址填的是内核地址nl_family为AF_NETLINKnl_pid为0。这个“0”代表内核不要填自己的nl_pid。3.2 解析多消息与NLMSG_DONErecv一次返回的buffer里可能包含多条消息也可能只包含一条。比如执行DUMP时内核会返回第一条链路的具体信息随后跟着第二条、第三条……最后以一条NLMSG_DONE消息结束。标准解析循环是这样写的for (struct nlmsghdr *nh (struct nlmsghdr *)recvbuf; NLMSG_OK(nh, len); nh NLMSG_NEXT(nh, len)) { if (nh-nlmsg_type NLMSG_DONE) { break; } if (nh-nlmsg_type NLMSG_ERROR) { struct nlmsgerr *err (struct nlmsgerr *)NLMSG_DATA(nh); fprintf(stderr, error: %d\n, err-error); break; } if (nh-nlmsg_type RTM_NEWLINK) { struct ifinfomsg *ifi (struct ifinfomsg *)NLMSG_DATA(nh); // 从这里开始遍历属性找到 IFLA_IFNAME 和 IFLA_MTU } }NLMSG_OK宏做的事情很简单检查当前消息长度是否还在buffer范围内。NLMSG_NEXT则按照nlmsg_len把指针推进到下一条消息。如果你不理会这些宏自己手动按字节偏移跳迟早会漏消息或读越界。3.3 解析属性数据拿到ifinfomsg之后网络接口的名字、MTU都存在后面的属性里。这时候需要遍历rtattr数组int remaining nh-nlmsg_len - NLMSG_LENGTH(sizeof(*ifi)); struct rtattr *rta (struct rtattr *)((char *)ifi NLMSG_ALIGN(sizeof(*ifi))); for (; RTA_OK(rta, remaining); rta RTA_NEXT(rta, remaining)) { if (rta-rta_type IFLA_IFNAME) { printf(interface: %s\n, (char *)RTA_DATA(rta)); } else if (rta-rta_type IFLA_MTU) { int mtu *(int *)RTA_DATA(rta); printf(mtu: %d\n, mtu); } }这里的关键是rtattr的rta_len已经包含了头部长度所以遍历下一个属性时要先用RTA_ALIGN把当前长度对齐到4的倍数。你写代码的时候直接调用RTA_NEXT就行但心里要清楚它做了什么。跑一遍这个程序你会看到内核把每一个网络接口的名字和MTU都返回回来。这个过程和ip link show拿到的数据基本一致。区别是你现在完全掌控了解析逻辑想过滤什么样的数据都随心所欲。4. 常见的大坑为什么消息总是收发失败4.1 pid到底填0还是自己的pid这是我在新手期踩过最久的一个坑。当我第一次写Netlink客户端时参考代码里sockaddr_nl的nl_pid填的是getpid()但构造消息头nlmsghdr里的nlmsg_pid我填的也是getpid()结果bind之后内核总是把我的socket标识搞成一个奇怪的值导致我收到的回包经常是空的。后来看内核源码才明白sockaddr_nl里的nl_pid填0最好让内核自动分配如果你自己填了一个非0值内核会拿它去和现有socket做唯一性校验一冲突就报EADDRINUSE。而nlmsghdr里的nlmsg_pid是消息头里的“源地址标识”主要是供对端判断“谁发来的”填getpid()也行填0也行。很多老示例代码爱写nlmsg_pid getpid()我认为这是历史惯性并不是必须的。一句话总结bind的时候填0让内核管分配消息头里的pid只是记录用途别把它当成必须匹配bind值的字段。4.2 拿了NLM_F_ACK却不读回包有些代码习惯在请求里加上NLM_F_ACK然后发完消息就开始等回包。内核收到带NLM_F_ACK的请求后确实会额外回一条NLMSG_ERROR消息——注意如果请求执行成功这条NLMSG_ERROR里的error字段的值是0并不是说出现了一个错误。很多新手看到消息类型是NLMSG_ERROR就以为请求失败了直接打印错误退出这是理解偏差。正确的处理逻辑是struct nlmsgerr *err (struct nlmsgerr *)NLMSG_DATA(nh); if (err-error 0) { // 请求执行成功 } else { // 请求失败error是负数errno }还有一种情况更隐蔽有的代码spawn一个线程专门发请求发完不读回包或者读了不回包就关socket。这样会触发netlink的接收队列堆积严重时内核会认为你的socket接收缓冲区满直接在发送端报ENOBUFS。我在做某防火墙管理模块时就碰到过连续下发几百条规则后突然开始大量丢包查了半天才发现是netlink socket上的消息没人及时读走。4.3 组播消息收不到如果你想监听链路层状态变化做法是在bind时设置nl_groupssa.nl_groups RTNLGRP_LINK;这样内核会把接口up/down事件广播到你的socket。但我当时遇到的问题是bind在socket创建后立刻执行按理说应该能收到后续消息结果等了半天一条也没来。后来发现是因为我监听时用了一个很老的系统那个平台的内核版本对RTNLGRP_LINK的支持有bug解决办法是换成ip monitor link这条命令先看内核本身能不能上报。如果你用现代内核这招一般不会有问题。另一个常见情况是绑定了组播组但socketfd被设成了非阻塞你开心地等recv返回可它返回EAGAIN你以为是没消息其实是消息还没到。Netlink的组播消息是异步的网络事件发生到进入你的socket之间存在延迟需要配合poll/select等待可读事件而不是立刻recv一次就判断“没有事件”。5. Generic Netlink想定制自己的内核/用户态协议时该怎么办5.1 为什么会有NETLINK_GENERICNETLINK_ROUTE等功能是内核预定义好的固定处理路由、邻居、链路这些网络对象。如果你想让自己的内核模块和用户态进程通信也想用Netlink那一套机制但不希望硬挤进NETLINK_ROUTE的消息类型空间那就得靠NETLINK_GENERIC通用Netlink简称genl。NETLINK_ROUTE的问题是消息类型字段nlmsg_type的取值要和内核已有的families严格区分否则会造成语义冲突。比如RTM_GETLINK是16后面新建一个模块也想用16那内核根本不知道你想干嘛。Generic Netlink就是来解决这个“多发家共用一张路由表”的难题的它通过一个控制协议动态分配每个family的唯一ID。你向内核注册myfamily内核返回一个用户态不知道的数字后续所有消息都使用这个数字作为nlmsg_type。5.2 先解决家族IDCTRL_CMD_GETFAMILY使用Generic Netlink的第一步是查询你感兴趣的family ID。系统里有一个固定的控制family名字叫nlctrl你可以给内核发一条消息让它告诉你myfamily的ID是多少。我用libnl3处理过一次之后回头用裸socket写了一遍发现核心流程是这样的创建socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC)。构造一个genlmsghdr设置cmd CTRL_CMD_GETFAMILY。在genlmsghdr后面附加一个属性CTRL_ATTR_FAMILY_NAME内容是你想查询的family名字字符串。发送后解析内核返回的CTRL_ATTR_FAMILY_ID拿到整数ID。有两点值得提醒第一CTRL_CMD_GETFAMILY请求也要带上NLM_F_REQUEST标志否则内核不理你第二如果你要查询的family名字不存在内核会返回NLMSG_ERRORerror字段是负的ENOENT这能帮你快速排查是不是内核模块没加载。5.3 一个最小示例查询自定义family的ID#include linux/genetlink.h // 假设你的模块注册的family名字是 my_demo_family char buf[512] {0}; struct nlmsghdr *nlh (struct nlmsghdr *)buf; genlmsghdr *genlh (genlmsghdr *)(buf NLMSG_HDRLEN); nlh-nlmsg_len NLMSG_LENGTH(GENL_HDRLEN); nlh-nlmsg_type GENL_ID_CTRL; // 控制家族固定是 16? 其实用 GENL_ID_CTRL 最稳妥 nlh-nlmsg_flags NLM_F_REQUEST; nlh-nlmsg_seq 1; genlh-cmd CTRL_CMD_GETFAMILY; // 附加 family name 属性 int attrlen 0; struct rtattr *rta (struct rtattr *)(buf NLMSG_HDRLEN GENL_HDRLEN); rta-rta_type CTRL_ATTR_FAMILY_NAME; rta-rta_len RTA_LENGTH(strlen(my_demo_family) 1); memcpy(RTA_DATA(rta), my_demo_family, strlen(my_demo_family) 1); nlh-nlmsg_len NLMSG_ALIGN(nlh-nlmsg_len) RTA_ALIGN(rta-rta_len);解析回包时按照CTRL_ATTR_FAMILY_ID的取值就是你要的family ID。拿到之后后续所有和这个family的通信都复用同一个socket消息类型直接填这个ID。整个流程和NETLINK_ROUTE没差多少差别只在于NETLINK_ROUTE的family是编译期写死的常量Generic Netlink则需要先查询一次。如果你不想手写这么麻烦的裸协议直接使用libnl3的genl_ctrl_resolve函数也行它把上述流程封装好返回family ID。但建议至少写一次裸版本因为排查问题时你知道查找ID背后发生了什么比只会调库函数的人走得远。6. 从看懂到上手Netlink的学习路径与调试技巧6.1 用strace看别人家的实现学Netlink最高效的方法绝不是翻开内核源码从第一行读而是先借助系统工具的“探针”看真实场景下内核回复的消息长什么样。比如strace -f -e tracenetwork -s 2000 ip link show你会看到类似这样的输出socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE) 3 bind(3, {sa_familyAF_NETLINK, nl_pid0, nl_groups00000000}, 12) 0 sendto(3, {{len40, typeRTM_GETLINK, flagsNLM_F_REQUEST|NLM_F_DUMP, seq...}, ...}, 40, 0, ...) 40 recvmsg(3, ...)这段输出直接告诉你ip命令实际发出的消息头参数。你可以照着它自己构造一模一样的请求然后通过打印收到的二进制内容和strace输出对照慢慢就理解了内核返回的每条NLMSG结构。我强烈建议你准备一个小脚本专门把收到的原始字节以hex格式dump出来然后用NLMSG_OK循环去打印type、len、flags看着数据一个个字段对上你会对Netlink的消息布局产生肌肉记忆。6.2 内核侧代码怎么读如果你要理解内核收到Netlink消息后做了什么打开源码时不要漫无目的。Linux内核里和Netlink直接相关的目录是net/netlink/核心文件是af_netlink.c。不过对入门者来说更常用的入口是net/core/rtnetlink.c——它实现了路由、链路相关消息的处理。你去搜rtnl_doit之类的函数就能看到内核从nlmsghdr中提取参数、调用具体网络配置函数的完整流程。读内核源码时我不建议你从头到尾读而是从你感兴趣的某个消息类型搜起。比如你好奇RTM_GETLINK的请求是怎么被处理的直接搜rtnl_getlink看它从ifinfomsg里读字段、构造回复消息、调用netlink_ack这几个步骤对应关系一目了然。这样读源码的次数多了你对Netlink的掌控力会远超只会调用例程的开发者。6.3 做实验时的三个建议第一尽量用网络命名空间。在实验机器上执行ip netns add testns在单独的网络空间里建虚拟接口这样即使你把路由表、地址搞乱了也不会影响宿主机。Netlink实验天然适合在netns里做因为你可以放心地创建veth、设置地址、删除路由。第二从一个只读操作入手。查询链路信息、查询路由表都不涉及修改系统状态出错了最多是拿不到数据不会把环境搞坏。等你对消息构造和解析有了把握再尝试RTM_NEWADDR这样的写操作。千万别一开始就写删除路由的命令手一抖就可能把默认路由删了。第三故意构造错误消息。比如把nlmsg_len填错或者把nlmsg_type设成一个不存在的类型观察内核返回的NLMSG_ERROR是什么值。这种“破坏性实验”能帮你快速建立对协议校验的直觉——你会知道内核在哪些环节做了严格的参数检查。最后再分享一个实际体会。很多人学Netlink是被各种宏和结构体吓退的但等你真正写完一个能跑通的客户端再去用ip monitor或者检查/proc/net/dev中的数据你会发现Linux网络管理工具背后的那份“确定性”全来自这里。Netlink不是那种学完马上能秀出漂亮界面的技术它更像一把钥匙能打开整个Linux网络栈的底层世界。学它的过程可能有点枯燥但每走一步你都能感觉到自己对系统理解深了一层。
RELATED

相关推荐

TCP吞吐量上不去?抓包实测排查带宽跑不满的实战指南

TCP吞吐量上不去?抓包实测排查带宽跑不满的实战指南

/* 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 4:01:39
Linux VM内核参数调优:内存管理、脏页回写与OOM实践

Linux VM内核参数调优:内存管理、脏页回写与OOM实践

/* 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 4:01:39
【跟踪】跟踪篇(4)--对于MOT16 Benchmark的理解

【跟踪】跟踪篇(4)--对于MOT16 Benchmark的理解

/* 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 4:01:39
MORE NEWS

更多资讯

📰

REA reverse-engineer-anything 技能完整安装指南:让 AI 编码助手在本地完成逆向取证

REA reverse-engineer-anything 技能完整安装指南:让 AI 编码助手在本地完成逆向取证 【免费下载链接】rea Reverse engineer anything with agents, from app behavior down to native binaries. 项目地址: https://gitcode.com/GitHub_Trending/rea2/rea R…

📰

为小说生成封面与插画:Denova 图像创作完全指南

【免费下载链接】denova An AI creative platform for novel writing and AI generated RPG, with built-in support for AI agents, Skills, subagent workflows, automations, image generation, and version control. 项目地址: https://gitcode.com/gh_mirrors/…

📰

esformatter编辑器深度集成:VS Code、Vim保存即格式化与CI风格检查配置指南

开发工具 【免费下载链接】esformatter ECMAScript code beautifier/formatter 项目地址: https://gitcode.com/gh_mirrors/es/esformatter 点击查看 免费下载 esformatter 是一款开源的 ECMAScript(JavaScript)代码格式化工具,它…

📰

基于微信小程序的“畅游广昌”智慧旅游服务平台设计与实现

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

📰

基于微信小程序的宠物社区系统设计与实现

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

📰

FairScan Scan-to-PDF Intent开发指南:让你的应用调用隐私优先的扫描器

【免费下载链接】FairScan An Android app to scan your documents — simple and respectful 项目地址: https://gitcode.com/gh_mirrors/fa/FairScan 点击查看 免费下载 FairScan 是一个开源的 Android 文档扫描应用,主打「简单、尊重用户」&#xff…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬