C# WebSocketServer源码实现:握手、帧解析与广播详解
简介一份面向C#开发者的WebSocket服务器源码包演示如何借助System.Net.WebSockets实现浏览器与服务器间的持久双向通信适合需要掌握实时交互应用或聊天服务开发的中级程序员。压缩包共18个文件以cs源码、csproj工程文件及sln解决方案为主外加aspx、config、js等辅助文件整体仅44KB结构精简便于快速梳理。已有577人学习下载。代码中可见WebSocketServer核心连接处理、WebSocketChatServer聊天广播逻辑及ChatClient客户端测试项目覆盖了HTTP Upgrade握手、消息收发、多客户端并发、心跳保活等关键机制。通过阅读这套迷你示例可以直观理解C# WebSocket服务的搭建、调试与扩展路径对自建实时通信模块有直接参考价值。1. 为什么一个 C# WebSocketServer 服务器源代码值得自己维护从拉取实时数据到内网推送做上位机或者写局域网工具的人迟早会遇到一个需求浏览器页面要盯着 C# 后端的数据看。轮询太浪费一秒一刷不够实时两秒一刷用户嫌慢。C# WebSocketServer 服务器源代码 这个标题要解决的就是用 C# 自己实现一个跑在 TCP 之上的 WebSocket 服务端把握手、帧解析、心跳、广播全部握在自己手里。适合的场景很明确内网监控面板、上位机实时曲线、物联网网关往网页推状态、远程触发指令。适合的人群是 C# 开发者尤其是写过 TCP 监听端口程序、但对 WebSocket 协议细节还不够熟的人。自己维护这套代码换来的是无框架依赖、单文件可部署、每一帧都能控制排查问题不用黑匣子猜。2. 握手实现用 TcpListener 在 C# 里把 HTTP 升级成 WebSocket 的 101 响应2.1 握手的核心Sec-WebSocket-Accept 不是随便填的WebSocket 和普通 TCP 长连接最大的区别是它先借 HTTP 协议完成一次升级握手。客户端发来的请求头里有三样东西必须拿到Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key。服务端要做的不是解析 HTTP 语义而是校验后回一个101 Switching Protocols再把Sec-WebSocket-Key拼上一个固定 GUID做 SHA1 哈希后转 Base64写进响应头Sec-WebSocket-Accept。这个 GUID 是协议固定的全世界的 WebSocket 握手都用它值就是258EAFA5-E914-47DA-95CA-C5AB0DC85B11。千万别自己发明一个字符串客户端那里的算法是固定的你用别的 GUID 算出的 accept 值浏览器一定会拒绝握手现象就是 Console 里报Error during WebSocket handshake但服务器端又看不到任何异常只能靠抓包才能定位。我一般会在握手前打印收到的原始请求头先人工确认Sec-WebSocket-Key有没有被代理截掉。请求头字段作用服务端处理Upgrade: websocket声明要升级协议不校验具体值但响应里必须回写Upgrade: websocketConnection: Upgrade声明连接要升级响应里必须回写Connection: UpgradeSec-WebSocket-Key16 字节随机值 Base64拼 GUID 做 SHA1 再 Base64回写Sec-WebSocket-AcceptSec-WebSocket-Version协议版本只支持 13其他版本直接拒绝2.2 最小握手代码先按行切头部再按长度等数据写握手的时候有个很常见的坑TCP 是流不是消息你第一次ReadAsync拿到的可能只是请求头的前半截。如果直接按ReadAsync返回的字节数去解析一定会偶发解析失败。正确做法是先把数据追加进一个StringBuilder一直读到底部出现\r\n\r\n才认为头部完整再去按行提取字段。// HandshakeHandler.cs using System.Net.Sockets; using System.Security.Cryptography; using System.Text; public class HandshakeHandler { private const string WsGuid 258EAFA5-E914-47DA-95CA-C5AB0DC85B11; public async Taskbool ReceiveAndRespondAsync(TcpClient client, CancellationToken ct) { var stream client.GetStream(); var buffer new byte[4096]; var sb new StringBuilder(); // 循环读取直到 \r\n\r\n 出现代表 HTTP 头部完整 while (sb.ToString().IndexOf(\r\n\r\n, StringComparison.Ordinal) 0) { int n await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct); if (n 0) return false; sb.Append(Encoding.ASCII.GetString(buffer, 0, n)); if (sb.Length 16384) return false; // 防御超长头部 } var headers sb.ToString(); var key ExtractHeader(headers, Sec-WebSocket-Key); if (string.IsNullOrEmpty(key)) return false; var accept ComputeAccept(key); var response HTTP/1.1 101 Switching Protocols\r\n Upgrade: websocket\r\n Connection: Upgrade\r\n $Sec-WebSocket-Accept: {accept}\r\n\r\n; await stream.WriteAsync(Encoding.ASCII.GetBytes(response), ct); return true; } }ExtractHeader要注意大小写不敏感很多客户端发的是sec-websocket-key全小写。按行拆分后取第一个冒号作为键值分隔点别用Split(:)直接拆因为值里可能出现冒号。ComputeAccept里先把key WsGuid转 ASCII再走 SHA1最后 Base64三步缺一不可。这段代码不区分客户端是 Chrome、微信内置浏览器还是自写客户端协议层是统一的。2.3 响应头必须一次写完别用 Write 两次我之前犯过一个小错误把101状态行和响应头分两次WriteAsync中间还插了一次日志打印。结果是浏览器间歇性握手失败因为部分客户端对响应延迟敏感而且网络包一拆接收端可能先看到空响应。正确的做法是把整个响应拼成一个字符串一次性WriteAsync写完立刻进入帧读取循环不要再往流里写日志或做耗时操作。还有一个值得提的点握手阶段读到的多余字节不要扔掉。我一般会在握手结束后把StringBuilder里\r\n\r\n之后剩余的内容保留下来因为客户端可能在握手完成的同时就发出了第一帧数据。这个现象在局域网环境和自写客户端里很常见如果直接丢弃第一帧消息就会神秘失踪表现为“连接成功但第一条消息永远收不到”。3. 帧解析细节按位拆出 Fin、Opcode 与 9/16/64 位载荷长度3.1 帧头只有 2 个字节起步但信息密度很高握手完成后双方向发送的都是二进制帧。帧头的前两个字节承载了这次传输的控制信息。第一个字节最高位FIN表示这是不是最后一帧低 4 位是Opcode中间 3 位是扩展协议用的 RSV正常情况下必须是 0。第二个字节最高位MASK表示载荷是否被掩码低 7 位是载荷长度。服务端收到的客户端帧MASK必须置 1。这个约束是协议硬性规定的浏览器和其他合规客户端都会带上 4 字节的掩码密钥。如果收到一个未掩码的帧按协议直接关闭连接。反过来服务端发出去的帧MASK必须置 0否则客户端会按协议错误处理直接断开。很多自写服务器翻车就翻在“收发共用一套编码函数”忘了参数上区分方向。位段长度名称取值含义byte0 bit71FIN1消息结束0还有后续分片帧byte0 bit6-43RSV必须为 0为 1 时说明启用了扩展协议byte0 bit3-04Opcode1文本2二进制8关闭9Ping10Pongbyte1 bit71MASK客户端帧为 1服务端帧为 0byte1 bit6-07载荷长度0-125 为实际长度126 走 16 位扩展127 走 64 位扩展3.2 读帧头不能只读一次5 种长度情况都要覆盖写一个ReadFrameHeaderAsync核心原则是“读不够就往死里等”。TCP 的ReadAsync可能一次只返回 1 个字节也可能一次返回 8KB 数据所以必须把帧头里每个字段的长度算清楚再按长度去读绝不能用ReadAsync的返回值作为“消息长度”。// FrameReader.cs public class FrameHeader { public bool Fin; public int Opcode; public bool Masked; public long PayloadLength; public byte[] MaskKey; } public static async TaskFrameHeader ReadFrameHeaderAsync( NetworkStream stream, byte[] buffer, CancellationToken ct) { await ReadExactlyAsync(stream, buffer, 2, ct); byte b0 buffer[0]; byte b1 buffer[1]; var header new FrameHeader { Fin (b0 0x80) ! 0, Opcode b0 0x0F, Masked (b1 0x80) ! 0, PayloadLength b1 0x7F }; if (!header.Masked) throw new InvalidDataException(客户端帧未掩码违反协议); if (header.PayloadLength 126) { await ReadExactlyAsync(stream, buffer, 2, ct); header.PayloadLength (buffer[0] 8) | buffer[1]; } else if (header.PayloadLength 127) { await ReadExactlyAsync(stream, buffer, 8, ct); header.PayloadLength 0; for (int i 0; i 8; i) header.PayloadLength (header.PayloadLength 8) | buffer[i]; if ((header.PayloadLength 0x8000000000000000) ! 0) throw new InvalidDataException(载荷长度高位被置位非法帧); } if (header.Masked) { header.MaskKey new byte[4]; await ReadExactlyAsync(stream, header.MaskKey, 4, ct); } return header; } private static async Task ReadExactlyAsync( NetworkStream stream, byte[] buffer, int count, CancellationToken ct) { int read 0; while (read count) { int n await stream.ReadAsync(buffer.AsMemory(read, count - read), ct); if (n 0) throw new EndOfStreamException(连接已关闭); read n; } }注意载荷长度的三档设计7 位最多表示 125超过 125 时第一个长度字节置 126跟随 2 字节 16 位长度超过 65535 时置 127跟随 8 字节 64 位长度。读取 64 位长度时我在循环里逐字节左移拼值这样规避了BitConverter.IsLittleEndian的平台差异服务器要在 x86 和 ARM 上表现一致就得这么写。3.3 掩码运算读载荷时逐个字节异或帧头读完接下来读载荷区。因为客户端帧带着掩码所以读出来的每个字节都要跟MaskKey[i % 4]做异或才能还原真实数据。这个运算是字节级的byte[] payload new byte[header.PayloadLength]; await ReadExactlyAsync(stream, payload, payload.Length, ct); if (header.Masked) { for (int i 0; i payload.Length; i) payload[i] ^ header.MaskKey[i % 4]; }掩码的意义不在于加密是早期协议为了防止缓存污染攻击留下的设计所以一定不要把它当成“解密”去对待。客户端发来的数据经过异或还原后文本帧直接按UTF8.GetString解码即可。这里有一个非常容易被忽略的细节解码必须等一整帧读完再做。如果你用ReadAsync返回的字节数组直接转字符串遇到消息被 TCP 拆成两段到达时就会出现半个中文导致的乱码而且不是每次都复现最容易在弱网环境翻车抓包都看不出来。3.4 消息重组Fin0 时要把 payload 暂存单帧 Fin1 表示消息结束了但 WebSocket 允许发送方把一个逻辑消息拆成多个分片帧。这些分片帧的 Opcode 全是0x0只有第一帧带真实类型1 或 2并且 Fin0。如果只处理 Opcode1/2完全忽略 Opcode0客户端一旦发送大消息自动分片服务端就会丢失数据。处理分片的标准做法是在每个连接上维护一个MessageBuilder// MessageBuilder.cs public class MessageBuilder { private MemoryStream _buffer new MemoryStream(); private int _messageType 0; public bool Push(byte opcode, byte[] payload, bool fin) { if (_buffer.Length 0) _messageType opcode; // 第一帧记录真实类型 _buffer.Write(payload); if (!fin) return false; Received _buffer.ToArray(); _buffer.SetLength(0); return true; } public byte[] Received { get; private set; } }为什么要做的这么重因为浏览器等客户端在发超过 64KB 的消息时不保证不分片。你只能被动接收。分片帧之间允许穿插 Ping/Pong 控制帧但不能穿插新的数据帧。如果_buffer里还攒着数据又来了一个 Opcode1 的新消息帧说明客户端协议实现有问题直接关连接是最安全的处理方式。4. 连接管理与广播在线表、心跳保活与并发发送锁4.1 用 ConcurrentDictionary 维护在线表别用 List多客户端连接是 WebSocket 服务器和普通 TCP 回显程序最大的分水岭。最常见的错误是用ListTcpClient存连接然后在广播时遍历边遍历边有人断开List在遍历时被修改直接抛集合已修改异常。我用ConcurrentDictionarystring, Session做在线表key 用Guid.NewGuid().ToString(N)生成的 32 位短 ID给每个连接一个唯一标识方便后续按客户端踢人、做单发和群组推送。// Session.cs public class WebSocketSession { public string Id { get; } Guid.NewGuid().ToString(N); public TcpClient Client { get; set; } public NetworkStream Stream { get; set; } public CancellationTokenSource Cts { get; } new CancellationTokenSource(); public DateTime LastPongAt { get; set; } DateTime.UtcNow; public SemaphoreSlim SendLock { get; } new SemaphoreSlim(1, 1); }Cts这个字段不是为了华丽它负责一件事当服务端主动关闭某个连接时通过Cts.Cancel()把阻塞在读循环里的ReadAsync打断否则那个线程会一直卡在连接上不释放。LastPongAt后面心跳用。SendLock是信号量用于保证同一时刻只有一个线程往NetworkStream上写数据避免广播线程和其他发送线程撞车。4.2 每连接一个异步循环把接收、处理、异常分开服务器主循环只做一件事接受 TCP 连接然后立刻把处理工作丢给后台任务自己继续 Accept。这样不会因为某个客户端发来脏数据导致整个服务器 Accept 卡住。每个连接的循环里顺序永远是读帧头 - 读载荷 - 按 Opcode 分发逻辑异常统一捕获finally 里清掉会话。// WebSocketServer.cs public async Task ProcessClientAsync(TcpClient client) { var session new WebSocketSession { Client client, Stream client.GetStream() }; _sessions[session.Id] session; try { var handshakeOk await _handshake.ReceiveAndRespondAsync(client, session.Cts.Token); if (!handshakeOk) return; // 进入帧处理循环 while (!session.Cts.IsCancellationRequested) { var header await FrameReader.ReadFrameHeaderAsync(session.Stream, _buffer, session.Cts.Token); // 先读整个 payload再按类型处理 var payload await FrameReader.ReadPayloadAsync(session.Stream, header, session.Cts.Token); // opcode 8关闭 9Ping 10Pong 1文本 2二进制 0分片续帧 await DispatchAsync(session, header, payload); } } catch (Exception ex) { // 记录异常日志然后走清理 Console.WriteLine($[{session.Id}] {ex.Message}); } finally { RemoveSession(session); } }每个连接一个循环意味着一个连接的消息处理再慢也不会拖累其他客户端。这里对“慢客户端”要有心理准备如果客户端不读数据TCP 窗口填满后WriteAsync也会阻塞所以要给每次发送套超时控制常见做法是WriteAsync时把CancellationToken设成 5 秒后触发取消。4.3 心跳保活Ping 要发回没回更要查WebSocket 有专门的控制帧Opcode9 是 PingOpcode10 是 Pong。客户端收到 Ping 后按协议自动回 Pong。心跳的意义不是证明“连接活着”而是释放半开连接。网络断开时TCP 不会立刻通知你如果只靠读循环阻塞那么一条物理断开的连接会永远占着线程和文件句柄。// 每个连接启动一个心跳定时器 var heartbeat new Timer(async _ { try { var stale DateTime.UtcNow - session.LastPongAt TimeSpan.FromSeconds(60); if (stale) { Console.WriteLine($[{session.Id}] 心跳超时关闭连接); session.Client.Close(); return; } // 发 Ping载荷里带上时间戳便于排查延迟 await session.Stream.WriteAsync(FrameEncoder.EncodeControlFrame(9, BitConverter.GetBytes(DateTime.UtcNow.Ticks))); } catch { /* 忽略主循环会发现连接断开 */ } }, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));参数怎么设我在内网一般是 30 秒发一次 Ping60 秒内没收到 Pong 就掐掉连接。公网环境网络抖动大Ping 间隔可以放到 45 秒超时放到 120 秒。注意这里有个矛盾心跳超时判断依赖LastPongAt而LastPongAt只在收到 Pong 帧时更新所以必须在帧分发逻辑里对 Opcode10 的帧单独赋值session.LastPongAt DateTime.UtcNow这一步漏了心跳会误杀所有正常连接。4.4 广播与单发并发发流必须上锁广播的时候最容易出灵魂拷问为什么NetworkStream.WriteAsync会抛InvalidOperationException因为NetworkStream不是线程安全的框架不允许两个线程同时写入同一个流。广播会产生并发写A 线程给张三发B 线程也在给张三发。解决办法是每个 Session 里的SendLockpublic async Task SendAsync(WebSocketSession session, byte[] payload, int opcode 1) { var frame FrameEncoder.EncodeDataFrame(opcode, payload, masked: false); await session.SendLock.WaitAsync(); try { await session.Stream.WriteAsync(frame); await session.Stream.FlushAsync(); } catch { session.Client.Close(); } finally { session.SendLock.Release(); } }注意FlushAsync在网络流上其实是无操作但保留它无害而且将来如果换成带缓冲的流不至于漏刷新。广播遍历集合时我用_sessions.ToArray()做快照遍历过程中客户端断开也不用担心集合被修改。发送失败的直接从在线表移除这里不用怕误删因为RemoveSession里会判断TryRemove的返回值。5. 避坑C# WebSocketServer 送上线的 5 个常见翻车点5.1 握手成功但连接立刻断开响应头写错导致浏览器直接拒绝现象服务端日志显示握手完成但浏览器 WebSocket 对象触发onclose代码 1006。原因响应头里\r\n被写成了\n或者响应头和正文之间少了空行。HTTP 解析要求每行以\r\n结尾头部和正文之间要有一个空行也就是连续两个\r\n。字符串拼接时一眼看不出问题但实际发出去的就是坏报文。解决把响应拼好后先Encoding.ASCII.GetBytes看一遍字节内容确认空行存在。另外不要用Environment.NewLine在 Linux 上它是\n必须写死\r\n。5.2 偶发消息不全每帧必须按长度读不能按 ReadAsync 返回值读现象收发消息 10 次里有一次是残的或者两条消息拼在一起高频率收发时尤其明显。原因TCP 是字节流一条 WebSocket 帧可能被拆成 3 个包到达ReadAsync 一次可能只返回其中一部分。如果直接把本次返回的字节当作一帧消息处理粘包半包就来了。解决所有读取的地方都走ReadExactlyAsync帧头 2 字节、扩展长度、掩码键、载荷区全部按精确数量读。任何“先读再看够不够”的逻辑都不要留。5.3 广播时偶发 ObjectDisposedException现象客户端断开后广播线程还在往它的流里写数据抛ObjectDisposedException或IOException。原因关闭连接只是把客户端从在线表移除但发送线程可能已经拿走了 Session 引用正卡在WriteAsync上。先关闭流再发送竞态条件就触发。解决发送逻辑统一走SendAsync里面先SendLock.WaitAsync再检查Client.Connected写入时捕获异常任何异常都视为连接已死立即 Close 并从在线表移除。不要在广播循环里逐个判断Connected后不处理异常因为判断到写入之间还有时间窗。5.4 收不到关闭帧服务端不知道客户端已经走了现象客户端直接断电或 App 被杀服务端在线表里连接一直挂着占用大量句柄。原因客户端异常退出时TCP 没有发 FIN服务端读循环永远阻塞等待心跳又没有做超时检测。解决按 4.3 的配置做 Ping/Pong 超时。同时给ReadAsync所在的 CancellationToken 加超时比如Cts.CancelAfter(TimeSpan.FromSeconds(120))两层保障任何一层触发都能清掉僵尸连接。5.5 服务端发二进制数据把客户端搞崩现象服务端用Encoding.UTF8.GetString(payload)转完再发给客户端图片、文件等二进制数据变成乱码或触发解析错误。原因很多代码只写了文本帧的收发遇到二进制数据直接套文本解码。WebSocket 的文本帧 Opcode1二进制帧 Opcode2处理逻辑必须分支二进制帧不能做 UTF8 解码直接原样转发或落盘。解决分发逻辑里按 Opcode 分路文本走 UTF8二进制走byte[]。客户端发来的二进制可能不分片也可能分片组装逻辑和文本完全一样只是解码这一步不同。6. 验证手段与生产化改造从 101 响应到敢接入真实业务这部分我每次都会先做三件事再决定要不要继续投入第一用命令行验证握手第二用 C# 自带ClientWebSocket验证收发第三做消息积压测试。命令行验证最快一条 curl 就能看到是否返回 101curl --include --no-buffer \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw \ -H Sec-WebSocket-Version: 13 \ http://127.0.0.1:8080/看到HTTP/1.1 101 Switching Protocols就说明握手实现没问题。但 curl 不会完成帧通讯所以接着用自写客户端做回环测试。ClientWebSocket是 .NET 自带的客户端不需要任何第三方包using var ws new ClientWebSocket(); await ws.ConnectAsync(new Uri(ws://127.0.0.1:8080/), CancellationToken.None); var send Encoding.UTF8.GetBytes({\action\:\ping\}); await ws.SendAsync(send, WebSocketMessageType.Text, true, CancellationToken.None); var buffer new byte[1024]; var result await ws.ReceiveAsync(buffer, CancellationToken.None); Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, result.Count));把服务端广播里加一个计数器在 WinForms 状态栏或者控制台日志里输出每秒收发的消息数和在线连接数就能提前发现广播瓶颈。上线前我会把三个改造做掉限制单帧最大载荷和最大连接数防止 OOM把控制台程序注册成 Windows 服务或丢进 NSSM 托管开机自启所有异常和连接事件落日志文件给每条连接打上 ID排查时才不用靠猜。这个方向是值得投入的尤其是内网实时通信场景。等连接数真到了几万甚至十万再做 NIO 或换成熟的通信框架但在这之前自己维护的这套源码能让你对网线里走的每一个字节都有掌控感。我的教训是最初做广播时贪图代码简单没做半包读取上线后偶发丢帧被业务方吐槽后来把所有读取改成精确长度才彻底消停。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

