C语言实现串口转网口:RS485/COM到TCP双向转发与队列背压

发布时间:2026/11/2 5:15:05

C语言实现串口转网口:RS485/COM到TCP双向转发与队列背压 简介《串口和网口数据相互转发》是一份面向嵌入式与工业控制方向学习者的底层通信实践代码围绕串口与以太网之间的双向数据交换解决上下位机、PLC/SCADA远程采集等场景中的协议衔接问题。压缩包共5个文件包含2个C源文件、1个头文件、1个Makefile及1份说明文档整体约4KB结构精简便于直接编译阅读。其核心涉及串口参数配置、Socket套接字收发、select多路I/O监听、多线程并发处理与异常容错并留有协议转换和调试测试的扩展空间。目前已有1709人学习下载适合想理解com/ip转发骨架、梳理select与线程同步思路或做课程设计参考的读者。1. 从一根 RS485 线到一张网卡串口和网口数据相互转发在真实场景里长什么样有一年给某实验室做设备联网现场是一台老称重仪表、一根 DB9 线、一台已经没有串口的工控机。仪表只会往串口上吐 ASCII上位机只认 TCP。改仪表固件不现实改上位机也不现实中间加一段串口和网口数据相互转发就成了唯一解。这类需求在 com/ip/c 这个方向里被反复提起用 C 写一个常驻进程一头 open 串口一头 listen 端口两头字节流原样对搬。它不解析协议、不做业务只解决“线上来的东西怎么进到网里”这一件事。适合手上有一堆串口设备、又想用一套网络架构统一采集的人也适合刚学 Linux 网络编程、想找一个能跑通的完整例子的开发者。2. 架构拆解一条报文从 COM 到 TCP 中间要过几道手这套东西看着简单两行伪代码就能描述串口读到什么就往 socket 写socket 读到什么就往串口写。真写起来翻了车的地方全在“两边速度不一样”和“两边生命周期不一样”上。串口是 115200 的慢速设备网口是几十兆的突发通道中间没有一个缓冲区兜着必然出现半包、粘包、写阻塞最后表现成上位机收到一堆断断续续的乱码。所以整个架构的核心其实不是转发是两条单向队列。2.1 三个角色与两条队列程序被拆成三个角色串口侧、网口侧、转发核心。转发核心不持有任何 fd只持有两个环形队列。数据方向是严格单向的一条队列只被一个生产者写、一个消费者读锁的争用面被压到最小。角色典型函数职责阻塞点串口生产者uart_rx_workerread(tty)→ 推入to_netread100ms 超时返回网口消费者net_tx_worker从to_net弹出 →send(sock)队列空时条件变量等待网口生产者net_rx_workerrecv(sock)→ 推入to_uartrecv阻塞在 socket 上串口消费者uart_tx_worker从to_uart弹出 →write(tty)队列空时条件变量等待这条链路一共四个线程加上一个主线程负责 accept。为什么不在一个线程里用poll同时管串口和 socket能而且代码更短。但串口侧的poll事件和 socket 侧的读写状态机混在一起写一旦要做“客户端断开时清空队列”“串口设备被拔掉时优雅退出”这类收尾动作分支会迅速膨胀。拆成四条单向流水线之后每一段只关心自己的输入输出出问题时strace -f -p挂上去一眼就能看出是哪一段卡住了。2.2 线程模型为什么是阻塞读加队列而不是 epoll很多人第一反应是上 epoll 做单线程事件循环。这个选择在“几千个连接”的服务端场景是对的在这里不对。原因有三个。第一串口是文件描述符但不是 socket它的可读事件在epoll里表现为EPOLLIN但串口的“可读”语义是“内核缓冲区里有数据”一个read能取多少完全取决于驱动。串口侧做非阻塞读你得自己维护一个半包缓冲等于把内核已经帮你做好的事又做了一遍。第二这套程序的连接数是个位数通常是“一个上位机连一台设备”或者“一台采集网关连一个服务端”。epoll 的优势在这个量级上等于零反而把“一条报文从哪来、到哪去”的调用栈打散了。第三也是最实际的一点现场调试的时候客户或者同事只会给你一句“不转了”。四条阻塞线程的架构你在任意时刻gdb -p上去看四个线程的栈就知道谁在等谁事件循环架构你得先打印当前事件集才能开始查。我一般会在线程入口加一行pthread_setname_nptop -H里能直接看到uart_rx、net_tx这些名字排查成本低一个数量级。代价当然有。线程数固定意味着连接数固定程序默认只支持一个 TCP 客户端后来的连接会把前一个踢掉。这在设备联网场景里是特性不是缺陷——多客户端抢同一路串口语义上本来就说不清谁该收到回包。2.3 数据流与背压队列满了到底该丢谁的数据这是整套设计里唯一需要拍脑袋的地方。队列不能无限长串口 115200 大概每秒 11KB网口一条千兆链路跑满能到 100MB/s两者的写入速率差三个数量级。队列长度定死之后一旦网口消费者写不出去对端不 recv、窗口打满、网线松了to_net会迅速填满。这时候有三个选择一是阻塞生产者让串口读线程停下来。后果是串口内核缓冲区溢出驱动层丢数据而且你根本不知道丢了多少。二是丢新数据队列满了就丢弃这次读到的内容。三是丢老数据把队列头部挤出去。设备协议大多带序号或者带帧尾校验丢新数据会让上位机看到一串“序号跳变”丢老数据会让上位机收到“时间倒流”。两种都难看但丢新数据至少保持了已经入队数据的时序完整性。我一般选第二种rq_push在队列满的时候带超时地等一下等不到就只写入剩余空间返回值告诉调用方实际写进去多少剩下的由调用方决定记日志还是直接扔。对应的日志里会打一行drop 128B, queue full现场看到这行就知道该查网口侧了而不是去怀疑串口线。提示队列容量别按“一秒的数据量”拍。串口 115200 下我一般给 8KB够兜住半秒的突发网口侧给 64KB因为上位机做批量读取时经常一次性拉走一大段。3. 编译与设备打开把最小可跑骨架立起来架构定了之后第一步不是写转发是先把“串口能打开、端口能 listen、程序能干净退出”这三件事跑通。很多项目死在第三步——CtrlC 之后端口还被占着重启报Address already in use然后开始怀疑人生。3.1 目录结构与依赖整套东西只依赖 libc 和 pthread没有第三方库这是它能在各种老旧工控机上直接编译的原因。我把代码分成四块每块一对.c/.hbridge/ ├── Makefile ├── src/ │ ├── main.c # 参数解析、信号注册、线程拉起与回收 │ ├── uart.c # termios 初始化、串口读写封装 │ ├── net.c # listen/accept/recv/send 封装 │ ├── ringq.c # 环形队列带互斥锁与条件变量 │ └── log.h # 带时间戳和级别的宏 └── tools/ └── test_tcp.py # 上位机侧的模拟客户端uart.c和net.c都不 include 对方的头文件两者通过ringq.h里的队列类型间接通信。这个约束很重要它保证了你以后想把串口换成 UDP、或者把 TCP 换成 WebSocket改动范围只在一个文件里。3.2 Makefile 和必须带的编译开关CC ? gcc CFLAGS ? -Wall -Wextra -O2 -g -pthread LDFLAGS ? -pthread TARGET bridge SRCS $(wildcard src/*.c) OBJS $(SRCS:.c.o) $(TARGET): $(OBJS) $(CC) $(LDFLAGS) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c -o $ $ clean: rm -f $(OBJS) $(TARGET)-pthread在编译和链接两处都要出现只写在一处会在某些 glibc 版本上链接失败。-g别省现场崩溃拿到的 core 没有符号表基本等于废纸。-Wall -Wextra打开的收益在send返回值这类地方特别明显——忽略send的部分写是这套程序最常见的 bug 来源。3.3 termios 串口初始化几个必须改的字段串口初始化是新手最容易照抄一段代码然后出问题的地方因为网上流传的版本大多来自二十年前的示例混着cfmakeraw之前的老写法。我用的是下面这一版static speed_t baud_to_speed(int baud) { switch (baud) { case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; case 57600: return B57600; case 115200: return B115200; case 230400: return B230400; default: return B115200; } } int uart_open(const char *dev, int baud) { int fd open(dev, O_RDWR | O_NOCTTY); if (fd 0) return -1; // 设备不存在或无权限错误码在 errno struct termios tio; if (tcgetattr(fd, tio) ! 0) { close(fd); return -1; } cfmakeraw(tio); // 关掉行缓冲、回显、信号字符让字节原样进出 tio.c_cflag | (CLOCAL | CREAD); // 忽略调制解调器控制线开启接收 tio.c_cflag ~CRTSCTS; // 关硬件流控除非对方明确要求 tio.c_cflag ~CSTOPB; // 1 位停止位 tio.c_cflag ~PARENB; // 无校验 tio.c_cflag ~CSIZE; tio.c_cflag | CS8; // 8 位数据位 tio.c_cc[VMIN] 0; // 配合 VTIME 使用读不到就按时间返回 tio.c_cc[VTIME] 1; // 100ms既不让线程空转也不至于延迟太高 cfsetispeed(tio, baud_to_speed(baud)); cfsetospeed(tio, baud_to_speed(baud)); if (tcsetattr(fd, TCSANOW, tio) ! 0) { close(fd); return -1; } tcflush(fd, TCIOFLUSH); // 清掉上电瞬间的残留字节 return fd; }逐段说明。O_NOCTTY防止这个串口成为进程的控制终端不加的话在容器里跑偶尔会收到莫名其妙的 SIGHUP。cfmakeraw一次把ICANON、ECHO、ISIG、IEXTEN全关掉让串口变成纯粹的字节通道——这是能不能转发二进制数据的分水岭只关ICANON而留着ISIG数据里出现 0x03 就会给你的进程发 SIGINT表现是“转发一段时间后进程自己退了”。VMIN0加VTIME1的组合含义是即使没有数据read也会在 100ms 后返回 0。这比设成阻塞模式好在退出时——g_running置零之后最迟 100ms 线程就能走到循环判断不需要靠信号打断。3.4 网口监听SO_REUSEADDR 和 TCP_NODELAY 一个都别省int tcp_listen(int port, int backlog) { int fd socket(AF_INET, SOCK_STREAM, 0); if (fd 0) return -1; int on 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, on, sizeof(on)); // 重启时端口不进入 TIME_WAIT 卡死 struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); addr.sin_port htons((uint16_t)port); if (bind(fd, (struct sockaddr *)addr, sizeof(addr)) ! 0) { close(fd); return -1; } if (listen(fd, backlog) ! 0) { close(fd); return -1; } return fd; }SO_REUSEADDR解决的是重启瞬间的bind失败这是开发阶段最烦人的问题之一。TCP_NODELAY在 accept 之后对新连接设置串口报文大多只有几十字节Nagle 算法攒包的延迟在 40ms 量级上位机那边看到的将是明显的“一顿一顿”。另外INADDR_ANY意味着监听所有网卡。如果机器同时连着办公网和设备网更稳妥的做法是加一个-a参数指定监听地址避免把设备串口暴露到不该暴露的网络上。4. 核心转发模块串口收、网口发两边怎么对齐骨架跑通之后真正写起来的就是队列和四个 worker。这一章把关键代码贴出来逐段过一遍重点在于锁的边界和错误处理不在于逻辑本身。4.1 环形队列锁只包住改指针的那几行typedef struct { unsigned char *buf; size_t cap; // 容量启动时定死运行期不 realloc size_t head; // 写位置 size_t tail; // 读位置 size_t used; // 已用字节数 int closed; // 关闭标志退出时置 1 唤醒所有等待者 pthread_mutex_t mtx; pthread_cond_t not_empty; pthread_cond_t not_full; } ringq_t;int rq_push(ringq_t *q, const unsigned char *src, size_t n) { pthread_mutex_lock(q-mtx); /* 队列满时最多等 200ms。绝不无限等否则生产者被拖住串口内核缓冲会溢出 */ while (q-used q-cap !q-closed) { struct timespec ts; clock_gettime(CLOCK_REALTIME, ts); ts.tv_nsec 200L * 1000 * 1000; if (ts.tv_nsec 1000000000L) { ts.tv_sec 1; ts.tv_nsec - 1000000000L; } if (pthread_cond_timedwait(q-not_full, q-mtx, ts) ! 0) break; } size_t space q-cap - q-used; if (n space) n space; // 写多少算多少剩下的由调用方丢弃并记日志 for (size_t i 0; i n; i) { q-buf[q-head] src[i]; q-head (q-head 1) % q-cap; } q-used n; pthread_cond_signal(q-not_empty); pthread_mutex_unlock(q-mtx); return (int)n; }used这个字段是刻意的冗余。用head tail判断空、用(head 1) % cap tail判断满看起来更省内存但会让队列实际容量变成cap - 1配容量的时候得算半天。多存一个size_t换一个清晰的cap语义值得。closed标志是退出路径的关键。没有它消费者线程会永远卡在pthread_cond_wait上pthread_join就永远不返回进程 CtrlC 之后变成僵尸。int rq_pop(ringq_t *q, unsigned char *dst, size_t n) { pthread_mutex_lock(q-mtx); while (q-used 0 !q-closed) pthread_cond_wait(q-not_empty, q-mtx); // 真正睡下去不占 CPU if (q-used 0 q-closed) { // 被关闭唤醒且确实没数据了 pthread_mutex_unlock(q-mtx); return -1; } if (n q-used) n q-used; for (size_t i 0; i n; i) { dst[i] q-buf[q-tail]; q-tail (q-tail 1) % q-cap; } q-used - n; pthread_cond_signal(q-not_full); pthread_mutex_unlock(q-mtx); return (int)n; }一个细节pthread_cond_wait返回后必须重新检查条件因为存在虚假唤醒。while而不是if是硬性要求。4.2 串口读线程与网口写线程void *uart_rx_worker(void *arg) { unsigned char tmp[512]; while (g_running) { ssize_t n read(g_uart_fd, tmp, sizeof(tmp)); if (n 0) { int w rq_push(g_to_net, tmp, (size_t)n); if (w n) log_warn(uart-net drop %d bytes, (int)(n - w)); } else if (n 0) { if (errno EINTR || errno EAGAIN) continue; log_error(uart read failed: %s, strerror(errno)); break; // 串口拔了退出线程由主线程收尾 } } return NULL; }tmp给 512 字节是个折中。串口驱动单次返回的数据一般不超过 FIFO 深度512 足够覆盖给太大只会在栈上浪费空间。void *net_tx_worker(void *arg) { unsigned char tmp[1024]; while (g_running) { int n rq_pop(g_to_net, tmp, sizeof(tmp)); if (n 0) break; // 队列已关闭 if (n 0) continue; int fd cli_snapshot(); // 加锁取当前连接 fd if (fd 0) continue; // 没有客户端数据直接丢 int off 0; while (off n) { /* MSG_NOSIGNAL 等价于忽略 SIGPIPE对端关闭时返回 EPIPE 而不是杀进程 */ ssize_t w send(fd, tmp off, (size_t)(n - off), MSG_NOSIGNAL); if (w 0) { off w; continue; } if (w 0 errno EINTR) continue; log_warn(client send failed: %s, strerror(errno)); cli_close_if(fd); // 只关自己快照到的那个 fd break; } } return NULL; }send的部分写循环是这套代码里第二常见的 bug 来源。TCP 的send不保证一次写完尤其是上位机读取慢的时候只调一次然后丢掉返回值表现就是“大部分时候正常压测时偶尔缺一段”而且因为数据本身没有帧边界你很难证明是这一段丢的。cli_close_if(fd)里带一个 fd 比较是为了避免旧消费者关掉刚被 accept 替换上来的新连接。4.3 accept、断链和 SIGPIPEvoid *accept_worker(void *arg) { while (g_running) { struct sockaddr_in peer; socklen_t len sizeof(peer); int cfd accept(g_listen_fd, (struct sockaddr *)peer, len); if (cfd 0) { if (errno EINTR) continue; log_error(accept failed: %s, strerror(errno)); usleep(100 * 1000); // 防 listen fd 出错时死循环打日志 continue; } int on 1; setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, on, sizeof(on)); setsockopt(cfd, SOL_SOCKET, SO_KEEPALIVE, on, sizeof(on)); log_info(client %s:%d connected, inet_ntoa(peer.sin_addr), ntohs(peer.sin_port)); cli_replace(cfd); // 单客户端模式踢掉旧的换成新的 } return NULL; }cli_replace里面是加锁替换g_client_fd然后对旧 fd 调shutdown(SHUT_RDWR)而不是直接close。区别在于直接close之后阻塞在recv上的net_rx_worker可能还没返回此时新 accept 拿到的 fd 号如果复用了刚释放的那个旧线程就会读到新连接的数据。这个窗口很小但在“上位机频繁重连”的场景下能稳定复现成“新连接一上来就收到一段不属于它的旧数据”。shutdown之后recv立刻返回 0线程走正常退出路径再由持有者closefd 复用的问题就不存在了。main里还有一行signal(SIGPIPE, SIG_IGN)。不写这一行对端关闭后第一次send就会给进程发 SIGPIPE默认动作是终止进程用户看到的现象就是“上位机关个窗口转发程序自己没了”。5. 排查笔记串口有数据但网口收不到问题通常在这五处这套代码上线之后我收集了一堆现场反馈八成以上都能归到下面五类。每一条按“现象是什么、根因在哪、怎么解决”写顺序就是排查顺序。5.1 现象open返回成功但read一直返回 0串口上明明有数据原因几乎总是串口参数没生效或者设备根本不是那个 tty。常见的情况是参数里写/dev/ttyS0而实际设备是插上 USB 转串口之后才出现的/dev/ttyUSB0或者tcsetattr返回了非 0 被忽略掉波特率设置失败串口停在默认的 9600。还有一个更隐蔽的两个进程同时打开了同一个串口后打开的那个把前一个的设置覆盖了两边都在读数据被随机分走。排查就三步ls -l /dev/serial/by-id/看设备真实路径用稳定路径而不是ttyUSB0检查tcsetattr的返回值并打日志lsof /dev/ttyUSB0确认没有别的进程在占。stty -F /dev/ttyUSB0 -a在程序运行时敲一下能看到当前实际生效的参数比猜快得多。5.2 现象网口能连上但收到的字节永远是半截帧尾差几个原因是串口侧用VMIN/VTIME做了超时切分而协议层有帧边界。假设上位机发来 100 字节串口侧read拿到 64 字节就返回了剩下 36 字节进了下一轮。对转发程序来说这没错它转的是字节流但上位机如果按“一次recv就是一帧”来解析就会看到 64 字节的残帧。解决分两种情况。如果上位机是字节流解析自己拼包、自己找帧头什么都不用改。如果上位机期待完整帧就得在转发层加一个“帧聚合”逻辑维护一个小缓冲按协议里的帧头长度 长度字段或者帧尾字符判断出完整一帧再入队。这一步本质上是把透明转发改成协议网关下一章会展开。5.3 现象跑一晚上进程自己没了日志最后一行没有任何报错八成是SIGPIPE 没屏蔽客户端异常断开拔网线、上位机崩溃、防火墙超时清理时进程被信号杀掉。表现特征很明显dmesg里没有 OOM程序目录下没有 core但 shell 的$?是 141128 13。修复是signal(SIGPIPE, SIG_IGN)配合send的MSG_NOSIGNAL双保险。同时把send/recv返回ECONNRESET、EPIPE的情况显式处理成“关掉这个连接继续等下一个”而不是让线程退出。另外建议在main里给SIGSEGV装一个只打印backtrace的 handler不指望它救命但至少能留下现场。5.4 现象上位机一关窗口转发程序和串口都不动了重启才好这是写阻塞。客户端关闭之后g_client_fd还是那个已经失效的 fdnet_tx_worker拿着它send第一次可能返回EPIPE但如果 fd 已经被内核回收并分配给了别的对象send可能就阻塞在那里等发送窗口。线程被卡住之后to_net队列迅速填满串口读线程跟着卡在rq_push上整条链路停摆。根子在连接的生命周期管理上。正确做法是把“当前连接”做成一个结构体accept 时创建、断开时销毁消费者持有的是引用而不是裸 fd简化版本至少要保证cli_close_if里比较 fd 相等再关并且send之前检查 fd 是否仍然有效。另一个务实的做法是给 socket 设SO_SNDTIMEO比如 3 秒超时就当连接已死处理。5.5 现象115200 下大量乱码降到 9600 反而正常这是纯粹的电平和线材问题不是软件问题但现场经常被归到软件头上。波特率一高对线材长度、屏蔽、共地、终端电阻都敏感。USB 转串口的廉价线在 115200 下丢字节非常普遍尤其是没有做磁隔离的场合。排查顺序先用短线下在同一台机器上环回TX和RX短接确认转接头本身没问题再换屏蔽双绞并确认两端共地如果设备侧是 RS485检查终端电阻和偏置电阻是否齐全。软件侧能做的只有把read的缓冲区调大、在日志里记录每次读到的字节数看是不是有规律地丢——有规律丢通常是驱动 FIFO 溢出无规律丢通常是电气问题。6. 二次开发从“能转”到能进机柜的样子代码能跑之后接下来是把它改成能在现场呆住的东西。下面三个方向是我每次接这类活儿都会做的改造。6.1 从透明转发改成带帧头的协议网关现场最常见的需求是“给每一帧加一个来源标识”。做法是在uart_rx_worker里把读到的字节先喂给一个轻量状态机识别出完整帧之后包一层自定义头再入队typedef struct { uint16_t magic; // 0xA55A uint16_t len; // 负载长度不含头 uint8_t src; // 0 串口, 1 网口 uint8_t rsv[3]; } frame_hdr_t;状态机按magic找帧头、按len等负载、按协议里的校验字段验尾。识别不出来的时候要有超时兜底把缓冲清空并且计数否则一个错误的长度字段就能让后面全乱。这一步做完之后程序就从“字节搬运工”变成了“协议转换器”上位机那边可以省掉自己拼包的逻辑。6.2 参数配置化和日志落盘命令行参数够用一时不够用一年。改造方式是把dev/baud/port/queue_size/log_level抽到一个bridge.conf里main里读一遍命令行参数可以覆盖。日志别只往 stdout 打加一个按天滚动的文件输出同时保留 stderr——现场运维看的是文件你 SSH 上去看的是 stderr两边都要。日志里至少要有连接建立与断开、队列丢包计数、串口读错误、每次进程启动时的完整配置。6.3 上线前怎么验证没有真设备的时候用socat拉一对虚拟串口就能把整链路跑通# 起一对互联的虚拟串口/tmp/ttyV1 给 bridge/tmp/ttyV2 给测试端 socat -d -d pty,raw,echo0,link/tmp/ttyV1 pty,raw,echo0,link/tmp/ttyV2 # 另开一个窗口跑转发 ./bridge -d /tmp/ttyV1 -b 115200 -p 5000然后用 Python 从两头对打import socket, time, serial # 串口侧发网口侧收 s socket.create_connection((127.0.0.1, 5000), timeout3) p serial.Serial(/tmp/ttyV2, 115200, timeout1) p.write(bHELLO-FROM-UART) time.sleep(0.2) print(tcp recv:, s.recv(64)) # 反向 s.sendall(bHELLO-FROM-TCP) time.sleep(0.2) print(uart recv:, p.read(64))两个方向都通了再用一个循环往上加压串口侧每秒发 200 帧、网口侧故意延迟读取观察丢包计数是否按预期增长、进程是否还能响应 CtrlC。这套验证我一般会写成一个脚本放进tools/改动之后再跑一遍比上真设备快得多。注意socat创建的虚拟串口对没有真实的流控和电气特性能验证逻辑不能验证 115200 下的稳定性那部分还得靠真实硬件。从那以后我每接一个转发的活都会先在机柜边上用虚拟串口把整条链路跑一遍再插真设备这一步多花的二十分钟抵得上现场排查一整天。这套代码的最小版本我已经在好几个项目里复用改的永远是配置和帧格式队列和线程那部分基本没动过。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/2 5:15:05

