<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MingLezのBlog]]></title><description><![CDATA[求知如流，载你去往更远的自己]]></description><link>https://xming.cyou/</link><image><url>https://xming.cyou//favicon-1.ico</url><title>MingLezのBlog</title><link>https://xming.cyou/</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Sat, 02 May 2026 22:14:35 GMT</lastBuildDate><atom:link href="https://xming.cyou//feed" rel="self" type="application/rss+xml"/><pubDate>Sat, 02 May 2026 22:14:34 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[Windows办公利器推荐——Quicker]]></title><description><![CDATA[<link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/yxcetb3rlzjo1oyoum.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/hayi2xoo3vnsxjbaul.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/9g7eppg8xwztsi6mzw.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/ic14w5ztt7a8ratof4.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/us4mu5t7e6zvpmchf7.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/a0ewrhofv9nfvi6cax.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/v64cve921rhb4183k0.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/cmc9uxkgnbtpno7m35.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/ukx1clmv6y0i84wa7o.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/z0oxo6yu0l5sst71z4.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/zxvjc06azgcwavhamp.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/d43znm5obsxxt5682e.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/cf2g25a0zfq0zocxrw.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/7lxosr6skmwx2fd282.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/devops/quicker">https://xming.cyou/posts/devops/quicker</a></blockquote><div><blockquote><p>📌 <strong>摘要</strong>：Quicker 是 Windows 平台上不可多得的效率利器。它不仅能通过快捷面板整合常用功能，还能通过丰富的动作库实现自动化办公。本文将为你介绍 Quicker 的核心优势，并分享如何通过简单的配置，让你的电脑操作变得如丝般顺滑。</p></blockquote>
<p>---</p><h1 id="--quicker">一、 为什么推荐 Quicker？</h1><p>我之前常用的开发本是 <code>MacBook</code>，之前装了<a href="https://www.better365.cn/bab2.html">Better And Better</a>，对我来说它简直是提升办公效率的神器（用过的应该同感）。
其实在大多开发过程中，谁不是在“写代码”和“查资料”之间反复横跳？以前我总觉得 <code>Tab</code> 键切换窗口或者手势切换桌面已经够丝滑了，直到我意识到：对于咱们这种追求极致效率的“强迫症”患者来说，每次切换窗口多耗费的 0.5 秒，简直就是对生命的亵渎！你想想，一天下来得切换多少次？把这些零碎时间省下来摸鱼……啊不，是用来学习提升，它难道不香吗？</p><p>后来我配了台Windows电脑，磨合期使用时诸多层面带来了不便，比如习以为常的高效开发方式、软件使用习惯以及命令行等等都多多少少有些区别。我就尝试在网络上找Macbook开发工具的一些替代品，今天介绍的<a href="https://getquicker.net/">Quicker</a>就是其中之一（结束废话文学）。哦，对了，它<strong>不止切换桌面</strong>那么简单，所以还请诸君花点时间看完整篇文章。</p><h2 id="">办公中的“效率杀手”</h2><p>之前在“废话文学”里我已经吹过它切换桌面的神操作了，这里再简单拆解一下它的“魔法原理”：其实它就是个“鼠标动作翻译官”，把你的手势指令精准翻译成系统快捷键。比如：</p><ul><li><code>Ctrl + Win + D</code>：一键召唤新桌面（如果你的键盘比较“傲娇”，可能还得加上 <code>Fn</code> 键）。</li><li><code>Ctrl + Win + →/←</code>：在不同桌面间丝滑横跳。</li></ul><p>但这只是 <code>Quicker</code> 的冰山一角。它最骚的操作在于：<strong>你可以为特定应用设置“专属动作”。也就是说，当你打开 <code>Excel</code> 时，它是你的数据处理大师；切换到 <code>PS</code> 时，它瞬间变身抠图神器。这种“见人说人话，见鬼说鬼话”的智能切换，简直是强迫症患者的福音</strong>。</p><p><strong>如果你觉得这还不够，那它的编程模块功能以及丰富的动作库绝对会让你大呼过瘾。无论是 <code>Excel</code> 一键插入复杂公式、<code>Word</code> 自动排版，还是 <code>CAD</code> 里一键标注数据等各种骚操作，基本都是可以的。</strong></p><p>最最最重要的是：日常使用它完全免费且无广告的！只有当你进化成追求极致的“极客”时，才需要考虑付费。毕竟，这么好用的工具，不拿来“压榨”一下生产力，简直是对开发者的不尊重，对吧？</p><p>举个我日常使用的例子：
以下是我在<a href="https://obsidian.md/zh/">Obsidian</a>中使用Quicker的界面，我安装了两个<em>动作</em>，写博客必备的图片压缩和图片水印，用起来还是挺方便的，免去了我去网页上传等操作步骤，这两个<em>动作</em>直接就可以在本地就把这两件事弄好。
<img src="https://blog.xming.cyou/api/v2/objects/image/yxcetb3rlzjo1oyoum.png" alt="示例1" height="440" width="440"/></p><h2 id="12-quicker-">1.2 Quicker 能为你做什么？</h2><p>简单来说，它提供了诸多编辑操作，虽然每一次使用时没感觉到节省出多少时间，但是日积月累的使用下来真的还是非常推荐的，节省的时间也非常可观。</p><h1 id="-">二、 快速上手</h1><h2 id="">安装与基本配置</h2><h3 id="">安装</h3><p><strong>方式一</strong>： 命令行安装（<code>Win + r</code>后输入<code>cmd</code>按回车键）：
命令行安装之后还要手动更新到最新版（所有没省下多少时间，推荐用第二种方式）</p><pre class="language-PowerShell lang-PowerShell"><code class="language-PowerShell lang-PowerShell"># 注意替换安装路径， 你也可以在 -e 后面接 -s 静默安装
winget install --id=LiErHeXun.Quicker -e --location &quot;D:\Program Files\Quicker&quot;
</code></pre><p><strong>方式二</strong>（<em>推荐</em>）：Windows 命令行自Win10系统开始后应该自带了<code>winget</code>命令，当然也可以到官网下载安装：<a href="https://getquicker.net/Download">getquicker.net</a> 
<img src="https://blog.xming.cyou/api/v2/objects/image/hayi2xoo3vnsxjbaul.png" alt="Download"/>
选择对应版本点击后等待下载完成后按提示安装即可。</p><p>安装过程以及注册登录步骤这里跳过，你可以使用推荐码 <em>1071306-8118</em>，填写邀请码如果后续购买专业版双方都可以获的额外90天的专业版使用时长，不填写购买专业版则没有时长赠送（免费版已够用！）</p>
<p>主界面应该如下图（我删除了某些默认动作以及新增了部分动作，大概如图就行）：
<img src="https://blog.xming.cyou/api/v2/objects/image/9g7eppg8xwztsi6mzw.png" alt="主界面"/></p><h3 id="">基本设置</h3><p>点击主界面顶部的⚙按钮，进入设置页面</p><p>设置开机自启
<img src="https://blog.xming.cyou/api/v2/objects/image/ic14w5ztt7a8ratof4.png"/>
全屏或指定应用黑名单，此设置可以避免一些意想不到事情发生，可以看到我把所有游戏目录加到了黑名单（<em>当时我还纳闷，打个游戏界面怎么没了😂</em>）
<img src="https://blog.xming.cyou/api/v2/objects/image/us4mu5t7e6zvpmchf7.png"/></p><h2 id="">设置动作</h2><p><img src="https://blog.xming.cyou/api/v2/objects/image/a0ewrhofv9nfvi6cax.png"/></p><p>进入动作管理后我们可以在全局应用下设置手势动作，如下图，我设置了鼠标右键画S线表示锁屏、然后还加了打开微信、切换桌面等手势。
<img src="https://blog.xming.cyou/api/v2/objects/image/v64cve921rhb4183k0.png"/></p><p>你也可以给某个具体应用设置独立的动作，注意，通用中的动作会在所用应用上应用，除非你覆盖了它。
如果点击下拉框没有你想要的应用，别灰心，请点击右边‘十字准星’按钮，然后选取你需要的应用，就行截图软件那样使用它即可。
<img src="https://blog.xming.cyou/api/v2/objects/image/cmc9uxkgnbtpno7m35.png"/></p><p>你可以在动作页添加新的动作，添加完成后还需要添加到轮盘菜单中，然后你到你应用的界面按下鼠标中键（视你配置的按键为主，可能初始按键不一致）就会出现轮盘，然后就可以很方便的调用动作了。
<img src="https://blog.xming.cyou/api/v2/objects/image/ukx1clmv6y0i84wa7o.png"/></p><p>你可能需要更多动作，<a href="https://getquicker.net/Exe/16/Actions?order=voteCountDesc&amp;p=1">Quicker的动作库</a>里有许多丰富的动作供你选择，你可以在这里找到各位大佬开发的不同应用的高效动作，覆盖PS、WPS、CAD、 Excel、 World等等应用。点击进入你想要安装的动作</p><p><img src="https://blog.xming.cyou/api/v2/objects/image/z0oxo6yu0l5sst71z4.png"/>
点击‘复制到剪切板’按钮
<img src="https://blog.xming.cyou/api/v2/objects/image/zxvjc06azgcwavhamp.png" alt="Copy Action"/></p><p>然后回到Quicker应用程序，点击动作空白的小方块，然后点击‘粘贴分享的动作’，继续直接添加至轮盘的步骤即可
<img src="https://blog.xming.cyou/api/v2/objects/image/d43znm5obsxxt5682e.png" alt="粘贴动作"/></p><h3 id="">悬浮动作</h3><p>你还可以将某个常用的动作，悬浮到桌面上：
<img src="https://blog.xming.cyou/api/v2/objects/image/cf2g25a0zfq0zocxrw.png" alt="悬浮动作"/></p>
<p>要用的时候直接点击即可，不必每次都打开面板。
例如，在处理 Excel 时，可以悬浮多个动作，方便处理。
<img src="https://blog.xming.cyou/api/v2/objects/image/7lxosr6skmwx2fd282.png"/></p><h3 id="">动作库推荐</h3><ul><li><a href="https://getquicker.net/Sharedaction?code=04393db9-f4bc-4871-7fb6-08db2506d1ed">Translator</a>: 非常好用的屏幕ORC翻译工具，提供了多家平台的翻译功能，请把你的有道词典卸载！！！</li><li><a href="https://getquicker.net/Sharedaction?code=ccc1805d-3d04-42a1-d9ef-08db2ba80230">Everything</a>: 硬盘文件检索工具，非常快速的搜索你想要的藏在不知在哪个角落吃灰的文件
# 三、 总结：让效率成为一种习惯</li></ul><p>Quicker 不仅仅是一个工具，它更是一种工作哲学的体现——将重复、繁琐的操作自动化，把宝贵的时间和精力解放出来，投入到更有价值的思考和创造中。从最初的窗口切换优化，到如今强大的动作库和编程模块，Quicker 已经超越了简单的快捷键集合，成为一个高度可定制的个人效率引擎。</p><p>通过本文的介绍，我们看到了 Quicker 如何：</p><ul><li><strong>简化操作：</strong> 将复杂的系统命令和多步骤任务封装成简单的手势或点击。</li><li><strong>智能适应：</strong> 根据不同的应用场景，提供定制化的动作支持，让工具“懂你”。</li><li><strong>拓展能力：</strong> 借助庞大的动作库和自定义编程能力，几乎可以实现任何你想要的自动化。</li></ul><p>正如文中所述，每一次看似微不足道的 0.5 秒节省，日积月累便能汇聚成可观的效率提升。更重要的是，当你习惯了 Quicker 带来的流畅体验后，你会发现它不仅仅是节省了时间，更是改变了你的工作流，让你能够以更专注、更高效的状态投入到工作中。</p><p>所以，不妨从现在开始，让 Quicker 成为你 Windows 办公的得力助手，让“效率”不再是一个口号，而是一种自然而然的习惯。</p></div><p style="text-align:right"><a href="https://xming.cyou/posts/devops/quicker#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/devops/quicker</link><guid isPermaLink="true">https://xming.cyou/posts/devops/quicker</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Mon, 13 Apr 2026 15:07:34 GMT</pubDate></item><item><title><![CDATA[Proxmox VE (PVE) 虚拟化]]></title><description><![CDATA[<link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/h44mkwc9k93152fujz.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/ehwe0mubv1bu7r12gs.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/m190yuu64207e77htt.jpg"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/tv8q2a188rvn95zm09.jpg"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/y9ba3ta4ohms86i3tu.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/12occixcywlztqygsq.png"/><link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/h1tf9zqcjaescfb98d.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/devops/pve">https://xming.cyou/posts/devops/pve</a></blockquote><div><h2 id="">一、引言</h2><h3 id="">为什么需要虚拟化？</h3><ul><li><strong>减少资源浪费</strong>：一套应用往往需要开发、测试、生产等多套独立环境，为避免相互污染，必须做环境隔离。传统物理机方案需要为每套环境单独采购服务器，硬件资源长期闲置、利用率极低。虚拟化技术将一台物理机切分为多个独立虚拟机，既实现了环境的完全隔离，又能共享硬件资源、大幅提升利用率，同时还能降低运维成本、支持动态扩容与在线迁移，通过镜像一键分发标准化环境，全方位解决传统物理架构的核心痛点。</li><li><strong>减少运维成本</strong>：传统物理机架构下，业务部署需要经历采购硬件、装机、布线、配置系统、调试环境等一系列流程，不仅周期长达数天，还需要投入大量人力物力，机房的电力、散热、维保等综合成本也居高不下。而虚拟化技术支持镜像一键克隆、秒级创建虚拟机，能快速完成业务环境的部署，大幅缩短业务上线周期；同时，集约化的部署模式减少了物理服务器的数量，直接降低了硬件采购、机房租赁、电力消耗等长期运维成本，让 IT 运维更高效、更经济</li><li><strong>资源隔离</strong>：虚拟化实现了真正的硬件级资源隔离，每个虚拟机都拥有独立的操作系统、进程空间、网络环境和资源配额，不同业务、不同环境在同一台物理机上运行时，会被严格划分资源边界，不会互相抢占算力，也不会因一方故障、病毒入侵影响其他业务的稳定运行。彻底解决了传统裸机混跑模式下，业务相互干扰、故障扩散、安全风险高的问题，为多业务、多环境的混合部署提供了安全可靠的基础</li><li><strong>动态扩容与迁移</strong>：传统物理机的扩容需要拆机加装硬件，业务迁移则需要重装系统、迁移数据，不仅操作繁琐，还会导致长时间的业务停机，严重影响业务连续性。而虚拟化技术支持<strong>在线动态扩容</strong>，无需停机就能为虚拟机增加 CPU、内存、存储等资源，灵活适配业务的弹性增长；同时支持<strong>虚拟机在线迁移（热迁移）</strong>，可以在不中断业务的前提下，将虚拟机从一台物理机无缝迁移到另一台，方便硬件维护、负载均衡，彻底解决了传统物理架构扩容难、迁移难、停机时间长的痛点</li><li><strong>一键分发环境</strong>：在传统开发模式中，开发、测试、生产环境的配置差异，是导致 “开发环境正常、线上出 bug” 的核心原因之一，排查和修复这类问题会消耗大量研发时间。虚拟化技术可以制作统一的系统镜像模板，包含标准化的操作系统、依赖环境、配置参数，开发、测试、生产环境都可以通过模板一键分发，快速复制完全一致的运行环境，彻底消除环境差异带来的问题，大幅提升开发测试效率和业务交付质量</li></ul><h3 id="">常见虚拟化方案对比</h3><p>在虚拟化领域，常见方案主要分为 <strong>Type-1（裸金属架构）</strong> 和 <strong>Type-2（宿主机架构）</strong>。以下是几种主流虚拟化方案的对比：</p><table><thead><tr><th> 方案                                   </th><th> 类型     </th><th> 适用场景            </th><th> 优点                                   </th><th> 缺点                     </th></tr></thead><tbody><tr><td> <strong>Proxmox VE(PVE)</strong>                  </td><td> Type-1 </td><td> 中小企业、免费解决方案     </td><td> 开源免费、基于Debian、支持LXC容器与KVM虚拟机、Web界面友好 </td><td> 学习曲线稍陡，对硬件兼容性有一定要求     </td></tr><tr><td> <strong>VMware ESXi</strong>                      </td><td> Type-1 </td><td> 行业龙头、企业级生产环境    </td><td> 行业标准、性能极高、稳定性极强、生态完善                 </td><td> 免费版限制多，硬件兼容性（HCL）极其挑剔  </td></tr><tr><td> <strong>Hyper-V</strong>                          </td><td> Type-1 </td><td> Windows生态环境     </td><td> 与Windows Server集成度高、性能优异             </td><td> 依赖Windows环境，管理界面相对单一   </td></tr><tr><td> <strong>Docker / Podman</strong>                  </td><td> 容器化    </td><td> 微服务、应用部署        </td><td> 轻量级、启动极快、资源占用极低                      </td><td> 隔离性弱于虚拟机，不适合运行完整操作系统   </td></tr><tr><td> <strong>Virtual Box / VMware Workstation</strong> </td><td> Type-2 </td><td> Windows系统下的开发测试 </td><td> 安装简单、支持多种桌面系统                        </td><td> 性能损耗大，依赖宿主操作系统，不适合生产环境 </td></tr></tbody></table><h3 id="-proxmox-vepve">为什么选择 Proxmox VE(PVE)？</h3><p>至于为什么选，我想大家的想法都是不谋而合吧，毕竟谁不愿意免费白嫖呢，还是开源的😄。
PVE的架构相当灵活，它支持<strong>LXC 容器</strong>用来跑轻量级业务，也可以使用<strong>KVM</strong>开辟一个全新隔离的环境 。VMware ESXi固然是行业标杆，但是免费版使用起来限制 诸多，而PVE则填补了VMware免费版的许多不足。</p>
<h2 id="-proxmox">二、安装 Proxmox</h2><p><strong>PVE下载地址：</strong> <a href="https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso">https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso</a></p>
<p><img src="https://blog.xming.cyou/api/v2/objects/image/h44mkwc9k93152fujz.png" alt="ProxmoxDownload" height="1004" width="1652"/></p>
<p>我这里选择的版本是<strong>VE 9.1</strong></p><h3 id="">制作启动盘</h3><p>制作启动盘请选择一个空的U盘或者没有任何重要数据的U盘，启动盘烧录时会把U盘数据格式化，⚠️注意备份重要数据‼️
<strong>下载烧录工具</strong>： <a href="https://etcher.balena.io/#download-etcher">https://etcher.balena.io/#download-etcher</a> （你也可以用其他工具，比如 <a href="https://www.ventoy.net/cn/download.html">Ventoy</a>、<a href="https://www.dabaicai.com/">大白菜</a>，只是我个人觉得<strong>balena Etcher</strong>相比其他同类工具要简洁好用）</p><p>下载完成后打开应该如图所示：</p>
<p><img src="https://blog.xming.cyou/api/v2/objects/image/ehwe0mubv1bu7r12gs.png" alt="balena Etcher" height="480" width="800"/></p>
<p><strong>选择下载好的PVE镜像文件 -&gt; 选择目标磁盘 -&gt; 开始烧录~</strong><br/>就是这么简单，等待烧录完成后启动盘就制作好了。</p>
<h3 id="u">选择U盘启动</h3><p>把制作好的启动盘插入 需要部署PVE系统的服务器，然后开机按<em>F12</em>(具体主板进入BIOS的方式可能不同，建议操作前请确认主板型号以及进入BIOS的方式)。</p><p>我的已经是安装完成了，启动盘设置为第一顺位后面的引导安装都比较简单，安装提示输入对应主机 / IP信息即可</p>
<p><img src="https://blog.xming.cyou/api/v2/objects/image/m190yuu64207e77htt.jpg" height="1279" width="1706"/></p>
<ol start="1"><li>开启虚拟化（已开启则忽略），<strong>没有开启则无法使用 VM，只能创建 LXC 容器</strong>。LXC 是系统级容器，与 Proxmox VE 共享宿主机内核。</li><li><p>调整启动顺序</p>
<p><img src="https://blog.xming.cyou/api/v2/objects/image/tv8q2a188rvn95zm09.jpg" alt="启动设置"/></p></li></ol><ol start="3"><li>保存并退出，等待Promox VE的引导界面</li><li>安装完成</li></ol><p><img src="https://blog.xming.cyou/api/v2/objects/image/y9ba3ta4ohms86i3tu.png"/></p>
<pre class=""><code class="">安装完成后如上图所示，系统会给出图形化访问地址（`https://IP地址:8006`）。后续的主要操作都在这个 Web 控制台中完成。到这里，PVE 系统已经安装完成，下面进入初始化与资源规划。</code></pre><h2 id="">三、首次登录与初始化</h2><p>安装完成后，在浏览器输入：https://&lt;你的PVE主机IP&gt;:8006
默认用户名：root
密码：你在安装PVE时设置的密码
<img src="https://blog.xming.cyou/api/v2/objects/image/12occixcywlztqygsq.png"/></p><p>安装完成后，先通过 <code>https://PVE-IP:8006</code> 登录 Web 控制台。建议第一天就把下面几件事做完，后续会省很多排障时间。</p><h3 id="1">1）软件源与系统更新</h3><p>很多人安装完第一步就创建虚拟机，结果后面出现下载慢、更新失败、证书报错。正确顺序是先把源和系统状态整理好。</p><h4 id="">关闭企业订阅源（无订阅环境）</h4><p>如果你不是企业订阅版，需要先关闭 <code>pve-enterprise</code>，否则更新时会报 401。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">sed -i &#x27;s/^deb/# deb/g&#x27; /etc/apt/sources.list.d/pve-enterprise.list
</code></pre>
<h4 id="">配置可用的软件源</h4><p>你可以按自己网络情况选择官方源或国内镜像源。这里给一组通用示例（以当前发行版名称为准）：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">cat &gt; /etc/apt/sources.list &lt;&lt;&#x27;EOF&#x27;
deb https://mirrors.ustc.edu.cn/debian/ bookworm main contrib non-free non-free-firmware
deb https://mirrors.ustc.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware
deb https://mirrors.ustc.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware
EOF
</code></pre>
<p>再添加 PVE 无订阅源：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">cat &gt; /etc/apt/sources.list.d/pve-no-subscription.list &lt;&lt;&#x27;EOF&#x27;
deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription
EOF
</code></pre>
<p>最后执行更新：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">apt update &amp;&amp; apt full-upgrade -y
reboot
</code></pre>
<h3 id="2">2）去除“无订阅”弹窗（可选）</h3><p>这是纯界面体验优化，不影响功能。若你不介意弹窗，可以跳过。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">cp /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js{,.bak}
sed -Ezi.bak &quot;s/(Ext.Msg.show\\(\\{\\s+title: gettext\\(&#x27;No valid sub)/void\\(\\{ \\/\\/\\1/g&quot; /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
systemctl restart pveproxy
</code></pre>
<h3 id="3pasted-image-20260401140516png">3）基础安全加固![[Pasted image 20260401140516.png]]</h3><h4 id="-root---">保留 root 应急，改为密钥 + 普通管理员日常登录</h4><p>不建议“直接禁用 root 然后不留后路”。更稳妥的做法：</p><ol start="1"><li>新建管理员用户（加入 <code>sudo</code>）</li><li>为该用户配置 SSH Key</li><li>验证可登录后，再限制 root 的远程密码登录</li></ol><pre class="language-bash lang-bash"><code class="language-bash lang-bash">adduser adminops
usermod -aG sudo adminops
</code></pre>
<p>编辑 <code>/etc/ssh/sshd_config</code>（建议至少包含以下项）：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">PermitRootLogin prohibit-password
PasswordAuthentication no # 关闭ssh密码认证
PubkeyAuthentication yes # 开启ssh密钥登录
</code></pre>
<p>应用配置：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">systemctl restart ssh
</code></pre>
<h3 id="4">4）时间同步与主机名检查</h3><p>虚拟化平台对时间漂移比较敏感，证书、集群、日志都会受影响。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">timedatectl set-timezone Asia/Shanghai
timedatectl status
hostnamectl status
</code></pre>
<hr/><h2 id="">四、网络初始化（先通再隔离）</h2><p>建议按“先桥接可用，再做 VLAN 隔离”的顺序推进，避免一上来就把管理网络配乱。</p><p>先做一个最小网络规划（按你的实际网段替换）：</p><table><thead><tr><th> 角色 </th><th> 示例网段 </th><th> 用途 </th><th> 可否出公网 </th></tr></thead><tbody><tr><td> 管理网（PVE） </td><td> 192.168.10.0/24 </td><td> 管理节点、Web 控制台、SSH </td><td> 建议受限放行 </td></tr><tr><td> 业务网（VM/LXC） </td><td> 192.168.20.0/24 </td><td> 业务服务对内互通 </td><td> 视业务决定 </td></tr><tr><td> 存储网（可选） </td><td> 192.168.30.0/24 </td><td> NFS/Ceph/备份流量 </td><td> 通常不出公网 </td></tr></tbody></table><h3 id="1vmbr0">1）桥接网络（vmbr0）</h3><p>最常见架构是：</p><ul><li><code>eno1</code>：物理网卡</li><li><code>vmbr0</code>：管理网桥（PVE 主机 IP 在 vmbr0 上）</li><li>虚拟机网卡挂到 <code>vmbr0</code></li></ul><p><code>/etc/network/interfaces</code> 示例（带注释，复制后按你的环境替换）：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">auto lo
iface lo inet loopback

# 物理网卡不直接配 IP，交给网桥 vmbr0 使用
iface eno1 inet manual

# 管理网桥：PVE 管理 IP 建议配在这里
auto vmbr0
iface vmbr0 inet static
    address 192.168.10.10/24    # 按你的管理网段修改（示例：10.0.0.10/24）
    gateway 192.168.10.1        # 按你的网关修改（示例：10.0.0.1）
    bridge-ports eno1           # 按你的物理网卡名修改（可能是 enp3s0 / eno2）
    bridge-stp off              # 家用/中小场景通常关闭 STP；复杂二层网络可按需开启
    bridge-fd 0                 # 转发延迟，保持默认 0
</code></pre>
<p>应用网络配置（远程操作请先确认带外管理可用）：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">ifreload -a
</code></pre>
<h3 id="2vlan-">2）VLAN 隔离（生产推荐）</h3><p>如果交换机端口已配置 Trunk，可在 <code>vmbr0</code> 开启 VLAN 感知：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">auto vmbr0
iface vmbr0 inet static
    address 192.168.10.10/24
    gateway 192.168.10.1
    bridge-ports eno1
    bridge-stp off
    bridge-fd 0
    bridge-vlan-aware yes       # 开启 VLAN 感知（前提：交换机端口允许对应 VLAN）
#   bridge-vids 10 20 30        # 可选：限制只允许通过的 VLAN
</code></pre>
<p>之后在虚拟机网卡里直接指定 VLAN Tag（如 10/20/30）即可实现业务隔离。</p><p>常见错配排查：</p><ul><li>VM 配了 VLAN Tag，但交换机端口不是 Trunk 或未放行该 VLAN</li><li>PVE 主机管理 IP 误配到了业务 VLAN，导致 Web 控制台失联</li><li>网卡名写错（<code>eno1</code>/<code>enpXsY</code>），导致网桥拉不起来</li><li>远程改网没有带外管理（IPMI/iDRAC/iLO），改错后无法回连</li></ul><hr/><h2 id="">五、资源整合与交付</h2><p>到这里再开始“造资源”，顺序建议：模板 → LXC/VM → 存储 → 硬件直通
<img src="https://blog.xming.cyou/api/v2/objects/image/h1tf9zqcjaescfb98d.png"/></p><h3 id="1">1）模板化（先做模板再批量部署）</h3><p>先准备一个干净系统，装好基础组件后做模板，可以把后续交付速度提升一个数量级。</p><p>典型流程：</p><ol start="1"><li>创建基础 VM（推荐 cloud-init 镜像）</li><li>做系统更新、时区、常用工具、监控 Agent</li><li>清理机器标识并关机</li><li>转换为模板</li><li>从模板克隆业务实例</li></ol><h3 id="2lxc-">2）LXC 容器（轻量业务优先）</h3><p>适合场景：</p><ul><li>内网工具、跳板、监控、日志采集</li><li>对内核隔离要求不高，但追求密度和启动速度</li></ul><p>建议：</p><ul><li>开启 <code>nesting</code> 仅在确有需求时使用</li><li>给每个容器设置 CPU/内存上限，避免“邻居噪声”</li><li>存储优先走本地 SSD 或低延迟共享存储</li></ul><h3 id="3kvm-">3）KVM 虚拟机（强隔离业务）</h3><p>适合场景：</p><ul><li>数据库、中间件、Windows、需要独立内核的服务</li><li>对安全边界和兼容性要求高的生产业务</li></ul><p>建议：</p><ul><li>CPU 类型优先 <code>host</code>（同构节点场景）</li><li>磁盘控制器优先 <code>VirtIO SCSI</code></li><li>网卡优先 <code>VirtIO (paravirtualized)</code></li><li>关键业务启用定期快照与备份策略</li></ul><h3 id="4">4）存储挂载（按业务分层）</h3><p>常见分层思路：</p><ul><li><code>local-lvm</code>：跑 VM/LXC 磁盘（高性能）</li><li><code>local</code>：ISO、备份、模板</li><li>NAS/NFS/Ceph：跨节点共享与集中备份</li></ul><p>实操建议：</p><ul><li>备份存储与生产存储分离</li><li>给备份任务设置保留策略（如 7/4/3：日/周/月）</li><li>定期做恢复演练，不只看“备份成功”</li></ul><h3 id="5gpunichba">5）硬件直通（GPU/NIC/HBA）</h3><p>当业务需要接近裸金属性能时，再考虑 PCI Passthrough。</p><p>典型使用场景：</p><ul><li>AI 推理/转码：GPU 直通</li><li>高性能网络：10G/25G 网卡直通</li><li>存储虚拟化：HBA 控制器整卡直通</li></ul><h4 id="bios--iommu">第一步：BIOS 打开虚拟化与 IOMMU</h4><ul><li>Intel 平台：开启 <code>Intel VT-x</code> + <code>Intel VT-d</code></li><li>AMD 平台：开启 <code>SVM</code> + <code>AMD-Vi/IOMMU</code></li><li>关闭 <code>CSM</code>（若你的环境支持纯 UEFI，通常更省事）</li></ul><h4 id="-iommu">第二步：开启内核 IOMMU</h4><p>编辑 <code>/etc/default/grub</code>：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># Intel 平台（任选其一，不要混用）
GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet intel_iommu=on iommu=pt&quot;

# AMD 平台（任选其一，不要混用）
GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet amd_iommu=on iommu=pt&quot;
</code></pre>
<p>更新引导并重启：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">update-grub
reboot
</code></pre>
<h4 id="-iommu-">第三步：验证 IOMMU 是否生效</h4><pre class="language-bash lang-bash"><code class="language-bash lang-bash">dmesg | grep -E &quot;DMAR|IOMMU&quot;
</code></pre>
<p>看到 <code>IOMMU enabled</code>、<code>DMAR</code> 相关输出通常表示已启用。</p><p>查看 PCI 设备与分组：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">lspci -nn
find /sys/kernel/iommu_groups/ -type l
</code></pre>
<h4 id="-vfio">第四步：把目标设备绑定到 VFIO</h4><p>先查设备 ID（示例）：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">lspci -nn | grep -Ei &quot;vga|3d|nvidia|amd|ethernet|sas&quot;
</code></pre>
<p>假设目标设备 ID 为 <code>10de:1eb8,10de:10f8</code>，写入：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">echo &quot;options vfio-pci ids=10de:1eb8,10de:10f8&quot; &gt; /etc/modprobe.d/vfio.conf
echo -e &quot;vfio\nvfio_iommu_type1\nvfio_pci\nvfio_virqfd&quot; &gt; /etc/modules-load.d/vfio.conf
update-initramfs -u -k all
reboot
</code></pre>
<p>重启后确认设备已由 <code>vfio-pci</code> 接管：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">lspci -nnk -d 10de:1eb8
</code></pre>
<p>如果看到 <code>Kernel driver in use: vfio-pci</code> 即表示绑定成功。</p><h4 id="-pve-">第五步：在 PVE 中分配给虚拟机</h4><p>在 VM 配置中添加 PCI 设备，常见建议：</p><ul><li>GPU 直通建议勾选 <code>All Functions</code></li><li>同时直通显卡和其音频函数（很多显卡是多功能设备）</li><li>必要时启用 <code>Primary GPU</code>（取决于系统与驱动）</li><li>使用 UEFI（OVMF）与 <code>q35</code> 机型兼容性通常更好</li></ul><h4 id="">回退方案（非常重要）</h4><p>若直通后宿主机异常或设备不可用，可按以下方式回退：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">rm -f /etc/modprobe.d/vfio.conf /etc/modules-load.d/vfio.conf
update-initramfs -u -k all
reboot
</code></pre>
<p>建议先在测试节点完成“启用 → 直通 → 回退”全流程演练，再上生产。</p></div><p style="text-align:right"><a href="https://xming.cyou/posts/devops/pve#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/devops/pve</link><guid isPermaLink="true">https://xming.cyou/posts/devops/pve</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Thu, 02 Apr 2026 03:09:35 GMT</pubDate></item><item><title><![CDATA[Mihomo（Clash.Meta）服务器代理配置]]></title><description><![CDATA[<link rel="preload" as="image" href="https://blog.xming.cyou/api/v2/objects/image/tmielql62tkmzq6o5y.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/devops/htttp-proxy">https://xming.cyou/posts/devops/htttp-proxy</a></blockquote><div><h2 id="">引言</h2><p>本教程旨在介绍如何通过 Docker Compose 部署 Mihomo（原 Clash.Meta）并梳理其运行限制。虽然最简便的方式是在服务器直接运行二进制文件，但长此以往，散落在系统后台的进程不仅难以溯源，更给运维审计带来了不便。容器化部署提供了以下优势：</p><ul><li><strong>环境隔离</strong>：避免与宿主机系统产生冲突</li><li><strong>版本管理</strong>：轻松升级/回滚版本</li><li><strong>配置持久化</strong>：配置文件与容器分离</li><li><strong>易于迁移</strong>：一次配置，多处部署</li><li><p><strong>资源限制</strong>：可精确控制 CPU/内存使用</p><p>如果你追求极致的部署速度且不在意管理成本，可以跳过本文，直接参考<a href="https://wiki.metacubex.one/startup/">Metacubex</a>。</p></li></ul><blockquote><p><strong>注意</strong>：受 Docker Hub 政策调整及国内网络环境影响，传统的国内镜像加速服务已近乎失效。在不配置系统代理的情况下，不仅无法搜索和拉取官方镜像，即便使用自建仓库或第三方镜像源，也常因镜像同步不全而导致部署中断。</p></blockquote>
<h2 id="">准备工作</h2><ol start="1"><li>离线部署镜像（<strong>在个人PC拉取镜像</strong>）
 <strong>在无法直接访问 Docker Hub 的环境下，我们可以先在本地（或海外服务器）通过 <code>docker save</code> 导出镜像包，再利用 <code>scp</code> 远程传输。</strong>
 
 <code>bash
     docker pull metacubex/mihomo:latest
     docker pull ghcr.io/metacubex/metacubexd:latest 
     # 导出镜像为 tar 包
     docker save metacubex/mihomo:latest ghcr.io/metacubex/metacubexd:latest | gzip &gt; mihomo-images.tar.gz
     # 格式：scp -i [私钥路径] [本地文件] [用户名]@[目标IP]:[远程路径]
     scp -i ~/.ssh/my_proxy_key.pem mihomo-images.tar.gz demo@目标服务器:~/
 </code></li><li><strong>登录目标服务器执行</strong>( 演示系统为：Ubuntu 24.04 LTS)
 <code>bash
 # 执行以下命令加载 Docker 镜像(报错的话建议加sudo，要么把当前用户加入sudo用户组)
 docker load &lt; mihomo-images.tar.gz
 # 删掉之前的镜像文件（删不删看个人）
 rm mihomo-images.tar.gz
 mkdir -p ~/.mihomo/config &amp;&amp; cd ~/.mihomo
 </code></li><li>内核参数配置
 <code>bash
 # 开启 IP 转发（必须）
 sudo sysctl -w net.ipv4.ip_forward=1
 sudo sysctl -w net.ipv6.conf.all.forwarding=1
 
 # 持久化配置
 echo &quot;net.ipv4.ip_forward=1&quot; | sudo tee -a /etc/sysctl.conf
 echo &quot;net.ipv6.conf.all.forwarding=1&quot; | sudo tee -a /etc/sysctl.conf
 sudo sysctl -p
 
 # 检查 TUN 设备（TUN 模式必需）
 ls -la /dev/net/tun
 # 如果不存在，创建它
 sudo mkdir -p /dev/net
 sudo mknod /dev/net/tun c 10 200
 sudo chmod 600 /dev/net/tun
 </code></li><li>创建<code>docker-compose.yaml</code></li></ol><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml"># version: &#x27;3&#x27;
services:
  mihomo: 
    container_name: mihomo
    image: metacubex/mihomo:latest
    restart: unless-stopped
    privileged: true # 开启特权模式（TUN 模式最简单的方式）
    pid: host # 共享 PID 命名空间（便于调试和进程管理）
    ipc: host # 共享 IPC 命名空间
    network_mode: host # 使用宿主机网络栈（性能最优，TUN 必需）
    cap_add:
      - ALL
    security_opt:
      - apparmor=unconfined # 禁用 AppArmor 限制（某些系统需要）
    volumes:
      - ~/.mihomo/config:/root/.config/mihomo
      - /dev/net/tun:/dev/net/tun # TUN 设备（TUN 模式必需）
      # 共享host的时间环境
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro

  # mihomo内置的有管理界面，也挺好用，如果不想额外折腾，下面可以删掉
  metacubexd: 
    container_name: metacubexd
    image: ghcr.io/metacubex/metacubexd
    restart: unless-stopped
    # 网络配置（与 mihomo 分离，仅提供 Web 访问）
    network_mode: bridge
    # 依赖 mihomo 服务（确保顺序启动）
    depends_on:
      mihomo:
    ports:
      - &#x27;127.0.0.1:28002:80&#x27;
    volumes:
      - ~/.mihomo/caddy:/config/caddy:rw
      # 共享host的时间环境
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
</code></pre>
<ol start="4"><li><p>下载代理文件 
 注意，需要下载为clash格式，命令：<code>curl -o config/config.yaml ✈️订阅链接</code></p><p> 编辑修改配置文件：</p><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml"> # port of HTTP
 port: 7890
 
 # port of SOCKS5
 socks-port: 7891
 
 allow-lan: true # 设置为true，允许局域网
 bind-address: &quot;*&quot;
 # Rule / Global/ DIRECT (default is Rule)
 mode: rule
 
 # set log level to stdout (default is info)
 # info / warning / error / debug
 log-level: info
 
 # A RESTful API for clash
 external-controller: 0.0.0.0:7090 # 这个是内置的管理界面，
 
 # Secret for RESTful API (Optional)
 secret: &quot;你的管理界面登录密码&quot;
 
 tun:
   enable: true
   stack: system          # 推荐使用 system 栈，兼容性最好
   auto-route: true       # 自动设置系统路由，实现“无感”代理
   auto-detect-interface: true # 自动识别物理网卡，防止流量回环
   dns-hierarchical: true
   mtu: 1400              # 调小 MTU，防止 TLS 握手时 Unexpected EOF
 
 dns:
   enable: true
   ipv6: false
   enhanced-mode: fake-ip
   listen: 0.0.0.0:53
   fake-ip-range: 198.18.0.1/16
   default-nameserver:
     - 119.29.29.29
     - 114.114.114.114
     - 223.5.5.5
     - 8.8.8.8
   nameserver:
     - 119.29.29.29
     - 114.114.114.114
     - 223.5.5.5
     - https://223.5.5.5/dns-query 
     - https://dns.alidns.com/dns-query 
     - https://doh.pub/dns-query 
   fake-ip-filter:
     - &quot;*.lan&quot;
     - &quot;*.local&quot;
     - +.miwifi.com
     - +.docker.io
     - +.market.xiaomi.com
     - +.push.apple.com
 </code></pre>
<p> 你也可以参考 <a href="https://clash.wiki/configuration/configuration-reference.html">clash.wiki</a>和 <a href="https://github.com/MetaCubeX/mihomo/blob/Meta/docs/config.yaml">MetaCubeX</a>配置适合自己的代理方式</p></li><li>启动容器
 <code>bash
 docker compose up -d
 docker logs -n 100 -f mihomo # 如果日志没报Error基本就没啥问题
 </code></li><li>访问管理界面你的<a href="https://yacd.metacubex.one/#/proxies">Yacd</a>或<code>192.168.X.X:28002</code> <img src="https://blog.xming.cyou/api/v2/objects/image/tmielql62tkmzq6o5y.png" alt="yacd"/>
代理不通不能使用等问题的 原因通常是防火墙和DNS问题 ，如果使用TUN模式，代理可能会劫持你的DNS服务 ，需要注意修改对应系统设置</li></ol></div><p style="text-align:right"><a href="https://xming.cyou/posts/devops/htttp-proxy#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/devops/htttp-proxy</link><guid isPermaLink="true">https://xming.cyou/posts/devops/htttp-proxy</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Thu, 02 Apr 2026 02:45:37 GMT</pubDate></item><item><title><![CDATA[Go 实用工具包——embed：简化文件加载流程]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/backend/embed">https://xming.cyou/posts/backend/embed</a></blockquote><div><h2 id="-goembed">为什么选择 go:embed</h2><ul><li>编译期打包：资源在构建阶段入库，运行期无需磁盘依赖与路径探测。</li><li>原生 API：无需第三方库，标准库 <code>embed</code>、<code>io/fs</code>、<code>net/http</code> 即可使用。</li><li>一致的跨平台行为：统一使用正斜杠路径，避免 Windows/Unix 差异。</li><li>更安全可控：避免运行期“配置文件不在预期位置”导致的故障。</li></ul><h2 id="">快速上手：单文件、目录与多文件</h2><p>嵌入单个文本文件为字符串：</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    _ &quot;embed&quot;
    &quot;fmt&quot;
)

//go:embed message.txt
var message string

func main() {
    fmt.Println(message)
}
</code></pre>
<p>嵌入目录并读取文件（以嵌入根为基准路径）：</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;embed&quot;
    &quot;fmt&quot;
)

//go:embed static
var staticFiles embed.FS

func main() {
    data, _ := staticFiles.ReadFile(&quot;static/index.html&quot;)
    fmt.Println(string(data))
}
</code></pre>
<p>使用通配符一次性嵌入多文件：</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;embed&quot;
    &quot;fmt&quot;
)

//go:embed templates/*.html partials/*.html
var tmplFS embed.FS

func main() {
    entries, _ := tmplFS.ReadDir(&quot;templates&quot;)
    for _, e := range entries {
        b, _ := tmplFS.ReadFile(&quot;templates/&quot; + e.Name())
        fmt.Printf(&quot;Loaded %s (%d bytes)\n&quot;, e.Name(), len(b))
    }
}
</code></pre>
<h2 id="-web">用于 Web：内嵌静态资源即开即用</h2><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;embed&quot;
    &quot;net/http&quot;
)

