简介这是一套基于uIP轻量级TCP/IP协议栈的TCP客户端与服务器实验工程面向具备一定C语言和STM32基础的嵌入式开发者及高校学习者对应ALIENTEK战舰STM32开发板ENC28J60网络模块实验。资源共223个文件以81个.h头文件和76个.c源码为核心辅以txt说明、pdf文档、png图片及Makefile等构建文件压缩包约2.59MB便于按工程结构查阅源码与实验资料。已有742人学习。读者可从中获得完整的uIP客户端/服务器示例结合源码理解协议栈分层、内存与数据包管理、ENC28J60初始化及SPI数据传输、STM32中断与定时器处理并梳理TCP连接建立、数据收发和断开流程。它适合用来对照实验工程学习嵌入式网络通信从网卡驱动到应用层API调用形成较完整的实践参考。1. uIP 加双端示例几百字节 RAM 里怎么把 TCP 跑起来一块只有几 KB RAM 的采集板需求朴素得不能再朴素每隔几秒把一包状态丢给内网的一台服务器服务器偶尔回一条配置。串口拉不了那么远加无线模块成本和功耗都上去了最后只能在板子上加一颗 SPI 接口的以太网控制器。麻烦在协议栈——常见那套完整 TCP/IP 动辄几十 KB RAM这颗 MCU 连一半都拿不出来。这套 uIP tcp_client tcp_server 的工程就是为这种场景准备的uIP 协议栈本体只占几个 KB 代码空间RAM 开销压在 1KB 以内TCP client 和 TCP server 各有一份可以直接套用的回调实现中间那层网卡驱动也留好了接口。适合谁手上资源紧、只要「连得上、发得出、收得到、断了能重连」、不追求高并发的嵌入式开发者。读它的价值不在于学 TCP 原理而在于看清一个真实的极简协议栈应用层到底该怎么写回调。2. 工程分层uip/、apps/、netdev/、arch/ 各管什么拿到这份代码先别急着编译。uIP 这类协议栈的坑有一半出在分层没看清——应用层以为自己能随时读uip_buf驱动层以为自己能随便调uip_input()最后联调时现象千奇百怪。花十分钟把四个目录的职责边界理顺后面省的时间不止十倍。2.1 四个目录的职责边界目录大致是这个形状不同移植版本文件名会有出入但分层不会变uip_demo/ ├── uip/ # 协议栈本体基本不用改 │ ├── uip.c # 核心IP/TCP/ICMP 处理与连接状态机 │ ├── uip_arp.c # ARP 请求与应答 │ ├── uip.h │ ├── uipopt.h # 编译期默认值弱定义 │ └── uip-conf.h # 工程侧覆盖默认值的地方 ├── apps/ │ ├── tcp_client/ │ │ ├── tcp_client_app.c # 主动连接 上报 重连退避 │ │ └── tcp_client_app.h │ └── tcp_server/ │ ├── tcp_server_app.c # 监听端口 按连接缓存 回包 │ └── tcp_server_app.h ├── netdev/ │ ├── netdev.h # 驱动抽象init / read / send / poll │ └── netdev_spi.c # SPI 网卡的具体实现 ├── arch/ │ ├── clock-arch.c # 提供 10ms 粒度的时钟 │ └── clock-arch.h └── main.c # 初始化 主循环目录职责改动频率谁调用谁uip/协议解析、连接状态机、重传定时几乎不改只在配置宏上动被main.c和apps/调用apps/业务回调写UIP_APPCALL主要改这里由协议栈回调反向调uip_send/uip_closenetdev/收发包、MAC 过滤、链路状态换硬件时改被main.c调用arch/时钟源喂uip_periodic的节拍换 MCU 时改被main.c调用关键约束是apps/里不直接碰uip_bufnetdev/里不直接碰uip_conn。中间这层约束不打破出问题时你至少知道该看哪。2.2 uip-conf.h 里必须先改掉的几个宏uIP 的默认配置是给「最小可用」准备的直接烧进去大概率不够用。uipopt.h里全是#ifndef ... #define的弱定义正确做法是在uip-conf.h里先定义再让它生效/* uip-conf.h —— 工程侧覆盖顺序不能反必须在 include uipopt.h 之前定义 */ #ifndef UIP_CONF_H #define UIP_CONF_H #define UIP_CONF_MAX_CONNECTIONS 3 /* 并发连接上限客户端占 1 个服务端留 2 个 */ #define UIP_CONF_MAX_LISTENPORTS 2 /* 监听端口数用几个就写几个 */ #define UIP_CONF_BUFFER_SIZE 200 /* uip_buf 大小一个 TCP 段装不下就得靠 MSS 限 */ #define UIP_CONF_TCP_MSS 96 /* 单个 TCP 载荷上限必须 BUFFER_SIZE - 40 */ #define UIP_CONF_RECEIVE_WINDOW 96 /* 接收窗口单包缓冲下不要超过 MSS 太多 */ #define UIP_CONF_UDP 0 /* 不用 UDP 就关掉省 flash */ #define UIP_CONF_ACTIVE_OPEN 1 /* tcp_client 必须打开 */ #endif几个数字的关系得算清楚不然编译过了也跑不通。uip_buf里要装下以太网头14 字节 IP 头20 TCP 头20 载荷所以TCM_MSS最大只能到BUFFER_SIZE - 54左右取整留点余量。RECEIVE_WINDOW在单缓冲实现下设置得比 MSS 大没意义对端发来的数据超过你能一次接住的范围只会触发窗口为零、双方互等。MAX_CONNECTIONS是最容易被低估的一项。每个连接对应一个struct uip_conn里面装着本地端口、远端 IP、序号、重传计数几十字节起步。设成 10 看着不多在只有 2KB RAM 的片子上就是灾难。我一般按「客户端 1 个 服务端同时在线数 1 个余量」来定。2.3 应用回调是怎么挂上去的UIP_APPCALL 与 appstateuIP 没有 socket 概念所有 TCP 事件都进同一个入口函数。uip-conf.h里通常这样声明/* 告诉协议栈所有 TCP 事件都进这个函数 */ void tcp_client_appcall(void); void tcp_server_appcall(void); #define UIP_APPCALL app_dispatch /* 有些移植版本用统一分发见下 */如果客户端和服务端同时存在就得做一次分发。uIP 的struct uip_conn里有个不太起眼的字段appstate类型是u8_t就是留给你标记这条连接属于谁的/* 统一入口按 lport 或 appstate 区分是客户端连接还是服务端连接 */ void app_dispatch(void) { /* 主动连接发起时我们把这个连接标成 1监听进来的连接默认是 0 */ if (uip_conn-appstate APP_ROLE_CLIENT) { tcp_client_appcall(); } else { tcp_server_appcall(); } }appstate只有 8 位别指望塞指针。要用更多上下文就自己开一个和uip_conns[]等长的数组用uip_conn - uip_conns[0]算出下标去索引。这个技巧在服务端那章会用到是 uIP 应用层最实用的一个写法。3. 跑通第一包驱动对接点与主循环节奏协议栈配置文件改完接下来是真正把包发出去。uIP 的移植工作量集中在两处网卡驱动的四个接口和主循环里那几个函数的调用顺序。顺序错了包能发出去但收不回来抓包看到的现象会非常迷惑。3.1 网卡驱动只需暴露四个函数netdev.h定义了一组最小接口具体实现按你手上那颗 SPI 以太网控制器来写/* netdev.h —— 驱动与协议栈之间的唯一契约 */ #ifndef NETDEV_H #define NETDEV_H #include stdint.h void netdev_init(void); /* 复位芯片、配 MAC 过滤、开接收 */ uint16_t netdev_read(void); /* 读一帧进 uip_buf返回长度0 表示无包 */ void netdev_send(void); /* 把 uip_buf 里 uip_len 字节发出去 */ uint8_t netdev_poll(void); /* 有包待收返回 1仅查状态不读数据 */ #endifnetdev_read()的实现里有个必须做对的细节以太网帧头那 14 字节也要读进uip_buf因为uip_arp_arpin()和uip_input()都假定uip_buf最前面是 MAC 头。常见错误是驱动只把载荷读进来结果 ARP 永远对不上PC 上ping全是超时。/* netdev_spi.c 片段读一帧到 uip_buf */ uint16_t netdev_read(void) { uint16_t len nic_read_frame(uip_buf, UIP_BUFSIZE); if (len 0) { return 0; } /* 去掉 4 字节 CRCuIP 不认尾巴校验 */ if (len 4) { len - 4; } if (len UIP_BUFSIZE) { /* 超长帧直接丢别指望上层处理 */ return 0; } return len; }uip_buf在 uIP 里是全局的类型跟你的链路层有关以太网场景下它被当作struct uip_eth_hdr *用。注意UIP_BUFSIZE和你的UIP_CONF_BUFFER_SIZE是同一个东西别在两处各写一个数。MAC 地址设置也是一处翻车高发点/* main.c 里必须做的事把 MAC 写进协议栈否则 ARP 应答里全是 00 */ uip_ethaddr.addr[0] 0x02; /* 本地管理地址避免和真实厂商前缀冲突 */ uip_ethaddr.addr[1] 0x00; uip_ethaddr.addr[2] 0x00; uip_ethaddr.addr[3] 0x12; uip_ethaddr.addr[4] 0x34; uip_ethaddr.addr[5] 0x56;同时把这个地址写进网卡芯片的过滤寄存器两边对不上芯片收到发给自己的包也会扔。3.2 主循环的四段式顺序不能换main.c的主循环是整个工程的心脏写错了要么丢包要么卡死int main(void) { clock_init(); netdev_init(); uip_init(); /* 清空 uip_conns初始化监听表 */ tcp_client_init(); tcp_server_init(); /* 内部调用 uip_listen */ set_ip_addr(); /* 静态 IP 或 DHCP 拿到之后设 */ while (1) { /* 第一段上一轮处理产生的回包先发出去 */ if (uip_len 0) { netdev_send(); uip_len 0; } /* 第二段收包并处理。注意 ARP 和 IP 走不同分支 */ if (netdev_poll()) { uip_len netdev_read(); if (uip_len 0) { if (BUF-type htons(UIP_ETHTYPE_IP)) { uip_arp_ipin(); /* 顺带学习源 MAC */ uip_input(); /* 协议栈处理可能产生回包 */ } else if (BUF-type htons(UIP_ETHTYPE_ARP)) { uip_arp_arpin(); /* ARP 请求应答回包长度写回 uip_len */ } if (uip_len 0) { uip_arp_out(); /* 补 MAC 头查 ARP 缓存 */ netdev_send(); uip_len 0; } } } /* 第三段500ms 定时驱动重传、超时、poll 回调 */ if (periodic_timer_expired()) { for (uint8_t i 0; i UIP_CONN_MAX; i) { uip_periodic(i); if (uip_len 0) { uip_arp_out(); netdev_send(); uip_len 0; } } } /* 第四段业务自己的定时器跑在协议栈之外 */ app_timer_tick(); } }顺序为什么是「先发、再收、再定时」uip_input()和uip_periodic()都会在uip_buf里组装回包并把长度写进uip_len它们是「生产者」。如果你在同一个循环里先收包、再让业务逻辑改uip_buf、最后才发业务可能已经把待发的包覆盖了。先发一轮把残留清干净再收新的逻辑上最不容易出岔子。uip_periodic(i)的循环上界是UIP_CONN_MAX这个宏要和UIP_CONF_MAX_CONNECTIONS保持一致。有人只循环到实际连接数结果连接关闭后槽位没人回收重传定时再也不走那条连接就永远挂着。3.3 clock-arch 与 500ms 节拍uip_periodic建议 500ms 调一轮重传计数和超时判定都基于这个节拍。arch/clock-arch.c只要提供两个东西/* clock-arch.c —— 给 uIP 一个单调递增的 10ms 计数 */ static volatile clock_time_t tick_count 0; /* 放在 10ms 定时器中断里或者在主循环里按 systick 累加 */ void clock_tick(void) { tick_count; } clock_time_t clock_time(void) { return tick_count; }注意clock_time_t的位宽。8 位类型在 2.56 秒就溢出了拿来做超时比较会瞬间跑飞。至少用 16 位保险起见用 32 位溢出比较用(now - last) interval这种差值写法别直接比大小。3.4 第一次握手怎么验证板子烧进去先别急着写业务代码。拿一台同网段的 PC按这个顺序验证ping板子的 IP。通了说明 ARP 和 IP 层没问题uip_buf的头 14 字节读对了。PC 上telnet 板子IP 端口号看服务端是否进uip_connected()。debug 串口打印一行即可。用netcat -l 端口在 PC 上开监听让板子主动连看是否进uip_connected()。抓包看三次握手最后一个 ACK 的序号对不对序号错说明uip_conn初始化有问题。这四步里任何一步失败都别往下写业务逻辑。协议栈层的问题拖到业务层排查信号会被彻底淹没。4. 双端拆解客户端上报与服务端监听的两套状态机客户端和服务端在 uIP 里共用同一个回调入口但事件语义完全不同。客户端是「自己发起、自己决定何时关」服务端是「被动接受、跟着对端走」。两套状态机分别写清比混在一个switch里强得多。4.1 客户端启动uip_connect 的返回值必须判空主动连接只有一行但对错全在返回值上/* tcp_client_app.c */ static struct client_ctx { u8_t state; /* 0空闲 1等连接 2待发送 3等确认 4退避 */ u8_t retry; /* 连续失败次数 */ u16_t backoff; /* 退避计数单位 500ms */ u8_t txbuf[64]; /* 组包缓冲必须独立于 uip_buf */ u16_t txlen; } s; void tcp_client_start(uip_ipaddr_t *srv, u16_t port) { if (s.state ! 0) { return; /* 已经在连了防重入 */ } if (uip_connect(srv, htons(port)) NULL) { s.state 4; s.backoff 20; /* 10 秒后再试 */ return; } s.state 1; }uip_connect()返回NULL不是异常是「连接槽位用完了」的正常信号。常见错误是直接uip_send()而不管返回值结果uip_conn是空的回调根本不会进来程序看上去在跑但一包都发不出去。把返回值判空当成硬性规定退避重试交给业务定时器。srv参数是uip_ipaddr_t *构造方式看你的 IP 表示法常见的是uip_ipaddr(addr, 192,168,1,100)。端口一定要过htons()uIP 内部全用网络序。4.2 回调状态机五个事件分支各有分工完整的回调不长这样void tcp_client_appcall(void) { /* 1. 连接建立只标记状态不在这里发数据 */ if (uip_connected()) { s.state 2; s.retry 0; return; } /* 2. 连接被关闭 / 复位 / 超时统一走退避 */ if (uip_closed() || uip_aborted() || uip_timedout()) { s.state 4; s.backoff 20; s.retry; if (s.retry 5) { s.backoff 200; /* 连续失败就拉长到 100 秒别把服务器打爆 */ } return; } /* 3. 发送时机poll周期回调、acked上一包确认了、rexmit要重传 */ if (s.state 2 (uip_poll() || uip_acked() || uip_rexmit())) { s.txlen report_build(s.txbuf, sizeof(s.txbuf)); if (s.txlen 0) { uip_close(); /* 没东西可发主动关别占槽位 */ s.state 0; return; } uip_send(s.txbuf, s.txlen); /* 一次回调只能调一次 */ s.state 3; } /* 4. 数据被确认主动关闭走四次挥手 */ if (s.state 3 uip_acked()) { uip_close(); s.state 0; s.retry 0; } }逐条看。uip_connected()里调用uip_send()在部分版本上是可以的但我不建议——连接刚建立时发送窗口可能还没完全就绪等一轮uip_poll()再发行为更可预测。uip_acked()表示「之前uip_send()发出去的数据对端确认了」。这个事件是发送成功唯一的权威信号别拿netdev_send()返回成功当发送成功那只是芯片接受了 DMA。uip_rexmit()是 uIP 告诉你「这一包超时了需要重发」。它出现时你之前挂在uip_send()里的数据已经被丢弃必须重新调一次uip_send()。很多人只处理uip_poll结果丢包之后连接就永久卡住。uip_closed()、uip_aborted()、uip_timedout()三个事件的处理逻辑几乎一样都是「清状态、退避」但要不要区分日志得区分线上排障时这三种原因完全不是一回事。4.3 uip_send 的窗口边界与缓冲区所有权uip_send(buf, len)这行调用有两个隐含限制写错一个就是玄学问题。第一len不能超过当前有效发送窗口。对端通告的窗口大小受UIP_CONF_RECEIVE_WINDOW影响你一次uip_send塞 200 字节过去对方窗口只有 96uIP 会把超出部分截断或者干脆报错。安全的做法是业务层自己按uip_mss()切块每块发完等uip_acked()再发下一块。第二buf的生命周期。uip_send()只是记下指针和长度真正的拷贝发生在回调返回之后。所以传栈上局部数组一定出问题/* 错误示范回调返回后 s.txbuf 所在的栈帧已经没了 */ void bad_appcall(void) { u8_t tmp[32]; fill(tmp, sizeof(tmp)); uip_send(tmp, sizeof(tmp)); /* 咖啡还没凉数据已经烂了 */ }必须传静态区或全局区的缓冲。上面s.txbuf放在结构体里就是为了活过回调。同理接收侧uip_appdata指向的也是uip_buf内部回调返回后立刻失效收到的数据必须在回调内memcpy走。4.4 服务端uip_listen 之后靠连接下标分流数据服务端初始化和客户端完全不同它只需要一句监听剩下的全靠回调/* tcp_server_app.c */ #define SRV_PORT 5000 static struct srv_ctx { u8_t used; u8_t overflow; u16_t len; u8_t buf[128]; } ctx[UIP_CONF_MAX_CONNECTIONS]; void tcp_server_init(void) { uip_listen(htons(SRV_PORT)); /* 端口用网络序槽位由 uIP 自己分配 */ } /* 用 uip_conn 指针反算下标这是 uIP 里区分连接的标准做法 */ static struct srv_ctx *my_ctx(void) { return ctx[uip_conn - uip_conns[0]]; } void tcp_server_appcall(void) { struct srv_ctx *c my_ctx(); if (uip_connected()) { c-used 1; c-len 0; c-overflow 0; return; } if (uip_newdata()) { uint16_t n uip_datalen(); if (c-len n sizeof(c-buf)) { memcpy(c-buf c-len, uip_appdata, n); c-len n; } else { c-overflow 1; /* 超长直接标记等这一帧收完再统一处理 */ } } /* 简单分帧换行符结尾算一帧完整命令 */ if (c-len 0 c-buf[c-len - 1] \n !c-overflow) { uip_send((void *)OK\n, 3); /* 回包同样只能一次 */ } if (uip_closed() || uip_aborted() || uip_timedout()) { c-used 0; c-len 0; c-overflow 0; } }uip_conn - uip_conns[0]这个减法不是炫技是 uIP 应用层最经济的上下文索引方式。uip_conns是协议栈内部的连接数组回调期间uip_conn指向其中某个元素指针相减得到的就是稳定下标。前提是ctx[]的长度必须等于UIP_CONF_MAX_CONNECTIONS少一个就会越界。uip_datalen()返回本段载荷长度uip_appdata指向载荷首字节。注意一段 TCP 数据可能被拆成多次uip_newdata()到达所以必须自己拼缓冲。反过来一次uip_newdata()也可能带来多帧分帧逻辑不能假设「一次回调正好一帧」。4.5 双端共存时的资源分配客户端占一个连接槽服务端每来一个连接也占一个。UIP_CONF_MAX_CONNECTIONS 3时最多同时跑 1 个客户端 2 个服务端连接。超过之后新来的 SYN 会被忽略PC 上表现为「连接超时」。我一般这样分配客户端固定占 1 个服务端留 2 个给「配置通道 调试通道」再多就得上外部 RAM 或者换更大的片。另外uip_buf是全局唯一的一份客户端和服务端的收发在物理上串行这也是极简协议栈的天然限制——高并发场景它确实不合适别硬撑。5. 排查记录把联调拖成三天的五个坑协议栈代码本身很少出错出错的都是对接层。下面五条是按「现象 → 原因 → 解决」整理的实打实的记录每一条都值得在动手前先看一眼。5.1 ping 通了TCP 死活连不上现象PC 上ping 板子IP正常telnet 板子IP 5000一直停在连接中抓包只有 SYN 没有 SYN-ACK。原因uip_listen()的端口参数没做字节序转换写成了uip_listen(5000)。uIP 内部按网络序比较5000被解释成0x1388实际监听的是 5000 这个值对应的端口但你抓包看到的 SYN 目标端口是 5000 的十进制表示两个数在字节序上错位永远匹配不上。解决所有端口参数统一过htons()包括uip_connect()的第二个参数。这条规则没有任何例外看到裸数字就当成 bug。5.2 第一次能连第二次 connect 返回 NULL现象上电后第一次上报成功之后uip_connect()一直返回NULL重启才恢复。原因连接结束后没走完整的关闭流程。要么是收到uip_closed()只清了业务状态没让协议栈释放槽位要么是业务代码在uip_acked()之前就停止调用了连接卡在 FIN-WAIT 状态直到超时。uIP 的连接槽位回收依赖状态机走完跳过一步就泄漏一个。解决在uip_acked()或确认无事可做时显式调uip_close()收到uip_closed()时打印一行日志确认真的进来了。再加一个兜底业务定时器发现某个连接超过 60 秒没有任何事件直接uip_abort()强拆。5.3 大包发出去只到一半或者内容错位现象上报 64 字节服务端只收到 30 多字节剩下的是上一包的残留。原因uip_send()的len超过了当前 MSSuIP 按窗口截断但业务层以为全发出去了下一次组包直接覆盖了同一块缓冲。另一半原因是一个回调里调了两次uip_send()后一次把前一次的uip_slen覆盖掉。解决发送前用uip_mss()拿到当前可用载荷长度超了就自己切块每块等uip_acked()再发下一块单个回调内只允许一次uip_send()这条当纪律执行。5.4 服务端收到的 appdata 是乱的像是下一包的数据现象处理函数里把uip_appdata存成指针留到回调外等业务定时器再去读读到的内容每次都不一样。原因uip_appdata指向uip_buf内部uip_buf是全局唯一的那块内存回调返回后立刻被下一个包覆盖。这不是 bug是单缓冲设计的必然结果。解决回调内必须memcpy到自己的缓冲。如果数据量大就在回调里就地把解析做完只把解析结果几个字段存下来别存原始指针。5.5 跑几个小时之后彻底不响应现象前几个小时一切正常之后 ping 都不通复位就好。原因主循环里有阻塞延时比如某个传感器的 I2C 读要 200ms把uip_periodic()的 500ms 节拍拖得不准重传计数算错或者网卡的接收溢出标志没清芯片内部 FIFO 满了之后不再置位接收中断。解决主循环里禁掉超过 10ms 的阻塞长耗时操作拆成状态机在netdev_poll()里加一段「连续 N 次没包但溢出标志置位就复位接收 FIFO」的兜底。这条血泪经验换来的一句话是极简协议栈对时序的宽容度比你想的低。6. 二次开发把示例改成能跑一年的业务固件示例代码跑通不等于能用。真上现场第一件事是把「一次性上报」改成「长连接 心跳」还是「短连接 定时重连」这个选择决定了后面所有代码结构。我的习惯是短连接理由很简单uIP 的重传退避策略比较粗长连接一旦碰上弱网就容易卡在某个状态里短连接每次都是干净的开始跑一年也不会积累状态。改造的核心是三处。第一把client_ctx里的txbuf换成环形缓冲业务侧只管往里塞发送状态机按uip_mss()分块取第二在uip_poll()分支加一个心跳计数超过 30 秒没成功上报就主动断连重来别指望协议栈自己发现对端已经没了第三把连接参数做成可配置项服务器 IP、端口、上报周期放进一个结构体方便出厂时用串口写入。验证方式我固定走三步先用netcat在 PC 上开个只收不回的端口看板子是否在超时后正确退避再用iptables把回包 DROP 掉模拟半开连接观察 60 秒兜底是否生效最后挂一晚上第二天看串口日志里有没有出现「连续失败超过 5 次」的记录。这三步跑完基本能确认状态机没有泄漏路径。改造项示例里的做法上线建议连接方式单次请求单次关闭保持短连接加定时触发缓冲固定 64 字节数组环形缓冲容量按最大报文 ×2断线判断只靠协议栈超时业务层心跳 60 秒兜底强拆配置硬编码 IP 和端口参数区可写掉电保存从那以后我每次移植这类极简协议栈都强制先跑一遍「拔网线—插回—再拔」的循环至少二十次确认连接槽位一个都没漏才敢往上写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
DataOps平台架构设计与实践指南 1. 项目概述:DataOps平台的核心价值在数据驱动的时代,企业每天需要处理TB级甚至PB级的数据流转。我曾参与过某金融科技公司的数据中台建设,亲眼目睹过这样的场景:凌晨3点,数据团队还在手动修复前一天跑批失败的ETL任务… · 2026/11/5 6:19:23
LoadRunner性能测试中超时配置与优化实践 1. 性能测试中的超时问题解析在性能测试领域,超时问题就像交通拥堵时的红绿灯——设置得太短会导致大量"误判",设置得太长又会掩盖真实问题。最近在某个电商系统的压力测试中,我们使用LoadRunner模拟3000并发用户时,突然… · 2026/11/5 6:19:23
变频器使用说明书编写:从选型安装到参数通讯与故障验收 简介:《深川变频器SVF-EV使用说明书》面向工业自动化电气工程师、设备调试与维护人员,以及需要选型应用高性能矢量通用变频器的技术读者,集中解决安装、接线、参数设置与日常维护缺少官方依据的问题。压缩包内为1个doc文档,约13.1… · 2026/11/5 6:19:23
生产环境 Tomcat 200 线程跑满并报连接池超时:一次虚拟线程阻塞点排查 本文摘要:开启虚拟线程后线程告警消失,HikariCP 连接池超时却更早出现。先用 JFR 事件排除 synchronized 阻塞,再按并发预算重算连接池容量。 一、问题与结论
凌晨告警:http-nio-8080-exec-* 200 个处理线程全部阻塞,… · 2026/11/5 9:02:54
【工程师选酒避坑指南】一桌家常菜,配什么酒 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 8:59:48
DouyinLiveRecorder 直播录制完整指南:主页地址配置一次,实现开播自动监控 DouyinLiveRecorder 直播录制完整指南:主页地址配置一次,实现开播自动监控 【免费下载链接】DouyinLiveRecorder 可循环值守和多人录制的直播录制软件,支持抖音、TikTok、Youtube、快手、虎牙、斗鱼、B站、小红书、pandatv、sooplive、flextv… · 2026/11/5 8:59:48
【后端开发】FastAPI基础 2026.6.21
一、代码
1.导包
from fastapi import FastAPI # from ... import ...手动配置的方法:
终端输入:(确保终端提示符前有 (.venv) 标记)
pip install fastapi uvicorn2.创建实例
FastAPI实例app
app FastAPI()3.定义接口
… · 2026/11/5 8:50:37
快速上手playwight 在自动化测试和网页爬虫的领域,Selenium 曾是无可争议的霸主。然而,随着现代 Web 应用越来越复杂(大量使用 React、Vue、单页应用等),传统的测试工具开始显得有些力不从心。今天,我们要聊的是由微软开源的现… · 2026/11/5 8:48:36
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38