大模型应用开发实战:Prompt工程、RAG、Agent与本地部署

大模型应用开发实战:Prompt工程、RAG、Agent与本地部署

1. 这门课到底在教什么?——不是“AI速成班”,而是大模型应用开发的实战切口 如果你最近刷到过“AI大模型应用开发”相关的课程推荐,大概率会看到类似标题:“如果你想学AI大模型应用开发,我真心推荐你看看这门课”。这…

2026/10/9 4:32:37 阅读更多 →
MAX96717 Host-to-Peripheral I2C实战配置指南

MAX96717 Host-to-Peripheral I2C实战配置指南

1. 为什么是MAX96717?——从车载摄像头链路瓶颈说起我第一次在客户现场看到MAX96717,是在一台刚下线的ADAS域控制器调试台前。工程师正对着示波器抓I2C波形,眉头拧成疙瘩:“Host端发了0x24写命令,Peripheral端就是不响…

2026/10/9 4:32:29 阅读更多 →
行人重识别中GAN作为特征修复器的工程实践

行人重识别中GAN作为特征修复器的工程实践

简介:本资源是一套基于生成对抗网络(GAN)实现行人重识别(ReID)的完整毕业设计实践方案,面向深度学习初学者与计算机视觉方向本科生,聚焦跨摄像头场景下的身份匹配问题,适用于课程设计…