//go:embed static/*
var assets embed.FS

func main() {
    http.Handle(&quot;/&quot;, http.FileServer(http.FS(assets)))
    http.ListenAndServe(&quot;:8080&quot;, nil)
}
</code></pre>
<p>运行后在 <code>http://localhost:8080</code> 直接访问嵌入的静态文件，无需额外拷贝。</p><h2 id="pdf">二进制资源：图片、PDF、压缩包</h2><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    _ &quot;embed&quot;
    &quot;fmt&quot;
)

//go:embed image.webp
var img []byte

func main() {
    fmt.Printf(&quot;Image size: %d bytes\n&quot;, len(img))
}
</code></pre>
<p>当资源非 UTF-8 文本时，使用 <code>[]byte</code> 可以避免编码问题。</p><h2 id="">路径与加载规则（重点答疑）</h2><ul><li>相对路径且仅限包内：<code>//go:embed</code> 的路径与模式必须为相对路径，基于“声明该变量的源文件所在目录”。不支持绝对路径。</li><li>统一分隔符：路径分隔一律使用正斜杠 <code>/</code>，即使在 Windows。示例：<code>&quot;static/index.html&quot;</code>。</li><li>模式匹配：支持通配符（遵循 <code>filepath.Match</code> 语义），如 <code>static/*</code>、<code>templates/*.html</code>。可以在同一指令中列出多个模式。</li><li>不允许越级：路径中不能包含 <code>..</code>，不可引用包目录之外的文件。</li><li>嵌入内容类型：可以嵌入任何常规文件类型。按变量类型区分读取方式：<code>string</code>（文本）、<code>[]byte</code>（二进制）、<code>embed.FS</code>（多文件/目录）。</li><li>FS 读取相对嵌入根：使用 <code>embed.FS</code> 时，<code>ReadFile</code> 的路径以嵌入根为基准；若嵌入的是目录 <code>static</code>，读取应为 <code>static/xxx</code>。</li><li>构建与可见性：嵌入在构建期完成；资源更新后需重新编译。仅能嵌入构建系统可见的常规文件，符号链接不会被跟随。</li></ul><p>结论：</p><ul><li>是否支持相对和绝对路径？仅支持相对路径（相对源文件目录），不支持绝对路径。</li><li>文件类型支持情况？任意常规文件均可嵌入；文本建议用 <code>string</code>，二进制用 <code>[]byte</code>；多文件/目录用 <code>embed.FS</code>。</li></ul><h2 id="">常见陷阱与最佳实践</h2><ul><li>控制二进制体积：大文件会显著增大可执行文件；将超大资源改用外部 CDN/对象存储更合适。</li><li>保持路径一致性：在所有平台都用 <code>/</code>；不要混用反斜杠 <code>\</code>。</li><li>指令位置与类型匹配：<code>//go:embed</code> 必须紧邻变量声明；变量类型仅支持 <code>string</code>、<code>[]byte</code>、<code>embed.FS</code>。</li><li>多模式整合：将多个子目录合并到同一 <code>embed.FS</code>，减少散乱的变量。</li><li>变更需重编：资源文件变化后务必重新构建以更新嵌入内容。</li></ul><h2 id="">一个更完整的模板渲染示例</h2><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;embed&quot;
    &quot;html/template&quot;
    &quot;net/http&quot;
)

