<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>夜街尘</title>
  <icon>https://aprilforest.cn/images/gamemechine.png</icon>
  <subtitle>眨眼间，已是千年之外</subtitle>
  <link href="https://aprilforest.cn/atom.xml" rel="self"/>
  
  <link href="https://aprilforest.cn/"/>
  <updated>2026-03-02T14:02:10.805Z</updated>
  <id>https://aprilforest.cn/</id>
  
  <author>
    <name>Asforest</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>MT3000路由器安装V2代理</title>
    <link href="https://aprilforest.cn/26061-2145.html"/>
    <id>https://aprilforest.cn/26061-2145.html</id>
    <published>2026-03-02T21:45:53.000Z</published>
    <updated>2026-03-02T14:02:10.805Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>最近入手了GL家的MT3000路由器，它的系统是基于OpenWRT制作的，所以可玩性非常高。</p><p>所以打算把V2客户端装在这个路由器上，这样使用和设置分流规则都比较方便。</p><p>一开始打算用命令行版本的xray，但是发现它没有解析订阅链接的功能，它只能按现有的参数去连接服务器。</p><p>然后我找到了一个叫v2raya的项目，这是一个web版本的v2客户端，可以自动更新订阅，非常方便。</p><p>其实我的需求很简单，只要提供一个http协议的代理监听端口，这样我电脑上的大多数软件就都可以连接上来。至于透明代理目前来看并没有需求，那么事情就简单了许多。</p><p>这里写一个教程留给大家参考。</p><p>首先是安装v2raya，v2raya有很多种安装方式，这里我们选择openwrt版本进行安装。</p><p><a href="https://github.com/v2rayA/v2raya-openwrt" class="bubble-link">https://github.com/v2rayA/v2raya-openwrt</a></p><p>首先按照教程，执行这条命令，添加v2raya-openwrt的三方仓库密钥</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">wget</span> https://downloads.sourceforge.net/project/v2raya/openwrt/v2raya.pub <span class="token parameter variable">-O</span> /etc/opkg/keys/94cc2a834fb0aa03<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后添加三方仓库源</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"src/gz v2raya https://downloads.sourceforge.net/project/v2raya/openwrt/<span class="token variable"><span class="token variable">$(</span><span class="token builtin class-name">.</span> /etc/openwrt_release <span class="token operator">&amp;&amp;</span> <span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable">$DISTRIB_ARCH</span>"</span><span class="token variable">)</span></span>"</span> <span class="token operator">|</span> <span class="token function">tee</span> <span class="token parameter variable">-a</span> <span class="token string">"/etc/opkg/customfeeds.conf"</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>好了之后更新一下软件源</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">opkg update<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>接着安装v2raya本体</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">opkg <span class="token function">install</span> v2raya<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>接着教程要我们安装防火墙，是这样写的</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token comment"># Check your firewall implementation</span><span class="token comment"># Install the following packages for the nftables-based firewall4 (command -v fw4)</span><span class="token comment"># Generally speaking, install them on OpenWrt 22.03 and later</span>opkg <span class="token function">install</span> kmod-nft-tproxy<span class="token comment"># Install the following packages for the iptables-based firewall3 (command -v fw3)</span><span class="token comment"># Generally speaking, install them on OpenWrt 21.02 and earlier</span>opkg <span class="token function">install</span> iptables-mod-conntrack-extra <span class="token punctuation">\</span>  iptables-mod-extra <span class="token punctuation">\</span>  iptables-mod-filter <span class="token punctuation">\</span>  iptables-mod-tproxy <span class="token punctuation">\</span>  kmod-ipt-nat6<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这边要做一个选择，OpenWrt 22.03和更新版要安装kmod-nft-tproxy，然后旧版的话安装下面这些东西。截止到撰写教程时，GL官方的系统是4.8.1版本，使用的是 OpenWrt 21.02-SNAPSHOT 版本，所以应该安装下面这些东西（但是官方系统已经提前安装好了）</p><p>接着还要安装luci-app，方便查看和管理。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">opkg <span class="token function">install</span> luci-app-v2raya<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后就可以在luci界面顶部的“服务”里找到“V2Raya”，勾选“启动”然后保存即可。</p><p><img src="/26061-2145/image-20260228215000005.png" alt="image-20260228215000005"></p><p>启动完毕后点“Open Web Interface”打开v2raya的界面。</p><p>第一次进来大概是长这样的，我们需要先设置好订阅链接，再点右上角的启动。</p><p><img src="/26061-2145/image-20260228215245817.png" alt="image-20260228215245817"></p><p>然后问题就来了，你会发现点启动后会转圈很久很久，即使刷新页面也不行。</p><p>这些因为v2raya在尝试启动xray，当xray起来后，v2raya会和xray通信以同步信息。</p><p>但是问题就是卡在xray这里了，虽然xray运行起来了，端口也开启监听了，但是v2raya怎么都无法与xray通信。</p><p>此时使用curl去测试xray的入站端口也会无限卡住。那么就可以断定是xray出了问题。</p><p>通过不懈的寻找，xray也有人反馈过这个问题，恰好也是GL-MT3000路由器。</p><p><a href="https://github.com/XTLS/Xray-core/issues/4722" class="bubble-link">https://github.com/XTLS/Xray-core/issues/4722</a></p><p>根据作者的解答，这是因为新版的xray使用了golang 1.24，而1.24在监听时会默认启动mptcp特性。</p><p>mptcp全程是Multipath TCP，也就是多径TCP，可以让一个TCP的数据走在多个网络链路中提升速度，这是一个Linux系统才支持的特性。</p><p>但是GL官方的系统比较老，而且系统内核没有对mptcp特性的支持，也就造成了虽然xray启动了tcp监听，但是接收不到任何数据。</p><p>v2raya始终无法和xray进行tcp通信从而确认启动成功了没有，最终超时后xray被v2raya结束进程。</p><p>既mptcp支持不完善，那么解决办法自然是关闭mcptch特性。</p><p>有个办法是在xray启动时设置环境变量<code>GODEBUG=multipathtcp=0</code>这样就会自动关闭mptcp特性了。</p><p>xray是v2raya启动的，利用子进程会继承父进程环境变量的特性，我们直接把这个环境变量传递给v2raya即可，由v2raya再继承给xray来关系mptcp特性。</p><p>v2raya的启动文件位于<code>/etc/init.d/v2raya</code>。</p><p>这里我们直接在41行后面新增一行<code>procd_append_param env GODEBUG="multipathtcp=0"</code></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">procd_open_instance <span class="token string">"<span class="token variable">$CONF</span>"</span>procd_set_param <span class="token builtin class-name">command</span> <span class="token string">"<span class="token variable">$PROG</span>"</span>procd_set_param <span class="token function">env</span> <span class="token assign-left variable">XDG_DATA_HOME</span><span class="token operator">=</span><span class="token string">"/usr/share"</span><span class="token comment"># 新增到这里</span>procd_append_param <span class="token function">env</span> <span class="token assign-left variable">GODEBUG</span><span class="token operator">=</span><span class="token string">"multipathtcp=0"</span>append_env <span class="token string">"config"</span> <span class="token string">"/etc/v2raya"</span>append_env <span class="token string">"log_file"</span> <span class="token string">"/var/log/v2raya/v2raya.log"</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>然后通过luci界面重启v2raya即可，再点击右上角的启动按钮，就能顺利启动xray了。</p><p><img src="/26061-2145/image-20260228221513259.png" alt="image-20260228221513259"></p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="硬件" scheme="https://aprilforest.cn/categories/%E7%A1%AC%E4%BB%B6/"/>
    
    <category term="软件" scheme="https://aprilforest.cn/categories/%E8%BD%AF%E4%BB%B6/"/>
    
    
    <category term="NAS" scheme="https://aprilforest.cn/tags/NAS/"/>
    
  </entry>
  
  <entry>
    <title>第一次来成都蔚蓝档案Only</title>
    <link href="https://aprilforest.cn/26018-0108.html"/>
    <id>https://aprilforest.cn/26018-0108.html</id>
    <published>2026-01-18T01:08:12.000Z</published>
    <updated>2026-03-02T14:02:12.693Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>周六来成都BAO玩啦，还是看到看到丸子老师转发动态才知道成都也有BAO。</p><p>本来我是VIP票准备提前排队的，但是一觉睡过惹，醒来已经是9点了。然后仔仔细细打扮了一下，就出门了。</p><p>上午是各种各样的小游戏环节。我I人社恐又犯啦，就没敢举手发言。</p><p><img src="/26018-0108/PRO_VID_20260117_103503_00_022_2026-01-18_00-07-33_%E6%88%AA%E5%9B%BE.jpg" alt="PRO_VID_20260117_103503_00_022_2026-01-18_00-07-33_截图"></p><p><img src="/26018-0108/PRO_VID_20260117_111311_00_026_2026-01-18_00-12-50_%E6%88%AA%E5%9B%BE.jpg" alt="PRO_VID_20260117_111311_00_026_2026-01-18_00-12-50_截图"></p><p>顺便逛了现场的摊位，有超多有趣的周边。</p><p><img src="/26018-0108/PRO_VID_20260117_105628_00_023_2026-01-18_00-28-55_%E6%88%AA%E5%9B%BE.jpg" alt="PRO_VID_20260117_105628_00_023_2026-01-18_00-28-55_截图"></p><p>我发现一对敲可爱的阿洛娜和普拉娜，然后就被我带回家啦。只有钱包受伤的世界达成了）</p><p><img src="/26018-0108/1768666668503.jpg" alt="1768666668503"></p><p>现场还有好多可爱的COSER老师。一开始我有点社恐不敢去合影，不过后来还是鼓起勇气来啦。其中好像还有两位工作人员，一位是坐在报到处的未花老师，还有一位是坐在糖画摊旁边的优香老师，唉正好是我最最喜欢的学生，必须集个邮哇。具体的图就不放了，我的建模确实算不上精致。不过可以放一些群友发的图。</p><p><img src="/26018-0108/1768668248540_%E5%9C%A3%E5%9C%B0%E9%9A%90%E4%BF%AE%E4%BC%9A%EF%BC%881.17%E7%A7%A4%E4%BA%9A%E6%B4%A5%E5%AD%90%EF%BC%89.jpg" alt="1768668248540_圣地隐修会（1.17秤亚津子）"></p><p><img src="/26018-0108/1768668059363_%E4%B8%89%E4%B8%83%EF%BC%881.17%E5%9C%A3%E8%AF%9E%E6%A2%93%EF%BC%89.jpg" alt="1768668059363_三七（1.17圣诞梓）"></p><p><img src="/26018-0108/1768668197380_%E9%98%BF%E5%A5%88%E5%8F%B6%E5%AD%90.jpg" alt="1768668197380_阿奈叶子"></p><p><img src="/26018-0108/1768668370438_%E9%BB%92%E5%B4%8E%E3%82%B3%E3%83%A6%E3%82%AD.jpg" alt="1768668370438_黒崎 コユキ"></p><p><img src="/26018-0108/1768668439464_%E5%9C%A3%E5%9C%B0%E9%9A%90%E4%BF%AE%E4%BC%9A%EF%BC%881.17%E7%A7%A4%E4%BA%9A%E6%B4%A5%E5%AD%90%EF%BC%89.jpg" alt="1768668439464_圣地隐修会（1.17秤亚津子）"></p><p><img src="/26018-0108/1768668538148_%E6%B0%B8%E6%81%92%E7%9A%84%E8%87%AA%E7%94%B1%E7%9A%84%E7%8B%82%E7%83%AD%E7%B2%89%E4%B8%9D%EF%BC%88117%E8%8A%B1%E7%8E%AF%E4%BA%9A%E6%B4%A5%E5%AD%90%EF%BC%89.jpg" alt="1768668538148_永恒的自由的狂热粉丝（117花环亚津子）"></p><p><img src="/26018-0108/3EEC0A377283DC19B5FB5FB458ED6F54.jpg" alt="3EEC0A377283DC19B5FB5FB458ED6F54"></p><p><img src="/26018-0108/1768665747213.jpg" alt="1768665747213"></p><p>这是其中一位老师合影后送给我的无料，好像还是手绘的，非常用心。芋泥味小孩可爱捏。</p><p><img src="/26018-0108/1768665767250.jpg" alt="1768665767250"></p><p>下午因为和我上CW课的时间冲突了，我趁着中午休息的时候转场去上课，只能暂时先离开啦。</p><p><img src="/26018-0108/IMG_20260117_121128_00_038_2026-01-17_22-52-40_%E6%88%AA%E5%9B%BE.jpg" alt="IMG_20260117_121128_00_038_2026-01-17_22-52-40_截图"></p><p><img src="/26018-0108/PRO_VID_20260117_153853_00_042_2026-01-17_22-56-33_%E6%88%AA%E5%9B%BE.jpg" alt="PRO_VID_20260117_153853_00_042_2026-01-17_22-56-33_截图"></p><p>一下课我就往回赶，但是时间还是太晚了。说好的见丸子老师本人也没有见到，好可惜哦。</p><p><img src="/26018-0108/PRO_VID_20260117_171041_00_044_2026-01-17_22-57-10_%E6%88%AA%E5%9B%BE.jpg" alt="PRO_VID_20260117_171041_00_044_2026-01-17_22-57-10_截图"></p><p>等我到的时候已经是DJ舞台阶段啦，非常热闹，但是我的耳朵确实受不了这么大声音，就在外面休息厅坐了一会就回家了。</p><p><img src="/26018-0108/PRO_VID_20260117_172209_00_045_2026-01-17_22-58-17_%E6%88%AA%E5%9B%BE.jpg" alt="PRO_VID_20260117_172209_00_045_2026-01-17_22-58-17_截图"></p><p>我刚坐上车就看到合影的通知了。本来按时间表的话，合影应该是5点就结束了，但是这会已经6点了，也怪我忘了询问工作人员合影过了没有。</p><p>说实话我很想很想让师傅掉头回去，但最终还是没有回去，今天两头跑真的是太累了，我就想回家躺着，一点也不想动，也算是第一次逛展的一个遗憾吧。<img src="/26018-0108/1768669172873.jpg" alt="1768669172873"></p><p>最后放一下这次BAO大家的合影，期待下次我也能和大家一起合影。</p><p><img src="/26018-0108/PANA1440_2.jpg" alt="PANA1440_2"></p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="游戏" scheme="https://aprilforest.cn/categories/%E6%B8%B8%E6%88%8F/"/>
    
    
    <category term="蔚蓝档案" scheme="https://aprilforest.cn/tags/%E8%94%9A%E8%93%9D%E6%A1%A3%E6%A1%88/"/>
    
  </entry>
  
  <entry>
    <title>BA的原声轨CD到啦</title>
    <link href="https://aprilforest.cn/25327-1431.html"/>
    <id>https://aprilforest.cn/25327-1431.html</id>
    <published>2025-11-23T14:31:16.000Z</published>
    <updated>2026-03-02T14:02:10.801Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>等了许久的BA原声轨音乐CD终于到啦，没什么，只是展（xuan）示（yao）一下（</p><p><img src="/25327-1431/IMG_20251114_141137.jpg" alt="IMG_20251114_141137"></p><p>里面一共有2张光盘，分为上盘和下盘，每张都收录了20首游戏内的音乐。封面是白子可爱捏（</p><p><img src="/25327-1431/IMG_20251114_141328.jpg" alt="IMG_20251114_141356"></p><p><img src="/25327-1431/IMG_20251114_141356.jpg" alt="IMG_20251114_141356"></p><p>至于为什么要1周年的CD而不是3周年4周年的，我觉得还是1周年的音乐最能代表这个游戏。</p><p>其中我比较喜欢的是《Constant Moderato》（登录背景音乐）和《Step by Step》（大厅背景音乐）。</p><p>音乐风格的话，最明显的就是 Future Bass（未来贝斯）风格，加上我也喜欢电子摇滚，所以很对我的XP。</p><p>包装里除了2盘CD，还送了一张日服的家具兑换码，可惜我是沙勒的sensei没法使用，而且对兑换码23年就已经过期了所以完全不用担心。</p><p>还有一个光盘加白子的亚克力挂件，和光盘的封面一样，可爱捏~</p><p><img src="/25327-1431/IMG_20251114_141052.jpg" alt="IMG_20251114_141052"></p><p>最后还附赠了一本小小的插画集，随便找一张对策委员会5人组拍个照啦，官方这个小小的心思我非常喜欢。虽然不是真正的美术设定集就是啦（</p><p><img src="/25327-1431/IMG_20251114_141552.jpg" alt="IMG_20251114_141552"></p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="游戏" scheme="https://aprilforest.cn/categories/%E6%B8%B8%E6%88%8F/"/>
    
    
    <category term="BlueArchive" scheme="https://aprilforest.cn/tags/BlueArchive/"/>
    
  </entry>
  
  <entry>
    <title>参加四川省无线电协会年会</title>
    <link href="https://aprilforest.cn/25313-2117.html"/>
    <id>https://aprilforest.cn/25313-2117.html</id>
    <published>2025-11-09T21:17:09.000Z</published>
    <updated>2026-03-02T14:02:12.689Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>周六去四川省无线电协会年会现场了，这是一年一度的爱好者聚会，名额非常难抢。</p><p><img src="/25313-2117/enrance.jpg" alt="enrance"></p><p>因为比较远所以坐地铁去花源，地铁快到时竟然开到高架在行进，我一直以为地铁是在地下的。从窗户可以看到外面的风景，花源那边相对是城郊的地方，感觉就像是坐高铁一样。</p><p>出地铁站后，说是有摆渡车来接我们，需要在438.500频率上呼叫，我还没拿对讲机就遇到一群爱好者也在前面叫车，然后就搭他们的车一起前往年会现场。</p><p><img src="/25313-2117/in-car.jpg" alt="in-car"></p><p>坐在我前面的这位是一位老前辈，呼号非常靠前，比我们这些晚辈早十多年入坑。他说距离上次来这边都是很多年前的事情了，这里变化了许多。</p><p>到现场后第一件事就是签到，如果忘记的话，不仅不能参加抽奖，还不能上桌吃饭噢。</p><p><img src="/25313-2117/sign-up.jpg" alt="sign-up"></p><p>到我的时候，编号已是791，我们算来的比较晚的，而且周围已经坐满了人。</p><p><img src="/25313-2117/1762695692236.jpg" alt="1762695692236"></p><p>进来以后，左边是展商区，右边是二手设备交易区。</p><p><img src="/25313-2117/people-mount-people-sea.jpg" alt="people-mount-people-sea"></p><p>老前辈带我认识协会的一些大佬们，这些前辈们我只在视频里看到过，今天在现场看见本人我确实开心的不得了。其中我一眼就认出凯大厨，每次看M哥视频BI8AQ集体台打比赛时，都是凯大厨负责伙食后勤。然后后面我还遇到M哥本人了，并且还合了影，平日里只能在视频里见到的大明星，今天有幸能见到本人那叫一个开心哇。</p><p><img src="/25313-2117/Bi8AQ-44.jpg" alt="Bi8AQ基地44_聚会调整天线参加2025年iOTA海岛比赛"></p><p>正巧这里是鸿凯德的展位，有好多电台摆在前面，必须狠狠看看实物。</p><p><img src="/25313-2117/dajia.jpg" alt="dajia"></p><p>最让我动心的是这个DM9100电台，整机体积真的真的好小哇，下面还配一个圆盘底座和支架。再加上UV双波段，最大射频功率足足25瓦（然后高功率发射电功率消耗也才70W，完全可以用一个PD充电宝加15V诱骗线供电），再加上我喜欢单色点阵LCD屏幕。要是有了这个机器，成都不管哪个中继台我都能随便喊到，再也不用被友台吐槽我讲话声音都是底噪。要不是本月预算吃紧，差点就当场拿下了（</p><p><img src="/25313-2117/1762695535474.jpg" alt="1762695535474"></p><p>然后旁边的展位是海能达，这是一个数模双用的背负式中继台，上面是主机，下面是可拆卸电池。可以在一些山区或者戈壁滩等无信号的地方临时快速架设一个无线电中继站，还能多个中继台之间组网，将一大片区域的电台连接起来。顺便捉到一位老 sensei（</p><p><img src="/25313-2117/1762695535466.jpg" alt="1762695535466"></p><p>旁边还有一个短波电台，也是背负式的，有点像影视剧中通信兵那样背的一个大盒子，然后竖一根很高的天线。但是面前的这个机器价值不菲，据说价格有好几十万，而且是主要供给给部队使用的，卧槽妥妥的军用品哇。</p><p><img src="/25313-2117/1762695535458.jpg" alt="1762695535458"></p><p><img src="/25313-2117/1762695535449.jpg" alt="1762695535449"></p><p>然后就是换卡留念，换卡是爱好者之间记录通联或者线下见面的纪念品，卡片一般是根据自己的喜好或者XP来设计的，很有个性。</p><p>这位是BA8AKD老师，我直接找他白嫖一张，这卡面一看就是老二次元了（</p><p><video preload="meta" poster="/25313-2117/akd.jpg" controls="" src="/25313-2117/qsl.mp4"></video></p><p><img src="/25313-2117/1762695535391.jpg" alt="1762695535391"></p><p>还有旁边的BG0ERE老师，崩铁一看也是老二次元了，比较讲究还专门用塑封袋包了一层。</p><p><img src="/25313-2117/1762794239186.jpg" alt="1762794239186"></p><p>然后是AHG，AHP和AHF，这些都是和我同一批考试的朋友（我是AHV），大家的卡面都好有特色。</p><p><img src="/25313-2117/1762695535406.jpg" alt="1762695535406"></p><p><img src="/25313-2117/1762695535401.jpg" alt="1762695535401"></p><p><img src="/25313-2117/1762695535396.jpg" alt="1762695535396"></p><p>现场真的是超级多的人，到处都能看到大家在架天线。</p><p><img src="/25313-2117/people.jpg" alt="people"></p><p><img src="/25313-2117/er-shou.jpg" alt="er-shou"></p><p>然后还有现场架设八木天线的BY8AA，天线是朝着欧洲方向的。然后使用的一个自动键在进行CW通联（没错就是摩尔斯电码发电报）</p><p><video preload="meta" poster="/25313-2117/aa.jpg" controls="" src="/25313-2117/ba8aa.mp4"></video></p><p><img src="/25313-2117/aa.jpg" alt="aa"></p><p><img src="/25313-2117/bw.jpg" alt="bw"></p><p>然后看到有综测机器，我就拿出自己带的几个天线去小小测试一下。</p><p>这根是老鹰的SRH775天线，测的结果是U段驻波不错，但是V段偏高，勉强能用。</p><p><video preload="meta" poster="/25313-2117/tx-test.jpg" controls="" src="/25313-2117/sr805.mp4"></video></p><p><img src="/25313-2117/tx-775.jpg" alt="tx-775"></p><p>然后就是大家一起合影，然后吃饭。</p><p><img src="/25313-2117/hy.jpg" alt="hy"></p><p><img src="/25313-2117/1762795188534.jpg" alt="1762795188534"></p><p><img src="/25313-2117/cf.jpg" alt="cf"></p><p><img src="/25313-2117/cf2.jpg" alt="cf2"></p><p><img src="/25313-2117/cai.jpg" alt="cai"></p><p>菜的味道是真不错，是地道的川菜。上面那盘凉拌牛肉是真的好吃，入口后第一秒是微甜，然后转为微麻，下一秒再转微辣，3种味道依次袭来，来一口真的回味无穷。然后其它菜也都是微辣，很合我口味。</p><p>吃完回家的时候，频率上又开始热闹起来了，大家都在组队拼车，我连一句话都插不进去。不过最后还是通过对讲机联系到一位老前辈，是他把我们带去花源地铁站，然后我才转地铁回到家里。</p><p>给我的感觉就是爱好者们真的好热情，来的时候我们会一起拼车，然后走的时候又非常乐意送我们一程。有需要的时候大家总是会互帮互助，总是给你一种大家是一家人的感觉。一天下来真的非常非常开心，希望明年还能再来。这里我相机没电了没录上，白嫖一下BTP老师是视频（</p><p><video preload="meta" poster="/25313-2117/QQ20251111-013822.png" controls="" src="/25313-2117/go-home.mp4"></video></p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    
    <category term="无标签" scheme="https://aprilforest.cn/tags/%E6%97%A0%E6%A0%87%E7%AD%BE/"/>
    
  </entry>
  
  <entry>
    <title>新的代理上网方式</title>
    <link href="https://aprilforest.cn/25291-0129.html"/>
    <id>https://aprilforest.cn/25291-0129.html</id>
    <published>2025-10-18T01:29:49.000Z</published>
    <updated>2026-03-02T14:02:12.693Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>起因是这样的，某些网站会打不开或者打开缓慢，一般都是在电脑上安装一个V2****软件。但我有不只一个电脑需要代理，不仅自己的电脑需要，公司的电脑也需要。一开始我是2个电脑上都安装了V2，但有些问题：一个是多占用一个在线客户端的名额，另一个是路由规则我修改还蛮频繁的，修改后还要两边来回同步，特别地不方便。</p><p>然后我想到，如果搭建一个公共的V2客户端，然后大家都使用这个来代理不就解决问题了吗？</p><p>然后我开始搜索解决方案，其中被提到最多的是利用网关或者旁路由来做透明代理。但看到一半我就放弃了，因为这通常需要给开发板刷openwrt一类的软件，然后在luci界面安装对应插件实现的。我的香橙派还要跑NAS的好不好，怎么可能刷openwrt呢。</p><p>另外就是透明代理本身的问题，虽然透明代理很好，可以让整个设备进入代理状态，无需另外在设备内设置代理参数，但这个方案也不太适合我，主要是网络环境的原因。首先就是我的香橙派部署的位置和公司的无线路由器之间有遮挡，而且路由器只开启了5G WIFI。这导致香橙派到路由器的访问速度不是很理想。</p><p>其次就是局域网经常会出现拥堵，比如有人在下载游戏或者拉取仓库更新时，其它人的带宽和延迟就会受到巨大的影响。这会带来一个问题，就是上网设备到代理设备之间的网速非常慢，如果是下载或者看视频还好，一旦打起游戏来延迟至少100ms以上。最后就是香橙派WIFI的出网带宽也就是100Mbps左右，作为转发设备，一进一出，还得打个半折只有50Mbps了，远远低于我电脑直连路由器的2400Mbps。</p><p>透明代理这条路不可行，只能找其他解决方法。我发现大部分支持代理的应用，都可以通过HTTP协议或者SOCKS 5协议连接到代理服务器。这样就可以实现分应用代理了，让浏览器这类对延迟不敏感的应用走代理，而其它应用直连，既可以获得满速下载和低延迟游戏，又可以访问打开缓慢的网页。</p><p>一开始我是打算使用XRAY的，因为它是是全平台的。虽然是命令行版本的，没有V2****那样好用的图形界面，但总好过没有。有个比较头疼的是XRAY它不支持订阅链接，可能还要自己写一个程序去解析订阅链接，解析完后再把对应的服务器列表生成为XRAY可以解析的配置文件格式。</p><p>我都定好方案准备开工了，但突然在XRAY的官方发现一个名叫V2.RAYA的项目（把点取消），这也是一个支持V.LESS和V.MESS协议的客户端，但是跟别的项目不一样的是，它不是基于图形界面的，而是基于webui的，这就对香橙派这类纯命令行系统非常友好了。</p><p><img src="/25291-0129/QQ20251018-005051.png" alt="QQ20251018-005051"></p><p>主界面可以填入订阅链接，可以定时自动更新订阅，然后中间部分就是选择要连接的服务器，和其它类似的基于桌面环境的软件没有什么不同的。然后也可以自己编写路由规则，选择哪些流量走代理，哪些流量直连，都非常方便。</p><p><img src="/25291-0129/QQ20251018-005110.png" alt="QQ20251018-005110"></p><p>另外一个比较方便的是它有2组监听端口，一组是全局模式的端口，也就是流量全部都走代理，而忽略自己编写的路由规则。另一组是分流模式的端口，顾名思义就是会按自己编写的路由规则进行分流。</p><p><img src="/25291-0129/QQ20251018-005615.png" alt="QQ20251018-005615"></p><p>能同时提供这2组端口的软件确实非常非常少，但是却很实用，平时可以用分流模式的端口，如果需要临时访问一些打不开或者打开缓慢的网站，可以在对应的机器上切换端口到全局模式的那个端口，这样就不用登录这个后台去修改规则了，临时使用一下很方便。</p><p>最后，它甚至还支持V.MESS协议入栈，那这个就很厉害了，众所周知，常用的代理协议HTTP和SOCKS 5都是不加密的，不适合公网使用，但是V.MESS是有加密功能，这样好像就可以实现远程公网代理？总之玩法还挺多样的。要说美中不足的，就是这个项目的文档补全，其中大部分章节都是TODO状态，也就是只写了个标题，内容是完全没有的。然后有些功能就只能靠猜，完全没有文档可以参考。</p><p>最后我通过Docker把这个项目部署到了香橙派上，偶尔应付一下GITHUB或者X或者P站是没有问题的。至于其它需要代理的软件，都可以通过HTTP或者SOCKS协议链接过来，这样就能集中编辑分流的路由规则了。</p><p>在这期间我也学习到了不少相关知识，比如XRAY的分流架构，其中V.MESS协议既可以用作入站，也可以用作出栈，这样理论上是可以一个客户端连另一个客户端，然后再连接另一个客户端，这样像链条一样一次把流量传递到服务端的。大佬前辈们的软件确实牛逼，写出自由度这么高的架构，玩法的上限完全取决于自己的想法。</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="软件" scheme="https://aprilforest.cn/categories/%E8%BD%AF%E4%BB%B6/"/>
    
    
    <category term="无标签" scheme="https://aprilforest.cn/tags/%E6%97%A0%E6%A0%87%E7%AD%BE/"/>
    
  </entry>
  
  <entry>
    <title>博客搬到NAS上了</title>
    <link href="https://aprilforest.cn/25291-0128.html"/>
    <id>https://aprilforest.cn/25291-0128.html</id>
    <published>2025-10-18T01:28:20.000Z</published>
    <updated>2026-03-02T14:02:12.689Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>前段时间用香橙派搭建了一个NAS。这次跟以往不一样，这次是打算正经做NAS使用的，不像之前那样纯粹是为了折腾。这一个多月来还是很的稳定，没有出现什么故障。虽然速度慢了点，可能才10Mb/s但是够用了。</p><p>接着我把博客也搬到NAS上来了，这样以后无论去哪里，只要把这台迷你NAS带上，随时随地都可以写文章了，再也不用在其它电脑上安装nodejs和md编辑器了。我习惯使用samba加obsidian/typora来书写文章，写完git推送到服务器上就自动运行cicd构建和发布了，非常方便。而且没写完的文章也可以接着继续写，因为是samba远程编辑md文件，完全不用担心数据在本地写好后忘记同步的问题。比如这篇文章就是使用香橙派上的hexo远程编写并预览的。</p><p>obsidian有手机版，这样及时躺在床上也可以写文章了，后面我应该会稍微增加文章的频率。而且我想开个系列，专门介绍一些好用的电脑小工具，所介绍的都是我亲自用过并且觉得还不错的。遇到好东西不能藏着掖着，得拿出来和大家分享一下。到时候先试试看吧，不知道能做几期（</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="硬件" scheme="https://aprilforest.cn/categories/%E7%A1%AC%E4%BB%B6/"/>
    
    <category term="软件" scheme="https://aprilforest.cn/categories/%E8%BD%AF%E4%BB%B6/"/>
    
    
    <category term="NAS" scheme="https://aprilforest.cn/tags/NAS/"/>
    
  </entry>
  
  <entry>
    <title>国庆回家</title>
    <link href="https://aprilforest.cn/25275-0138.html"/>
    <id>https://aprilforest.cn/25275-0138.html</id>
    <published>2025-10-02T01:38:44.000Z</published>
    <updated>2026-03-02T14:02:12.689Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>出门快一年了，国庆回家看看。</p><p>这期间入坑了业余无线电，还结识了很多四川的无线电朋友，所以我的呼号是在8区。我很期待和家乡的友台们通联，但是这边玩的人确实不多，不像四川有数不清的中继台，随便一呼就有很多友台回应你，算是意料之中。</p><p>偶尔呼叫到了一位友台BH6SFV，他告诉我附近好几个行政区的中继都是在同一个大链路上，横跨好几百公里。可能是爱好者数量实在不多，只能以这样的方式连接到更多的友台。</p><p>眨眼出来工作一年多了，现在回到家里甚至感觉有些陌生，明明在家里的各种习惯动作都形成肌肉记忆了，但总是当一个动作都做完好一会了，才反应过来我不是在公司里，而是在家里面。</p><p>妈跟我说工作不容易，上班的地方有巨大的变动，日子更加艰难了，生意不好做，停的停关的关，线上对线下冲击太大了。爸的工作我也很担心，他年龄大了，又要赚钱，又怕他身体吃不消，市场行情不好，干一天是一天。感觉出来工作才1年多，家里变化非常大，但我远在外地，实在帮不上家里什么忙。</p><p>岁月和疾病不饶人，人说没就没。爷爷跟我说附近的邻居爷爷走了，因为发病没有及时去医院，人就这么没了。这个爷爷我有很深的印象，是个文化人，也经常到我们家里来玩，为人很将礼貌和客气，我感觉有点不是滋味。爷爷有很多朋友我都认识，还打过交道，印象很深，这些年爷爷每次跟我说起谁谁谁走的时候，一想起来这辈子再也见不到的时候，我心理都不好受。这人嘛，可能平时健健康康的都很好，但突然那天就不行了，来的没有一点前兆。老人，每过一年都很不容易。</p><p>我想买个相机，每次回老家或者去看老人，亲戚，朋友的时候都记录下来。现在我工作了，一年回不了几次家，想趁每次回家的时候多拍一些视频，这样以后可以随时回看。目前看上了go3s，因为体积小，也隐蔽，不像手机那样，拍起来太过明显。如果后面有机会的话，也会做一些心得的分享。</p><p>本来国庆回家是很快乐的，但回家遇到这些事情我真快乐不起来。还是小学时好，各种美好的记忆都是小学时留下的。爷爷奶奶还有爸妈都很年轻那会，我也不同操心那么多事情。不过人不能总是沉浸在过去，时间是不会停止的，得向前走，越长大，只会面临越大的困难。</p><p><img src="/25275-0138/1759340352047.jpg" alt="1759340352047"></p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    
    <category term="无标签" scheme="https://aprilforest.cn/tags/%E6%97%A0%E6%A0%87%E7%AD%BE/"/>
    
  </entry>
  
  <entry>
    <title>NAS踩坑记录</title>
    <link href="https://aprilforest.cn/25264-2342.html"/>
    <id>https://aprilforest.cn/25264-2342.html</id>
    <published>2025-09-21T23:42:51.000Z</published>
    <updated>2026-03-02T14:02:10.805Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>之前的NAS一直是用虚拟机跑的，因为想用ZFS，所以在虚拟机里运行了一个TrueNAS Scale。电脑CPU的功耗区间很宽，待机时能降到10w以下，满载时能上到120W左右，因为是笔记本电脑有电池，相当于自带一个UPS不用担心意外断电。再加上是现成的设备，除了硬盘和内存以外，完全不用额外花钱，我使用起来一直很满意。</p><p><img src="/25264-2342/1758467876590.jpg" alt="1758467876590"></p><p>然后不出意外的话，就要出意外了。都说现在的电脑很皮实耐操，长时间不关机也影响不大。但是我这个笔记本呐，就偏偏不争气地坏了。现象是电脑上电1分钟左右就会卡死，一开始还以为是系统bug，想着重新安装一下系统应该就好了。</p><p>然后重装系统的时候发现就算在bios里也会卡死，我突然就有点慌了。后面我无意间发现电脑在死机后整机功耗会陡然降低，然后定到25W左右，无论怎么点鼠标键盘，都没有一丝波动（正常应该在30-60W之间波动）。</p><p>我第一感觉是CPU卡死了。因为之前大致测量过CPU Package功耗和整机功耗的差值，推算出来外围功耗大约在18-20W附近。然后我就反复重启进bios，趁着卡死之前不停调整CPU各种配置。意外发现重新开启CPU超线程之后，问题奇怪地消失了，就和从来没发生过一样。虽然电脑暂时恢复正常了，但我的NAS肯定不能继续在这台电脑上跑了，要换个其它地方。</p><p>首先想到的就是买个x86小主机，因为TrueNAS Scale只有x86版本，没有ARM的。但我估计TrueNAS可能会缺失wifi驱动。我这边受环境限制，只能连接wifi不能连有线，就算我搞到了网卡的linux驱动，TrueNAS也不一定让我安装，因为这个系统为了极致的数据安全，对shell访问做了特别严格的限制。</p><p>也想过使用PVE套一层TrueNAS，但是PVE的文档我确实看不进去，放弃了。</p><p>之所以想用TrueNAS，主要是它的ZFS文件系统，不仅有COW机制让快照只记录差异部分，还有校验和机制+数据巡检机制能够定期发现文件静默损坏。另外就是TrueNas几乎所有操作都是在WebUI里进行的，整个操作逻辑都是可视化的，非常直观不用敲任何命令行，很合我心意。</p><p><img src="/25264-2342/QQ20250921-232245.png" alt="QQ20250921-232245"></p><p>操作繁琐一点是没问题的，但是没有ZFS我不能接受。然后我就把目光放到了OpenMediaVault上，这个系统虽然整体体验没有TrueNAS好，甚至ZFS还要靠装插件才能支持。但是它支持ARM啊，它可以在现成的Debian系统上安装，就凭这一点它就是香饽饽啊。但当我真的咬下去的时候又发现不太对劲。</p><p>首先就是安装，ZFS默认没有集成到OMV里，需要自己下载插件去安装。安装时第一个遇到的就是网络问题，我明明在系统设置里添加了代理地址，但是安装ZFS插件时，就是甜蜜的不生效。原来它自己走了一个脚本去下载一堆依赖，我还是通过翻源码知道它偷摸着跑去调用wget下载文件，我又给wget设置配置文件，最终才把zfs装好。</p><p>OVM给我的感觉就是把命令行直接硬搬到WebUI上来，很多按钮点击后，显示的数据直接就是CLI那套文字排版，感觉还不如用CLI。最麻烦的是ZFS Pool创建不了，它的可用磁盘列表一直是空的，查文档说是因为盘的数据没有清，我清了好几遍还是这样。而且它那个文档，写了和没写一样，完全没有TrueNAS那么详尽。</p><p><img src="/25264-2342/QQ20250921-232341.png" alt="QQ20250921-232341"></p><p>最后还是我亲自用命令行去创建的ZPool。请问我都能命令行创建了，我还要你OMV干嘛？最头疼的当属于安装OMV后，我的香橙派开机时间直接从20秒变成3分钟，而且随便在OMV里改点什么东西，它Apply一下，又是2分钟转圈圈。</p><p>最后我气不过把OMV删了，我直接换原厂系统，打算自己从头定制一个NAS。</p><p>考虑到后期可能还会部署一些轻服务到NAS上，我专门买了一个4GB内存版本的香橙派Zero3，拿到手后第一时间刷系统，然后装zfs，因为zfs需要工作在内核空间里，所以它需要linux头文件来编译，这里官方手册里有提到，头文件默认没有安装，而是在<code>/opt</code>目录下有个deb包，需要自己install一下。我花了好久才再官方手册里找到正确方法。</p><p>zfs装好后就是接硬盘了，我选择的是PCIE4.0的硬盘通过一个USB硬盘盒连接到香橙派的USB接口上。然后这里就踩坑了，我用的致态的TiPro7000，这个硬盘貌似对电源功率要求比较高，需要峰值2A以上的供电，否则硬盘不识别。但USB 2.0接口规范中最大也就500mA啊喂，远远不及需要的2A电流。我感觉天塌了，怎么都没想到会卡到供电这里。</p><p>我突然发现我的充电器可以输出5V 3A的电流，但是硬盘却只能卡到1A附近，那么肯定是限流了。我用万用表打了一下，果然USB的VCC和充电器的VCC是断路的。那多半是有隔离芯片。我直接打开香橙派官网，找Zero3的原理图，找到了一个叫SY6280的电源管理芯片，其中ISET引脚就是用来限制电流。具体的电流是根据I=6800/ISET（欧姆）计算出来的，这里Zero3连接了一个6.8k电阻，也就是限流到1A。</p><p><img src="/25264-2342/1758458446235.jpg" alt="1758458446235"></p><p><img src="/25264-2342/image-20250921235434696.png" alt="image-20250921235434696"></p><p>正好前几天改装UVK6对讲机时我买了新的电烙铁，而且手边正好有剩下5.1k电阻（对没错，我只有5.1k的贴片电阻）。然后就是飞线的活，这比UVk6的那个16pin typec座子飞线要容易多了，两下就搞定了，然后用表打一下确定电阻降低到原来的一半。直接插电~ 开机~</p><p><img src="/25264-2342/1758458446245.jpg" alt="1758458446245"></p><p>烙铁是刚买的，正点原子的T90B，正好我的充电宝可以输出PD 140W功率，加热到350度只需要几秒，确实非常快。</p><p><img src="/25264-2342/1758458446270.jpg" alt="1758458446270"></p><p><img src="/25264-2342/1758458446226.jpg" alt="1758458446226"></p><p><img src="/25264-2342/1758458446217.jpg" alt="1758458446217"></p><p>这下硬盘终于能正确识别了，我迫不及待地导入ZFS存储池，然后天又塌了，原来TrueNAS的ZFS是定制过的，使用了一个什么特性。现在换到香橙派上它不支持这个特性了，无法挂载了。我傻眼了，无奈只好把数据备份到别的硬盘后，再将硬盘重新格式化，然后又把数据复制回来，这回终于能挂载了，系统也能正常识别了。</p><p>还没高兴几秒，又踩坑了。这回是zfs的权限问题。我的samba服务怎么都访问不了硬盘里的文件了，提示权限不足，我又去调整zfs的权限。不得不说TrueNAS确实很牛逼，它那个WebUI十分简单易用，有啥事点两下就完事了，都不用跟shell打交道。也正是太好用了，我是一点都没有接触过命令行版本的zfs，调起权限来只能两眼一抹黑，一遍调一遍查官方文档。</p><p>然后我发现Linux上的ZFS是不支持nfsv4身份验证的，这个只有BSD系统上的ZFS才支持，而恰巧我所有的文件夹都被设置成了nfsv4模式。我就很纳闷为啥TrueNAS Scale是基于debian的，它怎么就能支持？一查我才知道TrueNAS对内核进行专门定制过以支持nfsv4。好吧我认了，换回posix模式就是了。</p><p>最后总算是把NAS给搭建起来了，过程中我曾不止一次想打退堂鼓放弃这个ARM方案，想着就买一个小主机然后PVE+Truenas完事了，方便又省事。好在最后还是坚持下来了，NAS也正常工作没有一点问题，最后设置一下每天+每周+每月的定时快照，方便回退版本。得益于COW（写时复制）机制，创建快照几乎不会增加硬盘空间占用，而且速度飞快就几秒钟。</p><p>可能这个系统中唯一的短板，就是香橙派只有USB 2.0的接口，和它旁边那块PCIE4.0x4的固态硬盘比速度确实不太匹配。但我并不是很在意这个事情，因为我的预期里速度只要有5Mb/s就足够平常看一些视频或者其它文件下载上传使用了，遇到再大点的文件无非就是多等一等。就算能跑到更高的速度，硬盘功耗也会飙升，我的USB根本输出不了这么大的电流。前面给硬盘导入数据时，我使用的是USB连接的我的电脑，按理说我电脑是不会有供电问题的。但是一旦当硬盘写入速度上来之后，比如上到200Mb/s左右时，硬盘就会掉电，然后复制进程就卡死了。所以用USB 2.0也算是恰好避开了这个问题吧。</p><p>另一个遗憾是香橙派的wifi，虽然写着支持2.4G和5G双频wifi，但是5G频段的带宽只有100Mbps左右，使用上就是传送速度被锁死在了10Mb/s出头。要知道usb 2.0接口的速度都能跑到30Mb/s呢。</p><p><img src="/25264-2342/1758458446280.jpg" alt="1758458446280"></p><p>此外这也余下了不少预算，一块香橙派才200出头，而一个x86小主机动则800到1000的，而且功耗啊，噪音啊，都没有ARM这么好。这个香橙派体积还小，如果有需要的话可以随身带着，用最普通的充电宝就能开机，随时随地都可以访问文件。</p><p>其实在过程中我还留意到了OEC(T)这个盒子。它有2.5寸SATA硬盘接口，但是我买不到M.2 PCIE转2.5寸SATA的硬盘转接盒，另外OEC(T)它不支持WIFI，只有1个千兆网口。再加上体积稍微大了一点，最后没有选择这个。</p><p>其它瑞芯微的盒子我也都看过，单论性能总体肯定都是比H618要强不少的。但是我NAS的使用习惯是读多写少，读写的时间比例大约是9比1。zfs也开启了数据压缩，我用的是lz4算法。lz4算是一个比较注重速度的算法，压缩率和速度之间会更偏向速度，这一定程度上也减轻了CPU的计算压力。</p><p>我也有专门在香橙派上做过lz4benchmark的测试，纯内存速度大约1.1Gb/s左右还不错，即使是从SD卡上读取文件进行压缩，也能有个40Mb/s的速度。况且lz4本身是一个速度不对称的算法，也就是解压速度远远大于压缩速度，这又进一步减小了CPU压力。平时下载文件10Mb/s左右的速度时，CPU也就是10-20%的占用，很低。</p><p><img src="/25264-2342/1758458446203.jpg" alt="1758458446203"></p><p>最后我把这个NAS放在在桌子底下了，这里既不显眼，也不会有散热压力，而且离市电插头更近，我可以使用更短的typec线来减少供电损耗，因为这块硬盘本身就对供电要求比较高。</p><p>接下来我会试用一段时间，看看这个NAS的综合体验到底怎么样，然后再向大家分享一下具体的使用体验。</p><p><img src="/25264-2342/1758458446210.jpg" alt="1758458446210"></p><p>另附上之前给对讲机改绿色背光和加USB转串口的飞线，type 16pin接口真的太小了，相比之下香橙派这个飞线可以说是入门级别了（</p><p><img src="/25264-2342/1758468518257.jpg" alt="1758468518257"></p><p><img src="/25264-2342/1758468552790-1758469496025-13.jpg" alt="1758468552790"></p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="硬件" scheme="https://aprilforest.cn/categories/%E7%A1%AC%E4%BB%B6/"/>
    
    <category term="软件" scheme="https://aprilforest.cn/categories/%E8%BD%AF%E4%BB%B6/"/>
    
    
    <category term="无标签" scheme="https://aprilforest.cn/tags/%E6%97%A0%E6%A0%87%E7%AD%BE/"/>
    
  </entry>
  
  <entry>
    <title>新数据存储方案</title>
    <link href="https://aprilforest.cn/25206-2215.html"/>
    <id>https://aprilforest.cn/25206-2215.html</id>
    <published>2025-07-25T22:15:32.000Z</published>
    <updated>2026-03-02T14:02:12.693Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>想给自己的数据正经安个家了，这不得马上整个NAS？</p><p>我平时的NAS使用场景是这样的：</p><p>首先是我拍摄的照片和录像文件，这些文件是相机记录而来，有很多记忆在里面，而且一旦丢失几乎没有重新获取的途径。好在这些文件不大，只有几个Gb左右。</p><p>然后我会存储一些游戏资源，比如美术素材，UI资源，音效或者代码插件等。这部分加起来还不少，大约100GB左右，不过就算不幸文件丢失，很多素材就算丢失也能从网络上找回，一些热门资源就更容易找到了。</p><p>还有我在网上冲浪收集来的各种资源，比如各种游戏和和一些好用的工具软件这些。这部分空间占用还挺多的，虽然大部分资源也能冲网上找到，但是它也和照片视频一样，承载了很多记忆。</p><p>最后是以笔记为主的热数据，不大但是访问频繁。我的习惯是使用纯本地化的markdown文件来记录各种知识和笔记，再加上各种网站和博客对md的支持都相对较好，这样无论在哪里粘贴笔记内容都能有比较好的展示效果。</p><p>有了具体的需求，就可以开始选择存储方式了。这些数据加起来200GB左右。一开始我是想组RAID 1的，但是转念一想，RAID保护的其实是硬盘本身，而不是里面的数据。也就是RAID 1它只能保护因为外界原因造成的硬件损坏而丢失的数据，而内部误删的数据是救不回来的。</p><p>对我来说，误删丢失数据的概率要远大于硬件故障丢失数据的概率，所以我想靠更加频繁的冷备份来提升数据安全。考虑再三我打算使用单块固态硬盘来做NAS的存储盘，然后定期做快照，再将快照备份到对象存储上。</p><p>快照可以很好的避免文件误删带来的数据丢失，而对象存储备份又可以很好避免硬盘损坏的问题，同时它又是数据中心级别的可靠等级，比我自己做321原则备份要方便的多。对象存储我只用来备份数据，平时只写不读，恰好对象存储入网流量又是免费的，相当于我有存储空间才计费，再加上我数据不大，所以成本完全可控。可能唯一的出网流量就是在数据损失后恢复的时候，相比丢失的数据的价值，这点流量费其实可以忽略不计。</p><p>NAS软件部分，我使用Truenas作为操作系统，它基于ZFS文件系统，而ZFS有COW机制，在数据修改量少的情况下，做快照几乎不会占用太多存储空间，这很适合我这样数据读多写少的使用场景。同时这样的快照还可以解决备份软件在备份过程中文件被修改所导致的数据不一致的问题，两全其美了属于是。Truenas还有一个数据巡检的功能，可以设定每周或者每天对硬盘的数据进行读取，然后算校验和，找出损坏的文件。毕竟数据备份时，最怕的就是写进去的时候是好的，然后过了很长时间再来读取发现文件损坏了。</p><p>NAS硬件部分，一开始我是打算买个小主机来当NAS用的，但是看了一眼价格，n100小主机基本都在500-1000价格之间。最后我选择把Truenas部署到了自己笔记本的虚拟机上，然后用这个预算给笔记本加装了一个2T的固态硬盘和32GB内存。然后剩余的预算都给到对象存储的存储空间资源包上。这样笔记本有电池，相当于UPS，不用担心意外断电。笔记本待机功耗足够低，优化后整机功耗可以低到10W左右，同时桌面端的处理器峰值性能也远超n100。</p><p>系统方面，Truenas总体还是非常好用的，自带很多文件共享协议，比如NFS，SMB，ISCSI等，这些都可以开箱即用。而且全称都是在WebUI里操作的，完全不用敲命令行（当然给Truenas配网络的时候除外）可能唯一麻烦的地方就是每次电脑开机后，要手动启动一下Truenas的虚拟机。</p><p>要问我对这个NAS有什么不满意的地方，那么就是传输速度了。在本机访问NAS的读写速度大约在1.3GB/s左右，很快。但是一旦从局域网访问，速度瞬间掉到130MB/S，只有本机速度的十分之一，这还是2400Mbps的WIFI。如果换成1000Mbps的有线网卡只会更慢。整个链路上其实网络才是瓶颈，固态这么快的速度完全发挥不出来，然后恰好机械硬盘的传输速度正好在100到200MB/s之间，就非常适合。</p><p>这样一个NAS就初步成形了，虽然比较野鸡，但也能满足我的需求，目前已经稳定运行大约4个月了。虽然目前是在虚拟机里运行，但是等到后面数据多了，也可以用很低的成本把Truenas迁移到物理机上，只需要导出配置配置文件，然后把数据复制到物理硬盘上即可。</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="硬件" scheme="https://aprilforest.cn/categories/%E7%A1%AC%E4%BB%B6/"/>
    
    
    <category term="NAS" scheme="https://aprilforest.cn/tags/NAS/"/>
    
  </entry>
  
  <entry>
    <title>参加业余无线电A类考试</title>
    <link href="https://aprilforest.cn/25194-2320.html"/>
    <id>https://aprilforest.cn/25194-2320.html</id>
    <published>2025-07-13T23:20:23.000Z</published>
    <updated>2026-03-02T14:02:12.685Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>这周末去参加业余无线电A类考试了。人很多，不少都是高考完的准大学生们。现场还有很多女生，比我想象的要多一些。</p><p>考试地点在天府广场旁边一点，坐电梯到25楼就是四川省无线电协会了，据说A类和B类考试都是在这里举办的。</p><p><img src="/25194-2320/1752415105731.jpg" alt="1752415105731"></p><p>考试内容倒不是很难，就是不停刷题，和考驾照一样都是选择题。但是A类考试的题目数量只有365道，没有科目一那么多，其中很多还是初中物理和生活常识。虽然我第一次过来参加考试比较紧张，但也还是是顺利满分通过了考试。</p><p><img src="/25194-2320/1752415105725.jpg" alt="1752415105725"></p><p>然后我的第一个设备（对讲机）是国产百元神机UVK6了。便宜不贵，性能够用，支持TypeC充电，而且网上有很多三方固件可以刷机，玩法可以扩展不少。</p><p><img src="/25194-2320/IMG_20250713_222505.jpg" alt="IMG_20250713_222505"></p><p><img src="/25194-2320/1752417227898.jpg" alt="1752417227898"></p><p>其实我很早就知道有无线电这个爱好圈子了。但为了确认自己真的对无线电感兴趣，而非三分钟热度。我花了大概1个月的时间来仔细了解业余无线电，是干嘛的，玩什么，以及怎么玩。每天都会看各种UP主们的视频来深入了解。确定是真的喜欢之后再参加考试报名入坑。</p><p>然后我就错过了最近的4月份的考试，又恰好碰到十年一遇的业余无线电操作证换新证和题库更新这种大事件，时间一等就来到了7月份。本来还计划今年内把B证也考下来，目前看来要到明年了。</p><p>目前考完就等着下发操作证了，然后赶紧去设台，申请呼号哈哈。如果有HAM看到这里，也希望有天能够和你在空中相遇，73。</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="硬件" scheme="https://aprilforest.cn/categories/%E7%A1%AC%E4%BB%B6/"/>
    
    
    <category term="业余无线电" scheme="https://aprilforest.cn/tags/%E4%B8%9A%E4%BD%99%E6%97%A0%E7%BA%BF%E7%94%B5/"/>
    
  </entry>
  
  <entry>
    <title>运气最好的一年</title>
    <link href="https://aprilforest.cn/25034-2233.html"/>
    <id>https://aprilforest.cn/25034-2233.html</id>
    <published>2025-02-03T22:33:30.000Z</published>
    <updated>2026-03-02T14:02:12.697Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>二四年是我觉得运气最好的一年。虽然也有一些烦心事，但还是好事占多数。</p><h3 id="开心事工作"><a class="markdownIt-Anchor bubble-link" href="/25034-2233/#开心事工作"></a> 开心事——工作</h3><p>今年我（凭狗屎运）谋到一份不错的事业，公司氛围好，同事关系也不错，老板处处为我们谋福利。虽然是小团队但待遇一点不比大厂差。</p><p>还记得我2月份出来找工作的时候，投了很多家，连一次面试都约不上，每家投递时我都会根据需求调整简历内容和打招呼的话语，但可能付出越多期待也就越高吧，看着一个个在别人身上好使的求职技巧在我身上都不起作用时，多少有些崩溃。虽然我有预期第一次出来找工作不会太容易，但实际面对时，还是有被打击到。能找到现在这个公司，我已经很知足了，不敢再奢求更多了。</p><h3 id="庆幸事健康"><a class="markdownIt-Anchor bubble-link" href="/25034-2233/#庆幸事健康"></a> 庆幸事——健康</h3><p>其次是身体健康方面，做手术时偶然查出我尿酸高，已经到达临界点了，差一点就要痛风发作了。医生说我运气好，提前发现问题，要我注意饮食，给我开了药。现在情况好多了，如果下次复查还没有问题，就可以减少药量了。</p><p>身边的朋友一直跟我抱怨自己身体又怎么了怎么了，又要去医院看病拿药啊什么的。好像现在大家的身体都多多少少有些问题。相比起来我自己的问题反而不值一提，非常庆幸我能够提前发现健康问题。</p><h3 id="重要事学业"><a class="markdownIt-Anchor bubble-link" href="/25034-2233/#重要事学业"></a> 重要事——学业</h3><p>我的出身不太好，在一众求职竞争者里，是属于最先被刷学历的那一批。不过今年自考专升本终于考完毕业了，考了很多年了，终于结业了。之所以选择自考而不是全日制是因为有大把的时间可以自由支配，平时在家里想干啥干啥，想怎么安排时间就怎么安排，再加上家里人也理解我。所以那几年是我过的最舒服的几年，该玩的游戏都玩了，该考的证都考了，该学的知识也都学了。</p><p>可能从旁人角度来看这个学历含金量可能不如全日制的，但我觉得相比那个学历来说，时间才是更珍贵的东西，那点破含金量算个啥。虽然平时在家里没工作，但我觉得这几年里是我学到东西最多的时期，也是成长最快的时候。这里非常感谢我的英语老师，教会我学习的重要性和学习的方式，我觉得这是比知识本身更重要的东西。</p><p>同时大把的自由时间也给了我思考的空间，有很多道理都是那个时候想明白的。经常就是遇到一件蹊跷的事情，当时不明白为什么，就不断在心里琢磨，然后猛然发现原来当时对方的用意是这个，就突然一下子醒悟过来，非常后悔不能早点看明白背后的用意。</p><p>现在出门去上班了，每天都是各种公司的事情，学习也是毫无进展，博客也是停更了好久。总之就是很累，连游戏都不想打，只想看看不费脑子的视频来休息休息。不过虽然累吧但是目前的工作条件算是非常好了，我很喜欢这里，不敢再奢求更多了。</p><h3 id="麻烦事公司"><a class="markdownIt-Anchor bubble-link" href="/25034-2233/#麻烦事公司"></a> 麻烦事——公司</h3><p>既然有好事，那么必定也有烦心事。</p><p>首先就是：世界是由草台班子构成的，然后就是总有人嫌现在工作太好找（万一人家是富哥出来体验社会底层人民的生活的捏？）。</p><p>自从我来到这个公司，就觉得项目进度不太对劲。为什么4个月过去了，项目进度像刚立项1周的样子？为什么代码里全是bug，完全不能正常运行？一开始我还想着公司有几位大佬坐镇，我可得好好学习一下代码技巧，毕竟平时我都是一个人做开发，很少与其他开发者合作。这次是个学习的好机会。</p><p>然而随着我来到公司的时间长了，我开始变得不相信任何人，只有自己做的东西才是最可靠的。同事经常出些馊主意，怎么看都不是很靠谱的方法（属于早晚要还技术债的那种）。但我是组里最晚来的员工，有些事我只能给建议，而没法强制推行或者去争论不休，这是职场最基本的道理。事后我只能眼睁睁看着事情往坏的方向发展，自己什么都做不了，这种感觉很难受。</p><h3 id="教训事看病"><a class="markdownIt-Anchor bubble-link" href="/25034-2233/#教训事看病"></a> 教训事——看病</h3><p>今年八月，我痔疮犯了，血流不止，医生建议手术。但这是我长这么大第一次住院，我很怕。医生要我去做检查，我脚在往前走，但是脑子却想要身体拼命往后退，感觉身体和大脑在不停打架。</p><p>手术当天，医生过来叫我的床号，吓得我一激灵，特别特别慌，脑子一片空白，突然不知道该干什么了。我有点想跑，是因为长这么大第一次做手术，真的好害怕好害怕。我想硬着头皮上，是因为没有退路了，我的病情太严重了，每天都过得很痛苦，想早点结束这一切。</p><p>进入患者通道，走廊空调开的好低，像冬天一样。卧槽好冷，本来就很害怕了，一遇到寒气更吓人了。我先被推到麻醉室打麻药，然后护士再把我抬上手术室。好家伙，还好不是要我亲自躺上去，那不得更吓人了。手术过程反到还好，很平淡，完全不痛，大概1个多小时就好了。</p><p>出院后我就老实了，再也不敢吃辣和久坐了，因为知道痛了。</p><h3 id="担忧事人会老"><a class="markdownIt-Anchor bubble-link" href="/25034-2233/#担忧事人会老"></a> 担忧事——人会老</h3><p>每次回老家走亲戚我都不缺席，这几年，老家发生了很多变化。</p><p>村里年轻人，或者说我的同龄人，都见不到面了。他们过年会选择出去和同学玩，不再愿意和老一辈去走亲戚了。不知道是大家长大了，还是时代变了。</p><p>村里的老人，也在渐渐的过世。从一些我不认识的远房开始，到熟悉的亲戚，心里是越来越不好受，每年都要走人。经常听到爷爷奶奶说村里谁谁谁走了，可没办法，岁月不饶人。</p><p>有一些亲戚，是因为老一辈在，每年才会互相走动的。我在想，万一老一辈不在了，会发生什么，不敢想，完全不敢想。</p><p>每次回老家我都会跟着去，就算是坐沙发上一个人玩一天手机我也是愿意的，至少能在聚桌吃饭的时候，还能看看大家，和大家说说话，多留个印象。</p><p>今年我想买个相机，再回老家时，把遇到的事物都拍成视频，十多年还能看到。</p><h3 id="期待事新年展望"><a class="markdownIt-Anchor bubble-link" href="/25034-2233/#期待事新年展望"></a> 期待事——新年展望</h3><p>过去的一年里，我一直待在舒适圈里，舒适圈确实很舒服。但每过一年，我都会对自己有更高的期待和要求，也是该走出新手村去迎接新的挑战了。</p><p>首先要做的事情就是收窄我的学习或者折腾的范围，过去的几年里我尝试了很多我可能感兴趣或者想学习的东西，但现在我已经找到自己的前进方向了，我希望花更多时间精力在上面。</p><p>首先就是mcpatch的更新频率会变的很低。这里知道这个的人很少，我就有什么说什么了。这个项目起因是我15年开服的时候，发现mod更新很困难，所以做出的一个小插件。后来慢慢做起了规模，用户数也越来越多，口碑也越来越好。但我一直没有往盈利方面想，只是一直在用爱发电，不停更新。等到有人提醒我做付费版的时候，已经有些晚了。已经开源的软件基本不太可能闭源，而靠卖服务我时间又很亏。开发这个项目的收益远远低于我上班的收入，一直爱发电是不可能的，但我又不想它变味，所以只能选择“倒闭”，也就是缓更，我会保持一个很低的频率发布新版本，我想把更多的精力留给我期望发展的方向上去。如果有认识的人看到这里，还请不要把这个文字截图发到群里，谢谢你的理解！</p><p>我腊月29才从公司回到家里，腊月三十除夕一整天，我都在做v2的Java版移植工作，甚至春晚也没有去看。虽然嘴上说着什么把核心逻辑套壳一下就完事了，但为了保证代码的结构清晰，我把客户端所有的代码都手工移植了一遍，连肝了三四天总算是完成了第一个版本 。</p><p>独立游戏</p><p>另外就是新的一年里，我想做一个独立游戏（Demo），具体的题材先不透露了，但体量应该不会太大。我希望第一款作品能够顺利做完，质量只要过关就行。本来去年就已经开始了，但是临近年关变忙了，无奈先暂停了。今年希望能顺顺利利完成它，踏出去的第一步很重要。</p><h3 id="新年快乐"><a class="markdownIt-Anchor bubble-link" href="/25034-2233/#新年快乐"></a> 新年快乐</h3><p>这是一份晚到的年度总结，但各种事情忙的我晕头转向，直到年快过完了才腾出一段时间来。</p><p>希望新的一年里我能完成对自己的期待，也希望大家的工作和生活都顺顺利利的，事业一帆风顺，身体健康健康。</p><p>最后给看到这里的各位小伙伴拜个晚年，祝大家新年快乐，万事如意！</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="见闻" scheme="https://aprilforest.cn/categories/%E8%A7%81%E9%97%BB/"/>
    
    
    <category term="无标签" scheme="https://aprilforest.cn/tags/%E6%97%A0%E6%A0%87%E7%AD%BE/"/>
    
  </entry>
  
  <entry>
    <title>第一次来Comiday28漫展</title>
    <link href="https://aprilforest.cn/24284-1117.html"/>
    <id>https://aprilforest.cn/24284-1117.html</id>
    <published>2024-10-10T11:17:15.000Z</published>
    <updated>2026-03-02T14:02:12.693Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>国庆，托公司的福，去了一趟漫展。</p><p>第一次去漫展，多少有些新鲜，各种展子和Coser小哥哥小姐姐们很是热闹。</p><p>这次我们是作为摊主的身份过来宣传我们的独立游戏的，大部分时候需要接待客人，只有休息的时候可以去逛展子。</p><p>我负责布置现场的设备，包括电视机播放PV，准备好笔记本电脑运行试玩的Demo。</p><p>没想到第一天早上就出了岔子，前一天晚上在公司调好的电脑，到了现场就不充电了。</p><p>我一下子就紧张起来了，要是没有充电器，这个游戏本最多只能坚持1个小时，加上充电宝也只能增加2小时的续航。</p><p>好在开启电脑的省电模式后就可以充电了，也幸亏游戏Demo不吃性能，用核显+省电模式也能跑到80多帧。悬着的心终于放下了。</p><p><img src="/24284-1117/_1728352020522.jpg" alt="1728352020522"></p><p>后来发现是充电器功率不够，只有130w，但是我使用了一根带诱骗功能的电源线，会让电脑误以为接的是230w的原装充电器，所以电脑就火力全开，触发了充电器的过载保护。</p><p>然后来了漫展肯定要逛展子嘛。首先要去的肯定是BA的展子了，虽然不大，但是人相当的多。后面还有许多漂亮的Coser小姐姐，但是我这老社恐了，也没敢上去合个影。</p><p>走着我还发现有OMORI，好家伙，这么小众的游戏竟然也被我发现有展位了，一定要过去看看。最后带了两个立牌走了，因为还能不能有下次就不一定了。</p><p><img src="/24284-1117/_1728352020495.jpg" alt="1728352020495"></p><p>最后发现我们展子对面是一个工作室，电视机播放的一个作品的Demo我感觉很有意思。作品的名字叫做《你怎么不笑啊》。</p><p><img src="/24284-1117/_1728352020499.jpg" alt="1728352020499"></p><p><img src="/24284-1117/_1728352020510.jpg" alt="1728352020510"></p><p>美术风格很吸引我。是上世纪上美厂的风格，有点类似《大耳朵图图》的卡通风格。再加上绿色背景氛围，有种中式恐怖的感觉。就类似《三伏》里那种感觉。一下子拉高了我不少好感。</p><p><img src="/24284-1117/_1728352020516.jpg" alt="1728352020516"></p><p>游戏的UI是纯白的线框和文字组成的，背景里还有各种眼睛，太像《OMORI》了，这种感觉太熟悉了。要说作者没有玩过这个作品我是不信的。</p><p><img src="/24284-1117/_1728352020504.jpg" alt="1728352020504"></p><p>过去问过才得知，这个作品目前并没有上架Steam商店，这是作者的GameJam作品，更多的是像作为展示。不过他们有电脑可以试玩，我高兴的差点蹦起来。</p><p>但是实际上手玩过之后，发现这个demo的bug很多，主线都会卡住，重启三四次还是会卡住。哎哟，心里那个可惜哟。</p><p>最后，第二天漫展快要结束的时候，隔壁的《犹格索托斯的庭院》展位突然开始抽角色展牌哈哈哈，好多人都在排队，好不热闹。我们这边因为认识嘛，直接给了我们一个展牌，大家都想要带来公司，要不是太大了，没有地方放，肯定得带回来一个，哈哈哈哈。</p><p><img src="/24284-1117/_1728352310818.jpg" alt="1728352310818"></p><p><img src="/24284-1117/_1728352020487.jpg" alt="1728352020487"></p><p>放上BA的立牌收尾，虽然不是漫展上买到的（</p><p><img src="/24284-1117/_1728352020491.jpg" alt="1728352020491"></p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="游戏" scheme="https://aprilforest.cn/categories/%E6%B8%B8%E6%88%8F/"/>
    
    
    <category term="Comiday28" scheme="https://aprilforest.cn/tags/Comiday28/"/>
    
  </entry>
  
  <entry>
    <title>小刀拉屁股</title>
    <link href="https://aprilforest.cn/24244-2023.html"/>
    <id>https://aprilforest.cn/24244-2023.html</id>
    <published>2024-08-31T20:23:31.000Z</published>
    <updated>2026-03-02T14:02:12.689Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>上厕所时发现有鲜红的番茄酱，连滴带喷，把我吓坏了。医生说是痔疮犯了，给了我一些外用药，但是一周过去了，并没有任何效果。</p><p>遂向公司请假，打算回家做手术解决。</p><p><img src="/24244-2023/1725107324400.jpg" alt="1725107324400"></p><p>因为时间比较赶，加上火车没有票，就选择了坐飞机。行程是从双流出发，还是挺快的，大约1个半小时就抵达天河了。然后再地铁转到汉口站，再坐火车回家。</p><p>第一次做飞机，感觉航站楼真的好大，比火车站要大得多。而且因为是飞机，安检也会特别严格，包里每一件电子产品都要单独拿出来。但总体来说和坐火车区别不是很大，在手机上提前选座值机之后，进去可以往安检走，安检通过后就到对应登机口等待。然后跟随摆渡车登机。</p><p><img src="/24244-2023/1725107324378.jpg" alt="1725107324378"></p><p>我坐的这次航班的飞机是A320窄体客机，里面空间没有火车那么宽阔，不过我是回来看病，也不挑剔这些了。</p><p><img src="/24244-2023/1725107324389.jpg" alt="1725107324389"></p><p>到家后去医院，医生说我的情况已经是三期了，建议尽快手术。我也想手术解决，因为就算这次用药控制住了，后面还会再犯的，到时候我总不能再回家一趟，那样很折腾人。</p><p>医生给我安排住院，我边走边想，以前来医院都是陪家里人来，住院的都是家人或者亲戚，但是这次TM换成是我自己了。我是个很怕去医院的人，心里多少有点毛。</p><p>住院当天医生就开了泻药，还要灌肠的药。泻药非常难以下咽，有点像电解质的味道，反正很怪，很怪，喝完后还要大量喝水。我甚至还喝吐了，没办法又找医生开了一瓶药重新喝，难受加倍。</p><p>泻药是用来清理肠道的，将食物残渣全部排干净。喝药之后再喝水基本上就不会有尿意了，水会全部进入肠道，从屁股里排出去。前前后后跑厕所跑了很多次，新开的半卷卷纸，半天就用了快一半。而且因为反复跑厕所，把我的痔疮也弄的很严重了，特别的不舒服，感觉一直是肿的。</p><p>肚子排空后，就是做肠镜检查，多的不说了，住院期间最难受的项目，NM术后拉屎都没这个疼。我TM终于知道为什么会有无痛肠镜这个检查项目了。</p><p>转天安排手术，我是第三场。大约上午9点的样子，小推床到病房门口来呼我的名字，换病号服，脱鞋，躺上去，盖好深绿色的厚被子，医生推我进电梯，我们走手术专用电梯，前往手术室。</p><p>接着推我进入手术中心患者通道，进去之后走廊很冷，空调温度开的很低。里面错综复杂，很多医生和病人从我身边经过，不知道走了多久，到了麻醉大厅。为什么说是大厅，因为里面真的很大很大，有非常多的仪器和设备，我则被推倒靠墙角的地方停下。</p><p>麻醉医生过来和我确认身份后，让我侧躺，往背上打麻药，然后我就在这里等着。里面真的很大，不停有医生进进出出，大概二十多个人，我听到旁边的医生跟患者说，你是谁谁谁吗？你已经醒了，手术很成功，一会就把你送回病房。看来旁边的患者是打的全麻，做大手术。</p><p>到点了。医生再次过来把我推出麻醉大厅，又是推着我走了很久很久，才到达手术室。电动门打开之后，大家都在里面等我。除了主治医生和助手以外，还有两位护士，把我搬上手术台之后，手术就正式开始了。其实里面人没有想象的那么多，手术过程中，就是主治医生和实习生两个人在操作，护士走了一个，还有一个则蹲着旁边玩手机，氛围没有那么紧张，很轻松的。</p><p>手术台很窄，顶多半米宽，我两双小手都快无处安放，左臂在输液，左手还抓着一个氧气管要放在鼻子旁边。右手则绑着血压监护，必须平放。</p><p>手术过程其实到还好，也不怎么疼。没有我想象的那么吓人，就很普通的躺着就好了。</p><p>回到病房后，就开始各种打针，也就逐渐开始有尿意了。但我麻药没有消散，怎么都尿不出来，就让医生插了尿管，倒也还好，不像网上说的很疼很疼，也就一丁点不适而已，完全没有网上说的那么唬人。</p><p>我听别人说，术后为了防止伤口长到一起，都会塞一个纱布，可能会有异物感。但到我就不一样了，我是直接插管子，好家伙前一个管子后一个管子的，我基本上就告别下床了，只能躺床上，连起身都困难，顶多侧躺一下。</p><p>48小时之后就可以拔管子了，也可以四出走动了。刚从床上起来那一下，天旋地转，眼冒金星，站都站不稳，还是我爸扶着我一点一点往换药室挪的。真的是一点一点挪，50米的距离硬是走了5分钟。</p><p>后来在医院修养，可以到处走路活动了，就是不停打针，然后正常饮食。要出院的话，还需要大便正常解才可以。哇，第一次上厕所，那真的叫小刀拉屁股，疼的直接跳起来，双腿打颤，蹲不下去。还不能用力，用力伤口又会滴血，总之怪麻烦的。</p><p><img src="/24244-2023/1725107324356.jpg" alt="1725107324356"></p><p>听我爸说，手术室外的家属等待区，有一个显示屏，上面有每个病人的状态，比如正在麻醉，正在手术这些。我那天大概有四五十人同时在进行手术，而我因为是小手术，连名字上显示屏的资格都没有。好家伙，感情这里面是真的大。后来我散步过去时，发现旁边还有一个家属谈话间，里面医生正在跟几个家属说些什么，我不敢凑太近，怕晚上睡不着觉。</p><p>直到出院，一直都是我爸在医院照顾我，哪里都不能去，只能守在病房里，在医院待着，好人都能给你弄成病人，我爸也是很辛苦。</p><p>总之这一趟下来，再也不想去医院了，真的很折腾人。大家也记得多喝水，多活动，记得提肛，不要久坐，祝各位身体健康，不生病！</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    
    <category term="无标签" scheme="https://aprilforest.cn/tags/%E6%97%A0%E6%A0%87%E7%AD%BE/"/>
    
  </entry>
  
  <entry>
    <title>出门去上班了</title>
    <link href="https://aprilforest.cn/24196-0209.html"/>
    <id>https://aprilforest.cn/24196-0209.html</id>
    <published>2024-07-14T02:09:47.000Z</published>
    <updated>2026-03-02T14:02:12.689Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>博客断更了。我出门工作去了，第一个月总是最忙的时候。这也是我第一次找工作，不敢有丝毫松懈。</p><p>自从毕业后，我就回到家里了。因为所学的专业不是我感兴趣的，所以就一直在家里自己自学，直到现在才找到我的第一份工作。</p><p>这份工作虽然也是写程序，但又有些特别，这是一份游戏的编程工作。</p><p>自从20年之后，经济变得不景气了，一份保安工作都变得那么抢手。</p><p>所幸的是，即使是这样的大环境下，我还是找到了一份不错的工作。</p><p>我在这里负责给独立游戏开发程序部分，我的旁边坐的是文案和策划组，对面则是美术组。我则负责修复游戏的bug和开发新的功能和玩法。游戏虽然已经开放了Steam的试玩版，但目前还没有打磨好，我就不放出具体名字了哈。</p><p>这是一个成都的小公司，人不多，但团队氛围很友好，虽然每天都忙到很晚才歇息，但我很愿意加这个班。真的很开心能遇到这么好一个团队。</p><p>新品节那天，我们程序和策划组直接忙了一个通宵，连夜修复各种bug，到早上9点多才睡。很累，但感觉很像独立游戏人真实的写照。</p><p>一次偶然的机会，遇到Unity User Group活动，离我这里也就十几分钟的车程。还是头一次到现场听大佬们分享经验，感觉超级开心和激动。饭盒群里的几位巨佬也到场了，坐在第一排，但是我比较社恐没敢过去打招呼2333。</p><p>头一次来大城市，能遇到很多线下活动可以参与，还是蛮开心的。</p><p>下周好像BA和罗森有联动，成都就正好有一家主题店。虽然离我挺远的（40分钟地铁），但很想去看看打个卡，还在犹豫中~</p><p>刚入职的时候，还想着等一段时间熟悉代码后，应该就有多的空闲时间更新博客了。</p><p>但没想到的是，越是熟悉代码，活是越来越多23333。不过忙点也没什么不好的，毕竟团队氛围好，我是真的很情愿去做这个事情。不是被逼的喂(#`O′)</p><p>很晚了，先写到这里，以后更新更新频率会变慢，但我会尽量保持每月一更，可能会写点日常生活，也可能会整理一些技术笔记，具体看心情啦。</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="见闻" scheme="https://aprilforest.cn/categories/%E8%A7%81%E9%97%BB/"/>
    
    
    <category term="工作" scheme="https://aprilforest.cn/tags/%E5%B7%A5%E4%BD%9C/"/>
    
    <category term="游戏" scheme="https://aprilforest.cn/tags/%E6%B8%B8%E6%88%8F/"/>
    
  </entry>
  
  <entry>
    <title>垃圾回收的工作原理</title>
    <link href="https://aprilforest.cn/24140-2147.html"/>
    <id>https://aprilforest.cn/24140-2147.html</id>
    <published>2024-05-19T21:47:54.000Z</published>
    <updated>2026-03-02T14:02:12.689Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>程序运行时，每个变量都会找系统“借”来一块内存空间来存各种数据，使用完毕后再“还”给系统。系统会把还回去的内存又重新分配给别的变量去使用，如此循环。</p><p>既然是借来的，那就要有借有还。如果内存管理不当，申请内存的时候嘎嘎借，用完又不还给系统，时间长了，系统手上的空闲内存也会被消耗完，然后程序就崩溃了。</p><p>上面这个问题就是大家常说的“内存泄露”，这是一个很严重的bug。为了解决这个问题，编程语言分成了两个流派：手动管理派和自动管理派。</p><p>手动管理派的宗旨是“内存管理很重要，必须要交给人来管理”。其代表就是C/C++，所有的内存（严格来说是动态分配的内存）都要开发者亲自去new，用完再手动delete掉。</p><p>但人嘛，总有犯错的时候，特别是代码规模大起来之后，new语句和delete语句经常不在一个同函数里，稍微疏忽一下就会出现new了但是忘记delete，然后就内存泄露了。此时程序不会像除以零那样有明显的报错崩溃，而是直接忽略这个错误，代码继续往往前跑，就像什么事都没发生一样，时间久了就会吃光系统所有的可用内存，然后程序申请不了到新的内存了，就崩溃掉了。</p><p>后来随着计算机性能的发展，出现一种了自动管理内存的流派，宗旨是“内存管理很重要，人是会犯错误的，所以必须要交给计算机来管理”。其代表是C#/Lua/Python/Golang/JavaScript/Java这些语言。</p><p>这些语言的特点就是不用再像C/C++那样需要自己去delete内存了，写代码时只管new就好了，怎么爽怎么来。变量不用了直接置空，会有垃圾回收器去帮你自动释放内存。开发者的心智负担也降低了不少，可以把更多的精力放在开发业务上面。但是代价嘛，就是会降低一丢丢运行时的性能。</p><p>后来还出现了第三个流派，它既不需要开发者自己去new和delete内存，也没有运行时垃圾回收器会占用性能。可谓是独树一帜，程序跑的又快，内存又安全。不过这个不是今天的重点。</p><hr><p>是第二种流派：自动内存管理派。</p><p>自动内存管理派里面其实也分两种策略：引用计数和根搜索（也叫引用树查询），这两种策略都可以达到自动内存管理的目的，只是实现方式有所不同而已。</p><p>目前使用比较多的都是根搜索策略，C#/Lua/Js/Java这些都是基于根搜索的自动内存管理，而Python是基于引用计数+根搜索相结合的策略。</p><p>一般来说，只要提起“垃圾回收”，大家默认都是指“根搜索”策略。</p><h3 id="1引用计数reference-counting"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#1引用计数reference-counting"></a> 1.引用计数（Reference Counting）</h3><p>引用计数其实是自动内存管理中，一个很特殊的存在，它和其它基于根搜索的回收方式有很大的不同，所以单独把它拿出来。</p><p>它的原理和它的名字一样，就是给引用的数量做一下统计。举个栗子，老师告诉教室里的同学：最后一个走的人要关灯。那么每个人走的时候，只要教室里还有别人，就不能把灯关掉。如果没有别人了，只剩自己一个了，就要关灯。如果教室里来了新人，也要把这个规则告诉这个新人，这样大概就是引用计数的原理了。</p><p>在程序内部，也会有一个变量值来记录引用数量。也就是当前的内存对象，一共有多少其它变量仍在引用。每增加一个引用就+1，每减少一个引用就-1。减到0就释放掉这个内存。</p><ul><li>优点：内存回收时机是确定的，且几乎没有暂停时间</li><li>缺点：循环引用问题和更新引用计数的开销</li></ul><p>我个人认为引用计数并不算狭义上的的GC，因为它没有独立的垃圾回收线程（根搜索是有的），内存回收时间是可预期的，且不会有暂停时间。</p><p>引用计数看起来是一个很完美的垃圾回收方案，原理简单可靠，回收时机也是确定的。但实际上它有一些比较大缺点，导致它无法流行开来。</p><p>首先最明显的就是循环引用问题，如果A，B两个对象都用引用计数做自动回收。此时A引用B，B又引用A。它会导致本应该被回收的两个对象，互相卡着对方无法满足释放条件（即引用数量降到0），然后就成了内存泄露。</p><p><img src="/24140-2147/1.png" alt="QQ截图20240518200657"></p><p>解决方法是将这两个强引用中的一个替换为弱引用，不过弱引用的引入又会增加使用上的复杂度，使用不当又会造成不该被释放的对被提前释放了，后续的访问又会出现悬垂引用的问题（Dangling Pointer）。而基于根搜索的回收方案，在进行垃圾回收是有状态的，会记录扫描过的所有对象，所以天然可以解决循环引用的问题。</p><p>这是一点，还有一方面是每次更新引用计数时，会有额外开销：（<a href="https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Reference_counting" class="bubble-link">来源 Wikipedia</a>）</p><ol><li>更新引用的操作必须要是原子的，因为引用计数必须要保证线程安全，而原子操作比如原子加，原子减操作，比普通的非原子加减操作的开销都要大。特别是遇到修改引用计数特别频繁的地方，这个开销会进一步放大，开始变得不能忽视了。而对基于根搜索的策略来说，这个开销是零。</li><li>容易导致缓存失效（Cache Miss），因为每次更新引用计数，都要从内存中读取原始引用数量的值。这会导致CPU缓存失效，每次读值都要去访问主存。CPU一级缓存的访问大约1-2个时钟周期，而访问主内存则要上百个时钟周期，效率上差了一个数量级。</li></ol><p>这两个缺点也导致了引用计数垃圾回收没法流行开来，尤其是循环引用的问题，相比后者的性能问题更加严重。</p><h3 id="2根搜索策略tracing-garbage-collection"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#2根搜索策略tracing-garbage-collection"></a> 2.根搜索策略（Tracing Garbage Collection）</h3><p>根搜索策略是从所有根对象（Root Objects）开始，搜索它们都直接引用了哪些对象。然后再搜索这些对象又引用了哪些对象，直到遍历到对象引用树的尽头为止，这个过程叫可达性分析（<a href="https://en.wikipedia.org/wiki/Tracing_garbage_collection#Reachability_of_an_object" class="bubble-link">Wikipedia</a>）。</p><p>接着所有被遍历到的对象，会被标记为存活对象，而哪些没有被遍历到的对象，则是要回收的对象。</p><p>这很好理解，首先根对象包括：函数栈帧（细分为局部变量和方法形参和寄存器），全局变量。</p><p>如果某个对象没有被局部变量、方法形参、寄存器、全局变量中任意一个引用的话。那我们在代码里是无法直接访问到这块内存的（直接按固定的内存地址访问的方法不在讨论范围内），既然我们无法访问这个对象，那它存在就没有意义了，就是一个要被回收的内存垃圾。</p><p>垃圾回收器在每分配一个内存对象时，都会把这个对象的信息加到一个列表里。这样垃圾回收器就可以统计出自己一共分配了多少个对象和查询到每个对象的地址和长度等信息了。</p><p>然后在进行对象的可达性分析时，又会把所有可达的对象放到另一个列表里。遍历完所有对象后，只要对比这两个列表，就能找出哪些对象不可达了，接着把它们回收就好。（当然在实际的实现中不会真的去分配一个新列表来存储可达对象，而是会用一些优化手段来做到相同的效果，文章这里只是举个栗子）</p><p>垃圾回收器在遍历对象的时候，会给每一个遍历过的对象打一个标记，代表已经遍历过了，无需再遍历。这个设计很好地避免了引用计数中循环引用的问题。但遍历的过程本身又多出了一些其他的问题，这个后面再讲。</p><p>接下来介绍几种常见的，基于根搜索的垃圾回收算法。</p><h3 id="21标记清除mark-and-sweep"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#21标记清除mark-and-sweep"></a> 2.1.标记清除（Mark And Sweep）</h3><p>标记清除的回收过程很简单，首先进行可达性分析，找到要回收的对象后，将其内存释放掉即可。（这里引用知乎大佬<a href="https://zhuanlan.zhihu.com/p/297965515" class="bubble-link">子非鱼</a>制作的示意图）</p><p><img src="/24140-2147/2.jpg" alt="v2-fce0c5e32eabdf7630356b00583fdd91_r"></p><p><img src="/24140-2147/3.jpg" alt="v2-d759ec846ee4ae994d7bb7443426251b_r"></p><p>标记清除算法在每次回收完毕后，会记录下每个空档的位置和大小，并且全部放到一个链表里，叫“空闲链表”，等到下次要分配对象时，会优先从空闲链表里进行分配。</p><p>然后就会发现有个问题，存活的对象东一个，西一个，隔得很开。如果此时再要分配一个很大块的内存，可能没有任何一个空档能放得下了。</p><p><img src="/24140-2147/4.jpg" alt="v2-bee1c3619918dd0e36a66e2914070c6d_r"></p><p>那么此时垃圾回收器就只能再去找操作系统再要一块新的内存区域来分配这个很大的内存了。这个内存块不连续的问题，就叫“内存碎片化”，会导致内存的利用率降低。</p><p>标记清除的缺点很明显：内存碎片化、分配速度慢（因为每次要遍历空闲链表）</p><p>同时它也有优点：没有内存移动和复制的开销（毕竟每次整理内存时的复制啊，移动啊，多少都是会有性能开销的）</p><h3 id="22标记压缩mark-and-compacting"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#22标记压缩mark-and-compacting"></a> 2.2.标记压缩（Mark And Compacting）</h3><p>标记压缩和标记清除类似，但是会在每次回收完对象之后，将存活对象进行整理，然后把他们挪到一起。</p><p><img src="/24140-2147/3.jpg" alt="v2-d759ec846ee4ae994d7bb7443426251b_r"></p><p>对象回收完成后出现大量的细小的空间，这些空间不连续。</p><p>接着把这些存活的对象全部移动到一起，这样后面的空间就连续了，可以放下更大的对象了，空间利用率就提高了。</p><p>优点是可以避免内存碎片化。缺点是每次移动对象都有开销。</p><p><img src="/24140-2147/5.jpg" alt="v2-f5ca808c77ba507f051c655c27edc69d_r"></p><h3 id="22复制算法copy-collection"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#22复制算法copy-collection"></a> 2.2.复制算法（Copy Collection）</h3><p>复制算法的回收过程和上面两种方法略有不同。首先将整个内存区域分成两半，左边一半和右边一半。</p><p><img src="/24140-2147/6.jpg" alt="v2-4f762144140b2ac0450f5527eb533d6d_r"></p><p>然后每次只使用其中一半，另一半先空在那里。每次回收时，会交换使用两个区域。也就是说这次使用左半边，执行垃圾回收后，就会改为使用右半边，再回收时又会用回左半边，如此循环。</p><p>假设现在正在使用左半边的内存。</p><p><img src="/24140-2147/7.png" alt="v2-6b57db231600de94f23308d1036fc9a7_720w"></p><p>当触发回收时，会将所有存活的对象从左半边复制到右半边。</p><p><img src="/24140-2147/8.jpg" alt="v2-b654b978f683f78472a4974c21dce6f7_r"></p><p>接着将左半边内存全部释放掉，新分配的内存就开始存到右半边，然后等右半边回收的时候，再把存活对象重新复制回左半边，就像这样左右两边循环使用。</p><p><img src="/24140-2147/9.png" alt="v2-f44b5701bfb4f59b7b360364101fc323_720w"></p><p>复制算法的优点是存活对象少的时候，回收效率高，因为要复制的对象就变少了。如果所有对象都存活了，那么就要把所有对象全部复制一遍，怎么看都很亏。</p><p>缺点除了上面提到的，存活率高的情况下回收效率不高以外。还有内存空间利用率低的问题，因为复制算法每次只能使用一半的内存，还有一半就只能预留在那里，没法使用。</p><p><img src="/24140-2147/10.jpg" alt="v2-f7c0a21bf9354502293ab9bde5649670_r"></p><h3 id="分代垃圾回收策略generational-gc-or-ephemeral-gc"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#分代垃圾回收策略generational-gc-or-ephemeral-gc"></a> 分代垃圾回收策略（Generational GC or ephemeral GC）</h3><p>准确来说，分代GC不是一个具体的回收算法，而是一个策略：针对不同特点的内存对象，分别应用不同的垃圾回收算法。</p><p>根据观察（出处：<a href="https://en.wikipedia.org/wiki/Tracing_garbage_collection#Generational_GC_(ephemeral_GC)" class="bubble-link">Wikipedia</a>），刚创建的新的对象最容易被销毁，这些对象大多都是一些临时对象，用完就会丢弃的那种，这些对象叫“年轻代”的对象。</p><p>而经历过几次回收过后，仍然存活的对象，更加不容易销毁，这些对象往往是一些静态变量或者全局对象，生命周期特别长，这些对象叫“老年代”对象。</p><p>这些存活的年轻代的对象会进入下一代，也就是老年代中。</p><p>既然年轻代对象存活率低，那么就可以对这些对象使用复制算法来回收，复制算法的特点就是存活率的低的情况下，回收效率特别高。</p><p>而对于老年代对象，则比较适合使用标记清除或者标记压缩，这些算法的特点是对象存活率高的情况下回收效率高。</p><p>这样因地制宜的做法，相比只用单纯一种回收算法，其效率就提高了不少。</p><p>现代大部分编程语言的垃圾回收都会用到分代策略，这里为了举例只划分了2代，实际中可能会划分出3代，甚至4代，来进一步提升垃圾回收的效率。</p><h3 id="移动内存对象"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#移动内存对象"></a> 移动内存对象</h3><p>上面提到的几种自动内存管理方法中，有些算法会在回收垃圾时移动对象，有的则不会移动。比如引用计数和标记清除就不会移动，而复制算法和标记压缩则会移动对象。</p><p>如果不移动对象的话，程序运行时间长了可能会出现内存碎片化，当要分配一个大对象的时候，即使空闲空间的总量足够，但是却找不到一处连续的空间来，也会导致内存分配失败，降低内存的利用率。</p><p>看样子每次回收时移动内存对象才是更优解？其实并非如此，移动对象它也有自己的缺点，首先最明显的就是移动对象的过程是有开销的，会占用内存带宽，这个过程不是免费的。</p><p>其次就是内存的访问方式也会受限，当一个内存对象被移动之后，它的内存地址肯定会发生变化的，这时再按之前的内存地址去访问这个对象就会出问题，必须使用新的地址去访问。</p><p>拿C#来举例，内存分为两块，一块是垃圾回收器自动管理的内存，叫托管内存（Managed Memory），也就是平时在C#代码里面new出来的Class对象所占的内存。另一块内存则不是由垃圾回收器自动管理的内存，而是由C++层，开发者自己管理的内存，叫原生内存（Native Memory）。</p><p>如果从托管内存去访问这些移动的对象是没有问题的，垃圾回收器会确保你每次访问时都是正确的引用。而从原生内存直接访问就可能会出问题，因为垃圾回收器并不知道你从Native层引用了哪些托管对象，同时你也不知道托管对象什么时候会发生移动。有一种办法是将这些要与Native层交互的内存给固定在托管内存中，不让垃圾回收器去移动它们，就可以正常访问了。</p><p>而反观非移动式垃圾回收就不会有这个问题，因为每个对象都不会发生移动，所以和Native层交互就变得很容易了。</p><p>说完了移动式垃圾回收的两个缺点，现在来讲一下非移动式垃圾回收。首先它的优点是不移动内存，没有额外的带宽开销，同时在与Native层交互时也会更容易。</p><p>缺点是内存分配起来会比移动式垃圾回收要慢一些，这是因为移动式垃圾回收的内存是连续的，需要分配内存时，直接在最后一个对象的末尾进行分配就好了。</p><p>而非移动式垃圾回收会将每个空闲的小块内存，全部记录在一个空闲链表（Free List）里，分配的时候先从这个链表里找，看有没有足够大小的空闲块，如果有就直接分配，没有再分配到内存块的末尾。</p><p>可以看到相比移动式垃圾回收多了一个遍历链表的过程，特别是遇到大量的内存碎片化时，这个链表又会特别的长，遍历起来会更慢，直接雪上加霜。</p><p>这里有一个避免内存碎片化的小技巧，那就是在加载对象时，要先加载大的对象，再加载小的对象。因为计算机是有内存对齐这个说法的，所以几个大块内存加载后，中间大概率会有不少的小碎片空间，此时再加载小对象，就可以充分利用这些碎片空间了。如果加载顺序反过来，就吃不到这个优化的效果了。这个方法无论是移动式垃圾回收还是非移动式垃圾回收都可以使用。</p><h3 id="保守式gc和精准式gc"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#保守式gc和精准式gc"></a> 保守式GC和精准式GC</h3><p>GC其实还分两种：保守式GC（Conservative GC）和精准式GC（Precise GC），它俩最大的区别就是能不能精确识别垃圾。这个和前面提到的垃圾回收算法不同，这个更像是垃圾回收相关的一个特性，不会改变垃圾回收算法本身的逻辑。</p><p>比如一个对象A的地址是0x11223344，对象A此时已经要被销毁了，但同时又有个对象B，对象B有一个long类型的成员变量，值正好也是0x11223344。GC会不会把这个普通的数值给当做一个引用，然后阻止对象A正常销毁，这是个问题。</p><p>换做是保守式GC，它是无法判断一个long变量究竟是另一个对象的引用，还是一个普通的数值的，它只能靠猜。比如判断这个值是不是在托管堆的地址范围内，如果超出这个范围，那它肯定不是一个引用，而是一个普通的数值。</p><p>或者再去判断内存分配信息，查一下能不能找到和这个地址有关联的内存对象，如果找不到，那么它也不是有效引用。</p><p>毕竟是靠猜，总有猜错的时候。如果变量B里面恰好有个long值指向一个垃圾对象A，那么就会导致对象A也没法销毁。不过这种情况毕竟还是少数，总体来说对性能影响不大。</p><p>那保守式GC为什么要凭空多出这么个操作来呢，直接销毁不就好了吗，答案是不行，因为一个64位long变量和一个64位long类型的指针在运行时的数据是没有任何区别的，它们在计算机看来都是一串普通的二进制数。如果没有额外的类型信息，就算是人类，也是没法区分到底是个普通数值，还是一个对象引用的。</p><p>既然保守式GC没法区分到底是数值还是指针（引用），那么稳妥起见，把他们全部当做指针来看待好就了，所谓保守式GC，也就是保守在这里。毕竟如果把引用当做数值给忽略掉了的话，会导致指向的对象被意外销毁掉，从而出现悬垂引用，程序就执行出错了。再怎么说，内存占用稍微大一点，也比程序计算出错要可好的多。</p><p>此外，保守式GC也不支持移动内存对象（比如标记压缩算法），因为移动对象后，需要更新引用地址才能确保后续访问不会出现悬垂引用。而保守式GC它又没办法区别一个变量是引用还是普通的数值，如果把一个普通的数学运算的值当成引用给更新了，那么这个值后续所有的数学运算都会出现错误的结果，这相比性能下降一丢丢，显然又是不可接受的，毕竟性能差一点总比算出一个错误的结果要可接受一点。</p><p>再来说说精准式GC，这是目前的主流，它能依靠编译时生成的额外内存布局信息，来区分一个地址是普通的数值还是一个引用，从而可以更加精确的区别普通数值和垃圾对象，回收效率也比保守式GC更高。</p><p>但精准式GC需要编译器的支持，才能生成内存布局信息，如果编译器不支持，则只能使用保守式GC。</p><p>这里还有一个特例是C语言，可以把带有具体类型的指针比如<code>int*</code>在代码里手动转换成<code>int</code>类型存储，那么即使生成了内存布局信息也是无用的。虽然开发者心里清楚它不是一个数值而是个指针，但垃圾回收器它不知道，它觉得这就是普通的值，一旦垃圾回收器移动这个内存对象，又会出现悬垂引用的问题，所以也就只剩保守式GC这一个选择了。</p><h3 id="双色标记和三色标记"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#双色标记和三色标记"></a> 双色标记和三色标记</h3><p>大家肯定都或多或少听说过GC活动时用户线程会暂停。为什么会一定要暂停才能收回垃圾，这和垃圾回收时的遍历/扫描有关，垃圾回收器在遍历/扫描对象的时候，是不允许其它线程在运行的，因为只要有其它线程在运行，就有可能会修改某几个对象间的引用关系，这是不可接受的。没办法它只能选择把其它线程都停止，再开始扫描，运行结束再恢复其它线程的执行。</p><p>举个例子，老师在操场上清点学生人数时，会要求学生们站好不要乱动，老师才可以开始数人，如果老师从前往后数，数到一半，来了一个新学生站在队伍的开头，老师又不知道这个事情，最后就会漏数一个学生。</p><p>换到垃圾回收里也是同样的道理，垃圾回收器使用白名单机制做垃圾回收。也就是说有被“数”到“学生”，不会被销毁，没有被“数”到的“学生”，会被销毁。如果在回收期间不停止其它线程的运行，又恰好出现了这种喜欢“插队”的对象，那么这些“插队”的对象就会被误销毁。相比漏杀对象来说，误杀对象的后果更加严重，它会直接导致程序运行出错，而漏杀仅仅是增加一些内存占用。</p><h3 id="1双色标记收集方法"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#1双色标记收集方法"></a> 1.双色标记收集方法</h3><p>上面提到的需要一口气扫描完所有对象不能打断的方法，就是双色标记收集方法。在遍历开始之前，将所有对象标记成白色，然后开始根搜索扫描，所有走过的对象标记成黑色，然后再把所有没遍历到的对象全部回收即可。</p><p>双色标记它要求扫描时必须要暂停其它线程。暂停时间通常在几毫秒到几十毫秒之间，如果垃圾对象特别多，或者引用关系特别复杂，可能会高达几百毫秒甚至几秒。</p><p>有些游戏玩久了之后每隔一段时间就会小卡一下，正是GC在作怪，此时它正在回收内存。</p><p>GC带来的停顿大部分情况下是可以接受的，比如Web场景中，数据早到达几十毫秒和晚到达几十毫秒并没有太大的差别，甚至网络延迟都比这个时间要长。</p><p>同时GC的加入可以将更多的精力放到业务开发上，而不是操心怎么管理内存。这也是为什么大部分很火的编程语言都是带GC的，因为GC确实降低了开发的门槛。</p><h3 id="2三色标记收集方法"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#2三色标记收集方法"></a> 2.三色标记收集方法</h3><p>因为双色标记方法的缺陷，只能一口气把所有对象全部扫完，如果中间又让业务线程恢复运行了，那么之前扫过的结果就要作废，需要重头再扫。</p><p>为了解决这个问题，就出现了三色标记收集方法。使用黑白灰三色来标识每个对象：</p><ul><li>白色：GC还未扫描过的对象，扫描开始时所有对象都是白色</li><li>灰色：GC仅扫过对象本身，还没有扫描完其所引用的其它对象，也就是扫到一半还没有完全扫好的对象</li><li>黑色：GC扫描过自己，也扫描过所有引用的对象</li></ul><p>有了灰色的加入，现在垃圾回收过程就可以随时暂停了，如果一次要回收的内存压力太大，可以分多次回收，然后每次只扫描和回收一点，这样对用户线程的停顿就可以分摊到多帧里面了。</p><p>问题回到垃圾回收过程中，如果回收到一半，再将用户代码切出来执行后，此时引用关系肯定会被修改。随之而来的就是两个问：浮动垃圾和漏标。</p><p>拿图举例（这里引用知乎大佬<a href="https://zhuanlan.zhihu.com/p/527512898" class="bubble-link">新生代农民工</a>的插图）</p><p>当GC扫到一半时，会出现一部分是黑色对象，一部分是灰色，还有一部分则是白色。</p><p><img src="/24140-2147/20.jpg" alt="v2-ed44b1a969f41b5d7c76ba1e2c1a75a6_r"></p><p>此时GC暂停，让用户线程继续执行，然后再切回GC线程时，引用关系可能会被修改成这样。</p><p><img src="/24140-2147/21.jpg" alt="v2-f3aa5a7c30e0d1dea25a224a5ae389c5_r"></p><p>可以发现D到E之间的引用断开了，然后GC接着扫，扫完会是这样的情况。</p><p><img src="/24140-2147/22.jpg" alt="v2-8956ec62993cf69d5ba662aa9a2c8e83_r"></p><p>BCH木有扫到，是白色被正常回收没问题，但是EFG却变成了黑色，但他们本身有没有被A或者D对象所引用。那么EFG理应也是垃圾，但是EFG此时不是白色，也就无法被回收，这就是浮动垃圾，该被回收的对象没有被回收。</p><p>浮动垃圾本身影响并不是很大，因为下次进行垃圾回收时，它们又可以正常回收。</p><p>影响比较严重的是另一个问题：漏标。</p><p>我们从头开始开始，初始状态下引用关系是这样的。</p><p><img src="/24140-2147/20.jpg" alt="v2-ed44b1a969f41b5d7c76ba1e2c1a75a6_r"></p><p>扫描暂停时，用户断开了EG的引用，新增了DG的引用。此时GC扫描E时，发现E只引用了F。F没有引用任何对象，那么DEF这条引用分支线扫描完成，DEF也全部被标位黑色。</p><p>因为黑色代表一个对象本身和所有的子节点都被完整扫描过了，不用再重复扫描了，也就导致了垃圾回收器没能发现DG这条引用线，随后就把对象G当做了垃圾处理了。当再次从D访问G时，就会出现错误，这就是漏标问题，将不该被回收的对象给回收掉了。</p><p><img src="/24140-2147/23.png" alt="v2-9efd995c21dc39ee17e69a611651c54e_720w"></p><p>浮动垃圾的问题并不需要专门去解决，因为下次垃圾回收的时候一样可以回收到这些垃圾对象。</p><p>但漏标的问题就比较严重了，会导致程序出现错误。不同语言的运行时针对这个问题都有不同的解决方法，拿Lua举例。</p><p>一种方法是前向写屏障，当黑色对象新增白色对象引用的时候，把这个白色对象置为灰色，这样下次GC时就可以从这个变为灰色的新对象开始继续扫描了。</p><p>另一种方法是后向写屏障，把这个黑色对象退回成灰色对象，这样也可以进行重扫。</p><h3 id="参考链接"><a class="markdownIt-Anchor bubble-link" href="/24140-2147/#参考链接"></a> 参考链接</h3><ol><li><a href="https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Reference_counting" class="bubble-link">Garbage collection (computer science)</a></li><li><a href="https://en.wikipedia.org/wiki/Tracing_garbage_collection#Generational_GC_(ephemeral_GC)" class="bubble-link">Tracing garbage collection</a></li><li><a href="https://zhuanlan.zhihu.com/p/297965515" class="bubble-link">【JVM】GC的四种算法</a></li><li><a href="https://zhuanlan.zhihu.com/p/527512898" class="bubble-link">我是这么理解双色/三色标记清除GC算法的</a></li><li><a href="https://zhuanlan.zhihu.com/p/27939756" class="bubble-link">GC算法之引用计数</a></li><li><a href="https://blog.csdn.net/weixin_47184173/article/details/113622421" class="bubble-link">与其千篇一律，不如一篇文章彻底搞懂三色标记是如何处理漏标问题</a></li><li><a href="https://zhuanlan.zhihu.com/p/555405544" class="bubble-link">.NET CLR之垃圾回收（GC）</a></li><li><a href="https://zhuanlan.zhihu.com/p/386567376" class="bubble-link">Unity的未来，是固守Mono，还是拥抱CoreCLR？</a></li><li><a href="https://zhuanlan.zhihu.com/p/377672271" class="bubble-link">详解GC（一）理论篇</a></li><li><a href="https://www.cnblogs.com/nele/p/5673215.html" class="bubble-link">C#技术漫谈之垃圾回收机制(GC)(转)</a></li><li><a href="https://www.bilibili.com/video/BV1jz4y177JK/?vd_source=7c656a0d5e56305cec712d44b6f52423" class="bubble-link">【直播回放】Unity Memory 豆知识 20230714</a></li><li><a href="https://zhuanlan.zhihu.com/p/49623707" class="bubble-link">Mono中的BOEHM GC 原理学习（1）</a></li><li><a href="https://zhuanlan.zhihu.com/p/144875092" class="bubble-link">Lua设计与实现–GC篇</a></li></ol></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="软件" scheme="https://aprilforest.cn/categories/%E8%BD%AF%E4%BB%B6/"/>
    
    
    <category term="垃圾回收" scheme="https://aprilforest.cn/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/"/>
    
    <category term="内存管理" scheme="https://aprilforest.cn/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>搭建一个录播机</title>
    <link href="https://aprilforest.cn/24130-0029.html"/>
    <id>https://aprilforest.cn/24130-0029.html</id>
    <published>2024-05-09T00:29:29.000Z</published>
    <updated>2026-03-02T14:02:12.693Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>经常会错过一些B站UP的直播，便想搭建一个录播机，把直播录制下来，等到自己有时间再观看。这样不仅不用担心赶不上直播，也能反复回看和快进，很方便。</p><p>经过一番摸索后，我找到一个还不错的搭建录播机的方案，分享给大家。</p><p>我的要求是这样，除了录下视频，还要能录下直播间的弹幕，另外观看也要方便一点。</p><p>最终我的方案是使用<a href="https://ddtv.pro" class="bubble-link">DDTV</a>来录制，录制好后使用<a href="https://alist.nn.ci/zh" class="bubble-link">AList</a>进行在线观看，这是最终效果，和看视频一样，也是有直播间弹幕的。（岷叔经常凌晨三四点直播，我都睡了完全看不到）</p><p><img src="/24130-0029/GIF_2024-05-08_23-19-37.gif" alt="GIF 2024-05-08 23-19-37"></p><p>接下来就是教程了，使用Docker部署，这里使用docker compose来管理容器。</p><p>首先创建一个<code>docker-compose.yml</code>文件：</p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml"><span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">'3'</span><span class="token key atrule">services</span><span class="token punctuation">:</span>  <span class="token key atrule">ddtv</span><span class="token punctuation">:</span>  <span class="token comment"># ddtv有三个版本，这里使用最小的cli版本</span>    <span class="token key atrule">image</span><span class="token punctuation">:</span> ghcr.io/chkzl/ddtv/cli<span class="token punctuation">:</span>debian    <span class="token key atrule">container_name</span><span class="token punctuation">:</span> ddtv    <span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped    <span class="token key atrule">environment</span><span class="token punctuation">:</span>      <span class="token punctuation">-</span> <span class="token string">"PUID=0"</span>      <span class="token punctuation">-</span> <span class="token string">"PGID=0"</span>    <span class="token key atrule">volumes</span><span class="token punctuation">:</span>      <span class="token comment"># 这里我建议是把整个/DDTV目录挂载出来</span>      <span class="token comment"># 这个目录包含配置文件，和ddtv的热更新数据</span>      <span class="token comment"># 后续调试文件和修改配置会更方便</span>      <span class="token comment"># （这里直接挂载到同目录的ddtv_data文件夹下了）</span>      <span class="token punctuation">-</span> ./ddtv_data<span class="token punctuation">:</span>/DDTV            <span class="token comment"># 这个是录播的视频存放的目录，虽然它也在容器的/DDTV目录下</span>      <span class="token comment"># 但还是建议单独挂载到别的地方，比如外接的硬盘里</span>      <span class="token comment"># 因为录播视频的大小会非常的大</span>      <span class="token comment"># 我这里是直接挂载到了一个raid0的阵列里面</span>      <span class="token comment"># 所以需要把/mnt/array/records换成你自己的目录</span>      <span class="token punctuation">-</span> /mnt/array/records<span class="token punctuation">:</span>/DDTV/Rec  <span class="token key atrule">alist</span><span class="token punctuation">:</span>    <span class="token comment"># alist的话可以选择任意版本，这里就默认3.33了</span>    <span class="token key atrule">image</span><span class="token punctuation">:</span> xhofe/alist<span class="token punctuation">:</span>v3.33.0    <span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped    <span class="token key atrule">container_name</span><span class="token punctuation">:</span> alist    <span class="token key atrule">environment</span><span class="token punctuation">:</span>      <span class="token punctuation">-</span> <span class="token string">'PUID=0'</span>      <span class="token punctuation">-</span> <span class="token string">'PGID=0'</span>      <span class="token punctuation">-</span> <span class="token string">'UMASK=022'</span>    <span class="token comment"># 暴露端口不如外面没法访问alist</span>    <span class="token key atrule">ports</span><span class="token punctuation">:</span>      <span class="token punctuation">-</span> <span class="token string">'5400:5244'</span>    <span class="token key atrule">volumes</span><span class="token punctuation">:</span>      <span class="token comment"># 这一行是挂载alist自己的数据目录</span>      <span class="token punctuation">-</span> <span class="token string">'./alist_data:/opt/alist/data'</span>            <span class="token comment"># 这里把录播文件夹也挂载到alist容器下，好从网页观看</span>      <span class="token punctuation">-</span> <span class="token string">'/mnt/array:/array'</span>            <span class="token comment"># 这里我还把ddtv的数据目录也挂载了</span>      <span class="token comment"># 这样就可以很方便地在alist网页端修改ddtv配置文件了</span>      <span class="token punctuation">-</span> ./ddtv_data<span class="token punctuation">:</span>/ddtv_data<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>compose文件写好后，需要在旁边创建两个目录<code>alist_data</code>和<code>ddtv_data</code></p><p><img src="/24130-0029/QQ%E6%88%AA%E5%9B%BE20240508233124.png" alt="QQ截图20240508233124"></p><p>接着启动docker。</p><p>启动成功后，注意观察alist的日志，alist首次启动时会创建一个admin用户，同时给admin生成一个随机的密码，输出到终端里。这里注意复制一下，因为它只显示一次，不保存就没了。</p><p>接着我们打开alist的页面，这里使用127.0.0.1:5244就可以打开，然后使用admin用户名和刚刚复制的密码进行登录。</p><p>登录之后直接进入AList的后台管理页面，点击“存储”页面，添加两个存储。</p><p>第一个存储的驱动选择“本机存储”，挂载路径填写<code>/DDTV_DATA</code>（当然这个名字可以随意取），根文件夹路径填写<code>/ddtv_data</code>。意思是把容器的/ddtv_data目录挂载到alist里的/DDTV_DATA这个路径下，后面就可以使用/DDTV_DATA文件夹来在线修改ddtv的配置文件了。</p><p>第二个存储的驱动也选择本机存储”，挂载路径填写<code>/ARRAY</code>（当然这个名字可以随意取），根文件夹路径填写<code>/array</code>。意思是把容器的录播文件夹<code>/array</code>挂载到alist的/array这个路径下，后面就可以使用/array文件夹来观看录播视频了。</p><p>这里说一下，挂载路径和根文件夹路径不一定要名字一样，不一样也是可以正常读取到的。</p><p>第一次启动时，除了alist会生成默认admin账号以外，ddtv也会下载和更新一些依赖文件。同时ddtv还会要求我们登录B站账号，这里建议创建一个小号来进行录播。</p><p>登录方式主要靠扫码，Docker环境下好像是没法直接扫码的，我们需要手动打开ddtv数据目录下的二维码图片进行扫码。</p><p>此时我们已经配置好了alist，就直接从alist这边进行操作了。</p><p>打开刚挂载好的/DDTV_DATA这个ddtv的数据目录。找到BiliQR.png文件并打开。就可以看到二维码了，掏出手机扫码就好。</p><p><img src="/24130-0029/QQ%E6%88%AA%E5%9B%BE20240508234327.png" alt="QQ截图20240508234327"></p><p>扫码后ddtv会自动完成登录，没问题后我们就可以进行下一步，配置要录播的直播间了。</p><p>打开RoomListConfig.json文件，点击Markdown按钮，然后选择用Text Editor打开，这里面就记录了所有要录播的直播间数据。</p><p><img src="/24130-0029/QQ%E6%88%AA%E5%9B%BE20240508234458.png" alt="QQ截图20240508234458"></p><p>这个文件是json格式，每增加一个直播间都需要在data下新增一个json对象。这里简单说明一下每个参数的作用。完整的说明可以参考<a href="https://ddtv.pro/config/RoomListConfig.json.html#%E6%88%BF%E9%97%B4%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E7%9A%84%E6%A0%BC%E5%BC%8F%E4%BB%8B%E7%BB%8D" class="bubble-link">ddtv的文档</a></p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">{</span>    <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"籽岷"</span><span class="token punctuation">,</span>    <span class="token property">"Description"</span><span class="token operator">:</span> <span class="token string">"籽岷"</span><span class="token punctuation">,</span>    <span class="token property">"RoomId"</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span>    <span class="token property">"UID"</span><span class="token operator">:</span> <span class="token number">686127</span><span class="token punctuation">,</span>    <span class="token property">"IsAutoRec"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>    <span class="token property">"IsRemind"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>    <span class="token property">"IsRecDanmu"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>    <span class="token property">"Like"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>    <span class="token property">"Shell"</span><span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span>    <span class="token property">"IsTemporaryPlay"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><ul><li>name：up主的名字，这个参数会被用来生成录播文件的文件夹的名字，可以写中文，一般与up名字相同即可</li><li>description：描述，目前好像没有发现有什么作用，可以随便写</li><li>roomId：直播间的房间号（房间号选项已经被废弃了，建议使用下面的uid进行替代）</li><li>uid：主播的b站账号uid</li><li>IsAutoRec：是否开启自动录制，肯定要开啊，不然怎么录制呢</li><li>IsRemind：开播时是否发起通知，这个选项只有ddtv桌面版有作用，这里cli版本的ddtv没有效果</li><li>IsRecDanmu：是否录制弹幕礼物信息，选择开启，没有弹幕乐趣少一半</li><li>Like：特别标注。目前还没有实装这个功能，没有实际效果</li><li>Shell：录制完成后会执行一个shell命令，好像可以实现一些高级效果，这里留空</li></ul><p>这样就配置好了。我们重启docker即可生效，这样就会自动开始录制了。</p><p>录制完毕后大概会有这些文件：</p><p><img src="/24130-0029/QQ%E6%88%AA%E5%9B%BE20240508235530.png" alt="QQ截图20240508235530"></p><p>其中比较重要的是mp4文件和xml文件，mp4是视频文件很好理解。xml是弹幕文件，记录了直播间所有的弹幕信息。</p><p>之所以选择AList也是因为这一点，它可以直接读取xml弹幕文件并播放，不用再另外转码，很是方便！</p><p>我们点击mp4文件后，它就会自动加载弹幕了，不需要做任何额外工作。</p><p><img src="/24130-0029/QQ%E6%88%AA%E5%9B%BE20240508235801.png" alt="QQ截图20240508235801"></p><p>但如果你想要自定义弹幕样式的话，或者遇到一些情况，导致录播视频被切分成了两个文件的话，就会导致第二个文件弹幕与视频不同步，此时就要使用第二张方法，手动转换字幕了。</p><p>也就是把xml格式的弹幕转换成ass格式的字幕。同时做一下时间偏移和切分，这样即使录播视频被切成了多个文件，还可以获得相对不错的播放体验。</p><p>我这里使用<a href="https://github.com/hihkm/DanmakuFactory" class="bubble-link">hihkm/DanmakuFactory</a>来做弹幕到字幕的转换，首先需要到G站下载DanmakuFactory的文件，并解压。</p><p>解压好后只有单纯的DanmakuFactory用起来并不方便，我这里专门写了一个python脚本来做自动化的转换工作，文件我会放在文章末尾，是一个bat文件和一个python脚本，没有三方依赖可以直接使用。</p><p>大概的过程是调用alist的api下载xml格式的弹幕文件，然后切分并转换成多个ass格式的字幕文件，再上传回alist对应的目录里。</p><p>要使用这个脚本，需要将文章末尾的bat文件和python脚本分别保存成<code>convert.bat</code>和<code>convert.py</code>，并放到DanmakuFactory的文件夹里，像这样。</p><p><img src="/24130-0029/QQ%E6%88%AA%E5%9B%BE20240509000553.png" alt="QQ截图20240509000553"></p><p>在转换之前还要先编辑python脚本，修改alist的地址和登录的用户密码。</p><p>如果是https的alist地址，还需要把第23行的HTTPConnection修改为HTTPSConnection，这样就大功告成了。</p><p><img src="/24130-0029/image-20240509000719568.png" alt="image-20240509000719568"></p><p>然后我们运行bat文件，就可以输入要转行的文件了，这里的参数格式是这样：</p><pre class="line-numbers language-none"><code class="language-none">&lt;xml弹幕文件路径&gt; [时.分.秒/[时.分.秒/[时.分.秒]]]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>比如我要转换的弹幕文件是<code>/ARRAY/records/籽岷_544853/24-05-06/假期结束惹.xml</code>，同时录播的时候没有发生网络卡顿，最终录出来的只有单个mp4文件，那么只需要输入<code>/ARRAY/records/籽岷_544853/24-05-06/假期结束惹.xml</code>按回车即可。</p><p><img src="/24130-0029/QQ%E6%88%AA%E5%9B%BE20240509001402.png" alt="QQ截图20240509001402"></p><p>上面这种是比较理想的情况，如果录制时网卡了，导致一部分内容没录上，最终录出来的文件可能会变成两个mp4文件，此时就需要指定后面的时间参数了。</p><p>时间参数是用来做字幕切分的，比如我们录的时候网卡了，录出两段视频，<code>1.mp4</code>和<code>2.mp4</code>，其中1.mp4录的是从开播开始的0时0分0秒到第1时0分0秒，而中间网卡了一下，导致有十分钟的内容没有录上，那么2.mp4的内容就是1时10分0秒开始，直到主播下播的内容。</p><p>这种情况我们就要这样输入参数<code>/path/to/danmu.xml 1.10.0</code>，意思是在1时10分0秒这里切一刀（把1时10分0秒之后的所有弹幕单独复制出来再生成一个ass字幕文件），最终会生成两个ass字幕文件，分别覆盖1.mp4和2.mp4的内容，播放时手动指定一下要加载的字幕文件，这样观看时就不会出现字幕和视频不同步的问题了。（1.10.0也可以简写成1.10，会被识别成1小时10分0秒，同理只有一个一，也就是1会被识别成1小时0分0秒）</p><p>如果录出3段视频，那么我们就需要输入两段时间参数了，像这样<code>/path/to/danmu.xml 1.10.0/2.30.0</code>，1.10.0表示从1小时10分切一刀，2.30.0表示从2个半小时这里切一刀，最终会生成3个ass字幕文件，分别覆盖3个视频文件，这样观看就不会有问题啦。</p><p>如果使用字幕播放的话，需要在alist的视频播放页面手动关闭弹幕功能，否则弹幕会显示两遍，一遍是弹幕功能，一遍是字幕功能。</p><p>最后帖上转换脚本：</p><p><code>convert.bat</code>：（注意bat要用gbk编码保存，否则中文乱码，但不会影响使用。win10/win11的记事本默认使用utf8请注意）</p><pre class="line-numbers language-bat" data-language="bat"><code class="language-bat">@echo off:startSET /p input=输入路径和时间片 python convert.py %input%echo.goto start<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>convert.py</code>：（基于python11编写，无三方依赖库，理论上3.8也可以运行）</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> typing <span class="token keyword">import</span> Mapping<span class="token keyword">import</span> xml<span class="token punctuation">.</span>etree<span class="token punctuation">.</span>ElementTree <span class="token keyword">as</span> ET<span class="token keyword">from</span> pathlib <span class="token keyword">import</span> Path<span class="token keyword">import</span> sys<span class="token keyword">import</span> subprocess<span class="token keyword">import</span> http<span class="token punctuation">.</span>client <span class="token keyword">as</span> hc<span class="token keyword">import</span> json<span class="token keyword">import</span> urllib<span class="token punctuation">.</span>parse<span class="token keyword">import</span> time<span class="token keyword">import</span> os<span class="token punctuation">.</span>path <span class="token keyword">as</span> ospath<span class="token keyword">import</span> re<span class="token comment"># 常量定义</span>danmaku_factory <span class="token operator">=</span> <span class="token string">'DanmakuFactory_REL1.70CLI.exe'</span>alisthost <span class="token operator">=</span> <span class="token string">'192.168.1.151:5400'</span>username <span class="token operator">=</span> <span class="token string">'admin'</span>password <span class="token operator">=</span> <span class="token string">'andemin'</span><span class="token keyword">class</span> <span class="token class-name">AlistClient</span><span class="token punctuation">:</span>    <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> host<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> username<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> password<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        self<span class="token punctuation">.</span>user <span class="token operator">=</span> username        self<span class="token punctuation">.</span>pwd <span class="token operator">=</span> password        self<span class="token punctuation">.</span>conn <span class="token operator">=</span> hc<span class="token punctuation">.</span>HTTPConnection<span class="token punctuation">(</span>host<span class="token punctuation">,</span> timeout<span class="token operator">=</span><span class="token number">5</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>token <span class="token operator">=</span> <span class="token string">''</span>        <span class="token keyword">def</span> <span class="token function">read_file</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> <span class="token builtin">str</span><span class="token punctuation">:</span>        detail <span class="token operator">=</span> self<span class="token punctuation">.</span>__post<span class="token punctuation">(</span><span class="token string">'/api/fs/get'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token string">'path'</span><span class="token punctuation">:</span> path<span class="token punctuation">,</span> <span class="token string">'refresh'</span><span class="token punctuation">:</span> <span class="token boolean">True</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">True</span><span class="token punctuation">)</span>        link <span class="token operator">=</span> re<span class="token punctuation">.</span>sub<span class="token punctuation">(</span><span class="token string">r'^(https?:\/\/[^\/]+)'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">,</span> detail<span class="token punctuation">[</span><span class="token string">'raw_url'</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> self<span class="token punctuation">.</span>__get<span class="token punctuation">(</span>link<span class="token punctuation">,</span> <span class="token boolean">False</span><span class="token punctuation">)</span>        <span class="token keyword">def</span> <span class="token function">write_file</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> content<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        data <span class="token operator">=</span> content<span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>__req<span class="token punctuation">(</span><span class="token string">'PUT'</span><span class="token punctuation">,</span> <span class="token string">'/api/fs/put'</span><span class="token punctuation">,</span> data<span class="token punctuation">,</span> <span class="token boolean">True</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>            <span class="token string">'File-Path'</span><span class="token punctuation">:</span> urllib<span class="token punctuation">.</span>parse<span class="token punctuation">.</span>quote<span class="token punctuation">(</span>path<span class="token punctuation">,</span> safe<span class="token operator">=</span><span class="token string">':/@'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>            <span class="token string">'Content-Type'</span><span class="token punctuation">:</span> <span class="token string">'application/xml'</span><span class="token punctuation">,</span>            <span class="token string">'Content-Length'</span><span class="token punctuation">:</span> <span class="token builtin">len</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span>        <span class="token punctuation">}</span><span class="token punctuation">)</span>    <span class="token keyword">def</span> <span class="token function">__auth</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> hc<span class="token punctuation">.</span>HTTPResponse<span class="token punctuation">:</span>        <span class="token keyword">if</span> self<span class="token punctuation">.</span>token <span class="token operator">!=</span> <span class="token string">''</span><span class="token punctuation">:</span>            <span class="token keyword">return</span>        body <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'{{"username": "</span><span class="token interpolation"><span class="token punctuation">{</span>self<span class="token punctuation">.</span>user<span class="token punctuation">}</span></span><span class="token string">", "password": "</span><span class="token interpolation"><span class="token punctuation">{</span>self<span class="token punctuation">.</span>pwd<span class="token punctuation">}</span></span><span class="token string">"}}'</span></span>        headers <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token string">'Content-Type'</span><span class="token punctuation">:</span> <span class="token string">'application/json'</span><span class="token punctuation">}</span>        self<span class="token punctuation">.</span>conn<span class="token punctuation">.</span>request<span class="token punctuation">(</span><span class="token string">'POST'</span><span class="token punctuation">,</span> <span class="token string">"/api/auth/login"</span><span class="token punctuation">,</span> body<span class="token operator">=</span>body<span class="token punctuation">,</span> headers<span class="token operator">=</span>headers<span class="token punctuation">)</span>        raw_rsp <span class="token operator">=</span> self<span class="token punctuation">.</span>conn<span class="token punctuation">.</span>getresponse<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> raw_rsp<span class="token punctuation">.</span>getcode<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token number">200</span><span class="token punctuation">:</span>            <span class="token keyword">raise</span> raw_rsp        rsp <span class="token operator">=</span> json<span class="token punctuation">.</span>loads<span class="token punctuation">(</span>raw_rsp<span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        raw_rsp<span class="token punctuation">.</span>close<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> rsp<span class="token punctuation">[</span><span class="token string">'code'</span><span class="token punctuation">]</span> <span class="token operator">!=</span> <span class="token number">200</span><span class="token punctuation">:</span>            <span class="token keyword">raise</span> rsp        self<span class="token punctuation">.</span>token <span class="token operator">=</span>  rsp<span class="token punctuation">[</span><span class="token string">'data'</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'token'</span><span class="token punctuation">]</span>    <span class="token keyword">def</span> <span class="token function">__get</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> encode_url<span class="token punctuation">:</span> <span class="token builtin">bool</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> hc<span class="token punctuation">.</span>HTTPResponse<span class="token punctuation">:</span>        self<span class="token punctuation">.</span>__auth<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> encode_url<span class="token punctuation">:</span>            path <span class="token operator">=</span> urllib<span class="token punctuation">.</span>parse<span class="token punctuation">.</span>quote<span class="token punctuation">(</span>path<span class="token punctuation">,</span> safe<span class="token operator">=</span><span class="token string">':/@'</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>conn<span class="token punctuation">.</span>request<span class="token punctuation">(</span><span class="token string">'GET'</span><span class="token punctuation">,</span> path<span class="token punctuation">)</span>        raw_rsp <span class="token operator">=</span> self<span class="token punctuation">.</span>conn<span class="token punctuation">.</span>getresponse<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> raw_rsp<span class="token punctuation">.</span>getcode<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token number">200</span><span class="token punctuation">:</span>            <span class="token keyword">raise</span> raw_rsp        rsp <span class="token operator">=</span> raw_rsp<span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span>        raw_rsp<span class="token punctuation">.</span>close<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> rsp    <span class="token keyword">def</span> <span class="token function">__req</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> method<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> body<span class="token punctuation">:</span> <span class="token builtin">any</span><span class="token punctuation">,</span> encode_url<span class="token punctuation">:</span> <span class="token builtin">bool</span><span class="token punctuation">,</span> headers <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> hc<span class="token punctuation">.</span>HTTPResponse<span class="token punctuation">:</span>        self<span class="token punctuation">.</span>__auth<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> <span class="token builtin">isinstance</span><span class="token punctuation">(</span>body<span class="token punctuation">,</span> <span class="token builtin">dict</span><span class="token punctuation">)</span><span class="token punctuation">:</span>            body <span class="token operator">=</span> json<span class="token punctuation">.</span>dumps<span class="token punctuation">(</span>body<span class="token punctuation">)</span>            headers<span class="token punctuation">[</span><span class="token string">'Content-Type'</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">'application/json;charset=UTF-8'</span>        <span class="token keyword">if</span> encode_url<span class="token punctuation">:</span>            path <span class="token operator">=</span> urllib<span class="token punctuation">.</span>parse<span class="token punctuation">.</span>quote<span class="token punctuation">(</span>path<span class="token punctuation">,</span> safe<span class="token operator">=</span><span class="token string">':/@'</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>conn<span class="token punctuation">.</span>request<span class="token punctuation">(</span>method<span class="token punctuation">,</span> path<span class="token punctuation">,</span> body<span class="token operator">=</span>body<span class="token punctuation">,</span> headers<span class="token operator">=</span><span class="token punctuation">{</span>             <span class="token string">'Authorization'</span><span class="token punctuation">:</span> self<span class="token punctuation">.</span>token<span class="token punctuation">,</span>             <span class="token operator">**</span>headers         <span class="token punctuation">}</span><span class="token punctuation">)</span>        raw_rsp <span class="token operator">=</span> self<span class="token punctuation">.</span>conn<span class="token punctuation">.</span>getresponse<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> raw_rsp<span class="token punctuation">.</span>getcode<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token number">200</span><span class="token punctuation">:</span>            <span class="token keyword">raise</span> raw_rsp        rsp <span class="token operator">=</span> json<span class="token punctuation">.</span>loads<span class="token punctuation">(</span>raw_rsp<span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        raw_rsp<span class="token punctuation">.</span>close<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> rsp<span class="token punctuation">[</span><span class="token string">'code'</span><span class="token punctuation">]</span> <span class="token operator">!=</span> <span class="token number">200</span><span class="token punctuation">:</span>            <span class="token keyword">raise</span> rsp        <span class="token keyword">return</span> rsp<span class="token punctuation">[</span><span class="token string">'data'</span><span class="token punctuation">]</span>    <span class="token keyword">def</span> <span class="token function">__post</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> body<span class="token punctuation">:</span> <span class="token builtin">any</span><span class="token punctuation">,</span> encode_url<span class="token punctuation">:</span> <span class="token builtin">bool</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> hc<span class="token punctuation">.</span>HTTPResponse<span class="token punctuation">:</span>        <span class="token keyword">return</span> self<span class="token punctuation">.</span>__req<span class="token punctuation">(</span><span class="token string">'POST'</span><span class="token punctuation">,</span> path<span class="token punctuation">,</span> body<span class="token punctuation">,</span> encode_url<span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">split_filename</span><span class="token punctuation">(</span>path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    d <span class="token operator">=</span> path<span class="token punctuation">.</span>rindex<span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> <span class="token punctuation">(</span>path<span class="token punctuation">[</span><span class="token punctuation">:</span>d<span class="token punctuation">]</span><span class="token punctuation">,</span> path<span class="token punctuation">[</span>d <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token comment"># 初始化和检查输入</span>subtitle_path <span class="token operator">=</span> sys<span class="token punctuation">.</span>argv<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span>alist <span class="token operator">=</span> AlistClient<span class="token punctuation">(</span>alisthost<span class="token punctuation">,</span> username<span class="token punctuation">,</span> password<span class="token punctuation">)</span><span class="token comment"># print(sys.argv)</span><span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>sys<span class="token punctuation">.</span>argv<span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">2</span><span class="token punctuation">:</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">'need a file path followed by time semgents. such /a/b.xml as 1.3.4/5.6'</span><span class="token punctuation">)</span>    sys<span class="token punctuation">.</span>exit<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token comment"># 解析时间段</span>matches <span class="token operator">=</span> re<span class="token punctuation">.</span>findall<span class="token punctuation">(</span><span class="token string">r'((\d+\.?)+)+'</span><span class="token punctuation">,</span> sys<span class="token punctuation">.</span>argv<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>sys<span class="token punctuation">.</span>argv<span class="token punctuation">)</span> <span class="token operator">&gt;</span> <span class="token number">2</span> <span class="token keyword">else</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token keyword">if</span> matches <span class="token keyword">is</span> <span class="token boolean">None</span><span class="token punctuation">:</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">'wrong foramt'</span><span class="token punctuation">)</span>    sys<span class="token punctuation">.</span>exit<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span>segments <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token keyword">for</span> seg <span class="token keyword">in</span> <span class="token punctuation">[</span>seg<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token keyword">for</span> seg <span class="token keyword">in</span> matches<span class="token punctuation">]</span><span class="token punctuation">:</span>    split <span class="token operator">=</span> seg<span class="token punctuation">.</span>split<span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">)</span>    <span class="token keyword">while</span> <span class="token builtin">len</span><span class="token punctuation">(</span>split<span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">3</span><span class="token punctuation">:</span>        split<span class="token punctuation">.</span>insert<span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token string">'0'</span><span class="token punctuation">)</span>    segments<span class="token punctuation">.</span>append<span class="token punctuation">(</span>split<span class="token punctuation">)</span><span class="token comment"># 添加自己</span>segments<span class="token punctuation">.</span>insert<span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token comment"># 切分字幕</span>offset <span class="token operator">=</span> <span class="token number">0</span>index <span class="token operator">=</span> <span class="token number">0</span><span class="token keyword">for</span> <span class="token punctuation">(</span>h<span class="token punctuation">,</span> m<span class="token punctuation">,</span> s<span class="token punctuation">)</span> <span class="token keyword">in</span> segments<span class="token punctuation">:</span>    <span class="token comment"># 按时间进行过滤</span>    duration <span class="token operator">=</span> <span class="token builtin">float</span><span class="token punctuation">(</span>h<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">60</span> <span class="token operator">*</span> <span class="token number">60</span> <span class="token operator">+</span> <span class="token builtin">float</span><span class="token punctuation">(</span>m<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">60</span> <span class="token operator">+</span> <span class="token builtin">float</span><span class="token punctuation">(</span>s<span class="token punctuation">)</span>    offset <span class="token operator">+=</span> duration    danmu_content <span class="token operator">=</span> alist<span class="token punctuation">.</span>read_file<span class="token punctuation">(</span>subtitle_path<span class="token punctuation">)</span>    root <span class="token operator">=</span> ET<span class="token punctuation">.</span>fromstring<span class="token punctuation">(</span>danmu_content<span class="token punctuation">)</span>    delete <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>    <span class="token keyword">for</span> node <span class="token keyword">in</span> root<span class="token punctuation">:</span>        <span class="token keyword">if</span> node<span class="token punctuation">.</span>tag <span class="token operator">!=</span> <span class="token string">'d'</span><span class="token punctuation">:</span>            <span class="token keyword">continue</span>        p <span class="token operator">=</span> node<span class="token punctuation">.</span>attrib<span class="token punctuation">[</span><span class="token string">'p'</span><span class="token punctuation">]</span>        delimiter <span class="token operator">=</span> p<span class="token punctuation">.</span>index<span class="token punctuation">(</span><span class="token string">','</span><span class="token punctuation">)</span>        time <span class="token operator">=</span> <span class="token builtin">float</span><span class="token punctuation">(</span>p<span class="token punctuation">[</span><span class="token punctuation">:</span>delimiter<span class="token punctuation">]</span><span class="token punctuation">)</span>        rest <span class="token operator">=</span> p<span class="token punctuation">[</span>delimiter <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span>        time <span class="token operator">-=</span> offset        <span class="token keyword">if</span> time <span class="token operator">&lt;</span> <span class="token number">0</span><span class="token punctuation">:</span>            delete<span class="token punctuation">.</span>append<span class="token punctuation">(</span>node<span class="token punctuation">)</span>        <span class="token keyword">else</span><span class="token punctuation">:</span>            node<span class="token punctuation">.</span>attrib<span class="token punctuation">[</span><span class="token string">'p'</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'</span><span class="token interpolation"><span class="token punctuation">{</span>time<span class="token punctuation">:</span><span class="token format-spec">.3f</span><span class="token punctuation">}</span></span><span class="token string">,</span><span class="token interpolation"><span class="token punctuation">{</span>rest<span class="token punctuation">}</span></span><span class="token string">'</span></span>    <span class="token keyword">for</span> n <span class="token keyword">in</span> delete<span class="token punctuation">:</span>        root<span class="token punctuation">.</span>remove<span class="token punctuation">(</span>n<span class="token punctuation">)</span>    <span class="token comment"># 将弹幕转换为字幕格式，并上传</span>    temp_in_file <span class="token operator">=</span> Path<span class="token punctuation">(</span><span class="token string">'temp/in.xml'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>absolute<span class="token punctuation">(</span><span class="token punctuation">)</span>    temp_out_file <span class="token operator">=</span> Path<span class="token punctuation">(</span><span class="token string">'temp/out.ass'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>absolute<span class="token punctuation">(</span><span class="token punctuation">)</span>    temp_in_file<span class="token punctuation">.</span>parent<span class="token punctuation">.</span>mkdir<span class="token punctuation">(</span>exist_ok<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>    temp_out_file<span class="token punctuation">.</span>parent<span class="token punctuation">.</span>mkdir<span class="token punctuation">(</span>exist_ok<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>    <span class="token comment"># 写入临时文件</span>    <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>temp_in_file<span class="token punctuation">,</span> <span class="token string">'wb'</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>        f<span class="token punctuation">.</span>write<span class="token punctuation">(</span>ET<span class="token punctuation">.</span>tostring<span class="token punctuation">(</span>root<span class="token punctuation">,</span> encoding<span class="token operator">=</span><span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token comment"># 转换格式</span>    cli <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'</span><span class="token interpolation"><span class="token punctuation">{</span>danmaku_factory<span class="token punctuation">}</span></span><span class="token string"> -i "</span><span class="token interpolation"><span class="token punctuation">{</span>temp_in_file<span class="token punctuation">}</span></span><span class="token string">" -o "</span><span class="token interpolation"><span class="token punctuation">{</span>temp_out_file<span class="token punctuation">}</span></span><span class="token string">" --ignore-warnings'</span></span>    <span class="token comment"># print(cli)</span>    subprocess<span class="token punctuation">.</span>run<span class="token punctuation">(</span>cli<span class="token punctuation">,</span> check<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>    <span class="token comment"># 上传</span>    full<span class="token punctuation">,</span> ext <span class="token operator">=</span> split_filename<span class="token punctuation">(</span>subtitle_path<span class="token punctuation">)</span>    remote_filename <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'</span><span class="token interpolation"><span class="token punctuation">{</span>full<span class="token punctuation">}</span></span><span class="token string">_弹幕</span><span class="token interpolation"><span class="token punctuation">{</span>index <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">}</span></span><span class="token string">.ass'</span></span>    <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>temp_out_file<span class="token punctuation">,</span> <span class="token string">'rb'</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>        alist<span class="token punctuation">.</span>write_file<span class="token punctuation">(</span>remote_filename<span class="token punctuation">,</span> f<span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f'时间片段: </span><span class="token interpolation"><span class="token punctuation">{</span>h<span class="token punctuation">}</span></span><span class="token string">小时 </span><span class="token interpolation"><span class="token punctuation">{</span>m<span class="token punctuation">}</span></span><span class="token string">分钟 </span><span class="token interpolation"><span class="token punctuation">{</span>s<span class="token punctuation">}</span></span><span class="token string">秒: </span><span class="token interpolation"><span class="token punctuation">{</span>remote_filename<span class="token punctuation">}</span></span><span class="token string">'</span></span><span class="token punctuation">)</span>    index <span class="token operator">+=</span> <span class="token number">1</span><span class="token comment"># print('ok')</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="Web" scheme="https://aprilforest.cn/categories/Web/"/>
    
    <category term="软件" scheme="https://aprilforest.cn/categories/%E8%BD%AF%E4%BB%B6/"/>
    
    
    <category term="nas" scheme="https://aprilforest.cn/tags/nas/"/>
    
    <category term="直播" scheme="https://aprilforest.cn/tags/%E7%9B%B4%E6%92%AD/"/>
    
  </entry>
  
  <entry>
    <title>还可以再爬一次山</title>
    <link href="https://aprilforest.cn/24126-0122.html"/>
    <id>https://aprilforest.cn/24126-0122.html</id>
    <published>2024-05-05T01:22:03.000Z</published>
    <updated>2026-03-02T14:02:12.697Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>和朋友去泰山玩。这是我第二次出远门，和22年那次一样，也是晚上的车，出省。</p><p>这次出门没有第一次那么紧张了，但是硬卧车的空间很挤，而且窗户不能开，车里又是人挤人，还是有些不开心。不过好在朋友也睡我对面，紧绷的神经多少缓和了一些。</p><p>爬到一半天黑了，往后一看尽是城市的灯火，一条条马路和灯光非常漂亮，很像乘直升机在空中拍摄的那番景象，很是好看。</p><p><img src="/24126-0122/IMG_20240430_200932_.jpg" alt="IMG_20240430_192952"></p><p>最后一个门叫南天门，到这里基本上也就到达了山顶商业街。虽然已经晚上11点了，但是这里依然很热闹，灯火通明的。这里东西挺贵，蛋炒饭30元，分量还不多。虽然自己带了零食，但是我们几个还是想吃点热乎的东西。</p><p><img src="/24126-0122/IMG_20240430_211505_.jpg" alt="IMG_20240430_211505_.jpg"></p><p>从这里俯瞰下面的泰安市，还带点朦胧的雾气，真漂亮啊。</p><p><img src="/24126-0122/IMG_20240430_221453_.jpg" alt="IMG_20240430_221453"></p><p>11点半到日观峰，虽然已经来了很多人，但我们运气不错，找到一个好地方，前面朝向东方，右边还有一块特别高的石头挡风。我正好坐在那个垃圾桶旁边，有块比较平的大石头，正好盘腿坐上面，还有可以靠背的石头。</p><p>不得不说，1400多米高的山上晚上风还是很大，很冷，睡不着，只能玩手机硬扛着，也不想动。准备的零食也没怎么吃，没有一点胃口。要不是离垃圾桶近，真的是一口也吃不下。</p><p>凌晨3点左右大部队到达了，直接把道路堵个水泄不通，想上个厕所都不可能，别说位置有没有被抢，能不能挤到垃圾桶这里来都是个问号。</p><p>手机摄像不太行只能拍到这了</p><p><img src="/24126-0122/IMG_20240501_045931_.jpg" alt="IMG_20240501_045931"></p><p><img src="/24126-0122/IMG_20240501_051925_.jpg" alt="IMG_20240501_051925"></p><p>看完日出乘坐索道下山，人生第一次坐这个挺好奇的，缆车四周都是玻璃，视野超级好，好新鲜。坐到一半乘汽车下山，然后我车上睡了一路23333（实在太困了</p><p><video preload="meta" poster="/24126-0122/vlcsnap-2024-05-05-01h36m59s551.jpg" controls="" src="/24126-0122/202405050131.mp4"></video></p><p>我们还去逛了万达，大厅好像在举办什么活动，老远就听到超炮的op了，哦原来5月2日是炮姐生日（貌似ba国际服也在搞联动），现场好热闹，围了好多人。</p><p><img src="/24126-0122/IMG_20240502_124013_.jpg" alt="IMG_20240502_124013"></p><p>听说五一我的家乡也有漫展？我长这么大真是第一次见。不过就算再有，我可能也没机会去看了。</p><p>回家后要忙找工作的事情了，这也是趁着最后一次有时间才能和朋友一起出去玩，后面的话，休息应该是个很难得的东西，不会再有大把的连续时间可以一起旅游了。</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="生活" scheme="https://aprilforest.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    <category term="见闻" scheme="https://aprilforest.cn/categories/%E8%A7%81%E9%97%BB/"/>
    
    
    <category term="旅游" scheme="https://aprilforest.cn/tags/%E6%97%85%E6%B8%B8/"/>
    
    <category term="爬山" scheme="https://aprilforest.cn/tags/%E7%88%AC%E5%B1%B1/"/>
    
  </entry>
  
  <entry>
    <title>Protobuf的编码原理</title>
    <link href="https://aprilforest.cn/24108-1731.html"/>
    <id>https://aprilforest.cn/24108-1731.html</id>
    <published>2024-04-17T17:31:32.000Z</published>
    <updated>2026-03-02T14:02:10.805Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>用了很久Protobuf，对它的一些设计很好奇。比如字段的序号是干嘛的？不同版本的消息类怎么兼容？</p><p>这一切的一切，都和Protobuf的序列化机制有关，也就是一个消息类是怎么变成二进制数据的，然后又是怎么从二进制数据还原回消息类的。</p><p>接下来我基于protobuf 3语法和c#语言进行讲解，大部分内容参考自protobuf官方文档。</p><h3 id="varint编码"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#varint编码"></a> varint编码</h3><p>protobuf里很多地方都使用了varint编码，它有一个特点，就是可以将一些数值特别小的数据进行压缩，减少大小，用处非常广泛。</p><p>首先，这是一个最简单的消息类Test，只有一个字段a，是int类型。</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test</span> <span class="token punctuation">{</span>    <span class="token builtin">int32</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>接着给a赋值150后进行序列化，会得到一个3字节的十六进制数据。</p><pre class="line-numbers language-none"><code class="language-none">08 96 01<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>明明int32是4字节，怎么序列化后变成了3字节，还有1个字节去哪里了？</p><p>原因是protobuf使用了一个叫varint的变长编码方式来存储整数，这能减少序列化后的数据大小。</p><p>首先，protobuf中的int32是无符号整数，它能表示的范围是0到43亿（42,9496,7295）之间。</p><p>这个范围非常的大，但我们平常用的最多的可能就是几百，几千，几万这个样子，很少会用int32存储上千万和上亿的数字。</p><p>那么int32的高位大部分情况下都是0，我们都没有使用，空间被白白浪费掉了。</p><hr><p>要明白varint编码的原理，也许从解码的角度来看会更加容易。</p><p>首先在varint中，每个字节只有7个bits是用来存储实际的数字的，还有一个最高bit是用来存储continuation bit（连续位）的，这个位置也是符号位的所在位置。</p><p>比如数字150经过varint编码会变成十六进制的<code>96 01</code>，或者二进制的<code>10010110 00000001</code></p><p>要注意的是这里的<code>96 01</code>和<code>10010110 00000001</code>都是小端编码的多字节数据，为了方便理解这里将其调换顺序，改成大端的写法。然后用加号注明了continuation bits和用下划线注明实际的payload数据位。</p><pre class="line-numbers language-none"><code class="language-none">第2个字节 第1个字节-----------------00000001 10010110+_______ +_______<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>第一个字节<code>10010110</code>的continuation bit是1，就说明第一个字节的数据还没有写完，后面还有。</p><p>此时我们就看第二个字节<code>00000001</code>，第二个字节的continuation bit是0，就说明数据到这里就结束了，后面已经没有更多数据了。</p><p>这就是continuation bit的作用，用来指示数据结束了没有，后面还有没有更多的数据需要读取，直到遇到continuation bit位是0的字节才停下。</p><p>有了continuation bit之后，我们就可以把多个字节里的payload数据拼接起来了。比如上面的例子中，我们把continuation bit直接丢掉之后，就只剩下payload实际数据位了，只有7位了。</p><pre class="line-numbers language-none"><code class="language-none">第2个字节 第1个字节-----------------00000001 10010110 // 原数据 0000001  0010110 // 把continuation bits丢弃   00000010010110 // 把剩下的bits直接拼接起来<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>最后我们得到一个<code>00000010010110</code>的数据，把它丢入计算器，转换成十进制，就正好是150了。</p><p>如果是一个少于8bits的数据，比如只有7bits的十进制92（0101 1100），varint编码后就还是它本身0101 1100不变。</p><p>到这里已经能看出来varint的作用了，可以将一些很小数值的压缩存储，节省空间。</p><p>但是当varint遇到一些很大的值的时候，可能会变成负优化，因为varint编码中，每个字节只有7个bits的有效空间，如果拿来存储一个32bits的数据，比如十六进制的0x80808080，就会占用5个字节。</p><p>如果你的数据一直很大的话，就不建议使用varint编码类型了，而是使用定长的fixed32会更加合适。</p><h3 id="字段序号"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#字段序号"></a> 字段序号</h3><p>protobuf类的每一个字段，除了要写类型和名字以外，还得额外指定一个序号。</p><p>比如下面这个OldTest消息类，字段a的序号就是1，字段b的序号就是2，而且每个字段的序号还不能重复。</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">OldTest</span> <span class="token punctuation">{</span>    <span class="token builtin">string</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>    <span class="token builtin">int32</span> b <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>序号最主要的作用就是做新旧消息的兼容。比如上面的OldTest消息类，过了一段时间之后，因为业务需要，删了原先的b字段，然后增加了一个新的c字段，变成了这样。</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">NewTest</span> <span class="token punctuation">{</span>    <span class="token builtin">string</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>    <span class="token builtin">int32</span> c <span class="token operator">=</span> <span class="token number">3</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>假设服务端这边已经在使用后面的新版消息了，但客户端那边的因为一些原因无法更新版本，只能使用旧的版本。</p><p>现在将新版的消息发送到了旧版的客户端上，客户端在解析消息的时候，就会检查字段的序列号：a字段会正常返序列化，这个没有问题。</p><p>但是检查到b字段的时候，发现接受到的消息里面好像没有b字段的数据，此时客户端会给OldTest的b字段给一个默认值，b是int32类型的，所以会直接给默认值0。</p><p>最后还发现多了一个c字段，c的序号是3，没有任何可以对应的字段，所以直接跳过c。</p><p>这样即使两边的消息类版本的新旧不同，protobuf也能借助序号做出一定的兼容处理。</p><p>这里大家可能会有疑问，为什么不检测字段名，而是要单独写序号呢。这是因为字段名是字符串，发送的时候可能这个字段名本身占的长度比实际的数据还要大，这样就会造成数据的浪费。</p><p>使用序号同样也能确保字段的唯一性，而且所占的空间也大大缩小了。使用序号还有一个好处，那就是只要确保序号相同的情况下，字段的名字是可以随意修改的，不会影响序列化结果。比如上面的NewTest里的字段a，我不想叫它a了，我想叫它content，那么直接修改字段名就好了，只要不改动后面的序号和前面的类型，一样不会影响协议的兼容性，非常的实用。</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">NewTest</span> <span class="token punctuation">{</span>    <span class="token builtin">string</span> content <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>    <span class="token builtin">int32</span> c <span class="token operator">=</span> <span class="token number">3</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>上面解释了字段的作用，现在来探索一下字段是怎么存储的。在最开头我们提到过，有一个最简单的消息类Test，然后赋值<code>a = 150</code>，序列化后会得到十六进制的<code>08 96 01</code></p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test</span> <span class="token punctuation">{</span>    <span class="token builtin">int32</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>其中<code>96 01</code>已经在前面解释过了，这是二进制150在varint编码后的值。但是前面还有一个字节08。字段序列化的秘密可能就藏在这个08的里面。</p><h3 id="wire-type"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#wire-type"></a> Wire Type</h3><p>在说明序号的存储格式之前，首先要介绍一下protobuf的Wire Type是什么。（wire type我也不知道怎么翻译，类似于编码类型？）</p><p>上面我们提到过，对于一些比较小的数字，protobuf使用了varint编码来存储，可以减小数据大小。而对于一些很大的数据，比如2155905152（0x80808080），还使用varint编码的话就不合适了，因为varint的连续位的原因，实际的4字节数据编码后会变成5字节，这样就不划算了。此时直接将4字节原样存储是更合适的选择。</p><p>刚刚提到的varint编码方式，以及原样存储，都是Wire Type的一种。Wire Type一共有6中类型：</p><table><thead><tr><th>ID</th><th>Name</th><th>Used For</th></tr></thead><tbody><tr><td>0</td><td>VARINT</td><td>int32, int64, uint32, uint64, sint32, sint64, bool, enum</td></tr><tr><td>1</td><td>I64</td><td>fixed64, sfixed64, double</td></tr><tr><td>2</td><td>LEN</td><td>string, bytes, embedded messages, packed repeated fields</td></tr><tr><td>3</td><td>SGROUP</td><td>group start (deprecated)</td></tr><tr><td>4</td><td>EGROUP</td><td>group end (deprecated)</td></tr><tr><td>5</td><td>I32</td><td>fixed32, sfixed32, float</td></tr></tbody></table><ul><li><p>首先是最常用的varint类型，使用的范围非常广。</p></li><li><p>接着是定长的i64类型，固定占用64 bits也就是8个字节。</p></li><li><p>后面是len类型，这个类型和其它类型有个很大的不同，就是len是变长的，用来存储任意长度字节的，比如字符串，或者自定义的二进制字节数组等，使用起来是最灵活的。</p></li><li><p>再跟着是3和4，这俩在protobuf 3里面已经弃用了，可以不用管它。</p></li><li><p>最后是i32类型，也是定长的，和i64是一样的，只不过长度只有一半</p></li></ul><p>总的来说Wire Type是protobuf在序列化后使用底层类型，根据不同的数据特点所划分出来的。</p><p>有些Wire Type会对应多个编程语言里的类型，比如int32，int64，bool，enum这些类型都有共同点，就是数据大小在8字节以下，而且值大部分情况都不会很大，所以就很适合用varint来表示。</p><hr><p>知道了wire type之后，我们就可以来看前面提到的神秘字节0x08了。</p><p>这个08字节是有专门的名字的，它叫tag，这个tag里面就同时包含了字段的wire type和序号。</p><p>在tag里面，低3位是用来存储wire type的，3个bits可以表达的范围是0-7，而wire type总共才只有6种，所以使用3个bits来表示所有wire types是完全没有问题的。</p><p>剩余的位用来存储字段的序号。比如我们08从十六进制转换成二进制就是00001000。</p><p>可以看到低3位是000，正好对应wire type表里的varint编码。如果是001，则对应i64类型，也就是定长整数类型。</p><p>接着剩余的高位是00001，也就是1，正好对应字段a的序号<code>int32 a = 1;</code>。</p><p>那看到这里有朋友可能会有一些疑问：00001才只有5位，也才能表示0-31的范围。那我的消息类里面有超过32个字段那要肿么办，会不会编译失败？</p><p>其实不用担心，protobuf早就想好了解决方法：tag这个数据本身就是使用varint编码的。这样即使定义了超过32个字段，也无非就是让tag从1个字节变成2个字节而已，仅仅是数据变大了一点，所以完全不用担心字段数量不够用。</p><p>但是一般情况下，我们都会尽量保证字段数量不要超过32个，这样就可以使tag维持在1个字节以内，也可以减少序列化的大小（毕竟蚊子肉也是肉嘛）</p><p>这里有一个小经验，如果我们这样定义消息类，即使字段数量没有超过32个，但最终也会使用2字节来存储这个tag。</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test</span> <span class="token punctuation">{</span>    <span class="token builtin">int32</span> a <span class="token operator">=</span> <span class="token number">33</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>好了，到这里可以给protobuf对单个字段的序列化规则做个总结了，首先这个字段的序号和wire type会存储到一个叫tag的字节里，比如<code>int32 a = 1;</code>会序列化成08。（不一定是单个字节，因为是varint编码，字段超过32个的话也会使用多字节来存储字段的tag）</p><p>然后后面会跟着具体编码后的数据，比如<code>a = 150</code>就会序列化成96 01。组合起来就成了08 96 01，这三个字节就是这么来的。</p><h3 id="bool类型"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#bool类型"></a> bool类型</h3><p>对于bool类型，protobuf的处理规则很简单，直接将其编码为定长整数int32类型，固定占用4个字节，值等于0就是false，否则就是true。</p><h3 id="负数"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#负数"></a> 负数</h3><p>varint编码很适合存储无符号的整数。但是对于有符号的数来说，尤其是负数，无论是用原码存储，还是用补码存储，都是一件很不划算的事情。</p><p>比如一个64位有符号整数-2，它的原码是：</p><pre class="line-numbers language-none"><code class="language-none">10000000 00000000 00000000 00000010 // 原码<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>而补码是：</p><pre class="line-numbers language-none"><code class="language-none">11111111 11111111 11111111 11111110 // 补码形式<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>甚至反码：</p><pre class="line-numbers language-none"><code class="language-none">11111111 11111111 11111111 11111101 // 反码形式<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>可以看到无论用那种，最高位，也就是符号位都是1，这就会导致varint的优化机制失效，明明是一个最简单的不过的-2，却要用10个字节去存储，有点杀鸡用牛刀了。</p><p>那么protobuf给出的方法是既不使用原码存储，也不使用补码使用，而是使用ZigZag编码存储，zigzag编码将符号位用一种很讨巧的形式进行了存储，使其可以吃到varint的优化。</p><p>ZigZag编码的原理其实很简单，一个数p，如果它是正数，那么就将它乘以2，也就是<code>p * 2</code>。如果是负数，就将它乘以2再减一，也就是<code>|p| * 2 - 1</code>。</p><p>这样我们拿到一个zigzag编码后的数后，就可以通过判断奇偶性来还原符号位，奇数一定是一个负数，偶数一定是正数。同时乘以2这个操作可以直接通过移位完成，CPU直接有对应的指令支持，效率也很高。这样就避免了符号位打断varint的优化了。</p><h3 id="浮点数"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#浮点数"></a> 浮点数</h3><p>浮点数的存储比较简单，如果是double类型，会直接按bits存进8个bytes里，大小端虽然官方文档里没有提及，我猜测可能是以小端模式存储的。同理float类型会存储在4个bytes里。</p><h3 id="len变长数据"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#len变长数据"></a> LEN变长数据</h3><p>对于string这样的变长数据，protobuf的存储方式其实也很简单。</p><p>举个栗子，首先定义一个消息类Test2</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test2</span> <span class="token punctuation">{</span>    <span class="token builtin">string</span> b <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>然后将b赋值为"testing"，序列化后的数值就是：</p><pre class="line-numbers language-none"><code class="language-none">12 07 74 65 73 74 69 6e 67<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>首先，最开始的12是字段的tag，也就是<code>00010 010</code>，00010是字段的序号2，010是LEN类型的wire type。</p><p>然后跟着的是一个07，这个07是一个varint，用来表示整个数据的长度，"testing"的长度正好是7，所以这里就是07。</p><p>既然长度是7，那么后面紧挨着的7个字节，就是对应的数据了，也就是"testing"。</p><pre class="line-numbers language-none"><code class="language-none">12 07 [74 65 73 74 69 6e 67]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>LEN类型的存储方式就是：先存储varint数据长度，然后再存储实际的数据。</p><h3 id="嵌套消息类型"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#嵌套消息类型"></a> 嵌套消息类型</h3><p>对于嵌套消息类型的处理也很简单，比如这里有一个Test3类型，里面有一个Test2类型的字段。</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test3</span> <span class="token punctuation">{</span>    <span class="token positional-class-name class-name">Test1</span> c <span class="token operator">=</span> <span class="token number">3</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>那么先将字段c正常序列化成二进制数据，然后再将这个嵌套类型当做bytes处理就好了。下面是为了方便理解的写法（虽然这样写不太准确）</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test3</span> <span class="token punctuation">{</span>    <span class="token builtin">bytes</span> c <span class="token operator">=</span> <span class="token number">3</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><h3 id="可选字段"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#可选字段"></a> 可选字段</h3><p>在定义消息字段的时候，会有一个optional关键字可以用。只要写上这个关键字，就代表这个字段是可选的，即使不填充这个字段也不会有问题，就像下面这样。</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test4</span> <span class="token punctuation">{</span>    <span class="token keyword">optional</span> <span class="token builtin">string</span> d <span class="token operator">=</span> <span class="token number">4</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>要理解可选字段如何序列化，我们首先要了解protobuf中record（记录）的概念。</p><p>record其实很好理解，一个消息里面的每个字段，序列化之后都是一个单独的record。</p><p>首先我们定义一个消息类Test，里面有两个字段a和d。</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test4</span> <span class="token punctuation">{</span>    <span class="token builtin">int32</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>    <span class="token builtin">string</span> d <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>接着我们给a赋值为63，d赋值为"testing"，再将其序列化。序列化后的值就是：</p><pre class="line-numbers language-none"><code class="language-none">08 96 01 12 07 74 65 73 74 69 6e 67<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后每个字段都是一个record，我们按record划分一下这组bytes。</p><pre class="line-numbers language-none"><code class="language-none">08 96 01 | 12 07 [74 65 73 74 69 6e 67]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>08 96 01是字段a序列化后的record数据，后面的12 07 74 65 73 74 69 6e 67是字段d序列化后端数据。</p><p>消息类里面的每个字段序列化成record后，都是直接追加在前一个record后面的。而每个record又有自己的长度边界信息，所以也不会相互干扰。</p><p>同时record里面也包含了序号信息，这样即使序列化的时候，record没有按消息定义里的字段顺序来排列，也不会影响最终的解析结果。</p><p>现在我们再来看可选字段，前面我们知道了序列化后的数据是由一串records组成的。那么这里也很容易想到，我们在序列化可选字段的时候，如果发现它为空，那么就跳过对应的record数据不序列化，最终的序列化数据里就会少一个record。</p><p>等到反序列化读取的时候，发现少了一个record数据，反序列化程序就知道了这是一个刻意留空的字段了。</p><p>不得不说，protobuf很巧妙地用字段的序号来实现了可选字段，在没有增加新的数据位的情况下就实现了可选字段，妙啊。</p><h3 id="重复字段"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#重复字段"></a> 重复字段</h3><p>重复字段的处理会相对复杂一点，protobuf有两种策略来序列化重复字段，一种是普通编码，另一种是紧密编码（packed）。</p><p>普通编码使用上没有任何限制，不管是什么类型都可以使用。而紧密编码只能用在原始类型（promitive type）的字段上。</p><p>原始类型在protobuf里的定义就是除了string和bytes以外的所有标量类型（scalar value types），包括这些：double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool。</p><p>可以看到这些类型都有一些共同点，那就是数据的大小都很小，不会很大，且大多都使用varint编码。</p><p>首先来说普通编码，因为每个字段都有自己的序号，按序号序列化成一个个records，那么处理重复字段的方法就很简单了，比如有这么一个消息类Test：</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test5</span> <span class="token punctuation">{</span>  <span class="token keyword">repeated</span> <span class="token builtin">string</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>a是一个重复字段，我们给a赋值为<code>["test", "str1"]</code>，那么序列化后会变成：</p><pre class="line-numbers language-none"><code class="language-none">08 04 [74 65 73 74] | 08 04 [73 74 72 31]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>可以看到字段a被序列化了2次，生成了2个records。这样当反序列化程序读取到了两个相同序号的字段时，就会把它们当做一个列表收集起来了，也就实现了重复字段的传输。</p><p>而对于原始类型来说，protobuf为了保证效率，会使用紧密编码来存储它们。</p><p>需要说明的是，在protobuf2里，要在字段后面手动标明<code>[packed=true]</code>才会开启紧密编码。但是在protobuf3中，所有原始类型默认就已经开启了紧密排列。</p><p>普通编码时，每多出一个元素就会多序列化一个record，而record里面是包含tag部分的，虽然tag大部分情况下只有一个字节长，但是对应原始类型来说，原始类型本身大部分情况下也只有一个字节长，现在又加上了tag，长度从1个字节变成了2个字节的长度，多少有些不太划算了。</p><p>紧密编码正好就是用来解决这个问题的，它直接把所有元素打包到一起，所有元素共用一个tag，这样存储空间的利用率也就起来了。</p><p>紧密编码的存储原理是使用LEN可变长类型来存储重复字段，比如写一个重复的int32字段：</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test5</span> <span class="token punctuation">{</span>    <span class="token comment">// protobuf3中已经默认启用原始类型的紧密编码，索引这里不需要额外再写什么</span>    <span class="token keyword">repeated</span> <span class="token builtin">int32</span> f <span class="token operator">=</span> <span class="token number">6</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>我们给f赋值为<code>[3, 270, 86942]</code>再将其序列化后，就得到了一个3206038e029ea705。这个数据看起来很吓人，其实把它拆开以后还是很好理解的：</p><pre class="line-numbers language-none"><code class="language-none">32 06 [03, 8e 02, 9e a7 05]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>首先我们读取开头的0x32，二进制是00110 010，00110是6，代表字段f的序号，这点没问题。010代表这个record是wire type，010是2，对应上方表里的LEN类型，到这里也没问题。</p><p>LEN类型我们知道，数据的开头是一个varint编码的长度描述符。我们读取第二个字节0x06，发现这个字节的最高位，也就是varint连续位的是0，那么就表示所有的有效数据都在这一个字节里，也就是06。这个06就代表这个LEN类型的字段，后面有多少个字节。</p><p>然后我们把后面的6个字节单独拿出来：</p><pre class="line-numbers language-none"><code class="language-none">[03 8e 02 9e a7 05]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>此时还不知道这6个字节是什么意思，只知道它是3个int32类的数据。</p><p>首先从第一个字节0x03开始读取，因为int32也是使用varint编码的，我们在读取0x03的时候，也要检查它的连续位是否为1，这里显然为0，说明整个0x03就是第一个数据了。这里正好对应我们原始数据<code>[3, 270, 86942]</code>中的第一个数据3。</p><p>我们在03后面画上逗号进行分割：</p><pre class="line-numbers language-none"><code class="language-none">[03, 8e 02 9e a7 05]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>再来看第二个字节0x8e，8e的二进制编码是10001110，可以看到最高位，也就是varint的连续位是1，说明这个数据还没有写完，后面还有。那再读取一下8e后面的一个字节02。0x02这个字节是最高位是0，说明这个varint值是由2个字节组成的。</p><p>我们首先将0x8e和0x02翻译成二进制：<code>1000 1110</code>、<code>0000 0010</code>。丢弃连续位，变成<code>0001110</code>、<code>0000010</code>。然后将剩下的7 + 7 bits直接拼接在一起，变成<code>00000100001110</code>，也就是<code>100001110</code>，转换成十进制正好等于270。（注意varint是使用小端存储的，拼接位的时候从人类的角度看起来是反过来的，会有点反直觉）</p><p>我们在8e 02后面画上逗号进行分割：</p><pre class="line-numbers language-none"><code class="language-none">[03, 8e 02, 9e a7 05]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>接着是字节0x9e，二进制是<code>1001 1110</code>，连续位是1，说明后面还有数据。读取第二个字节0xa7，二进制是<code>1010 0111</code>，连续位是1，说明后面还有数据，读取第三个字节0x05。二进制是<code>0000 0101</code>，此时连续位是0了，说明没有数据了。收集到的9e a7 05拼接起来后，正好等于原数据中的第三个元素86942。此时已经过去了6个字节，LEN数据也正好到达末尾，解码到这里就结束了。</p><p>这里有一个小细节，如果LEN的长度描述和varint实际读取到长度不一样，是会出问题的。拿上面那个例子来说，如果最后一个字节不是05，而是85（0x85的连续位是1），反序列化时就会读取到超过有效范围之外的数据，可能会导致数据错误，这一点需要注意。不过实际使用时都是由protobuf在帮我们完成数据的序列化和反序列化工作，还是不用担心它会出现问题的。假如是自己序列化数据的话，就需要留意一下这个问题了。</p><h3 id="map类型"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#map类型"></a> Map类型</h3><p>Map类型的处理非常简单，以下两种代码在序列化后是等价的</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test6</span> <span class="token punctuation">{</span>    <span class="token map class-name">map<span class="token punctuation">&lt;</span><span class="token builtin">string</span><span class="token punctuation">,</span> <span class="token builtin">int32</span><span class="token punctuation">&gt;</span></span> g <span class="token operator">=</span> <span class="token number">7</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>等效代码：</p><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf"><span class="token keyword">message</span> <span class="token class-name">Test6</span> <span class="token punctuation">{</span>    <span class="token keyword">message</span> <span class="token class-name">g_Entry</span> <span class="token punctuation">{</span>        <span class="token keyword">optional</span> <span class="token builtin">string</span> key <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>        <span class="token keyword">optional</span> <span class="token builtin">int32</span> value <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">;</span>    <span class="token punctuation">}</span>    <span class="token keyword">repeated</span> <span class="token positional-class-name class-name">g_Entry</span> g <span class="token operator">=</span> <span class="token number">7</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="序列化后字段顺序"><a class="markdownIt-Anchor bubble-link" href="/24108-1731/#序列化后字段顺序"></a> 序列化后字段顺序</h3><p>因为有了字段的序号，序列化后，各个records之间的相对顺序就不再重要的，而protobuf也要求反序列化程序不能依赖records的顺序，而是要跟句record里的序号做反序列化更加合适。</p><hr><p>封面来源：X@SR_LD_FR（原贴发布于 12:13 AM · Apr 1, 2024）</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="软件" scheme="https://aprilforest.cn/categories/%E8%BD%AF%E4%BB%B6/"/>
    
    
    <category term="Protobuf" scheme="https://aprilforest.cn/tags/Protobuf/"/>
    
  </entry>
  
  <entry>
    <title>不买Win掌机了</title>
    <link href="https://aprilforest.cn/24091-1613.html"/>
    <id>https://aprilforest.cn/24091-1613.html</id>
    <published>2024-03-31T16:13:46.000Z</published>
    <updated>2026-03-02T14:02:12.685Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>我老想买Win掌机了。</p><p>起因是我的Steam上有很多剧情化叙事的游戏，我想躺在床上玩，不想时时刻刻坐在电脑前面，所以想买个Win掌机每天晚上睡觉前可以在床上玩会儿。</p><p>但是市面上在售的win掌机都挺贵的，而且大部分掌机都是奔着玩3A的性能去的，动不动就是7840u的配置。</p><p>我想要一个性能能玩GalGame的，屏幕8寸左右，色彩好一些，然后续航长一点这样的。如果有可拆卸手柄就更好了。</p><p>尤其是可拆卸手柄我很想要，要是掌机带个手柄，周围的人（特别是长辈，有刻板印象）总觉得你这个是游戏机，专门打游戏的，你是个不务正业的人，整天无所事事，就知道玩害人不浅的东西，以后早晚变成个废人。</p><p>如果手柄能拆的话可以直接化身平板电脑，问就是买来学习的，和普通的平板电脑没什么两样，真要打游戏直接手柄装口袋里打，问就是在看别人玩游戏的视频。不然老一辈看到就要跟小一辈开始教育了，你们看那个谁谁谁，整天只知道打游戏，连出门都要抱着个游戏机玩，他这样迟早完成废人，你们千万不能学他啊，就应该把游戏给禁了，害人不浅的东西。</p><p>我也知道我有些过于在乎身边人的想法了，但是现在相比以前已经好多了。小时候的我就是这样过来的，玩什么东西都得藏着掖着，不能跟任何人分享我有什么好玩的东西，否则迟早传到家里人耳朵里，我就得倒大霉。</p><p>目前看来Legion GO是最符合我的要求的，屏幕又大，手柄还能拆，又是大厂的产品。但是它确实好贵啊，目前还买不起，我只能寻找其它的替代方案。</p><p>正好最近极客湾发了一期视频，讲怎么在手机上用mobox去模拟运行x86游戏，不需要root的那种。正好我手上有个骁龙8+的手机，就去尝试了一下。</p><p>大概的原理是box64会把x86的指令集转译成arm，然后交给wine做windows到linux的api翻译。图形API的话有dxvk做转译，8gen3的GPU性能已经接近1650ti了，所以完全没问题。</p><p>我尝试了一下，这个方案最大的优点是可以装电脑上的steam可以同步存档，然后把steam里的游戏下载下来玩。</p><p>我随便运行了一个2d游戏，发现转译后的帧率可以跑到90帧左右，分辨率1080p，GPU占用80%的样子。感觉很可玩啊。</p><p><img src="/24091-1613/mmexport1711868384307.jpg" alt="mmexport1711868384307"></p><p><img src="/24091-1613/1711870602567.jpg" alt="1711870602567"></p><p>我也做了下对比，同样的游戏，场景的加载速度基本上吊打Switch。手机这边转译后加载只要2s，电脑上1s左右，而Switch上要10s还要多，就离谱。</p><p>但是这个手机转译的方案也有缺点，不是所有游戏都能转译成功的，有相当一部分游戏还是没法玩的。而且steam本身转译起来特别吃内存，动不动就吃6-7个G的RAM，经常会闪退，启动十次只有1到2次能成功，体验非常不好，可能是我手机内存太小了，下次有机会换个内存大点的试试。</p><p>最后我选择的方案是串流，用Steam Link串流，在电脑上运行，然后传输到手机上显示。</p><p>串流的延迟大约50ms左右能接受（包括视频解码），而且玩游戏时，电脑也一直在我身边，不用配置公网串流，也挺省心的。最后就是感觉手机屏幕小了点，要是有个平板就好了。</p><p>这么算下来，去买Win掌机的确实没有太大必要。续航不行就不说了，还挺重。我连Switch Lite都嫌沉，真要整个Win掌机估计也只能放桌子上玩了，拿着肯定是玩不了的。</p><p>而且Win掌机还很贵，我想了想，完全可以把这5k的预算，加到我换下台电脑上，显卡可以直接上2个档，甚至还有多的。</p><p>或者是等后面mobox再完善一点，也许好多游戏又可以玩了，用手机转译也挺好的，毕竟现在手机的性能都这么强了，转译运行一些2d独立游戏应该问题不大。</p><p>这么一折腾下来，也算是知道了自己真正的诉求吧。有些自己瞧不上的东西，到头来却是最契合自己的方案。以后也长教训了，不要对事物抱有偏见，应该多去尝试一下，总能有意想不到的收获。</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="游戏" scheme="https://aprilforest.cn/categories/%E6%B8%B8%E6%88%8F/"/>
    
    <category term="硬件" scheme="https://aprilforest.cn/categories/%E7%A1%AC%E4%BB%B6/"/>
    
    
    <category term="掌机" scheme="https://aprilforest.cn/tags/%E6%8E%8C%E6%9C%BA/"/>
    
  </entry>
  
  <entry>
    <title>不整NAS了</title>
    <link href="https://aprilforest.cn/24091-1603.html"/>
    <id>https://aprilforest.cn/24091-1603.html</id>
    <published>2024-03-31T16:03:34.000Z</published>
    <updated>2026-03-02T14:02:12.685Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>前段时间用香橙派搭建了一个简单的NAS，外挂了一块固态作为存储。</p><p>体积确实是蛮小的，可以随时揣口袋里带走。虽然它没有USB 3.0接口，我的硬盘盒只能跑到2.0的速度。但我不嫌弃它，平时我无非就是看看NAS里的照片啊，文档什么的，偶然看看视频也没问题。</p><p><img src="/24091-1603/IMG_20240212_130915.jpg" alt="IMG_20240212_130915"></p><p><img src="/24091-1603/IMG_20240212_130926.jpg" alt="IMG_20240212_130926"></p><p>前些日子我在朋友家里住了一个星期，因为不是在自己家嘛，所有的电子设备用完必须要拔掉。NAS也就没办法一直24小时开机，只能在要用它的时候，放自己衣服口袋里，然后用小充电宝供电，再用电脑远程访问，不会让别人看到。</p><p>在自己家里随便玩没事，但在别人家还是不要太张扬的好。我不想给别人留一个喜欢瞎折腾，整的和hacker一样的的形象。我知道这是对hacker的误解，我也不是做security相关的，但是大部分普通人并不理解，我也不想闹乌龙。</p><p><img src="/24091-1603/IMG_20240212_130544.jpg" alt="IMG_20240212_130544"></p><p>虽然NAS已经可以在开机状态下随身携带了，看起来很方便。但它不能带大负载，否则SOC和硬盘都会过热，总之就是用起来很麻烦，我也开机甚少。</p><p>后来我气不过，就把所有数据都搬到我的电脑上了，结果发现这么直接用好像也很方便？数据本身在对象存储是有双重备份的，所以不怕丢失。</p><p>从那以后我就一直在想，NAS对我来说是不是个伪需求。我喜欢的也许不是NAS本身的功能，而是折腾的过程？因为这些数据就只有我一个人访问，数据也不大才150GB左右，放在NAS和电脑上甚至没有太大区别，也不用担心数据安全什么的。</p><p>我好像就没有认真想过自己的需求是什么，总想着这也要，那也要。结果折腾了一圈什么都没留下，还是回到了最简单的存储方式：把文件直接丢电脑上。甚至觉得有些好笑。</p></body></html>]]></content>
    
    
    <summary type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</summary>
    
    
    
    <category term="硬件" scheme="https://aprilforest.cn/categories/%E7%A1%AC%E4%BB%B6/"/>
    
    
    <category term="NAS" scheme="https://aprilforest.cn/tags/NAS/"/>
    
  </entry>
  
</feed>