2026/10/7 22:59:00 阅读更多 →

最新新闻

无线网络安全实验全流程:从抓包到WPA2握手审计

无线网络安全实验全流程:从抓包到WPA2握手审计

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

2026/10/9 4:31:52 阅读更多 →
无人机飞控系统原理与传感器作用详解:从硬件组成到PID控制实战

无人机飞控系统原理与传感器作用详解:从硬件组成到PID控制实战

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

2026/10/9 4:31:52 阅读更多 →
基于STM32F103VET6的变频逆变器方案:SPWM、死区与硬件设计实战

基于STM32F103VET6的变频逆变器方案:SPWM、死区与硬件设计实战

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

2026/10/9 4:31:52 阅读更多 →
U-Boot Kbuild构建系统深度解析:从Kconfig到u-boot.bin的完整链路

U-Boot Kbuild构建系统深度解析:从Kconfig到u-boot.bin的完整链路

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

2026/10/9 4:31:52 阅读更多 →
STM32传感器信号链设计:从物理量到可靠感知的七道关卡

STM32传感器信号链设计:从物理量到可靠感知的七道关卡

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

2026/10/9 4:31:52 阅读更多 →
哈希集合巧解最长连续序列:从O(n log n)到O(n)的思维跃迁

哈希集合巧解最长连续序列:从O(n log n)到O(n)的思维跃迁

第一次在力扣上刷到“最长连续序列”这道题的时候,我骨子里的第一反应和绝大多数人一模一样:排序,然后从前往后数一遍,答案不就出来了?直到我看见题目要求——时间复杂度得达到 O(n)——才意识到事情没那么简单。这道题…

2026/10/9 4:30:52 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/7 13:34:55 阅读更多 →