//go:embed templates/*.html
var views embed.FS

func main() {
    t := template.Must(template.ParseFS(views, &quot;templates/*.html&quot;))
    http.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
        _ = t.ExecuteTemplate(w, &quot;home.html&quot;, map[string]string{&quot;Title&quot;: &quot;Embed Demo&quot;})
    })
    _ = http.ListenAndServe(&quot;:8080&quot;, nil)
}
</code></pre>
<h2 id="">小结</h2><p><code>go:embed</code> 让资源管理从“运行期找文件”变为“编译期打包”。掌握“相对路径、统一分隔符、类型选择”这三点，就能在 CLI、Web 服务、微服务工具中干净地管理静态与模板资源，极大简化部署与加载流程。</p></div><p style="text-align:right"><a href="https://xming.cyou/posts/backend/embed#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/backend/embed</link><guid isPermaLink="true">https://xming.cyou/posts/backend/embed</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Sun, 01 Mar 2026 05:29:40 GMT</pubDate></item><item><title><![CDATA[Server-Sent Events]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/backend/sse">https://xming.cyou/posts/backend/sse</a></blockquote><div><h2 id="-sse">为什么选择 SSE</h2><ul><li>面向“服务器→浏览器单向推送”的场景，浏览器原生支持 EventSource，无需第三方库</li><li>基于标准 HTTP（含 HTTP/2），与现有网关、负载均衡、鉴权、缓存体系兼容度高</li><li>自动断线重连与消息续传（Last‑Event‑ID）由浏览器内置完成</li><li>只需文本数据，天然适合通知、进度、日志、监控等单向推送</li></ul><h2 id="">原理与协议</h2><ul><li>连接与建立<br/>客户端通过 EventSource 发起 GET 请求，请求头隐式包含 <code>Accept: text/event-stream</code>；服务器以 <code>Content-Type: text/event-stream</code> 响应并保持连接打开，通过持续的 HTTP 响应流推送数据。</li><li>事件格式<br/>一条事件由多行键值对组成并以连续两个换行符结束。常见字段：
<ul><li><code>event:</code> 事件类型，默认是 <code>message</code></li><li><code>data:</code> 消息主体数据（可多行，每行一个 <code>data:</code>）</li><li><code>id:</code> 事件唯一标识（用于断线续传）</li><li><code>retry:</code> 重连时间间隔（毫秒）
示例：
<code>
data: This is a message
</code></li></ul></li></ul><p>  或带事件类型与 JSON 数据：</p><pre class=""><code class="">  event: userupdate
  data: {&quot;username&quot;:&quot;john_doe&quot;,&quot;status&quot;:&quot;online&quot;}
  </code></pre>
<ul><li>断线重连<br/>浏览器自动重试，若服务器曾发送 <code>id:</code>，浏览器在重连时携带 <code>Last-Event-ID</code>，服务器可从该 ID 之后继续发送。</li><li>保活与代理<br/>许多代理会因长时间无数据而断开，服务器可定期发送注释行（以冒号起始，如 <code>: keepalive</code>）作为保活。建议设置 <code>Cache-Control: no-cache</code>、<code>Connection: keep-alive</code>，必要时对部分代理设置 <code>X-Accel-Buffering: no</code>。</li></ul><h2 id="sse--websocket-">SSE 与 WebSocket 对比</h2><ul><li>通信模型<br/><ul><li>SSE：服务器→浏览器单向文本流，浏览器原生 EventSource，自动重连与续传</li><li>WebSocket：在独立的 TCP 连接上建立全双工通信，使用协议升级与帧（frame）进行数据传输，需自行处理重连与续传</li></ul></li><li>基础设施与兼容性<br/><ul><li>SSE：走标准 HTTP，易穿透与复用现有网关、鉴权、中间件；适合通知、状态推送、日志流</li><li>WebSocket：适合 IM、协作编辑、游戏等强双向低延时场景</li></ul></li><li>性能与实现<br/><ul><li>SSE：实现简单、成本低；消息量大或需要二进制时不适用</li><li>WebSocket：灵活强大，但实现与维护复杂度更高</li></ul></li></ul><h2 id="">后端</h2><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;encoding/json&quot;
    &quot;io&quot;
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;strconv&quot;
    &quot;strings&quot;
    &quot;time&quot;

    &quot;github.com/r3labs/sse/v2&quot;
)

func main() {
    // 创建 SSE 服务器与单一流（建议统一使用一个流，通过 event 类型区分）
    server := sse.New()
    server.CreateStream(&quot;events&quot;)

    mux := http.NewServeMux()
    mux.HandleFunc(&quot;/events&quot;, func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;)
        server.ServeHTTP(w, r)
    })

    mux.HandleFunc(&quot;/publish&quot;, func(w http.ResponseWriter, r *http.Request) {
        // 支持通过 type 指定事件类型（默认 message），便于后端统一发布
        // 事件类型建议：
        // - message：文本通知，适合短消息或提示
        // - tick：时间戳字符串，适合心跳与时钟
        // - userupdate：JSON 用户状态，如 {&quot;username&quot;:&quot;john&quot;,&quot;status&quot;:&quot;online&quot;}
        // - stock：JSON 行情，如 {&quot;symbol&quot;:&quot;ACME&quot;,&quot;price&quot;:123.45}
        // - score：JSON 比分，如 {&quot;home&quot;:1,&quot;away&quot;:2}
        // - location：JSON 位置，如 {&quot;lat&quot;:31.23,&quot;lng&quot;:121.47}
        // - progress：JSON/数字进度，如 {&quot;percent&quot;:42}
        // - system：文本系统事件，如 &quot;ok&quot; 或日志片段
        body, _ := io.ReadAll(r.Body)
        msg := strings.TrimSpace(string(body))
        if msg == &quot;&quot; {
            msg = r.URL.Query().Get(&quot;msg&quot;)
        }
        if msg == &quot;&quot; {
            http.Error(w, &quot;msg required&quot;, http.StatusBadRequest)
            return
        }
        eventType := r.URL.Query().Get(&quot;type&quot;)
        if eventType == &quot;&quot; {
            eventType = &quot;message&quot;
        }
        server.Publish(&quot;events&quot;, &amp;sse.Event{
            ID:    []byte(strconv.FormatInt(time.Now().UnixNano(), 10)),
            Event: []byte(eventType),
            Data:  []byte(msg),
        })
        w.WriteHeader(http.StatusNoContent)
    })

    go func() {
        // tick：时间戳字符串，适合心跳与时钟
        ticker := time.NewTicker(5 * time.Second)
        for t := range ticker.C {
            server.Publish(&quot;events&quot;, &amp;sse.Event{
                ID:    []byte(strconv.FormatInt(time.Now().UnixNano(), 10)),
                Event: []byte(&quot;tick&quot;),
                Data:  []byte(t.Format(time.RFC3339)),
            })
        }
    }()

    go func() {
        // stock：行情 JSON，适合实时价格或指标
        type Stock struct {
            Symbol string  `json:&quot;symbol&quot;`
            Price  float64 `json:&quot;price&quot;`
        }
        t := time.NewTicker(7 * time.Second)
        for tt := range t.C {
            _ = tt
            payload, _ := json.Marshal(Stock{Symbol: &quot;ACME&quot;, Price: 123.45})
            server.Publish(&quot;events&quot;, &amp;sse.Event{
                ID:    []byte(strconv.FormatInt(time.Now().UnixNano(), 10)),
                Event: []byte(&quot;stock&quot;),
                Data:  payload,
            })
        }
    }()

    go func() {
        // score：比分 JSON，适合体育赛事
        type Score struct {
            Home int `json:&quot;home&quot;`
            Away int `json:&quot;away&quot;`
        }
        t := time.NewTicker(9 * time.Second)
        h, a := 0, 0
        for range t.C {
            h++
            payload, _ := json.Marshal(Score{Home: h, Away: a})
            server.Publish(&quot;events&quot;, &amp;sse.Event{
                ID:    []byte(strconv.FormatInt(time.Now().UnixNano(), 10)),
                Event: []byte(&quot;score&quot;),
                Data:  payload,
            })
        }
    }()

    go func() {
        // location：位置 JSON，适合地图轨迹
        type Location struct {
            Lat float64 `json:&quot;lat&quot;`
            Lng float64 `json:&quot;lng&quot;`
        }
        t := time.NewTicker(11 * time.Second)
        lat, lng := 31.2304, 121.4737
        for range t.C {
            lat += 0.0003
            lng += 0.0002
            payload, _ := json.Marshal(Location{Lat: lat, Lng: lng})
            server.Publish(&quot;events&quot;, &amp;sse.Event{
                ID:    []byte(strconv.FormatInt(time.Now().UnixNano(), 10)),
                Event: []byte(&quot;location&quot;),
                Data:  payload,
            })
        }
    }()

    go func() {
        // progress：进度（数字或 JSON），适合长任务状态
        type Progress struct {
            Percent int `json:&quot;percent&quot;`
        }
        for {
            for p := 0; p &lt;= 100; p += 20 {
                payload, _ := json.Marshal(Progress{Percent: p})
                server.Publish(&quot;events&quot;, &amp;sse.Event{
                    ID:    []byte(strconv.FormatInt(time.Now().UnixNano(), 10)),
                    Event: []byte(&quot;progress&quot;),
                    Data:  payload,
                })
                time.Sleep(3 * time.Second)
            }
        }
    }()

    go func() {
        // system：系统事件文本，适合日志片段或健康检查
        t := time.NewTicker(30 * time.Second)
        for range t.C {
            server.Publish(&quot;events&quot;, &amp;sse.Event{
                ID:    []byte(strconv.FormatInt(time.Now().UnixNano(), 10)),
                Event: []byte(&quot;system&quot;),
                Data:  []byte(&quot;ok&quot;),
            })
        }
    }()

    log.Fatal(http.ListenAndServe(&quot;:8080&quot;, mux))
}
</code></pre>
<h2 id="">前端</h2><pre class="language-html lang-html"><code class="language-html lang-html">&lt;script&gt;
// 订阅统一流 events，根据不同 event 类型分别处理
const es = new EventSource(&quot;http://localhost:8080/events?stream=events&quot;);

// 默认事件类型 message：文本通知
es.onmessage = (e) =&gt; {
  console.log(&quot;message&quot;, e.data);
};
// tick：时间戳字符串
es.addEventListener(&quot;tick&quot;, (e) =&gt; {
  console.log(&quot;tick&quot;, e.data);
});
// stock：行情 JSON
es.addEventListener(&quot;stock&quot;, (e) =&gt; {
  try { console.log(&quot;stock&quot;, JSON.parse(e.data)); } catch { console.log(e.data); }
});
// score：比分 JSON
es.addEventListener(&quot;score&quot;, (e) =&gt; {
  try { console.log(&quot;score&quot;, JSON.parse(e.data)); } catch { console.log(e.data); }
});
// location：位置 JSON
es.addEventListener(&quot;location&quot;, (e) =&gt; {
  try { console.log(&quot;location&quot;, JSON.parse(e.data)); } catch { console.log(e.data); }
});
// progress：进度 JSON/数字
es.addEventListener(&quot;progress&quot;, (e) =&gt; {
  try { console.log(&quot;progress&quot;, JSON.parse(e.data)); } catch { console.log(e.data); }
});
// system：系统事件文本
es.addEventListener(&quot;system&quot;, (e) =&gt; {
  console.log(&quot;system&quot;, e.data);
});
&lt;/script&gt;
</code></pre>
<pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// 组件中使用：将不同事件类型渲染到页面
const es = new EventSource(&quot;http://localhost:8080/events?stream=events&quot;);
es.onmessage = (e) =&gt; {
  const el = document.getElementById(&quot;out&quot;);
  if (el) el.textContent = e.data;
};
es.addEventListener(&quot;tick&quot;, (e) =&gt; {
  const el = document.getElementById(&quot;tick&quot;);
  if (el) el.textContent = e.data;
});
es.addEventListener(&quot;progress&quot;, (e) =&gt; {
  const el = document.getElementById(&quot;progress&quot;);
  try { const p = JSON.parse(e.data); if (el) el.textContent = p.percent + &quot;%&quot;; }
  catch { if (el) el.textContent = e.data; }
});
</code></pre>
<h2 id="">运行与测试</h2><ul><li>启动后端：<code>go run main.go</code>，监听 8080 端口</li><li>打开页面：在浏览器控制台观察事件输出</li><li><p>发布消息：<code>POST</code> 文本到 <code>http://localhost:8080/publish?type=message</code> 或 GET <code>http://localhost:8080/publish?type=message&amp;msg=hello</code>，浏览器会收到 <code>events</code> 流的推送（根据 <code>type</code> 作为事件类型）</p></li></ul><h2 id="">最佳实践与注意事项</h2><ul><li>保活与缓冲：周期性发送注释行维持连接活跃；必要时设置 <code>X-Accel-Buffering: no</code></li><li>事件整形：多行数据逐行 <code>data:</code> 前缀；使用 <code>id</code> 支持断线续传（配合 <code>Last-Event-ID</code>）</li><li>并发与背压：为 client 通道设置有限缓冲并在广播时使用非阻塞写入；根据场景调整缓冲与丢弃策略</li><li>安全与鉴权：走标准 HTTP，可复用 Cookie、Token、签名等鉴权方式；若需鉴权刷新，结合短期令牌与重连策略</li></ul><h2 id="">适用场景</h2><ul><li>实时通知与社交动态提示</li><li>股票行情与金融市场数据更新</li><li>体育赛事比分与统计实时更新</li><li>地理位置追踪与地图轨迹刷新</li><li>系统监控面板：服务器状态、日志流与指标</li><li>长任务进度、批处理状态、构建或部署流水输出</li><li>面板与监控看板的低频实时更新</li></ul><h2 id="">参考文献</h2><p><a href="https://apifox.com/apiskills/what-is-sse-and-sse-debug-tool/">什么是 SSE？</a></p><p><a href="https://apifox.com/apiskills/sse-vs-websocket/">SSE 和 WebSocket 的区别，差异对比</a></p></div><p style="text-align:right"><a href="https://xming.cyou/posts/backend/sse#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/backend/sse</link><guid isPermaLink="true">https://xming.cyou/posts/backend/sse</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Sat, 28 Feb 2026 10:55:39 GMT</pubDate></item><item><title><![CDATA[React & Next.js 进阶实战：从原理到生产级应用]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/frontend/react">https://xming.cyou/posts/frontend/react</a></blockquote><div><h2 id="--nextjs-">一、 深度剖析 Next.js 渲染模式</h2><p>Next.js 的核心竞争力在于极其灵活的混合渲染机制。理解每种模式的 <strong>触发时机</strong>、<strong>数据流向</strong> 和 <strong>适用场景</strong> 是架构选型的基础。</p><h3 id="11--the-rendering-spectrum">1.1 渲染光谱 (The Rendering Spectrum)</h3><table><thead><tr><th style="text-align:left"> 渲染模式 </th><th style="text-align:left"> 简述 </th><th style="text-align:left"> 构建/执行时机 </th><th style="text-align:left"> 数据新鲜度 </th><th style="text-align:left"> 典型场景 </th></tr></thead><tbody><tr><td style="text-align:left"> <strong>SSG</strong> (Static Site Generation) </td><td style="text-align:left"> 纯静态生成 </td><td style="text-align:left"> <code>npm run build</code> 时 </td><td style="text-align:left"> 低 (下次构建前不变) </td><td style="text-align:left"> 营销页、文档、帮助中心 </td></tr><tr><td style="text-align:left"> <strong>ISR</strong> (Incremental Static Regeneration) </td><td style="text-align:left"> 增量静态再生 </td><td style="text-align:left"> 构建时 + 运行时按需 (Revalidation) </td><td style="text-align:left"> 中 (可配置秒级更新) </td><td style="text-align:left"> 电商列表页、新闻资讯 </td></tr><tr><td style="text-align:left"> <strong>SSR</strong> (Server-Side Rendering) </td><td style="text-align:left"> 服务端渲染 </td><td style="text-align:left"> 每次请求 (Request Time) </td><td style="text-align:left"> 高 (实时) </td><td style="text-align:left"> 个人中心、即时数据看板 </td></tr><tr><td style="text-align:left"> <strong>CSR</strong> (Client-Side Rendering) </td><td style="text-align:left"> 客户端渲染 </td><td style="text-align:left"> 浏览器运行时 </td><td style="text-align:left"> 高 (依赖 API 延迟) </td><td style="text-align:left"> 管理后台 dashboard、复杂交互页 </td></tr></tbody></table><h3 id="12-app-router-rsc-vs-cc">1.2 App Router 下的范式转移：RSC vs CC</h3><p>Next.js 13+ 引入的 App Router 彻底改变了组件模型，核心在于 <strong>React Server Components (RSC)</strong>。</p><h4 id="react-server-components-rsc">React Server Components (RSC)</h4><p>默认情况下，<code>app</code> 目录下的组件都是 RSC。</p><ul><li><strong>优势</strong>：
<ul><li><strong>零 Bundle Size</strong>：组件代码及其依赖（如大型日期库、Markdown 解析库）仅在服务端运行，<strong>不会</strong>被打包发送到浏览器。</li><li><strong>后端直连</strong>：可直接连接数据库、读取文件系统，无需经过 API Layer。</li></ul></li><li><strong>限制</strong>：无法使用 <code>useState</code>, <code>useEffect</code>, 无法绑定 DOM 事件 (<code>onClick</code>)。</li></ul><p><strong>实战：在 Server Component 中获取数据</strong>
Next.js 扩展了 <code>fetch</code> API，并支持在组件中直接使用 <code>async/await</code>。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">// app/page.tsx (默认是 Server Component)
async function getData() {
  const res = await fetch(&#x27;https://api.example.com/data&#x27;, { 
    next: { revalidate: 3600 } // ISR: 每小时重新验证一次
  });
  if (!res.ok) throw new Error(&#x27;Failed to fetch data&#x27;);
  return res.json();
}

export default async function Page() {
  const data = await getData(); // 直接 await，像写后端代码一样简单

  return (
    &lt;main&gt;
      &lt;h1&gt;{data.title}&lt;/h1&gt;
      &lt;p&gt;{data.content}&lt;/p&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<h4 id="client-components-cc">Client Components (CC)</h4><p>当且仅当你在文件顶部声明 <code>&#x27;use client&#x27;</code> 时，该组件及其导入的所有子组件才会成为 Client Component。</p><ul><li><strong>注意</strong>：Client Component 依然会在服务端进行<strong>预渲染 (Pre-rendering)</strong> 生成初始 HTML，然后在客户端进行 <strong>水合 (Hydration)</strong>。它不仅仅是 CSR。</li></ul><h4 id="-leaf-pattern">最佳实践：叶子节点模式 (Leaf Pattern)</h4><p>为了最大化 RSC 的优势，应尽量将 Client Component 推向组件树的末端（叶子节点）。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">// ❌ 错误做法：在根布局就把整个应用变成 Client Component
// app/layout.tsx
&#x27;use client&#x27; 
export default function RootLayout({ children }) { ... }

// ✅ 正确做法：将交互逻辑隔离在独立组件中
// app/layout.tsx (Server Component)
import { SearchBar } from &#x27;./SearchBar&#x27;; // SearchBar 是 Client Component

export default function RootLayout({ children }) {
  return (
    &lt;html&gt;
      &lt;body&gt;
        &lt;nav&gt;&lt;SearchBar /&gt;&lt;/nav&gt; {/* 只有 SearchBar 会打包 JS 发送给浏览器 */}
        &lt;main&gt;{children}&lt;/main&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<h3 id="13--hydration">1.3 深度解析：水合 (Hydration)</h3><p>水合是现代 React/Next.js 应用中最关键但也最容易被误解的概念。</p><h4 id="">什么是水合？</h4><p>简单来说，水合是 React <strong>&quot;接管&quot;</strong> 服务端生成的静态 HTML 的过程。</p><ol start="1"><li><strong>服务端 (SSR/SSG)</strong>：React 组件在服务器运行，生成 HTML 字符串。此时页面是<strong>静态的</strong>，按钮点击无反应。</li><li><strong>浏览器</strong>：下载并显示 HTML (FCP - First Contentful Paint)。</li><li><strong>下载 JS</strong>：浏览器下载 React 框架和组件代码。</li><li><strong>水合 (Hydration)</strong>：React 在客户端再次运行组件逻辑，生成虚拟 DOM，并与现有的真实 DOM 节点进行对比（Diff）。如果匹配，React 就会把事件监听器（Event Listeners）绑定到 DOM 上，使页面变得<strong>可交互</strong>。</li></ol><h4 id="-hydration-mismatch">常见灾难：水合不匹配 (Hydration Mismatch)</h4><p>当服务端生成的 HTML 与客户端初次渲染的 HTML 不一致时，React 会抛出 Hydration Error，并强制客户端重新渲染整个节点，导致性能损耗和 UI 闪烁。</p><p><strong>典型错误场景</strong>：</p><ol start="1"><li><strong>时间戳/随机数</strong>：
<code>tsx
// ❌ 服务端生成的时间与客户端渲染时的时间不一致
&lt;div&gt;{new Date().toLocaleTimeString()}&lt;/div&gt;
&lt;div&gt;{Math.random()}&lt;/div&gt;
</code></li><li><strong>浏览器特有 API</strong>：
<code>tsx
// ❌ 服务端没有 window 对象，渲染出不同内容
&lt;div&gt;{typeof window !== &#x27;undefined&#x27; ? &#x27;Client&#x27; : &#x27;Server&#x27;}&lt;/div&gt;
</code></li><li><strong>HTML 嵌套错误</strong>：
<ul><li><code>&lt;p&gt;</code> 标签里嵌套 <code>&lt;div&gt;</code>（这是非法的 HTML，浏览器会自动修复 DOM 结构，导致 React 找不到预期的节点）。</li></ul></li></ol><h4 id="">解决方案</h4><ol start="1"><li><p><strong>使用 useEffect 延迟渲染</strong>：确保逻辑只在客户端执行。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">const [mounted, setMounted] = useState(false);
useEffect(() =&gt; setMounted(true), []);

if (!mounted) return null; // 或返回加载占位符
return &lt;div&gt;{window.innerWidth}&lt;/div&gt;;
</code></pre>
</li><li><p><strong>suppressHydrationWarning</strong>：如果你明确知道内容会不一致（如时间戳），可以强制 React 忽略警告。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">&lt;time suppressHydrationWarning&gt;{new Date().toLocaleTimeString()}&lt;/time&gt;
</code></pre>
</li></ol><hr/><h2 id="-react-hooks-">二、 React Hooks 进阶与性能优化</h2><p>Hooks 也就是 React 的&quot;魔法&quot;，但如果不理解其背后的 <strong>引用稳定性 (Referential Equality)</strong> 原理，极易导致性能灾难。</p><h3 id="21-usememo">2.1 useMemo：不仅仅是缓存计算</h3><p>很多开发者认为 <code>useMemo</code> 只是为了缓存昂贵的计算结果，其实它更重要的作用是 <strong>保持引用稳定</strong>。</p><h4 id="">核心场景</h4><ol start="1"><li><strong>昂贵的计算</strong>：避免每次渲染都跑一遍几千次循环。</li><li><strong>引用稳定性（关键）</strong>：当一个对象/数组被作为 <strong>子组件的 Props</strong> 或 <strong>其他 Hooks 的依赖项</strong> 时。</li></ol><h4 id="">场景实战：避免副作用连锁反应</h4><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">// ❌ 反例：每次父组件渲染，filterOptions 都会生成新的对象引用
const Parent = () =&gt; {
  const filterOptions = { role: &#x27;admin&#x27;, status: &#x27;active&#x27; }; 
  
  // 导致 Child 即使被 React.memo 包裹也会重渲染
  // 导致 useEffect 即使逻辑没变也会再次触发
  return &lt;Child options={filterOptions} /&gt;;
};

// ✅ 正确：锁定引用
const Parent = () =&gt; {
  const filterOptions = useMemo(() =&gt; ({ 
    role: &#x27;admin&#x27;, status: &#x27;active&#x27; 
  }), []); // 依赖为空，整个生命周期引用不变
  
  return &lt;Child options={filterOptions} /&gt;;
};
</code></pre>
<h4 id="">避坑指南</h4><ul><li><strong>不要过度优化</strong>：<code>useMemo</code> 本身有开销。对于简单的 <code>a + b</code> 或者简单的字符串拼接，使用 <code>useMemo</code> 是负优化。</li></ul><h3 id="22-usecallback">2.2 useCallback：函数引用的定海神针</h3><p><code>useCallback</code> 是 <code>useMemo</code> 的函数特化版本。</p><h4 id="">核心场景</h4><ol start="1"><li><strong>传递给 memo 化的子组件</strong>：如果子组件用 <code>React.memo</code> 优化了，但 props 里的函数每次父组件渲染都变，优化就会失效。</li><li><strong>作为 Hook 的依赖</strong>：比如自定义 Hook 需要接收一个回调函数。</li></ol><h4 id="-stale-closure">场景实战：闭包陷阱 (Stale Closure)</h4><p>这是 <code>useCallback</code> (以及 <code>useEffect</code>) 最容易出错的地方。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">const [count, setCount] = useState(0);

// ❌ 陷阱：这个函数创建时捕获了当时的 count (比如 0)
// 如果依赖数组为空 []，无论后续 count 变成多少，log 出来的永远是 0
const handleLog = useCallback(() =&gt; {
  console.log(&#x27;Current count:&#x27;, count); 
}, []); // 缺少 count 依赖

// ✅ 修正方案 A：添加依赖
const handleLogFixed = useCallback(() =&gt; {
  console.log(&#x27;Current count:&#x27;, count);
}, [count]); // 每次 count 变了，生成新函数（这有时会违背初衷）

// ✅ 修正方案 B：使用 Ref 穿透闭包 (Ref Pattern)
// 当你既想保持函数引用不变，又想访问最新 state 时
const countRef = useRef(count);
useEffect(() =&gt; { countRef.current = count; }, [count]);

const handleLogAdvanced = useCallback(() =&gt; {
  console.log(&#x27;Current count:&#x27;, countRef.current);
}, []); // 永远不更新引用，但能拿到最新值
</code></pre>
<h3 id="23-useeffect">2.3 useEffect：副作用管理的艺术</h3><h4 id="">最佳实践</h4><ol start="1"><li><strong>一个 Effect 做一件事</strong>：不要把数据获取、事件监听、DOM 操作塞在一个 <code>useEffect</code> 里。</li><li><strong>处理竞态条件 (Race Conditions)</strong>：在异步数据获取中非常常见。</li></ol><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">useEffect(() =&gt; {
  let ignore = false;
  
  async function fetchProfile() {
    const result = await api.fetchUser(userId);
    // 如果组件卸载了，或者 userId 变了导致开启了新一轮 effect，
    // 这里的 ignore 就会变成 true，从而丢弃旧的请求结果
    if (!ignore) setProfile(result);
  }

  fetchProfile();

  return () =&gt; { ignore = true; };
}, [userId]);
</code></pre>
<h3 id="24-useref">2.4 useRef：脱离数据流的逃生舱</h3><p><code>useRef</code> 返回一个可变的 ref 对象，其 <code>.current</code> 属性被初始化为传入的参数。<strong>修改 <code>.current</code> 不会触发组件重新渲染</strong>。</p><h4 id="">核心场景</h4><ol start="1"><li><strong>访问 DOM 节点</strong>：最经典用法，操作 focus、scroll 等。</li><li><strong>存储 Mutable 变量</strong>：类似 Class 组件的 <code>this.xxx</code> 实例变量。
<ul><li>存储定时器 ID。</li><li>存储上一次的 Props 值用于对比 (Previous Value)。</li></ul></li></ol><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">// 场景：实现 usePrevious Hook
function usePrevious(value) {
  const ref = useRef();
  // 每次渲染都会执行，但在 return 之前更新
  // 所以本次渲染拿到的 ref.current 还是上次的值
  useEffect(() =&gt; {
    ref.current = value;
  }, [value]);
  return ref.current;
}
</code></pre>
<h3 id="25-uselayouteffect-dom-">2.5 useLayoutEffect：同步 DOM 更新</h3><p>签名与 <code>useEffect</code> 相同，但它会在所有的 DOM 变更之后 <strong>同步</strong> 调用。</p><h4 id="">核心场景</h4><ul><li><strong>防止闪烁</strong>：当你需要测量 DOM 元素尺寸并根据尺寸重新调整布局时。</li><li>如果使用 <code>useEffect</code>，用户可能会先看到初始布局，然后瞬间跳变到新布局。<code>useLayoutEffect</code> 会阻塞浏览器绘制，直到执行完毕，确保用户只看到最终状态。</li></ul><h3 id="26-useimperativehandle">2.6 useImperativeHandle：定制暴露给父组件的实例值</h3><p>应当尽量避免使用。但在某些场景（如封装复杂的 UI 库组件）下，你可能需要向父组件暴露特定的方法（如 <code>focus()</code>, <code>scrollToBottom()</code>），而不是整个 DOM 节点。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">const FancyInput = forwardRef((props, ref) =&gt; {
  const inputRef = useRef();
  
  useImperativeHandle(ref, () =&gt; ({
    focus: () =&gt; {
      inputRef.current.focus();
    },
    // 只暴露 focus，不暴露其他原生 DOM 方法，更安全
  }));

  return &lt;input ref={inputRef} /&gt;;
});
</code></pre>
<h3 id="27--hooks-react-18">2.7 并发模式 Hooks (React 18+)</h3><p>React 18 引入了并发渲染，带来了提升用户体验的新 Hooks。</p><h4 id="usetransition">useTransition</h4><p>用于将某些状态更新标记为 <strong>&quot;Transition&quot; (非紧急更新)</strong>。</p><ul><li><strong>场景</strong>：输入框输入（紧急）导致的大列表过滤（非紧急）。</li><li><strong>效果</strong>：保持 UI 响应，不会因为列表渲染阻塞输入框打字。</li></ul><h4 id="usedeferredvalue">useDeferredValue</h4><p><code>useTransition</code> 是包装更新动作，<code>useDeferredValue</code> 是包装状态值本身。类似于防抖 (Debounce)，但更智能（由 React 调度决定延迟多久）。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">const deferredQuery = useDeferredValue(query);
// 列表只依赖 deferredQuery，当 query 快速变化时，deferredQuery 会滞后更新
// 从而减少列表重渲染次数
</code></pre>
<hr/><h2 id="-">三、 高级状态管理模式</h2><p>随着应用复杂度增加，<code>useState</code> 往往力不从心，而引入 Redux 又显得过重。</p><h3 id="31-context--usereducer-redux">3.1 Context + useReducer：轻量级 Redux</h3><p>这种模式适合管理中等复杂度的全局状态（如主题、用户信息、购物车）。</p><h4 id="">性能痛点与优化</h4><p>Context 有一个著名的性能问题：<strong>Provider 更新时，所有 Consumer 都会重渲染</strong>，即使它们只使用了 State 的一部分。</p><p><strong>优化策略：读写分离</strong></p><p>将 State (数据) 和 Dispatch (修改方法) 分离到两个不同的 Context 中。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">// 优化后的 Context 结构
export const UserStateContext = createContext&lt;State | null&gt;(null);
export const UserDispatchContext = createContext&lt;Dispatch&lt;Action&gt; | null&gt;(null);

export const UserProvider = ({ children }) =&gt; {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    &lt;UserDispatchContext.Provider value={dispatch}&gt;
      &lt;UserStateContext.Provider value={state}&gt;
        {children}
      &lt;/UserStateContext.Provider&gt;
    &lt;/UserDispatchContext.Provider&gt;
  );
};

// 收益：
// 只需要触发更新的组件（比如一个按钮）可以只 consume UserDispatchContext。
// 当 state 变化时，这个按钮组件 **不会** 重渲染，因为它不依赖 UserStateContext。
</code></pre>
<h3 id="32--state-colocation">3.2 状态下放 (State Colocation)</h3><p>在引入全局状态管理前，先问自己：<strong>这个状态真的需要全局吗？</strong>
将状态尽可能的下放到离使用它最近的父组件，甚至是组件内部，是提升 React 应用性能最简单有效的方法。</p><hr/><h2 id="-react--anti-patterns">四、 React 常见反模式 (Anti-Patterns)</h2><h3 id="41--useeffect-">4.1 在 useEffect 中计算衍生状态</h3><p><strong>❌ 错误示范</strong>：</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">const [firstName, setFirstName] = useState(&#x27;John&#x27;);
const [lastName, setLastName] = useState(&#x27;Doe&#x27;);
const [fullName, setFullName] = useState(&#x27;&#x27;);

// 多余的渲染 pass：render -&gt; useEffect -&gt; setState -&gt; re-render
useEffect(() =&gt; {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
</code></pre>
<p><strong>✅ 正确示范</strong>：</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">// 直接在渲染过程中计算。React 极快，不要担心这点计算量。
const fullName = `${firstName} ${lastName}`; 
</code></pre>
<h3 id="42-prop-drilling-">4.2 Prop Drilling (属性透传)</h3><p>当看到 <code>&lt;Child user={user} /&gt;</code> -&gt; <code>&lt;GrandChild user={user} /&gt;</code> -&gt; <code>&lt;GreatGrandChild user={user} /&gt;</code> 时，你需要重构。</p><p><strong>解法 1：Component Composition (组件组合)</strong>
将组件作为 <code>children</code> 或 <code>props</code> 传递，而不是传递数据。</p><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">// 改造前
&lt;Page user={user} /&gt; // Page 内部要把 user 一层层传下去

// 改造后
&lt;Page&gt;
  &lt;Avatar user={user} /&gt; {/* 直接在这里使用 user */}
&lt;/Page&gt;
</code></pre>
<p><strong>解法 2：Context</strong>
如果组件层级确实太深且无法组合，再考虑 Context，比如主题切换、用户信息等。</p><hr/><p>通过避免这些反模式，并结合前文提到的渲染策略与 Hooks 技巧，你已经掌握了构建高质量 React 应用的核心心法。</p></div><p style="text-align:right"><a href="https://xming.cyou/posts/frontend/react#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/frontend/react</link><guid isPermaLink="true">https://xming.cyou/posts/frontend/react</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Sun, 08 Feb 2026 09:38:24 GMT</pubDate></item><item><title><![CDATA[深入理解HTTP协议的演进]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/devops/http">https://xming.cyou/posts/devops/http</a></blockquote><div><h2 id="-http09--10">一、 HTTP/0.9 &amp; 1.0：协议的诞生与雏形</h2><h3 id="1-http09-1991">1. HTTP/0.9 (1991年)</h3><ul><li><strong>设计初衷</strong>：极简的数据传输协议。</li><li><strong>特征</strong>：
<ul><li><strong>单行协议</strong>：请求只有一行，例如 <code>GET /index.html</code>。</li><li><strong>无 Header</strong>：没有请求头和响应头。</li><li><strong>仅限 HTML</strong>：服务器只能返回 HTML 格式的字符串。</li><li><strong>连接模型</strong>：<strong>短连接</strong>。每次请求都需要建立 TCP 连接，响应结束后立即断开。</li></ul></li></ul><h3 id="2-http10-1996">2. HTTP/1.0 (1996年)</h3><p>随着互联网的发展，仅传输文字已无法满足需求。</p><ul><li><strong>核心改进</strong>：
<ul><li><strong>引入 Header</strong>：允许传输元数据（Metadata）。</li><li><strong>状态码 (Status Code)</strong>：如 200, 404, 500，明确告知请求结果。</li><li><strong>多媒体支持 (Content-Type)</strong>：可以传输图片、音频、视频等二进制文件。</li><li><strong>基础缓存</strong>：引入 <code>Expires</code> 和 <code>Last-Modified</code>。</li></ul></li><li><strong>致命痛点</strong>：
<ul><li><strong>连接无法复用</strong>：依然沿用<strong>短连接</strong>模型。加载一个包含 10 张图片的网页，需要建立 11 次 TCP 连接（1 次 HTML + 10 次图片）。</li><li><strong>性能损耗</strong>：TCP 三次握手和慢启动（Slow Start）机制导致严重的延迟。</li></ul></li></ul><hr/><h2 id="-http11-1997">二、 HTTP/1.1：标准确立与性能优化 (1997年)</h2><p>HTTP/1.1 是目前互联网上使用最广泛的协议版本，它主要解决了连接复用问题。</p><h3 id="1-">1. 核心特性</h3><ul><li><strong>持久连接 (Keep-Alive)</strong>：
<ul><li>引入 <code>Connection: keep-alive</code>（默认开启）。</li><li>允许在一个 TCP 连接上发送多个 HTTP 请求，大幅减少了 TCP 握手的开销。</li></ul></li><li><strong>管道化 (Pipelining)</strong>：
<ul><li>试图允许客户端同时发送多个请求，而无需等待响应。</li><li><strong>失败告终</strong>：由于服务器必须按顺序返回响应，且实现复杂，现代浏览器默认均未开启此功能。</li></ul></li><li><strong>高级缓存控制</strong>：
<ul><li>引入 <code>Cache-Control</code> (如 <code>max-age</code>, <code>no-cache</code>)，提供了比 HTTP/1.0 更精准的缓存策略。</li><li>引入 <code>ETag</code> (实体标签)，通过内容哈希校验资源是否变更，比时间戳更可靠。</li></ul></li><li><strong>Host 头</strong>：
<ul><li>强制要求请求包含 <code>Host</code> 头。这使得<strong>虚拟主机 (Virtual Hosting)</strong> 成为可能，即一台物理服务器可以托管多个域名。</li></ul></li><li><strong>丰富的方法 (Methods)</strong>：
<ul><li>规范了 <code>PUT</code>, <code>DELETE</code>, <code>OPTIONS</code>, <code>PATCH</code> 等方法，为后来的 RESTful API 奠定了基础。</li></ul></li><li><strong>断点续传</strong>：
<ul><li>引入 <code>Range</code> 头，允许请求资源的特定部分（如视频拖拽播放）。</li></ul></li></ul><h3 id="2-http--head-of-line-blocking">2. 遗留痛点：HTTP 队头阻塞 (Head-of-Line Blocking)</h3><p>虽然 Keep-Alive 复用了连接，但 HTTP/1.1 的请求处理依然是<strong>串行</strong>的。</p><ul><li><strong>现象</strong>：在一个 TCP 连接中，如果第一个请求处理很慢（例如数据库查询耗时 2秒），后续的请求（哪怕只是一个 1KB 的 CSS 文件）都必须排队等待。</li><li><strong>临时方案</strong>：浏览器为了缓解这个问题，通常会针对同一个域名同时建立 <strong>6 个 TCP 连接</strong>（并发限制）。但这大大增加了服务器的资源消耗。</li></ul><hr/><h2 id="-http2-2015">三、 HTTP/2：二进制分帧与多路复用 (2015年)</h2><p>HTTP/2 是对 HTTP 传输层的彻底重构，旨在应用层解决性能瓶颈。</p><h3 id="1--binary-framing">1. 二进制分帧 (Binary Framing)</h3><ul><li><strong>改变</strong>：HTTP/1.x 是文本协议（可读性好，但解析低效），HTTP/2 是二进制协议。</li><li><strong>机制</strong>：将传输信息分割为更小的<strong>帧 (Frame)</strong>，并进行二进制编码。</li></ul><h3 id="2--multiplexing--">2. 多路复用 (Multiplexing) —— 最核心变革</h3><ul><li><strong>机制</strong>：在单一 TCP 连接上，可以并行交错发送无数个请求和响应。</li><li><strong>优势</strong>：彻底解决了 <strong>HTTP 1.1 的队头阻塞</strong>问题。</li><li><strong>效果</strong>：浏览器不再需要建立 6 个连接，通常<strong>一个域名只需要一个 TCP 连接</strong>。</li></ul><h3 id="3--hpack">3. 头部压缩 (HPACK)</h3><ul><li><strong>背景</strong>：HTTP 请求头通常包含大量重复信息（如 Cookie, User-Agent），且每次请求都要重复发送，浪费带宽。</li><li><strong>机制</strong>：客户端和服务器共同维护静态字典和动态字典，使用索引号代替重复的 Header 字段，大幅减少传输体积。</li></ul><h3 id="4--server-push">4. 服务端推送 (Server Push)</h3><ul><li><strong>机制</strong>：服务器可以预测客户端需要的资源（如请求 index.html 时主动推送 style.css）。</li><li><strong>现状</strong>：由于缓存一致性难以处理，该特性在实践中并未广泛流行，Chrome 已计划移除，推荐使用 <code>103 Early Hints</code>。</li></ul><h3 id="5-tcp-">5. 新的痛点：TCP 队头阻塞</h3><p>HTTP/2 虽然解决了应用层的队头阻塞，但将所有数据都压在一个 TCP 连接上，导致了<strong>TCP 层面的队头阻塞</strong>。</p><ul><li><strong>现象</strong>：TCP 保证数据按序到达。一旦发生<strong>丢包</strong>，操作系统内核会暂停将后续数据包交付给应用层，直到丢失的包重传成功。</li><li><strong>结果</strong>：在弱网环境下（高丢包率），HTTP/2 的性能表现可能反而不如 HTTP/1.1（因为 HTTP/1.1 有多个连接，一个断了不影响其他）。</li></ul><hr/><h2 id="-http3-udp--2022-rfc-">四、 HTTP/3：基于 UDP 的革新 (2022年 RFC 标准化)</h2><p>为了解决 TCP 的固有缺陷，HTTP/3 抛弃了 TCP，改用基于 UDP 的 <strong>QUIC</strong> 协议。</p><h3 id="1-quic--quick-udp-internet-connections">1. QUIC 协议 (Quick UDP Internet Connections)</h3><p>HTTP/3 将传输层从 TCP 换成了 UDP，并在应用层（用户态）实现了可靠传输机制。</p><h3 id="2--independent-streams">2. 真正的独立流 (Independent Streams)</h3><ul><li><strong>机制</strong>：每个流（Stream）在逻辑上是独立的。</li><li><strong>优势</strong>：<strong>彻底解决 TCP 队头阻塞</strong>。如果 Stream A 的数据包丢失，只会阻塞 Stream A，Stream B 和 C 依然可以正常传输。</li></ul><h3 id="3-0-rtt-">3. 0-RTT 极速握手</h3><ul><li><strong>机制</strong>：QUIC 将传输握手和 TLS 加密握手合并。</li><li><strong>效果</strong>：
<ul><li>首次连接：1 RTT。</li><li>再次连接：0 RTT（直接发送加密数据）。</li></ul></li></ul><h3 id="4--connection-migration">4. 连接迁移 (Connection Migration)</h3><ul><li><strong>背景</strong>：TCP 连接由四元组（源IP、源端口、目标IP、目标端口）标识。切换网络（如 Wi-Fi 切 4G）会导致 IP 变化，连接断开。</li><li><strong>机制</strong>：QUIC 使用 <strong>Connection ID (CID)</strong> 标识连接。</li><li><strong>优势</strong>：只要 CID 不变，即使用户 IP 发生变化，连接依然保持，无需重连。这对移动端应用极其重要。</li></ul><hr/><h2 id="-go--gin--echo">五、 实战指南：Go 语言实现 (Gin / Echo)</h2><p>在 Go 语言中，启用 HTTP/2 非常简单，因为标准库 <code>net/http</code> 原生支持。启用 HTTP/3 则需要引入 <code>quic-go</code>。</p><h3 id="1-gin--http2">1. Gin 框架 (HTTP/2)</h3><p>只要开启 TLS (HTTPS)，Go 会自动协商升级到 HTTP/2。</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;log&quot;
    &quot;github.com/gin-gonic/gin&quot;
)

func main() {
    r := gin.Default()

    r.GET(&quot;/ping&quot;, func(c *gin.Context) {
        // c.Request.Proto 将显示 &quot;HTTP/2.0&quot;
        c.String(200, &quot;Protocol: &quot;+c.Request.Proto)
    })

    // HTTP/2 强制要求 HTTPS
    // 使用 mkcert localhost 生成本地测试证书
    log.Println(&quot;Gin running on https://localhost:8443&quot;)
    if err := r.RunTLS(&quot;:8443&quot;, &quot;server.crt&quot;, &quot;server.key&quot;); err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<h3 id="2-echo--http2">2. Echo 框架 (HTTP/2)</h3><p>同理，使用 <code>StartTLS</code> 即可。</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;net/http&quot;
    &quot;github.com/labstack/echo/v4&quot;
)

func main() {
    e := echo.New()
    e.GET(&quot;/&quot;, func(c echo.Context) error {
        return c.String(http.StatusOK, &quot;Protocol: &quot;+c.Request().Proto)
    })
    e.Logger.Fatal(e.StartTLS(&quot;:8443&quot;, &quot;server.crt&quot;, &quot;server.key&quot;))
}
</code></pre>
<h3 id="3--http3-quic">3. 启用 HTTP/3 (QUIC)</h3><p>需要使用 <code>github.com/quic-go/quic-go/http3</code> 包。</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;github.com/gin-gonic/gin&quot;
    &quot;github.com/quic-go/quic-go/http3&quot;
)

func main() {
    r := gin.Default()
    r.GET(&quot;/&quot;, func(c *gin.Context) {
        // 关键：添加 Alt-Svc 头，告知浏览器该服务支持 HTTP/3
        c.Header(&quot;Alt-Svc&quot;, `h3=&quot;:443&quot;; ma=2592000`)
        c.String(200, &quot;Hello via &quot;+c.Request.Proto)
    })

    // 启动 HTTP/3 服务器 (监听 UDP)
    go func() {
        server := http3.Server{
            Addr:    &quot;:443&quot;,
            Handler: r,
        }
        log.Println(&quot;HTTP/3 server listening on :443&quot;)
        err := server.ListenAndServeTLS(&quot;server.crt&quot;, &quot;server.key&quot;)
        if err != nil {
            log.Fatal(err)
        }
    }()

    // 同时启动 HTTP/1.1 &amp; HTTP/2 (监听 TCP) 作为兼容降级
    log.Println(&quot;HTTP/1.1 &amp; HTTP/2 server listening on :443&quot;)
    err := http.ListenAndServeTLS(&quot;:443&quot;, &quot;server.crt&quot;, &quot;server.key&quot;, r)
    if err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<hr/><h2 id="-">六、 协议选型建议</h2><table><thead><tr><th style="text-align:left"> 场景 </th><th style="text-align:left"> 推荐协议 </th><th style="text-align:left"> 技术理由 </th></tr></thead><tbody><tr><td style="text-align:left"> <strong>微服务内部调用 (RPC)</strong> </td><td style="text-align:left"> <strong>gRPC (HTTP/2)</strong> </td><td style="text-align:left"> 追求极致性能、多语言支持、二进制传输。gRPC 基于 HTTP/2 设计。 </td></tr><tr><td style="text-align:left"> <strong>简单的内部 API / 调试</strong> </td><td style="text-align:left"> <strong>HTTP/1.1</strong> </td><td style="text-align:left"> 文本协议易于调试（curl/postman），无 TLS 证书负担，部署简单。 </td></tr><tr><td style="text-align:left"> <strong>面向公网的 Web 服务</strong> </td><td style="text-align:left"> <strong>HTTP/2 (必须)</strong> </td><td style="text-align:left"> 当前行业标准。多路复用显著提升加载速度，且浏览器强制要求 HTTPS。 </td></tr><tr><td style="text-align:left"> <strong>移动端 App / 弱网环境</strong> </td><td style="text-align:left"> <strong>HTTP/3</strong> </td><td style="text-align:left"> 0-RTT 建连和抗丢包能力，能显著改善移动网络（4G/5G/Wi-Fi 切换）下的用户体验。 </td></tr><tr><td style="text-align:left"> <strong>实时互动 (直播/游戏)</strong> </td><td style="text-align:left"> <strong>HTTP/3 或 WebSocket</strong> </td><td style="text-align:left"> 利用 UDP 的低延迟特性。 </td></tr></tbody></table><p><strong>总结</strong>：对于绝大多数公网 Web 应用，<strong>HTTP/2 + HTTPS</strong> 是标准配置。如果你专注于移动端体验优化，应该开始尝试 <strong>HTTP/3</strong>。</p></div><p style="text-align:right"><a href="https://xming.cyou/posts/devops/http#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/devops/http</link><guid isPermaLink="true">https://xming.cyou/posts/devops/http</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Sat, 07 Feb 2026 09:02:28 GMT</pubDate></item><item><title><![CDATA[WebSocket：从理解概念、应用场景到实战]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/backend/websocket">https://xming.cyou/posts/backend/websocket</a></blockquote><div><h2 id="1--websocket">1. 为什么我们需要 WebSocket？</h2><h3 id="11-http-">1.1 HTTP 的局限性</h3><p>在 WebSocket 出现之前，为了实现“实时”效果，开发者们通常采用以下几种技术：</p><ul><li><strong>轮询 (Polling)</strong>：客户端每隔一段时间（如 1 秒）向服务器发送 HTTP 请求询问是否有新数据。
<ul><li><em>缺点</em>：浪费带宽和服务器资源，大部分请求可能是空的。</li></ul></li><li><strong>长轮询 (Long Polling)</strong>：客户端发起请求，服务端挂起请求直到有数据才返回。
<ul><li><em>缺点</em>：虽然减少了无效请求，但依然存在 HTTP 头部开销大、连接频繁建立释放的问题。此外，服务端需要长时间维持大量挂起的连接，在高并发场景下会严重消耗服务器资源（如线程、文件描述符），处理不当极易引发资源泄露。</li></ul></li></ul><p>无论采用哪种轮询方式，核心痛点在于 HTTP 协议的<strong>被动性</strong>。请求必须由客户端发起，服务端无法主动推送。这种同步的“请求-响应”机制导致客户端为了获取最新状态，不得不进行频繁的无效询问或长时间的等待，这在实时性要求高的场景下显得极不合理。
HTTP 协议本质上是<strong>半双工</strong>、<strong>无状态</strong>、<strong>请求-响应</strong>模式的。服务器无法主动向客户端推送数据。</p><h3 id="12-websocket-">1.2 WebSocket 的核心优势</h3><p>WebSocket 是一种在单个 TCP 连接上进行<strong>全双工 (Full Duplex)</strong> 通信的协议。</p><ul><li><strong>全双工</strong>：客户端和服务器都可以随时向对方发送数据，不再受限于“请求-响应”模式。</li><li><strong>低开销</strong>：建立连接后，数据传输时不再需要携带臃肿的 HTTP 头部，只需要极少的帧头（Frame Header）。</li><li><strong>低延迟</strong>：无需频繁建立连接，数据传输实时性更高。</li></ul><h2 id="2-websocket-">2. WebSocket 协议深度解析</h2><p>WebSocket 协议 (RFC 6455) 建立在 TCP 之上，它复用了 HTTP 的握手通道，但随后升级为独立的 WebSocket 协议。</p><h3 id="21--handshake">2.1 握手流程 (Handshake)</h3><p>连接的建立始于一个标准的 HTTP GET 请求，但带有一些特殊的 Header：</p><p><strong>客户端请求：</strong></p><pre class="language-http lang-http"><code class="language-http lang-http">GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
</code></pre><ul><li><code>Connection: Upgrade</code> 和 <code>Upgrade: websocket</code>：告诉服务器我想升级协议。</li><li><code>Sec-WebSocket-Key</code>：一个随机的 Base64 字符串，用于验证服务器是否支持 WebSocket。</li></ul><p><strong>服务端响应：</strong></p><pre class="language-http lang-http"><code class="language-http lang-http">HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
</code></pre><ul><li><code>101 Switching Protocols</code>：表示服务器同意升级。</li><li><code>Sec-WebSocket-Accept</code>：服务器通过 <code>Sec-WebSocket-Key</code> 拼接固定的 GUID (<code>258EAFA5-E914-47DA-95CA-C5AB0DC85B11</code>) 后进行 SHA-1 摘要并 Base64 编码得出。客户端校验此值以确认连接成功。</li></ul><h3 id="22--data-frame">2.2 数据帧 (Data Frame)</h3><p>握手完成后，双方通过数据帧传输数据。WebSocket 的帧结构设计得非常紧凑：</p><pre class="language-text lang-text"><code class="language-text lang-text">      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+
</code></pre>
<ul><li><strong>FIN (1 bit)</strong>：<strong>结束标志位 (Finish)</strong>。
<ul><li>如果为 <code>1</code>，表示这是消息的最后一个分片（Fragment）。</li><li>如果为 <code>0</code>，表示还有后续分片。这允许发送端在不知道完整消息长度的情况下就开始传输（流式传输）。</li></ul></li><li><strong>RSV1, RSV2, RSV3 (1 bit each)</strong>：<strong>保留位 (Reserved)</strong>。
<ul><li>必须设置为 <code>0</code>，除非协商了扩展（Extension）定义了它们的含义。</li><li>例如：在使用 <code>permessage-deflate</code> 扩展进行压缩时，RSV1 可能会被置为 <code>1</code>。</li></ul></li><li><strong>Opcode (4 bits)</strong>：<strong>操作码</strong>，定义有效负载的数据类型。
<ul><li><code>0x0</code>: <strong>延续帧 (Continuation Frame)</strong>。用于分片传输，表示该帧内容应追加到上一帧之后。</li><li><code>0x1</code>: <strong>文本帧 (Text Frame)</strong>。UTF-8 编码的文本。</li><li><code>0x2</code>: <strong>二进制帧 (Binary Frame)</strong>。任意二进制数据。</li><li><code>0x3 - 0x7</code>: 保留用于未来的非控制帧。</li><li><code>0x8</code>: <strong>连接关闭 (Connection Close)</strong>。</li><li><code>0x9</code>: <strong>Ping</strong>。心跳请求。</li><li><code>0xA</code>: <strong>Pong</strong>。心跳响应。</li><li><code>0xB - 0xF</code>: 保留用于未来的控制帧。</li></ul></li><li><strong>Mask (1 bit)</strong>：<strong>掩码标志位</strong>。
<ul><li>定义“有效负载数据”是否添加了掩码。</li><li><strong>规则</strong>：客户端发送给服务端的帧<strong>必须</strong>设置为 <code>1</code>；服务端发送给客户端的帧<strong>必须</strong>设置为 <code>0</code>。</li><li><strong>目的</strong>：防止代理服务器缓存中毒攻击（Proxy Cache Poisoning）。</li></ul></li><li><strong>Payload Length (7 bits)</strong>：<strong>有效负载长度</strong>。
<ul><li><code>0-125</code>：实际数据长度。</li><li><code>126</code>：实际长度由随后的 16 位（Extended payload length）表示。</li><li><code>127</code>：实际长度由随后的 64 位（Extended payload length）表示。</li></ul></li><li><strong>Masking-Key (0 or 4 bytes)</strong>：<strong>掩码密钥</strong>。
<ul><li>仅当 Mask 位为 <code>1</code> 时存在。</li><li>这是一个 32 位的随机数，用于对负载数据进行异或（XOR）解密。</li></ul></li><li><strong>Payload Data</strong>：<strong>应用数据</strong>。
<ul><li>实际传输的业务数据（如果是文本帧，则是 UTF-8 文本；如果是控制帧，可能包含状态码等）。</li></ul></li></ul><h2 id="3-typescript">3. 客户端实战：TypeScript</h2><p>我们将使用原生 <code>WebSocket</code> API 并结合 TypeScript 封装一个具有自动重连、心跳检测功能的客户端类。</p><h3 id="31-">3.1 核心代码实现</h3><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// ws-client.ts

type MessageHandler = (data: any) =&gt; void;

interface WSOptions {
    url: string;
    reconnectInterval?: number; // 重连间隔
    heartbeatInterval?: number; // 心跳间隔
}

export class WSClient {
    private ws: WebSocket | null = null;
    private options: WSOptions;
    private shouldReconnect = true;
    private messageHandlers: Set&lt;MessageHandler&gt; = new Set();
    private heartbeatTimer: number | null = null;

    constructor(options: WSOptions) {
        this.options = {
            reconnectInterval: 3000,
            heartbeatInterval: 30000,
            ...options,
        };
    }

    // 初始化连接
    public connect() {
        try {
            this.ws = new WebSocket(this.options.url);
            this.bindEvents();
        } catch (e) {
            console.error(&#x27;WebSocket connection failed:&#x27;, e);
            this.reconnect();
        }
    }

    private bindEvents() {
        if (!this.ws) return;

        this.ws.onopen = () =&gt; {
            console.log(&#x27;WebSocket Connected&#x27;);
            this.startHeartbeat();
        };

        this.ws.onmessage = (event) =&gt; {
            try {
                // 假设服务端也返回 JSON 格式
                const data = JSON.parse(event.data);
                
                // 处理心跳响应（Pong）
                if (data.type === &#x27;pong&#x27;) {
                    console.log(&#x27;Received Pong&#x27;);
                    return;
                }

                this.messageHandlers.forEach(handler =&gt; handler(data));
            } catch (e) {
                console.warn(&#x27;Non-JSON message received:&#x27;, event.data);
            }
        };

        this.ws.onclose = (event) =&gt; {
            console.log(&#x27;WebSocket Closed:&#x27;, event.code, event.reason);
            this.stopHeartbeat();
            this.reconnect();
        };

        this.ws.onerror = (error) =&gt; {
            console.error(&#x27;WebSocket Error:&#x27;, error);
            // onerror 通常紧接着 onclose，重连逻辑在 onclose 处理
        };
    }

    // 发送消息
    public send(data: any) {
        if (this.ws &amp;&amp; this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
        } else {
            console.error(&#x27;WebSocket is not connected&#x27;);
        }
    }

    // 注册消息监听
    public onMessage(handler: MessageHandler) {
        this.messageHandlers.add(handler);
        return () =&gt; this.messageHandlers.delete(handler); // 返回取消订阅函数
    }

    // 自动重连机制
    private reconnect() {
        if (!this.shouldReconnect) return;
        
        console.log(`Attempting to reconnect in ${this.options.reconnectInterval}ms...`);
        setTimeout(() =&gt; {
            this.connect();
        }, this.options.reconnectInterval);
    }

    // 心跳检测
    private startHeartbeat() {
        this.stopHeartbeat();
        this.heartbeatTimer = window.setInterval(() =&gt; {
            this.send({ type: &#x27;ping&#x27; });
        }, this.options.heartbeatInterval);
    }

    private stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
    }

    // 手动关闭
    public close() {
        this.shouldReconnect = false;
        this.stopHeartbeat();
        this.ws?.close();
    }
}
</code></pre>
<h2 id="4-">4. 服务端实战</h2><p>我们将分别展示 Go 和 Node.js (TypeScript) 的服务端实现，两者都将实现基本的消息回显（Echo）和广播功能。</p><h3 id="41-go-">4.1 Go 语言实现</h3><p>Go 语言处理 WebSocket 非常高效。我们将使用官方推荐的第三方库 <code>github.com/gorilla/websocket</code>。</p><pre class="language-go lang-go"><code class="language-go lang-go">// main.go
package main

import (
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;sync&quot;

    &quot;github.com/gorilla/websocket&quot;
)

// 升级器配置
var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true; // 允许所有跨域请求，生产环境需严格配置
    },
}

// 简单的客户端管理器
type ClientManager struct {
    clients    map[*websocket.Conn]bool
    broadcast  chan []byte
    register   chan *websocket.Conn
    unregister chan *websocket.Conn
    mutex      sync.Mutex
}

func (manager *ClientManager) start() {
    for {
        select {
        case conn := &lt;-manager.register:
            manager.mutex.Lock()
            manager.clients[conn] = true
            manager.mutex.Unlock()
            log.Println(&quot;New client connected&quot;)
        case conn := &lt;-manager.unregister:
            manager.mutex.Lock()
            if _, ok := manager.clients[conn]; ok {
                delete(manager.clients, conn)
                conn.Close()
                log.Println(&quot;Client disconnected&quot;)
            }
            manager.mutex.Unlock()
        case message := &lt;-manager.broadcast:
            manager.mutex.Lock()
            for conn := range manager.clients {
                err := conn.WriteMessage(websocket.TextMessage, message)
                if err != nil {
                    log.Printf(&quot;Write error: %v&quot;, err)
                    conn.Close()
                    delete(manager.clients, conn)
                }
            }
            manager.mutex.Unlock()
        }
    }
}

var manager = ClientManager{
    broadcast:  make(chan []byte),
    register:   make(chan *websocket.Conn),
    unregister: make(chan *websocket.Conn),
    clients:    make(map[*websocket.Conn]bool),
}

func handleConnections(w http.ResponseWriter, r *http.Request) {
    // 1. 升级 HTTP 为 WebSocket
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatal(err)
    }
    
    // 2. 注册客户端
    manager.register &lt;- ws

    // 3. 循环读取消息
    defer func() {
        manager.unregister &lt;- ws
    }()

    for {
        _, msg, err := ws.ReadMessage()
        if err != nil {
            break
        }
        log.Printf(&quot;Received: %s&quot;, msg)
        
        // 简单处理：将收到的消息广播给所有人
        manager.broadcast &lt;- msg
    }
}

func main() {
    go manager.start()
    http.HandleFunc(&quot;/ws&quot;, handleConnections)
    log.Println(&quot;Server started on :8080&quot;)
    err := http.ListenAndServe(&quot;:8080&quot;, nil)
    if err != nil {
        log.Fatal(&quot;ListenAndServe: &quot;, err)
    }
}
</code></pre>
<h3 id="42-typescript-nodejs-">4.2 TypeScript (Node.js) 实现</h3><p>Node.js 中最常用的库是 <code>ws</code>。</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// server.ts
import { WebSocketServer, WebSocket } from &#x27;ws&#x27;;
import http from &#x27;http&#x27;;

const server = http.createServer();
const wss = new WebSocketServer({ server });

wss.on(&#x27;connection&#x27;, (ws: WebSocket) =&gt; {
    console.log(&#x27;New client connected&#x27;);

    ws.on(&#x27;message&#x27;, (message: Buffer) =&gt; {
        const msgStr = message.toString();
        console.log(&#x27;Received:&#x27;, msgStr);

        // 处理 Ping/Pong（如果客户端发 JSON 格式的心跳）
        try {
            const data = JSON.parse(msgStr);
            if (data.type === &#x27;ping&#x27;) {
                ws.send(JSON.stringify({ type: &#x27;pong&#x27; }));
                return;
            }
        } catch (e) {
            // 非 JSON 消息，继续处理
        }

        // 广播消息给所有客户端
        wss.clients.forEach((client) =&gt; {
            if (client.readyState === WebSocket.OPEN) {
                client.send(msgStr); // 原样转发
            }
        });
    });

    ws.on(&#x27;close&#x27;, () =&gt; {
        console.log(&#x27;Client disconnected&#x27;);
    });

    ws.on(&#x27;error&#x27;, (err) =&gt; {
        console.error(&#x27;Socket error:&#x27;, err);
    });
});

server.listen(8081, () =&gt; {
    console.log(&#x27;Node.js WebSocket Server started on :8081&#x27;);
});
</code></pre>
<h2 id="5-">5. 进阶实践：打造健壮的连接</h2><h3 id="51--heartbeat">5.1 心跳检测 (Heartbeat)</h3><p>网络连接可能因为各种原因（NAT 超时、网络波动）中断，但双方并未感知到（TCP 半开连接）。</p><ul><li><strong>机制</strong>：客户端定时发送 <code>Ping</code> 消息，服务端回复 <code>Pong</code>。</li><li><strong>策略</strong>：如果客户端在一定时间内未收到 <code>Pong</code>，或服务端在一定时间内未收到任何消息，则主动断开连接并触发重连。</li></ul><h3 id="52-">5.2 断线重连</h3><p>在客户端实现指数退避 (Exponential Backoff) 算法，避免网络恢复瞬间大量客户端同时重连冲击服务器。</p><ul><li>例如：第 1 次失败等待 1s，第 2 次 2s，第 3 次 4s... 直到达到最大上限。</li></ul><h3 id="53-">5.3 弱网优化策略</h3><p>在移动端或网络不稳定的环境下，仅仅依靠重连和心跳是不够的。我们需要更主动的策略来提升用户体验：</p><ol start="1"><li><p><strong>消息队列与 ACK 机制</strong></p><ul><li><strong>问题</strong>：断线期间产生的消息会丢失；重连后消息顺序可能错乱。</li><li><strong>方案</strong>：客户端维护一个发送队列。每条消息带上唯一 ID (<code>msg_id</code>)。发送后不立即删除，只有收到服务端回复的 <code>ACK</code> 确认包后才从队列移除。重连成功后，自动重发队列中未确认的消息。</li></ul></li><li><p><strong>网络状态监听</strong></p><ul><li>利用 HTML5 的 <code>navigator.onLine</code> 属性和 <code>window.addEventListener(&#x27;online&#x27;)</code> / <code>offline</code> 事件。</li><li>一旦监听到 <code>offline</code>，立即暂停心跳和消息发送，将 UI 切换为“连接中...”状态；监听到 <code>online</code>，立即触发一次重连尝试。</li></ul></li><li><p><strong>数据压缩</strong></p><ul><li>对于数据量较大的应用，开启 WebSocket 的协议扩展 <code>permessage-deflate</code>。</li><li>虽然会增加 CPU 开销，但能显著减少传输数据量，在弱网下能有效提高传输成功率。</li></ul></li><li><p><strong>智能降级</strong></p><ul><li>如果 WebSocket 连接连续失败多次（如公司防火墙屏蔽），应自动降级为 <strong>HTTP 长轮询 (Long Polling)</strong> 模式，确保核心业务可用。</li></ul></li></ol><h2 id="6-websocket-">6. WebSocket 鉴权实践</h2><p>WebSocket 的鉴权是一个常见的痛点，因为浏览器原生的 WebSocket API <strong>不支持设置自定义 HTTP Header</strong>（例如 <code>Authorization: Bearer &lt;token&gt;</code>）。</p><h3 id="61-query--token">6.1 主流方案：Query 参数携带 Token</h3><p>这是目前最通用、兼容性最好的方案。</p><ul><li><strong>原理</strong>：在建立连接的 URL 中携带 Token。</li><li><strong>流程</strong>：
<ol start="1"><li>客户端登录，获取 Token。</li><li>发起连接：<code>new WebSocket(&#x27;wss://api.example.com/ws?token=eyJhbGciOi...&#x27;)</code></li><li>服务端在 <code>Upgrade</code> 握手阶段，解析 URL Query 参数，验证 Token。</li><li>验证通过 -&gt; 允许升级 (HTTP 101)；验证失败 -&gt; 拒绝连接 (HTTP 401)。</li></ol></li></ul><p><strong>优点</strong>：</p><ul><li>实现简单，兼容所有客户端。</li><li>在连接建立之初就拦截非法请求，节省服务器资源。</li></ul><p><strong>缺点</strong>：</p><ul><li>Token 可能会暴露在服务器日志或浏览器历史记录中（建议使用一次性 Ticket 或短期 Token 缓解）。</li></ul><h4 id="-nodejs-">代码实现 (Node.js 示例)</h4><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 基于 ws 库的鉴权
import { WebSocketServer } from &#x27;ws&#x27;;
import http from &#x27;http&#x27;;
import { URL } from &#x27;url&#x27;;

const server = http.createServer();
const wss = new WebSocketServer({ noServer: true }); // 开启 noServer 模式，手动处理升级

// 模拟 Token 验证函数
function verifyToken(token: string): boolean {
    return token === &#x27;valid-secret-token&#x27;;
}

server.on(&#x27;upgrade&#x27;, (request, socket, head) =&gt; {
    const url = new URL(request.url || &#x27;&#x27;, `http://${request.headers.host}`);
    const token = url.searchParams.get(&#x27;token&#x27;);

    if (!token || !verifyToken(token)) {
        socket.write(&#x27;HTTP/1.1 401 Unauthorized\r\n\r\n&#x27;);
        socket.destroy();
        return;
    }

    // 鉴权通过，完成升级
    wss.handleUpgrade(request, socket, head, (ws) =&gt; {
        wss.emit(&#x27;connection&#x27;, ws, request);
    });
});

wss.on(&#x27;connection&#x27;, (ws) =&gt; {
    console.log(&#x27;Authenticated client connected!&#x27;);
    ws.send(&#x27;Welcome, authorized user!&#x27;);
});

server.listen(8082);
</code></pre>
<h3 id="62-">6.2 其他鉴权方案简述</h3><ol start="1"><li><p><strong>连接后第一时间认证</strong></p><ul><li><strong>原理</strong>：先建立普通的 WebSocket 连接，连接成功后的第一条消息必须是包含 Token 的认证消息。</li><li><strong>优点</strong>：灵活，Token 不会暴露在 URL 中。</li><li><strong>缺点</strong>：服务端需要维护“未认证”状态的连接，如果攻击者建立大量连接但不发送认证包，会消耗服务器资源（需配合超时断开机制）。</li></ul></li><li><p><strong>Cookie 鉴权</strong></p><ul><li><strong>原理</strong>：WebSocket 握手请求会自动携带同域下的 Cookie。</li><li><strong>优点</strong>：利用浏览器原生机制，无需额外代码。</li><li><strong>缺点</strong>：无法跨域（或跨域配置复杂），不适用于非浏览器客户端（如移动端 App 需手动管理 Cookie），易受 CSRF 攻击。</li></ul></li><li><p><strong>Ticket 机制 (推荐用于高安全场景)</strong></p><ul><li><strong>原理</strong>：
<ol start="1"><li>客户端先向 HTTP 接口 POST Token，服务端校验通过后生成一个临时的、一次性的短 Ticket。</li><li>客户端使用 Ticket 建立 WebSocket 连接：<code>ws://...?ticket=xxx</code>。</li><li>服务端验证 Ticket 有效性并立即销毁。</li></ol></li><li><strong>优点</strong>：Token 不泄露，URL 中的 Ticket 即使泄露也已失效。</li></ul></li></ol><h2 id="7-">7. 生产环境部署指南</h2><p>在本地开发完成后，将 WebSocket 服务部署到生产环境时，有两个关键点必须注意：<strong>反向代理配置</strong>和<strong>SSL/TLS 加密</strong>。</p><h3 id="71-nginx-">7.1 Nginx 反向代理配置</h3><p>大多数 Web 服务都会使用 Nginx 作为网关。默认情况下，Nginx 不会转发 <code>Upgrade</code> 和 <code>Connection</code> 头部，导致 WebSocket 握手失败（通常报 400 或 426 错误）。</p><p>你需要显式添加以下配置：</p><pre class="language-nginx lang-nginx"><code class="language-nginx lang-nginx"># nginx.conf
location /ws {
    proxy_pass http://backend_server_pool;
    proxy_http_version 1.1; # WebSocket 必须使用 HTTP/1.1
    
    # 核心配置：转发 Upgrade 和 Connection 头
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &quot;upgrade&quot;;
    
    # 其他常用配置
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_read_timeout 600s; # 延长超时时间，避免 Nginx 主动断开长连接
}
</code></pre>
<h3 id="72--wss-ssltls">7.2 必须使用 WSS (SSL/TLS)</h3><p>在生产环境中，<strong>强烈建议（甚至强制）使用 <code>wss://</code> 协议</strong>，而不是 <code>ws://</code>。</p><ol start="1"><li><strong>安全性</strong>：防止数据被中间人窃听或篡改（WebSocket 握手包含敏感的 Auth Token）。</li><li><strong>穿透性</strong>：这是更实际的原因。许多公司的防火墙或代理服务器不认识 WebSocket 协议，可能会直接阻断非加密的 <code>ws://</code> 连接。而 <code>wss://</code> 基于 TLS 加密，在中间设备看来就是普通的 HTTPS 流量，能有效规避拦截，提高连接成功率。</li></ol><h2 id="8--websocket-">8. 各语言主流 WebSocket 库推荐</h2><p>在真实的企业级开发中，我们通常不会从零手写 WebSocket 协议解析，而是站在巨人的肩膀上。以下是各主流语言经过大规模生产环境验证的“杀手级”库：</p><h3 id="81-typescript--nodejs">8.1 TypeScript / Node.js</h3><ul><li><strong><a href="https://github.com/websockets/ws">ws</a></strong>
<ul><li><em>特点</em>：极度轻量、快速，最接近原生协议。</li><li><em>场景</em>：追求极致性能，或者你需要自己封装上层逻辑（如心跳、鉴权）。</li></ul></li><li><strong><a href="https://socket.io/">Socket.IO</a></strong>
<ul><li><em>特点</em>：<strong>不仅仅是 WebSocket</strong>。它自带了一套协议，支持自动降级（WebSocket 不可用时切回长轮询）、自动重连、房间（Rooms）、广播（Broadcast）。</li><li><em>场景</em>：快速构建复杂的实时应用（聊天室、游戏），需要兼容老旧浏览器或复杂网络环境。<strong>注意</strong>：客户端也必须使用 Socket.IO Client，不能用原生 WebSocket 连接。</li></ul></li></ul><h3 id="82-go">8.2 Go</h3><ul><li><strong><a href="https://github.com/gorilla/websocket">gorilla/websocket</a></strong>
<ul><li><em>特点</em>：Go 社区的事实标准，文档详尽，API 稳定。</li><li><em>场景</em>：绝大多数标准业务场景。</li></ul></li><li><strong><a href="https://github.com/gobwas/ws">gobwas/ws</a></strong>
<ul><li><em>特点</em>：<strong>零内存分配</strong>（Zero-copy upgrade），追求极致性能。</li><li><em>场景</em>：你需要编写能够处理百万级连接的高性能网关（Gateway）。</li></ul></li></ul><h3 id="83-java">8.3 Java</h3><ul><li><strong><a href="https://docs.spring.io/spring-framework/reference/web/webflux-websocket.html">Spring Boot WebSocket</a></strong>
<ul><li><em>特点</em>：企业级开发首选，与 Spring 生态（Security, MVC）无缝集成。支持 STOMP 协议（一种消息子协议）。</li><li><em>场景</em>：传统的企业级后端服务，CRUD 业务系统。</li></ul></li><li><strong><a href="https://netty.io/">Netty</a></strong>
<ul><li><em>特点</em>：异步事件驱动的网络框架，性能怪兽。</li><li><em>场景</em>：高性能游戏服务端、即时通讯（IM）底层设施。</li></ul></li></ul><h3 id="84-python">8.4 Python</h3><ul><li><strong><a href="https://github.com/python-websockets/websockets">websockets</a></strong>
<ul><li><em>特点</em>：基于 Python <code>asyncio</code> 构建，现代、优雅、性能好。</li><li><em>场景</em>：基于 FastAPI 或 Aiohttp 的异步微服务。</li></ul></li><li><strong><a href="https://github.com/django/channels">Django Channels</a></strong>
<ul><li><em>特点</em>：将 WebSocket 引入 Django，保持了 Django 的开发体验。</li><li><em>场景</em>：基于 Django 的全栈 Web 应用。</li></ul></li></ul><h2 id="9-">9. 总结</h2><p>WebSocket 是构建实时应用的首选技术。通过本文，我们了解了：</p><ol start="1"><li>它解决了 HTTP 轮询的低效问题。</li><li>客户端需要处理好<strong>心跳保活</strong>和<strong>断线重连</strong>。</li><li>服务端（Go/Node.js）的核心是管理连接池和广播消息。</li><li>鉴权推荐使用 <strong>Query 参数携带 Token</strong> 或 <strong>Ticket 机制</strong>。</li><li>上线时务必配置好 <strong>Nginx</strong> 并开启 <strong>WSS</strong>。</li><li>善用成熟的<strong>开源库</strong>（如 ws, gorilla, netty 等）来避免重复造轮子。</li></ol><h2 id="10-">10. 参考文献</h2><ol start="1"><li><a href="https://tools.ietf.org/html/rfc6455">RFC 6455 - The WebSocket Protocol</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket_API">MDN Web Docs - WebSocket API</a></li><li><a href="https://juejin.cn/post/7086021621542027271">WebSocket｜概念、原理、用法及实践</a></li></ol></div><p style="text-align:right"><a href="https://xming.cyou/posts/backend/websocket#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/backend/websocket</link><guid isPermaLink="true">https://xming.cyou/posts/backend/websocket</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Sat, 07 Feb 2026 07:51:52 GMT</pubDate></item><item><title><![CDATA[Go Map底层实现——Swiss Table]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/backend/swiss-table">https://xming.cyou/posts/backend/swiss-table</a></blockquote><div><h2 id="1-go-123-">1. 回顾过去：Go 1.23 以前的拉链法</h2><h3 id="11-">1.1 核心结构</h3><ul><li><strong><code>hmap</code></strong>：Map 的头节点，保存了 <code>count</code>、<code>B</code>、<code>buckets</code> 指针等。</li><li><strong><code>bmap</code> (Bucket)</strong>：每个桶固定存储 8 个键值对。为了内存对齐，它将 8 个 <code>tophash</code> 放在一起，接着是 8 个 <code>key</code>，最后是 8 个 <code>value</code>。</li><li><strong><code>overflow</code></strong>：当桶满时，通过 <code>overflow</code> 指针挂载一个新的 <code>bmap</code>。</li></ul><h3 id="12-">1.2 冲突与扩容</h3><ul><li><strong>哈希冲突</strong>：使用<strong>拉链法</strong>。冲突的 Key 会被放入溢出桶中。</li><li><strong>扩容因子</strong>：固定为 6.5。</li><li><strong>扩容方式</strong>：当负载因子超过 6.5 或溢出桶过多时，触发双倍扩容或等量扩容，采用<strong>渐进式搬迁</strong>。</li></ul><hr/><h2 id="2--vs-">2. 硬核对比：拉链法 vs 开放寻址法</h2><table><thead><tr><th style="text-align:left"> 维度 </th><th style="text-align:left"> 拉链法 (1.24之前) </th><th style="text-align:left"> 开放寻址法 (Swiss Table, 1.24+) </th></tr></thead><tbody><tr><td style="text-align:left"> <strong>内存布局</strong> </td><td style="text-align:left"> 离散（溢出桶通过指针连接） </td><td style="text-align:left"> 紧凑（数据在 Group 内连续存储） </td></tr><tr><td style="text-align:left"> <strong>CPU 缓存</strong> </td><td style="text-align:left"> 频繁 Cache Miss（追踪指针） </td><td style="text-align:left"> 缓存友好（利用 Cache Line） </td></tr><tr><td style="text-align:left"> <strong>冲突处理</strong> </td><td style="text-align:left"> 挂载链表（拉链） </td><td style="text-align:left"> 二次探测（寻找新插槽） </td></tr><tr><td style="text-align:left"> <strong>查找加速</strong> </td><td style="text-align:left"> 逐个对比 tophash </td><td style="text-align:left"> <strong>SIMD 并行匹配</strong> 8 个插槽 </td></tr><tr><td style="text-align:left"> <strong>空间利用率</strong> </td><td style="text-align:left"> 较低（存在大量指针和空闲槽位） </td><td style="text-align:left"> 较高（负载因子从 6.5 提升至 7/8） </td></tr><tr><td style="text-align:left"> <strong>性能上限</strong> </td><td style="text-align:left"> 受限于内存延迟 </td><td style="text-align:left"> 接近硬件极限（单指令多数据） </td></tr></tbody></table><hr/><h2 id="3--bucket--group">3. 核心数据结构：从 Bucket 到 Group</h2><p>Go 1.24 弃用了 <code>bmap</code>，引入了 <strong>Group</strong> 和 <strong>Metadata</strong> 的概念。</p><h3 id="31-hmap">3.1 hmap：新的元数据头</h3><p>Map 的头部结构体依然叫 <code>hmap</code>，但内部字段发生了变化：</p><pre class="language-go lang-go"><code class="language-go lang-go">type hmap struct {
    count     int    // 元素总数
    flags     uint8  // 状态位（写入中、迭代中等）
    B         uint8  // 指数级大小，2^B 表示 Group 的对数
    hash0     uint32 // 哈希种子，对抗哈希攻击的关键

    // 核心存储指针
    table     unsafe.Pointer // 指向当前的存储表（包含元数据数组和数据数组）
    oldtable  unsafe.Pointer // 扩容时的旧表指针，用于增量搬迁
    
    // ... 
}
</code></pre>
<h3 id="32-control-bytefingerprint">3.2 控制字节（Control Byte）与指纹（Fingerprint）</h3><p>这是 Swiss Table 的精髓。每个插槽不再只靠 <code>tophash</code> 过滤，而是使用 1 个字节（8 位）的<strong>控制位</strong>：</p><ul><li><strong><code>0b10000000</code> (0x80)</strong>：<code>Empty</code>。表示此槽位完全为空。</li><li><strong><code>0b11111110</code> (0xfe)</strong>：<code>Deleted</code>（墓碑标记）。表示元素已删，但<strong>探测链不能断</strong>。</li><li><strong><code>0b0xxxxxxx</code> (h2)</strong>：<code>Occupied</code>。最高位为 0，后 7 位存储哈希值的低 7 位，称为 <strong>h2 指纹</strong>。</li></ul><hr/><h2 id="4-simd-">4. 性能杀手锏：SIMD 并行匹配</h2><p>在查找一个 Key 时，Go 1.24 的逻辑如下：</p><ol start="1"><li><strong>哈希拆分</strong>：将 64 位哈希值拆为 <code>h1</code>（高 57 位）和 <code>h2</code>（低 7 位）。</li><li><strong>定位 Group</strong>：通过 <code>h1</code> 找到对应的 Group。</li><li><strong>向量化查找</strong>：
<ul><li>现代 CPU（如 amd64 上的 SSE2/AVX）支持 SIMD 指令。</li><li>运行时会一次性加载 Group 内 8 个插槽的元数据（共 8 字节）。</li><li>使用一条指令（类似于 <code>PCMPEQB</code>）将这 8 个字节与目标的 <code>h2</code> 指纹进行比较，瞬间得到一个掩码。</li></ul></li><li><strong>精确校验</strong>：如果掩码指示有匹配项，再去内存里取 Key 进行 <code>equal</code> 比较。</li></ol><p><strong>为什么快？</strong> 因为它把原本需要 8 次循环的比较，压缩成了一次指令和一次位运算，且元数据与数据在内存布局上高度紧凑。</p><hr/><h2 id="5-triangular-probing">5. 冲突解决：三角形探测（Triangular Probing）</h2><p>由于不再有溢出桶，当某个 Group 满员且指纹不匹配时，Go 1.24 使用一种特殊的二次探测——<strong>三角形探测（Triangular Probing）</strong>来寻找下一个 Group：</p><ul><li><strong>探测公式</strong>：<code>p(i) = (h1 + i*(i+1)/2) mod 2^B</code></li><li><strong>为什么不是线性探测？</strong> 线性探测容易产生“一级聚集”（Primary Clustering），即大量数据挤在一起，导致探测链越来越长。</li><li><strong>为什么不是通用平方探测？</strong> 通用的平方探测（如 $ai^2 + bi + c$）计算相对复杂。而三角形探测在表大小为 $2^k$ 时，已被数学证明能遍历到表中所有的槽位，且计算只需简单的算术运算。</li><li><strong>为什么不使用多次哈希（Double Hashing）？</strong> 多次哈希在每次冲突时都需要重新计算哈希值，CPU 指令开销过大。在 Swiss Table 这种对性能极其敏感的场景下，增加哈希计算次数会直接抵消掉开放寻址带来的缓存红利。</li><li><strong>三角形探测的优势</strong>：它的步长随探测次数增加，能有效缓解聚集效应，同时保持了极高的计算效率。</li><li><p><strong>墓碑标记（Tombstone）</strong>：在删除元素时，槽位会被标记为 <code>Deleted</code> (0xfe)。这是为了告诉查找流程：“这里虽然没数据，但探测链在此并未中断，请继续往后找”。只有遇到真正的 <code>Empty</code> (0x80) 才会停止探测。</p><hr/></li></ul><h2 id="6-">6. 扩容：更激进，也更高效</h2><h3 id="61-load-factor">6.1 负载因子（Load Factor）的飞跃</h3><ul><li><strong>旧版</strong>：负载因子约为 <strong>6.5/8</strong>。</li><li><strong>Go 1.24+</strong>：由于 Swiss Table 的高效过滤，负载因子提升到了 <strong>~87.5% (7/8)</strong>。</li><li><strong>结果</strong>：在同样的内存开销下，1.24 版能多装约 7% 的数据，且在高负载下的性能劣化远小于旧版。</li></ul><h3 id="62-">6.2 搬迁逻辑</h3><p>扩容依然是<strong>渐进式（Incremental）</strong>的。当触发扩容（负载因子超标或探测链过长）时：</p><ol start="1"><li>分配一个 2 倍大小的新表。</li><li>每次进行 <code>mapassign</code> 或 <code>mapdelete</code> 操作时，搬迁旧表中的 Group 到新表。</li><li>这种设计保证了扩容过程不会造成突发的长尾延迟（P99 延迟）。</li></ol><hr/><h2 id="7-">7. 开发者需要注意什么？</h2><ol start="1"><li><strong>内存占用更低</strong>：在海量 Map 场景下，升级到 1.24 后通常能观察到 5%~10% 的 RSS 内存下降。</li><li><strong>迭代随机性</strong>：虽然底层重写，但 <code>map</code> 的迭代顺序依然是伪随机的，不要依赖它。</li><li><strong>性能红利</strong>：这是一个“透明”的优化，升级工具链即可享受，无需修改任何代码。</li></ol><h2 id="">总结</h2><p>Go 1.24 的 Map 重构是工程领域“榨干硬件性能”的典范。它通过 Swiss Table 将算法（开放寻址）与硬件（SIMD、Cache Line）深度绑定。对于开发者而言，这不仅意味着更快的执行速度，更标志着 Go 语言在追求极致性能的道路上又迈出了坚实的一步。</p><hr/><h2 id="">参考资料：</h2><ul><li><a href="https://golang.design/go-questions/map/principal/">Go Map的实现原理</a></li><li><a href="https://go.dev/blog/swisstable">Faster Go maps with Swiss Tables - Go Blog</a></li></ul></div><p style="text-align:right"><a href="https://xming.cyou/posts/backend/swiss-table#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/backend/swiss-table</link><guid isPermaLink="true">https://xming.cyou/posts/backend/swiss-table</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Sat, 17 Jan 2026 10:21:54 GMT</pubDate></item><item><title><![CDATA[使用Patchright的时候如何绕过Cloudflare自动化检测]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://xming.cyou/posts/devops/playwright-captcha">https://xming.cyou/posts/devops/playwright-captcha</a></blockquote><div><h2 id="">安装项目依赖</h2><pre class="language-shell lang-shell"><code class="language-shell lang-shell">python -m venv .venv
source .venv/bin/activate
pip install patchright
pip install playwright_captcha
# 或使用 uv 安装
# uv init demo
# uv install playwright_captcha
# uv install patchright


#安装浏览器，当然你也可以不用安装 playwright 的浏览器，也可以使用系统自带的浏览器，通过 CDP 协议连接
# (.venv) D:\Dev\Blog-demo\playwright&gt;patchright install -h
# Usage: playwright install [options] [browser...]

# ensure browsers necessary for this version of Playwright are installed

# Options:
#   --with-deps   install system dependencies for browsers
#   --dry-run     do not execute installation, only print information
#   --list        prints list of browsers from all playwright installations
#   --force       force reinstall of stable browser channels
#   --only-shell  only install headless shell when installing chromium
#   --no-shell    do not install chromium headless shell
#   -h, --help    display help for command


# Examples:
#   - $ install
#     Install default browsers.

#   - $ install chrome firefox
#     Install custom browsers, supports chromium, chromium-headless-shell, chromium-tip-of-tree-headless-shell, chrome, chrome-beta, msedge, msedge-beta, msedge-dev, bidi-chromium, firefox, webkit, webkit-wsl.

# 选择安装单个浏览器即可
patchright install chromium
</code></pre>
<h2 id="">唤起浏览器代码</h2><pre class="language-python lang-python"><code class="language-python lang-python">from pathlib import Path
from patchright.async_api import BrowserContext, async_playwright, Page
from typing import Union
from playwright_captcha import ClickSolver, CaptchaType, FrameworkType


class Browser:
    def __init__(self):
        self._context: Union[BrowserContext, None] = None

    @property
    def context(self) -&gt; BrowserContext:
        if self._context is None:
            raise RuntimeError(&quot;Browser context is not initialized.&quot;)  
        return self._context

    async def start(self, user_data_dir: Path | str = Path.home() / &quot;.my_browser_data&quot;):
        try:
            pw = await async_playwright().start()
            # Simulate starting the browser and creating a context
            self._context = await pw.chromium.launch_persistent_context(
                user_data_dir=user_data_dir,  # 用户数据目录，用于持久化保存 Cookie、缓存等浏览器状态
                headless=False,  # 是否以无头模式运行（无界面）。注：某些防爬机制下，headless 模式更易被检测
                chromium_sandbox=False,  # 是否启用 Chromium 沙箱，禁用它可以减少在某些环境（如 Docker）下的运行限制
                ignore_default_args=[&quot;--enable-automation&quot;],  # 忽略默认的自动化标志，防止网站检测到正在使用自动化工具
                viewport={&quot;width&quot;: 1920, &quot;height&quot;: 1080},  # 设置浏览器视口的宽度和高度
                has_touch=False,  # 是否模拟支持触摸事件
                is_mobile=False,  # 是否模拟移动设备模式
                handle_sighup=False,  # 是否在接收到 SIGHUP 信号时关闭浏览器
                handle_sigterm=False,  # 是否在接收到 SIGTERM 信号时关闭浏览器
                handle_sigint=False,  # 是否在接收到 SIGINT 信号（如 Ctrl+C）时关闭浏览器
                timezone_id=&quot;Asia/Shanghai&quot;,  # 设置浏览器的时区
            )
        except Exception as e:
            raise e

    async def stop(self):
        if self._context:
            await self._context.close()
            self._context = None

    @classmethod
    async def handle_patch(cls, page: Page):
        solver = ClickSolver(
            framework=FrameworkType.PLAYWRIGHT,
            page=page,
            max_attempts=5,
            attempt_delay=8,
        )

        try:
            await solver.prepare()
            return solver
        except Exception as e:
            raise e

    @classmethod
    async def handle_turnstile(cls, page: Page, solver: ClickSolver):
        try:
            await solver.solve_captcha(
                captcha_container=page, captcha_type=CaptchaType.CLOUDFLARE_TURNSTILE
            )
        except Exception as e:
            ... # 如果没有检测到验证码，则跳过  

</code></pre>
<p><strong>CDP 模式链接：</strong></p><pre class="language-shell lang-shell"><code class="language-shell lang-shell"> async def launch_by_cdp(self, cdp_endpoint: str = &quot;http://127.0.0.1:9222&quot;):
        &quot;&quot;&quot;
        需要事先启动浏览器，命令行示例：
        MacOS:
            /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=&quot;/path/to/your/custom/profile&quot;
        Windows:
            C:\\Program\ Files\\Google\\Chrome\\Application\\chrome.exe --remote-debugging-port=9222 --user-data-dir=&quot;C:\path\to\your\custom\profile&quot;
        Linux:
            /usr/bin/google-chrome-stable --remote-debugging-port=9222 --user-data-dir=&quot;/path/to/your/custom/profile&quot;
        Args:
            cdp_endpoint (_type_, optional): _description_. Defaults to &quot;http://127.0.0.1:9222&quot;.

        Raises:
            e:   _description_
        &quot;&quot;&quot;
        try:
            pw = await async_playwright().start()
            browser = await pw.chromium.connect_over_cdp(cdp_endpoint)
            self._context = browser.contexts[0]  # 假设只使用第一个上下文
        except Exception as e:
            raise e
</code></pre> 
<h2 id="">使用</h2><pre class="language-python lang-python"><code class="language-python lang-python">import asyncio
from browser import Browser


async def main():
    b = None
    try:
        b = Browser()
        await b.start(&quot;./chrome_data&quot;)
        page = await b.context.new_page()

        # 关键
        solver = await Browser.handle_patch(page)
        await page.goto(&quot;https://www.example.com&quot;, wait_until=&quot;load&quot;)
        await asyncio.sleep(2)  # 等待页面加载完成
        await Browser.handle_turnstile(page, solver) # 处理处理验证码
    except Exception as e:
        print(e)
    finally:
        if b is not None:
            await b.stop()

if __name__ == &quot;__main__&quot;:
    asyncio.run(main())
</code></pre>
<h2 id="-metamask--google-">如何捕获 Metamask 弹窗和 Google 弹窗等？</h2><p>我们在自动化测试或者需要与 Google 扩展交互时，通过老办法的方式就是直接打开<code>chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/home.html#unlock</code>页面，但是这种不太用好，因为有时弹窗的界面与打开的页面相互干扰，导致自动化程序可能会运行异常，我们可以通过下面方式监测任何新的页面打开：</p><pre class="language-python lang-python"><code class="language-python lang-python">    @asynccontextmanager
    async def handle_expect_page(self, timeout: int = 3000):
        page: Union[Page, None] = None
        try:
            async with self.context.expect_page(timeout=timeout) as page_info:
                page = await page_info.value
                yield page

        except Exception as e:
            raise e
        
        finally:
            if page is not None:
                await page.close()
</code></pre>
<p>你只需要在在期望打开新页面之后调用它，他就会返回新页面的Page对象，如果超出时间没有任何页面打开则会报TimeoutError。注意，这里使用了<code>@asynccontextmanager</code>装饰器，你需要使用async with语句调用，或者你可以修改这个函数。</p><h2 id="">总结</h2><p>通过以上代码，我们实现了在 Playwright 中自动处理验证码的功能。通过自定义的<code>ClickSolver</code>类，我们可以轻松地处理各种验证码，包括 Google reCAPTCHA 和 Cloudflare Turnstile。同时，我们还提供了<code>Browser</code>类来简化 Playwright 的使用，使得代码更加简洁和易读。
这个 Demo 只是演示如何处理 Cloudflare Turnstile 验证码，如果需要处理 Google reCAPTCHA 验证码，需要替换 captcha<em>type=CaptchaType.CLOUDFLARE</em>TURNSTILE</p></div><p style="text-align:right"><a href="https://xming.cyou/posts/devops/playwright-captcha#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://xming.cyou/posts/devops/playwright-captcha</link><guid isPermaLink="true">https://xming.cyou/posts/devops/playwright-captcha</guid><dc:creator><![CDATA[MingLez]]></dc:creator><pubDate>Sat, 10 Jan 2026 14:32:13 GMT</pubDate></item></channel></rss>