卡尔曼滤波+模板匹配:轻量单目标跟踪的遮挡处理实践

简介:面向计算机视觉初学者与目标跟踪方向的学习者,这份项目围绕遮挡与重叠场景下的单目标跟踪展开,将卡尔曼滤波的预测—更新机制与最大IOU匹配结合,用于缓解目标被部分遮挡或相互重叠时跟丢的问题,属于机器学习在视觉…

2026/11/2 5:15:05

Windows内网邮件服务器部署:SMTP/POP3/IMAP配置与排障

简介:Foxmail Server For Windows v2.0 公测版面向需要在 Windows 环境下搭建邮件服务的个人与小型团队,提供 SMTP、POP3、LDAP 与 MIME 支持,可通过 Foxmail、Outlook 等客户端或全中文 Web 界面收发和处理邮件,管理员亦能借助 W…

2026/11/2 5:10:05

Unity多版本共存与跨平台构建环境管理实践

上周有个做外包的朋友问我,说他手头同时在维护四个项目,一个锁在很老的LTS上,一个是去年升过的大版本,还有两个是新起的Demo,想在同一台机器上"几分钟内全部跑起来",问我有没有什么一键搞定的路子…

2026/11/2 6:15:09

3 条命令装好 BBDown 并下载 B站 视频

3 条命令装好 BBDown 并下载 B站 视频 【免费下载链接】BBDown Bilibili Downloader. 一个命令行式哔哩哔哩下载器. 项目地址: https://gitcode.com/gh_mirrors/bb/BBDown 想把 B 站视频留存到本地离线看时,网页播放器不提供下载入口,而在线解析站…

2026/11/2 6:10:08

C++ 实现QT信号槽

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

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/1 1:01:33

SSM电子书店源码全解析:结构、原理、部署与排错实战

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

2026/11/2 1:04:04

工业级SPI隔离设计:硬件抗扰与信号完整性实战指南

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

2026/11/2 1:04:04

PLFM_RADAR:主动探测数据通道,构建低误报的数据感知系统

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

2026/11/2 1:04:04

工业SPI隔离设计:MAX14431与STM32F746硬件抗干扰实战

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

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/1 1:01:33

SSM电子书店源码全解析:结构、原理、部署与排错实战

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

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