<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>咸鱼的小鱼塘</title>
    <link>https://blog.zjyl1994.com/</link>
    <description>Recent content on 咸鱼的小鱼塘</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sun, 25 Oct 2026 15:04:00 +0800</lastBuildDate>
    <atom:link href="https://blog.zjyl1994.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>友人</title>
      <link>https://blog.zjyl1994.com/friends/</link>
      <pubDate>Tue, 10 Jul 2018 18:56:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/friends/</guid>
      <description>&lt;h1 id=&#34;咱的朋友们&#34;&gt;咱的朋友们&lt;/h1&gt;
&lt;p&gt;现在这个时代是浮躁的时代，忙着赚钱忙着被生活压迫，已经很少有人能静下心坐下来写点什么。&lt;/p&gt;
&lt;p&gt;相比于乱七八糟充满营销号贩卖焦虑的微信公众号，我更喜欢那种自己建站自己维护的独立博客。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="咱的朋友们">咱的朋友们</h1>
<p>现在这个时代是浮躁的时代，忙着赚钱忙着被生活压迫，已经很少有人能静下心坐下来写点什么。</p>
<p>相比于乱七八糟充满营销号贩卖焦虑的微信公众号，我更喜欢那种自己建站自己维护的独立博客。</p>
<p>一个精心打磨的博客，背后都是一个充满工匠精神的灵魂。而这种人，正是现在快节奏社会所欠缺的。</p>
<p>他们注定不会平凡。</p>
<p>所以，隆重推荐我的朋友给你们，希望你们也能互相认识，成为好朋友~</p>
<p>(按添加时间排序，排名不分先后)</p>
<ol>
<li><strong>Rex Zeng</strong> ⇒ <a href="https://www.rexskz.info/" rel="noopener noreferrer" target="_blank">https://www.rexskz.info/</a></li>
<li><strong>Printempw</strong> ⇒ <a href="https://printempw.github.io/" rel="noopener noreferrer" target="_blank">https://printempw.github.io/</a></li>
<li><strong>Rikumi</strong> ⇒ <a href="https://craft.rkm.mx/" rel="noopener noreferrer" target="_blank">https://craft.rkm.mx/</a></li>
<li><strong>辣樱</strong> ⇒ <a href="https://senajun.github.io/" rel="noopener noreferrer" target="_blank">https://senajun.github.io/</a></li>
<li><strong>新一</strong> ⇒ <a href="https://a-wing.top/" rel="noopener noreferrer" target="_blank">https://a-wing.top/</a></li>
<li><strong>白泠</strong> ⇒ <a href="https://kaf.moe/" rel="noopener noreferrer" target="_blank">https://kaf.moe/</a></li>
<li><strong>量子幽灵</strong> ⇒ <a href="https://blog.quantumghost.dev/" rel="noopener noreferrer" target="_blank">https://blog.quantumghost.dev/</a></li>
<li><strong>Alpha UMi</strong> ⇒ <a href="https://cynosura.one" rel="noopener noreferrer" target="_blank">https://cynosura.one</a></li>
</ol>
<p>更新于 2026.10.25，已检查链接有效性</p>
]]></content:encoded>
    </item>
    <item>
      <title>关于</title>
      <link>https://blog.zjyl1994.com/about/</link>
      <pubDate>Sun, 20 Aug 2017 21:38:52 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/about/</guid>
      <description>&lt;h1 id=&#34;关于我&#34;&gt;关于我&lt;/h1&gt;
&lt;p&gt;zjyl1994，现网名“鱼sifu”，之前叫“咸鱼”，这也是站名的由来。&lt;/p&gt;
&lt;p&gt;普通的后端程序员，到处逛吃逛吃的民间食物品尝家。&lt;/p&gt;
&lt;p&gt;16年毕业于某师范大学计算机专业，目前在某中厂做后端开发。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="关于我">关于我</h1>
<p>zjyl1994，现网名“鱼sifu”，之前叫“咸鱼”，这也是站名的由来。</p>
<p>普通的后端程序员，到处逛吃逛吃的民间食物品尝家。</p>
<p>16年毕业于某师范大学计算机专业，目前在某中厂做后端开发。</p>
<p>专职 Golang 程序员，之前主写 PHP，也写过不少 Python。</p>
<p>24年成为明日方舟玩家，主推干员<a href="https://prts.wiki/w/%E6%B6%A4%E7%81%AB%E6%9D%B0%E8%A5%BF%E5%8D%A1" rel="noopener noreferrer" target="_blank">杰西卡</a>。</p>
<p>希望有一天可以赚很多的钱，完成自己想做的事情。</p>
<h1 id="关于这个博客">关于这个博客</h1>
<p>16年开始，自己买服务器装Wordpress写博客。由于数据损毁，又没有备份，所以之前的文章全都丢了。</p>
<p>第2代开始用静态网站生成器+Markdown编写文章，部署在自己的私有服务器上。</p>
<p>现在的博客算是第3代自建博客，整体托管在Cloudflare Pages上。同时做了全站图片的WebP化改造。</p>
<p>这个博客基于 <a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo静态网站生成器</a> 构建，搭配的 <a href="https://themes.gohugo.io/themes/hugo-papermod/" rel="noopener noreferrer" target="_blank">PaperMod主题</a>。</p>
<p>博客的源码在 <a href="https://github.com/zjyl1994/blog" rel="noopener noreferrer" target="_blank">zjyl1994/blog</a> 。</p>
<h1 id="关于这些内容">关于这些内容</h1>
<p>本站内容如非特殊说明，都是我自己写的原创内容。</p>
<p>对于本站原创内容，使用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" rel="noopener noreferrer" target="_blank">署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)</a> 协议进行授权。</p>
]]></content:encoded>
    </item>
    <item>
      <title>RLCD看板</title>
      <link>https://blog.zjyl1994.com/post/rlcd_dashboard/</link>
      <pubDate>Sun, 21 Jun 2026 10:30:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/rlcd_dashboard/</guid>
      <description>&lt;p&gt;最近在抖音刷到了一个大佬制作的黄金价格看板，询问后得知硬件是微雪的RLCD开发板，淘宝搜了一下只要150。
于是萌生了我也要一个的想法，立刻下单。由于我对嵌入式一窍不通，所以本次全程是VibeCoding的。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>最近在抖音刷到了一个大佬制作的黄金价格看板，询问后得知硬件是微雪的RLCD开发板，淘宝搜了一下只要150。
于是萌生了我也要一个的想法，立刻下单。由于我对嵌入式一窍不通，所以本次全程是VibeCoding的。</p>
<h1 id="设备介绍">设备介绍</h1>
<p><img alt="官方图片" decoding="async" height="1200" loading="eager" src="/post/rlcd_dashboard/0.webp" width="1600">
</p>
<p><a href="https://docs.waveshare.net/ESP32-S3-RLCD-4.2/" rel="noopener noreferrer" target="_blank">设备介绍</a></p>
<p>内置了温湿度传感器，WiFi、蓝牙、还有喇叭和麦克风，处理器选用的ESP32-S3处理器配8M的PSRAM，16M的Flash可以放下一套还可以的ttf字体。</p>
<h1 id="第一版">第一版</h1>
<p>首先考虑我的需求，我日常使用OpenCode进行开发，每次Vibe期间我都会并行去做其他内容，经常会发生AI完成后我不知道，等很久回来才发现AI结束出错了（或者等待我给出权限）。所以我需要实现一个消息提示面板，AI完成工作后发送振铃通知给我。</p>
<p>于是有了第一版设计, 基调是使用WiFi链接，MQTT推送消息线给板子，实现主板和控制解耦。这样我在开发过程中可以使用现有的MQTT服务器和工具链进行调试，而不用准备对应的业务服务器。</p>
<p><img alt="第一版时钟页面" decoding="async" height="750" loading="lazy" src="/post/rlcd_dashboard/1.webp" width="1000">

<img alt="第一版消息页面" decoding="async" height="750" loading="lazy" src="/post/rlcd_dashboard/2.webp" width="1000">
</p>
<h1 id="第二版">第二版</h1>
<p>在使用了一段时间后，我发现大字时钟非常浪费显示内容，而且集成的Unifont只有16px，放在办公桌上并不能一眼看出来。所以我就有了重构的想法。</p>
<p>新的重构计划是，画面分成四个部分，底部显示消息，左上角显示时钟，右上角显示一些金融相关信息，方便实时查看资产情况。
<img alt="第二版界面" decoding="async" height="1000" loading="lazy" src="/post/rlcd_dashboard/3.webp" width="563">
</p>
<p>后来发现还有改造空间，底部的消息区太大了，所以重新调整显示布局，于是有了最终版本</p>
<p><img alt="第二版改" decoding="async" height="750" loading="lazy" src="/post/rlcd_dashboard/4.webp" width="1000">
</p>
<p>调整字号图标这些细节后得到了</p>
<p><img alt="第二版改二" decoding="async" height="750" loading="lazy" src="/post/rlcd_dashboard/5.webp" width="1000">
</p>
<p>左侧展示时钟、日期，下面两组Agent红绿灯展示不同OpenCode的状态，WORK时灯会闪烁。右侧展示我关心的金融数据。底部是可以自定义推送的三行文本消息。</p>
<p>技术细节上来说，右侧INFO区是一个自动翻页的多行文本最长支持48行，底部是一个三行文本消息区，会自动计算位置居中显示。超出的也会滚动显示并且支持timeout和按键已读/召回消息。</p>
<p>由于板子性能有限，MQTT只提供不同组件的直接操控。一些定制逻辑我开发了一个服务端进行编排，包括OpenCode插件上报的状态、金融数据的配置和刷新。</p>
<p>现在的版本我开源并放在 <a href="https://github.com/zjyl1994/rlcd-dashboard" rel="noopener noreferrer" target="_blank">GitHub</a>
可以直接去仓库中打开查看。</p>
<p>board中存储的是设备固件，可以使用<code>idf.py build flash</code>直接构建。
plugin中存储的是OpenCode插件，可以复制到<code>~/.config/opencode/plugins</code>,修改上报的服务器即可。
server中存储的是Go编写的服务，封装插件上报状态和金融定时逻辑转化为MQTT消息进行推送。</p>
<h1 id="后记">后记</h1>
<p>本来我是想用MicroPython实现一版本的，为此我还移植了一套MicroPython，后来想来我也是用OpenCode代工，不如直接让AI写ESP-IDF C/C++。</p>
<p>这个我自己移植的版本也可以看看 <a href="https://github.com/zjyl1994/MICROPYTHON_WAVESHARE_ESP32_S3_RLCD_42" rel="noopener noreferrer" target="_blank">MicroPython For RLCD</a></p>
<p>整个开发过程中，最重要的就是官方样例代码，让AI去读官方怎么实现的，然后抄到你的项目来，这样才能顺利驱动所有底层硬件设备。有些不清楚的内容也可以把原理图截图给AI来读图识别，把结论交给CodeAgent施工。</p>
<h2 id="链接">链接</h2>
<ol>
<li><a href="https://docs.waveshare.net/ESP32-S3-RLCD-4.2/" rel="noopener noreferrer" target="_blank">官方文档</a></li>
<li><a href="https://www.waveshare.net/w/upload/e/e6/ESP32-S3-RLCD-4.2-schematic.pdf" rel="noopener noreferrer" target="_blank">官方原理图</a></li>
<li><a href="https://github.com/waveshareteam/ESP32-S3-RLCD-4.2" rel="noopener noreferrer" target="_blank">官方样例代码</a></li>
<li><a href="https://github.com/zjyl1994/rlcd-dashboard" rel="noopener noreferrer" target="_blank">我的看板项目源码</a></li>
<li><a href="https://github.com/zjyl1994/MICROPYTHON_WAVESHARE_ESP32_S3_RLCD_42" rel="noopener noreferrer" target="_blank">我的MicroPython移植</a></li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>2025 上海游记</title>
      <link>https://blog.zjyl1994.com/post/2025-shanghai/</link>
      <pubDate>Sun, 11 Jan 2026 10:27:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/2025-shanghai/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;趁着2025的尾巴，去了一次上海。
拍了一些照片，记录一下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;上海三件套&#34;&gt;上海三件套&lt;/h1&gt;
&lt;p&gt;经典上海打卡三件套，这个机位在北外滩公园，人很少视野开阔，拍起来也很有特色。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;上海三件套&#34; decoding=&#34;async&#34; height=&#34;1080&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/2025-shanghai/%E4%B8%89%E4%BB%B6%E5%A5%971.webp&#34; width=&#34;810&#34;&gt;

&lt;img alt=&#34;上海三件套&#34; decoding=&#34;async&#34; height=&#34;1080&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/2025-shanghai/%E4%B8%89%E4%BB%B6%E5%A5%97.webp&#34; width=&#34;1440&#34;&gt;
&lt;/p&gt;
&lt;h1 id=&#34;外滩&#34;&gt;外滩&lt;/h1&gt;
&lt;p&gt;最经典的外滩机位，人山人海，不是元旦假期的晚上都挪不动脚步。
&lt;img alt=&#34;外滩&#34; decoding=&#34;async&#34; height=&#34;1080&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/2025-shanghai/%E5%A4%96%E6%BB%A90.webp&#34; width=&#34;1440&#34;&gt;

&lt;img alt=&#34;外滩&#34; decoding=&#34;async&#34; height=&#34;1080&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/2025-shanghai/%E5%A4%96%E6%BB%A91.webp&#34; width=&#34;1440&#34;&gt;

&lt;img alt=&#34;外滩&#34; decoding=&#34;async&#34; height=&#34;1080&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/2025-shanghai/%E5%A4%96%E6%BB%A92.webp&#34; width=&#34;1440&#34;&gt;
&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>趁着2025的尾巴，去了一次上海。
拍了一些照片，记录一下</p>
</blockquote>
<h1 id="上海三件套">上海三件套</h1>
<p>经典上海打卡三件套，这个机位在北外滩公园，人很少视野开阔，拍起来也很有特色。</p>
<p><img alt="上海三件套" decoding="async" height="1080" loading="eager" src="/post/2025-shanghai/%E4%B8%89%E4%BB%B6%E5%A5%971.webp" width="810">

<img alt="上海三件套" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E4%B8%89%E4%BB%B6%E5%A5%97.webp" width="1440">
</p>
<h1 id="外滩">外滩</h1>
<p>最经典的外滩机位，人山人海，不是元旦假期的晚上都挪不动脚步。
<img alt="外滩" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%A4%96%E6%BB%A90.webp" width="1440">

<img alt="外滩" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%A4%96%E6%BB%A91.webp" width="1440">

<img alt="外滩" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%A4%96%E6%BB%A92.webp" width="1440">
</p>
<p>万国建筑群,这边好多银行大楼至今仍然是银行
<img alt="外滩" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%A4%96%E6%BB%A93.webp" width="810">

<img alt="外滩" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%A4%96%E6%BB%A94.webp" width="1440">
</p>
<p>还有很著名的和平饭店
<img alt="和平饭店" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%92%8C%E5%B9%B3%E9%A5%AD%E5%BA%97.webp" width="1440">
</p>
<h1 id="静安寺">静安寺</h1>
<p>闹市中的金色佛寺，里面真的有大和尚在做法事。</p>
<p><img alt="静安寺" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E9%9D%99%E5%AE%89%E5%AF%BA2.webp" width="1440">

<img alt="静安寺" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E9%9D%99%E5%AE%89%E5%AF%BA.webp" width="810">

<img alt="静安寺" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E9%9D%99%E5%AE%89%E5%AF%BA3.webp" width="810">

<img alt="静安寺" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E9%9D%99%E5%AE%89%E5%AF%BA4.webp" width="1440">
</p>
<h1 id="豫园城隍庙">豫园&amp;城隍庙</h1>
<p>豫园是典型的江南园林，门票要提前网上预约
<img alt="豫园" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E8%B1%AB%E5%9B%AD.webp" width="1440">

城隍庙不用门票，财神和月老那边人都络绎不绝，出于尊重没怎么拍神仙。这是外面小吃街的饭店。
<img alt="城隍庙" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%9F%8E%E9%9A%8D%E5%BA%99.webp" width="810">
</p>
<h1 id="武康大楼">武康大楼</h1>
<p>莫名其妙火起来的打卡点，路过也去拍了照。
旁边维持秩序的安保人员也在好奇这有什么拍的。
<img alt="武康大楼" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E6%AD%A6%E5%BA%B7%E5%A4%A7%E6%A5%BC.webp" width="810">

<img alt="武康大楼" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E6%AD%A6%E5%BA%B7%E5%A4%A7%E6%A5%BC2.webp" width="810">
</p>
<h1 id="孙先生">孙先生</h1>
<p>上海名人故居很多，孙先生的故居也在这。
找机会也得去南京拜一拜孙先生。
<img alt="孙先生" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%AD%99%E5%85%88%E7%94%9F.webp" width="810">

<img alt="孙先生手迹" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%AD%99%E5%85%88%E7%94%9F%E6%89%8B%E8%BF%B9.webp" width="810">
</p>
<h1 id="其他">其他</h1>
<p>很著名的抗战遗迹，致敬800壮士
<img alt="四行仓库" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%9B%9B%E8%A1%8C%E4%BB%93%E5%BA%93.webp" width="1440">
</p>
<p>上海美术馆又名中华艺术宫，是当年世博会的中国馆，好多好多画。
<img alt="上海美术馆-鲁迅" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E4%B8%8A%E6%B5%B7%E7%BE%8E%E6%9C%AF%E9%A6%86-%E9%B2%81%E8%BF%85.webp" width="1440">

很多艺术画作，看懂的看不懂的，就这一张我很熟悉，因为他是上学时候教材里的鲁迅先生。</p>
<p><img alt="朱家角" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E6%9C%B1%E5%AE%B6%E8%A7%92.webp" width="810">

江南水乡普通古镇，一条商业街沿河设置，后面全是在卖的小房子。老外超级多，韩语泰语还有东南亚口音的中文都有，看老外穿汉服打卡也挺有意思的。
另外，这的小杨生煎门店超小要等位。离地铁口也挺远，至少2km。</p>
<p><img alt="地铁口" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E5%9C%B0%E9%93%81%E5%8F%A3.webp" width="810">

很普通的城市一景，应该没人猜得到这是哪里吧？</p>
<p><img alt="磁悬浮" decoding="async" height="1080" loading="lazy" src="/post/2025-shanghai/%E7%A3%81%E6%82%AC%E6%B5%AE.webp" width="810">

凭当日机票磁悬浮8折，40块钱，8分钟不到就到了浦东机场。</p>
<h1 id="后记">后记</h1>
<p>上海元旦的温度和广东这边冬季差不多，没想象中的冷。
这下北上广深四个一线城市我算是都去过了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>2026 数字生活提升计划</title>
      <link>https://blog.zjyl1994.com/post/2026-digit-life-enhance/</link>
      <pubDate>Sun, 21 Dec 2025 10:27:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/2026-digit-life-enhance/</guid>
      <description>&lt;p&gt;为了迎接2026年，我计划提升我的数字生活体验。
因此决定把密码管理器、云笔记迁移到境内的服务器上提升使用体验。&lt;/p&gt;
&lt;p&gt;我有一个很重要的刚需就是Web端，公司电脑上有深信服等审计工具，所以很多需要安装客户端的产品我都不能使用。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>为了迎接2026年，我计划提升我的数字生活体验。
因此决定把密码管理器、云笔记迁移到境内的服务器上提升使用体验。</p>
<p>我有一个很重要的刚需就是Web端，公司电脑上有深信服等审计工具，所以很多需要安装客户端的产品我都不能使用。</p>
<p>本篇文章我不会详细介绍每个服务的用法，相关服务知名度很高，网上视频非常多，我这里只记录我自己个人觉得的最佳部署方式。</p>
<h1 id="服务部署">服务部署</h1>
<p>服务器部署容器时，我选择使用podman+quadlet的方式，这样可以与systemd进行集成，不需要管理散落的docker compose文件。</p>
<p>fedora已经自带podman了，如果是ubuntu可以使用apt安装。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install podman
</span></span></code></pre></div><h2 id="vaultwarden-密码管理器">Vaultwarden 密码管理器</h2>
<p>Vaultwarden是一个Bitwarden的服务器端实现，我将把我的密码管理器迁移到这个服务器上。</p>
<p>新建一个文件<code>~/.config/containers/systemd/vaultwarden.container</code>，内容如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Vaultwarden container</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Container]</span>
</span></span><span class="line"><span class="cl"><span class="na">AutoUpdate</span><span class="o">=</span><span class="s">registry</span>
</span></span><span class="line"><span class="cl"><span class="na">Image</span><span class="o">=</span><span class="s">ghcr.io/dani-garcia/vaultwarden:latest</span>
</span></span><span class="line"><span class="cl"><span class="na">Exec</span><span class="o">=</span><span class="s">/start.sh</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">DOMAIN=https://vault.example.com</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">ROCKET_PORT=8080</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">ROCKET_ADDRESS=0.0.0.0</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">SIGNUPS_ALLOWED=false</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">INVITATIONS_ALLOWED=false</span>
</span></span><span class="line"><span class="cl"><span class="na">Volume</span><span class="o">=</span><span class="s">%h/vaultwarden:/data/</span>
</span></span><span class="line"><span class="cl"><span class="na">PublishPort</span><span class="o">=</span><span class="s">127.0.0.1:8181:8080</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">default.target</span>
</span></span></code></pre></div><p>然后执行</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user daemon-reload
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> vaultwarden
</span></span><span class="line"><span class="cl">systemctl --user start vaultwarden
</span></span></code></pre></div><p>配置你的反代指向<code>http://127.0.0.1:8181</code>即可访问你部署的实例。</p>
<p>注意，首次需要设置<code>SIGNUPS_ALLOWED=true</code>注册第一个账号，注册后再关闭，避免其他人进来注册滥用。如果你真有共享需求也应该通过邀请的形式发送链接给好友，而不是开放自助注册。</p>
<h2 id="trilium-笔记">Trilium 笔记</h2>
<p>Trilium是一个基于Web的笔记应用，我将把我的笔记迁移到这个服务器上。</p>
<p>在此之前我都是使用Joplin+WebDAV，但是Joplin刚需客户端同步，没法直接Web编辑，并且E2EE只对服务器端设防客户端却是明文存储，所以经过精挑细选选择了Trilium。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Trilium Notes Container (Quadlet)</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Container]</span>
</span></span><span class="line"><span class="cl"><span class="na">Image</span><span class="o">=</span><span class="s">docker.io/triliumnext/trilium:v0.100.0</span>
</span></span><span class="line"><span class="cl"><span class="na">Volume</span><span class="o">=</span><span class="s">%h/trilium:/home/node/trilium-data</span>
</span></span><span class="line"><span class="cl"><span class="na">PublishPort</span><span class="o">=</span><span class="s">127.0.0.1:9980:8080</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span></code></pre></div><p>然后执行</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user daemon-reload
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> trilium
</span></span><span class="line"><span class="cl">systemctl --user start trilium
</span></span></code></pre></div><p>美中不足的就是，截至写文的时候还没有rootless镜像，导致镜像目录中的文件还都是<code>UID:10099</code>的权限。不过仅仅影响备份场景，日常使用不受影响。</p>
<p>配置你的反代指向<code>http://127.0.0.1:9980</code>即可访问你部署的实例。首次访问需要设置主密码，务必记住。</p>
<h2 id="backrest-备份">Backrest 备份</h2>
<p>有了以上这些重要的自部署服务，最重要的就是备份。
考虑到我现有的服务器资源都在深圳，互相备份不是好的灾备方案，最好的选择就是异地备份。
所以直接去Backblaze开了一个美西的B2存储桶专供备份使用，B2在10GB以内是免费的，超出以后每1TB $5/月，比较划算。</p>
<p><strong>注意，开的存储桶请选择private权限，除非你想让全世界都可以下载。</strong></p>
<p>部署过程：</p>
<ol>
<li>下载<a href="https://github.com/restic/restic/releases/tag/latest" rel="noopener noreferrer" target="_blank">restic</a>，一般vps选择linux_amd64版本。</li>
<li>上传到vps，bunzip2解压，复制到<code>/usr/bin</code>目录下。</li>
<li>下载<a href="https://github.com/garethgeorge/backrest/releases/tag/latest" rel="noopener noreferrer" target="_blank">backrest</a>，一般vps选择linux_x86_64版本。</li>
<li>上传到vps，tar xvfz 解压，执行其中的<code>install.sh</code>。</li>
</ol>
<p>之后你就可以在 <code>http://localhost:9898</code> 访问backrest的控制面板了，配置合适的反代即可通过域名访问。</p>
<p><strong>不要使用ubuntu中的restic，版本太老backrest不支持。要自己去下载最新二进制文件安装。</strong></p>
<p>界面配置概念和restic相同，创建Repo，创建要备份的计划，触发时间，设置保留副本数即可。</p>
<p>由于Trilium的权限问题，所以Backrest不适合放在容器中使用，我直接在主机上用Root权限的systemd运行，这样能直接读写需要备份的数据。</p>
<h1 id="笔记app">笔记App</h1>
<p>Trilium Web端直接访问域名即可，输入主密码就能在任何设备上记录并查阅你的笔记。</p>
<p>安卓平台有一个第三方安卓App <a href="https://github.com/FliegendeWurst/TriliumDroid" rel="noopener noreferrer" target="_blank">TriliumDroid</a>，在我的三星手机上运行一切正常，在小米手机上每次都需要重新同步才能使用，不过不耽误什么。</p>
<h1 id="附录">附录</h1>
<ul>
<li><a href="https://github.com/dani-garcia/vaultwarden" rel="noopener noreferrer" target="_blank">Vaultwarden</a></li>
<li><a href="https://github.com/TriliumNext/Trilium" rel="noopener noreferrer" target="_blank">Trilium</a></li>
<li><a href="https://github.com/FliegendeWurst/TriliumDroid" rel="noopener noreferrer" target="_blank">TriliumDroid</a></li>
<li><a href="https://github.com/restic/restic" rel="noopener noreferrer" target="_blank">Restic</a></li>
<li><a href="https://github.com/garethgeorge/backrest" rel="noopener noreferrer" target="_blank">Backrest</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>2025 博客改版</title>
      <link>https://blog.zjyl1994.com/post/2025_blog_refactor/</link>
      <pubDate>Mon, 10 Nov 2025 22:28:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/2025_blog_refactor/</guid>
      <description>&lt;p&gt;马上要迎来新的一年了，对我的博客做了一些小小的改动&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更换博客主题为PaperMod&lt;/li&gt;
&lt;li&gt;更换字体为霞鹜文楷&lt;/li&gt;
&lt;li&gt;升级Hugo版本&lt;/li&gt;
&lt;li&gt;规范化图片存储格式，跨入webp时代&lt;/li&gt;
&lt;li&gt;移除不被支持的旧页面&lt;/li&gt;
&lt;li&gt;更换favicon为《上任达维镇》的杰西卡头像~&lt;/li&gt;
&lt;/ol&gt;</description>
      <content:encoded><![CDATA[<p>马上要迎来新的一年了，对我的博客做了一些小小的改动</p>
<ol>
<li>更换博客主题为PaperMod</li>
<li>更换字体为霞鹜文楷</li>
<li>升级Hugo版本</li>
<li>规范化图片存储格式，跨入webp时代</li>
<li>移除不被支持的旧页面</li>
<li>更换favicon为《上任达维镇》的杰西卡头像~</li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>Docker 配置代理</title>
      <link>https://blog.zjyl1994.com/post/docker_proxy/</link>
      <pubDate>Mon, 06 Oct 2025 11:50:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/docker_proxy/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;国内的网络环境大家都懂，没有科学工具的可以不用看了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我的开发环境Win11+VirtualBox中跑的Fedora Server，后台无头启动，所以常规的代理配置方法不奏效。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>国内的网络环境大家都懂，没有科学工具的可以不用看了。</p>
</blockquote>
<p>我的开发环境Win11+VirtualBox中跑的Fedora Server，后台无头启动，所以常规的代理配置方法不奏效。</p>
<p>VirtualBox虚拟机配置的桥接网络和HostOnly，使用HostOnly网卡与宿主机通信，
此时就能固定宿主机IP为192.168.56.1了。
假设此时主机上开了局域网代理在7890端口，所以我们得到了一个稳定的http/https代理：<code>http://192.168.56.1:7890</code></p>
<p>首先修改Docker Daemon的配置文件，添加<code>/etc/systemd/system/docker.service.d/http-proxy.conf</code>，内容如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">[Service]
</span></span><span class="line"><span class="cl">Environment=&#34;HTTP_PROXY=http://192.168.56.1:7890&#34;
</span></span><span class="line"><span class="cl">Environment=&#34;HTTPS_PROXY=http://192.168.56.1:7890&#34;
</span></span><span class="line"><span class="cl">Environment=&#34;NO_PROXY=localhost,127.0.0.1,192.168.56.0/24&#34;
</span></span></code></pre></div><p>然后重启Docker Daemon：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">sudo</span> <span class="n">systemctl</span> <span class="n">daemon</span><span class="o">-</span><span class="n">reload</span>
</span></span><span class="line"><span class="cl"><span class="n">sudo</span> <span class="n">systemctl</span> <span class="n">restart</span> <span class="n">docker</span>
</span></span></code></pre></div><p>上面的代理只能加速<code>docker pull/push</code>的速度
在使用<code>docker build</code>的时候，构建阶段也需要配置代理，否则会异常缓慢。</p>
<p>构建时需要添加网络参数<code>--network=host</code>，并且指定<code>--build-arg</code>参数挂代理。
最后能得到完整的构建命令：
<code>docker build --network=host --build-arg http_proxy=http://192.168.56.1:7890 --build-arg https_proxy=http://192.168.56.1:7890 -t your_image_name:latest .</code></p>
<p>日常使用可以在<code>~/.bashrc</code>添加<code>alias</code>，方便使用：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">alias docker-build=&#39;docker build --network=host --build-arg http_proxy=http://192.168.56.1:7890 --build-arg https_proxy=http://192.168.56.1:7890&#39;
</span></span></code></pre></div><p>这样，直接使用<code>docker-build -t your_image_name:latest .</code>即可。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cap 的 Go 后端</title>
      <link>https://blog.zjyl1994.com/post/cap-go/</link>
      <pubDate>Thu, 11 Sep 2025 07:38:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/cap-go/</guid>
      <description>&lt;h1 id=&#34;cap&#34;&gt;Cap&lt;/h1&gt;
&lt;p&gt;Cap是个基于PoW工作量证明的验证码体系，用来给每一次请求添加一定的CPU成本，防止恶意请求。真正的客户只要计算一次，消耗一次成本。恶意尝试需要计算成千上万次，大大增加成本，使其放弃尝试。&lt;/p&gt;
&lt;p&gt;这个验证码的主要适用场景是保护登录表单，防止暴力破解在你的接口上跑字典。让攻击者的每次请求都有更高的CPU成本。如果你需要智能防爬，可以考虑使用Anubis这款WAF。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="cap">Cap</h1>
<p>Cap是个基于PoW工作量证明的验证码体系，用来给每一次请求添加一定的CPU成本，防止恶意请求。真正的客户只要计算一次，消耗一次成本。恶意尝试需要计算成千上万次，大大增加成本，使其放弃尝试。</p>
<p>这个验证码的主要适用场景是保护登录表单，防止暴力破解在你的接口上跑字典。让攻击者的每次请求都有更高的CPU成本。如果你需要智能防爬，可以考虑使用Anubis这款WAF。</p>
<p>原版的 <a href="https://capjs.js.org/" rel="noopener noreferrer" target="_blank">Cap</a> 基于Javascript实现，提供了独立的验证服务来方便和其他程序进行集成。但是对于我这个Redis依赖都不愿意引入的人来说，是非常难受的。所以我搞了个Go后端，方便直接集成到现有的系统里。</p>
<h1 id="那么如何使用呢">那么如何使用呢？</h1>
<ol>
<li>首先你需要安装我写的包 <code>go get github.com/zjyl1994/cap-go</code>。</li>
<li>实现IStorage，提供token数据的保存接口。</li>
<li>注册POST接口，<code>/cap/challenge</code>，对接<code>cap.CreateChallenge</code>的输出。</li>
<li>注册POST接口，<code>/cap/redeem</code>，对接<code>cap.RedeemChallenge</code>的输入输出。</li>
<li>对接前端验证组件，这里使用官方的<code>@cap.js/widget</code>组件对接，记得<code>data-cap-api-endpoint</code>为你注册的验证端口地址。</li>
<li>用户操作前端组件验证通过后，会给一个token，你可以使用<code>cap.ValidateToken</code>来验证token是否有效。</li>
</ol>
<h1 id="示例">示例</h1>
<p>这里有我写的简单示例，可以直接运行。</p>
<p><a href="https://github.com/zjyl1994/cap-go/tree/master/examples" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/cap-go/tree/master/examples</a></p>
<p>当然，这里为了简化实现，没有正确实现过期动作，推荐真正使用的时候引入freecache，提供空间固定的kv存储。</p>
<p>下面给一个我在生产环境使用的IStorage：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;time&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;github.com/coocood/freecache&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">freeCacheStorage</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">cache</span><span class="w"> </span><span class="o">*</span><span class="nx">freecache</span><span class="p">.</span><span class="nx">Cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// NewFreeCacheStorage 创建一个新的存储实例</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// capacity 单位是字节，例如 100 * 1024 * 1024 表示 100MB</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">NewFreeCacheStorage</span><span class="p">(</span><span class="nx">capacity</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="nx">freeCacheStorage</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">freeCacheStorage</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">cache</span><span class="p">:</span><span class="w"> </span><span class="nx">freecache</span><span class="p">.</span><span class="nf">NewCache</span><span class="p">(</span><span class="nx">capacity</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">f</span><span class="w"> </span><span class="o">*</span><span class="nx">freeCacheStorage</span><span class="p">)</span><span class="w"> </span><span class="nf">Get</span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">value</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">f</span><span class="p">.</span><span class="nx">cache</span><span class="p">.</span><span class="nf">Get</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">key</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nb">string</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">f</span><span class="w"> </span><span class="o">*</span><span class="nx">freeCacheStorage</span><span class="p">)</span><span class="w"> </span><span class="nf">Set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">expire</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">ttl</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">expire</span><span class="p">.</span><span class="nf">IsZero</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ttl</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="c1">// 不过期</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ttl</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nf">Until</span><span class="p">(</span><span class="nx">expire</span><span class="p">).</span><span class="nf">Seconds</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">ttl</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">ttl</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="c1">// freecache 最小为 1 秒</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">f</span><span class="p">.</span><span class="nx">cache</span><span class="p">.</span><span class="nf">Set</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">key</span><span class="p">),</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span><span class="w"> </span><span class="nx">ttl</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">f</span><span class="w"> </span><span class="o">*</span><span class="nx">freeCacheStorage</span><span class="p">)</span><span class="w"> </span><span class="nf">Del</span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">f</span><span class="p">.</span><span class="nx">cache</span><span class="p">.</span><span class="nf">Del</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">key</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>如果你有持久化需求，也可以实现该接口直接把数据写入数据库。
不过对于登陆表单这个场景来说，没有必要。所以单纯的内存存储就够了。</p>]]></content:encoded>
    </item>
    <item>
      <title>打造自己的 Seedbox</title>
      <link>https://blog.zjyl1994.com/post/bt-seedbox/</link>
      <pubDate>Wed, 26 Mar 2025 23:45:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/bt-seedbox/</guid>
      <description>&lt;h1 id=&#34;背景&#34;&gt;背景&lt;/h1&gt;
&lt;p&gt;曾经看番的好去处B站越来越烂了，导致完全没兴趣续费大会员。所以我尝试回到过去，重新捡起来 BT 下载。&lt;/p&gt;
&lt;p&gt;很庆幸，2025年动漫花园和nyaa都还在，而且一直有更新。为此我还组装了一部小的全闪下载机进行 BT 下载。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="背景">背景</h1>
<p>曾经看番的好去处B站越来越烂了，导致完全没兴趣续费大会员。所以我尝试回到过去，重新捡起来 BT 下载。</p>
<p>很庆幸，2025年动漫花园和nyaa都还在，而且一直有更新。为此我还组装了一部小的全闪下载机进行 BT 下载。</p>
<p>但是很快我就发现一个问题，速度非常不理想，首先我的宽带是一个N层NAT的私人宽带，不仅无法连接很多Tracker，而且无法贡献上传。
下完就跑可不是什么好行为，这算是 BT 吸血，所以我决定自建一个BT Seedbox，在公网上下载顺便保种，然后用 HTTP 拖回本地进行观看。</p>
<h1 id="vps-选择">VPS 选择</h1>
<p>BT不同于PT，有一个最大的问题就是蜜罐钓鱼，当你连上某些蜜罐以后，版权方就会给你的主机商发DMCA函。
一般这个时候主机商就会封掉你的机器保平安，根据主机商的ToS，你可能被清退或者Ban机。</p>
<p>一般来说此时就需要DMCA Free的主机商了，其中 BuyVM 的卢森堡机房是很多人都在推荐的，卢森堡是欧洲非常小的大公国，独立主权。根据 BuyVM 的 ToS，处理DMCA需要卢森堡当地线下出警。（基本上约等于是不会封的了）</p>
<p>不过这个机房在6个月左右会进行一次迁移，根据主机商在 LET 的说法，新的搬迁目的地会是瑞士。
据他所称，搬去瑞士后服务不变，拭目以待吧。</p>
<h1 id="搭建-seedbox">搭建 Seedbox</h1>
<h2 id="基本环境">基本环境</h2>
<p>首先需要一台 BuyVM 的卢森堡主机，通常需要抢购。3.5刀一个月，1C1G 20G的硬盘，G口无限流量。北京时间每天晚上9-10点补货。</p>
<p>同时，你也需要去抢购一下对应区域的存储块，1.25刀/256GB/月。也是每天晚上9-10点补货。（存储块无法跨区挂载，也无法在没有机器的情况下提前购买）1T以下的小型存储块非常抢手，一般半小时不到就没了，所以关注几个 BuyVM 补货频道，收到通知第一时刻起来抢很容易成功。</p>
<p>我现在的环境是3.5刀的主机+2.5刀的存储块，每月花费6刀，按当前汇率年付约500人民币。</p>
<p>购买好以后，需要在 Stallion 后台绑定存储块。然后进行分区，我所使用的挂载点是 <code>/data</code>。</p>
<p>考虑到内存使用量可能会较高，所以添加了2GB的Swap文件，不过从后面来看，基本没有使用到。</p>
<p>这一部分网上教程很多，跟着教程走就好了。</p>
<h2 id="安装-bt-客户端">安装 BT 客户端</h2>
<p>我使用的是 qBittorrent-Enhanced-Edition 作为 BT 客户端，他能防止迅雷吸血等多种 BT 问题。</p>
<p>首先需要安装 qBittorrent-Enhanced-Edition 客户端，他是一个静态编译的二进制文件，不需要任何依赖。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wget https://github.com/c0re100/qBittorrent-Enhanced-Edition/releases/download/release-5.0.4.10/qbittorrent-enhanced-nox_x86_64-linux-musl_static.zip
</span></span><span class="line"><span class="cl">unzip qbittorrent-enhanced-nox_x86_64-linux-musl_static.zip
</span></span><span class="line"><span class="cl">sudo mv ./qbittorrent-nox /usr/local/bin/qbittorrent-nox
</span></span><span class="line"><span class="cl">sudo chmod +x /usr/local/bin/qbittorrent-nox
</span></span></code></pre></div><p>创建对应的用户和数据文件夹</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo useradd -m qbit
</span></span><span class="line"><span class="cl">sudo mkdir -p /data/qbittorrent
</span></span><span class="line"><span class="cl">sudo chown qbit:qbit /data/qbittorrent
</span></span></code></pre></div><p>然后需要配置启动它的 systemd 文件,创建<code>/etc/systemd/system/qbittorrent.service</code>文件并粘贴以下内容。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">[Unit]
</span></span><span class="line"><span class="cl">Description=qBittorrent-nox service
</span></span><span class="line"><span class="cl">Documentation=man:qbittorrent-nox(1)
</span></span><span class="line"><span class="cl">Wants=network-online.target
</span></span><span class="line"><span class="cl">After=local-fs.target network-online.target nss-lookup.target
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[Service]
</span></span><span class="line"><span class="cl">Type=simple
</span></span><span class="line"><span class="cl">PrivateTmp=false
</span></span><span class="line"><span class="cl">User=qbit
</span></span><span class="line"><span class="cl">ExecStart=/usr/local/bin/qbittorrent-nox
</span></span><span class="line"><span class="cl">TimeoutStopSec=1800
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[Install]
</span></span><span class="line"><span class="cl">WantedBy=multi-user.target
</span></span></code></pre></div><p>保存后，使用以下命令启动 qBittorrent 服务</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl daemon-reload
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> qbittorrent.service
</span></span><span class="line"><span class="cl">sudo systemctl start qbittorrent.service
</span></span></code></pre></div><p>此时运行<code>systemctl status qbittorrent.service</code>可以查看服务状态。</p>
<p>可以看到 qbee 的 web 端口已经在 localhost:8080 启动了，
会有一行 Admin Password 记录了本次部署的初始密码。
但是此时我们还无法访问，因为我们还没有配置反向代理。</p>
<h2 id="配置反向代理">配置反向代理</h2>
<p>Caddy 作为新时代的 HTTP 服务器，已经进入 Ubuntu 的软件源了，可以从 <code>sudo apt install caddy</code> 直接进行安装。
当然你会玩 Nginx 的话，也可以自己配置。</p>
<p>首先需要创建一个 <code>/etc/caddy/Caddyfile</code> 文件，内容如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">qbittorrent.example.com {
</span></span><span class="line"><span class="cl">    reverse_proxy http://localhost:8080
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">files.example.com {
</span></span><span class="line"><span class="cl">    file_server {
</span></span><span class="line"><span class="cl">        root /data/qbittorrent
</span></span><span class="line"><span class="cl">        browse
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>然后保存后，使用以下命令启动 Caddy 服务</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> caddy.service
</span></span><span class="line"><span class="cl">sudo systemctl start caddy.service
</span></span></code></pre></div><h2 id="配置-bt-客户端">配置 BT 客户端</h2>
<p>访问 <code>qbittorrent.example.com</code> 后，会提示你输入用户名和密码，默认的用户名是 <code>admin</code>，密码就是你在 <code>systemctl status qbittorrent.service</code> 中看到的 Admin Password。
然后点击界面上的“蓝色齿轮”进入 qBittorrent 的设置，<code>Behavior</code> -&gt; <code>Language</code> -&gt; <code>User Interface Language</code>，找到“简体中文”然后点<code>Save</code>按钮就切换到中文了。</p>
<p>再次点击“蓝色齿轮”进入 qBittorrent 的设置，配置以下内容：
<code>下载</code> -&gt; <code>默认保存路径</code> 填写 <code>/data/qbittorrent</code>。
<code>WebUI</code> -&gt; <code>验证</code> -&gt; <code>用户名</code>和<code>密码</code> 改成你想要的值。
然后点击最下面的<code>保存</code>按钮。</p>
<p>此时，就都配置完了，你可以使用 qBittorrent 来愉快的下载 BT 了。</p>
<h2 id="访问下载的文件">访问下载的文件</h2>
<p>访问 <code>files.example.com</code> 就可以查看到下载好的文件。</p>
<p>卢森堡到大陆的网络质量很差，无法支撑大部分下载内容的在线流畅播放，所以最好还是用其他手段拖回本地 NAS。</p>
<p>这种就可以使用Aria2或者其他离线挂机软件以接近国际端口满速往回拖了。</p>
<h1 id="后记">后记</h1>
<p>在我写这篇文章的时候，我的 Seedbox 已经运转超过24小时了。</p>
<p>理论上来说，搞这个是比叔叔那边充钱要贵的，但是 BT 嘛，玩的开心最重要。</p>
<p>也许正在 BT 下载的你，Peer 列表里和你交换数据的某一台卢森堡主机就是我的 :P</p>
]]></content:encoded>
    </item>
    <item>
      <title>Golang 优化之合并请求</title>
      <link>https://blog.zjyl1994.com/post/writegroup/</link>
      <pubDate>Sun, 23 Mar 2025 20:00:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/writegroup/</guid>
      <description>&lt;p&gt;很多时候，一些请求是可以合并的，比如库存扣减常用的 &lt;code&gt;UPDATE qty = qty -1&lt;/code&gt;
如果我们在并发场景下，每个请求都去执行一次 &lt;code&gt;UPDATE qty = qty -1&lt;/code&gt;，那么就会有大量的请求竞争同一行数据，造成性能瓶颈。
所以我们可以通过合并请求的方式，将多个请求合并成一个请求，一个动作完成多次扣减，然后再执行。&lt;/p&gt;
&lt;p&gt;下面就是我封装好的工具包，用于合并请求。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>很多时候，一些请求是可以合并的，比如库存扣减常用的 <code>UPDATE qty = qty -1</code>
如果我们在并发场景下，每个请求都去执行一次 <code>UPDATE qty = qty -1</code>，那么就会有大量的请求竞争同一行数据，造成性能瓶颈。
所以我们可以通过合并请求的方式，将多个请求合并成一个请求，一个动作完成多次扣减，然后再执行。</p>
<p>下面就是我封装好的工具包，用于合并请求。</p>
<p>WriteGroup 会根据 key 进行合并动作，把一组相同 key 的请求合并成一个数组交给回调函数进行处理。
回调函数要求输入输出数量保持一致，否则可能产生异常问题。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">writegroup</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;context&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;sync&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;time&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">WriteGroup</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="w"> </span><span class="kt">any</span><span class="p">]</span><span class="w"> </span><span class="kd">interface</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">Do</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">fn</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="p">[]</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">[]</span><span class="nx">WriteResult</span><span class="p">[</span><span class="nx">R</span><span class="p">])</span><span class="w"> </span><span class="p">(</span><span class="nx">R</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 组写入方法</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">writeGroup</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="w"> </span><span class="kt">any</span><span class="p">]</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">batchTimeout</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">bufferSize</span><span class="w">   </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">batchSize</span><span class="w">    </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">workerMu</span><span class="w">     </span><span class="nx">keyedMutex</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">chMu</span><span class="w">         </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">workerCh</span><span class="w">     </span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kd">chan</span><span class="w"> </span><span class="nx">writeTask</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 具体的写入任务</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">writeTask</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="w"> </span><span class="kt">any</span><span class="p">]</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">input</span><span class="w">  </span><span class="nx">T</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">result</span><span class="w"> </span><span class="kd">chan</span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">WriteResult</span><span class="p">[</span><span class="nx">R</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 承载返回值</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">WriteResult</span><span class="p">[</span><span class="nx">R</span><span class="w"> </span><span class="kt">any</span><span class="p">]</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Result</span><span class="w"> </span><span class="nx">R</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Err</span><span class="w">    </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nx">NewWriteGroup</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="w"> </span><span class="kt">any</span><span class="p">](</span><span class="nx">bufferSize</span><span class="p">,</span><span class="w"> </span><span class="nx">batchSize</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">batchTimeout</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span><span class="p">)</span><span class="w"> </span><span class="nx">WriteGroup</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">]</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">writeGroup</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">]{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">bufferSize</span><span class="p">:</span><span class="w">   </span><span class="nx">bufferSize</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">batchSize</span><span class="p">:</span><span class="w">    </span><span class="nx">batchSize</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">batchTimeout</span><span class="p">:</span><span class="w"> </span><span class="nx">batchTimeout</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">workerCh</span><span class="p">:</span><span class="w">     </span><span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kd">chan</span><span class="w"> </span><span class="nx">writeTask</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">]),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="o">*</span><span class="nx">writeGroup</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">])</span><span class="w"> </span><span class="nf">Do</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">fn</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="p">[]</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">[]</span><span class="nx">WriteResult</span><span class="p">[</span><span class="nx">R</span><span class="p">])</span><span class="w"> </span><span class="p">(</span><span class="nx">R</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// 初始化任务队列并投递</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">w</span><span class="p">.</span><span class="nx">chMu</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span><span class="w"> </span><span class="c1">// 如果队列填满，后面无法解锁，此处会阻塞等待</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ch</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nx">workerCh</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ch</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="nx">writeTask</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">],</span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nx">bufferSize</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">w</span><span class="p">.</span><span class="nx">workerCh</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">ch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">w</span><span class="p">.</span><span class="nx">chMu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">resultCh</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="nx">WriteResult</span><span class="p">[</span><span class="nx">R</span><span class="p">],</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">writeTask</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">]{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">input</span><span class="p">:</span><span class="w">  </span><span class="nx">input</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">result</span><span class="p">:</span><span class="w"> </span><span class="nx">resultCh</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// 启动处理线程消耗ch中的数据</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">workerMu</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nx">workerMu</span><span class="p">.</span><span class="nf">GetMutex</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">workerMu</span><span class="p">.</span><span class="nf">TryLock</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// 抢到锁的进行处理</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">lock</span><span class="w"> </span><span class="o">*</span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">defer</span><span class="w"> </span><span class="nx">lock</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="c1">// 封装的工作函数，负责执行writeTask并发送执行结果</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">worker</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">jobs</span><span class="w"> </span><span class="p">[]</span><span class="nx">writeTask</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">])</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">jobInputs</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">jobs</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">job</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">jobs</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">jobInputs</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">job</span><span class="p">.</span><span class="nx">input</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">results</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">fn</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">jobInputs</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">job</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">jobs</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">job</span><span class="p">.</span><span class="nx">result</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">results</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="c1">// 启动读取超时定时器</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">timeoutTimer</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nf">NewTimer</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">batchTimeout</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">defer</span><span class="w"> </span><span class="nx">timeoutTimer</span><span class="p">.</span><span class="nf">Stop</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="c1">// 循环读直到ch无数据或关闭再退出</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">jobs</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="nx">writeTask</span><span class="p">[</span><span class="nx">T</span><span class="p">,</span><span class="w"> </span><span class="nx">R</span><span class="p">],</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nx">bufferSize</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">timeoutTimer</span><span class="p">.</span><span class="nf">Reset</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">batchTimeout</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">PROC</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// 读ch直到满或超时</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">timeoutTimer</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span><span class="w"> </span><span class="c1">// 超时情况，处理已在队列中的部分</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">jobs</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">							</span><span class="nf">worker</span><span class="p">(</span><span class="nx">jobs</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="k">break</span><span class="w"> </span><span class="nx">PROC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="k">case</span><span class="w"> </span><span class="nx">val</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// 队列关闭，处理队列中的部分</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">							</span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">jobs</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">								</span><span class="nf">worker</span><span class="p">(</span><span class="nx">jobs</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">							</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">							</span><span class="k">break</span><span class="w"> </span><span class="nx">PROC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="c1">// 队列正常，有新消息输入</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="nx">jobs</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">jobs</span><span class="p">,</span><span class="w"> </span><span class="nx">val</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">jobs</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nx">batchSize</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// 超过批量上限，处理一波</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">							</span><span class="nf">worker</span><span class="p">(</span><span class="nx">jobs</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">							</span><span class="k">break</span><span class="w"> </span><span class="nx">PROC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">						</span><span class="c1">// 没超过上限进入下一轮继续读</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="k">if</span><span class="w"> </span><span class="nx">ctr</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">ch</span><span class="p">);</span><span class="w"> </span><span class="nx">ctr</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// 队列中没有了，停止工作</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}(</span><span class="nx">workerMu</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// 获得计算好的结果，支持根据context超时</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="o">*</span><span class="nb">new</span><span class="p">(</span><span class="nx">R</span><span class="p">),</span><span class="w"> </span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">case</span><span class="w"> </span><span class="nx">ret</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">resultCh</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">ret</span><span class="p">.</span><span class="nx">Result</span><span class="p">,</span><span class="w"> </span><span class="nx">ret</span><span class="p">.</span><span class="nx">Err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">keyedMutex</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">m</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Map</span><span class="w"> </span><span class="c1">// map[string]*sync.Mutex</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">km</span><span class="w"> </span><span class="o">*</span><span class="nx">keyedMutex</span><span class="p">)</span><span class="w"> </span><span class="nf">GetMutex</span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">mu</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">km</span><span class="p">.</span><span class="nx">m</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">mu</span><span class="p">.(</span><span class="o">*</span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">newMu</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="p">{}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">value</span><span class="p">,</span><span class="w"> </span><span class="nx">loaded</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">km</span><span class="p">.</span><span class="nx">m</span><span class="p">.</span><span class="nf">LoadOrStore</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">newMu</span><span class="p">);</span><span class="w"> </span><span class="p">!</span><span class="nx">loaded</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">newMu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">value</span><span class="p">.(</span><span class="o">*</span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>掌玩mini 2024 调教手记</title>
      <link>https://blog.zjyl1994.com/post/iplay_mini_2024_v2/</link>
      <pubDate>Thu, 29 Aug 2024 00:10:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/iplay_mini_2024_v2/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;这是掌玩 mini 的第二篇文章,其他内容可以参考第一篇。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;上一次写的测评文章我看了一下，2024-02-29，今天08-29，算用了快小半年，体验还不错，也调教的差不多了。&lt;/p&gt;
&lt;p&gt;首先本文只针对 掌玩mini 2024 NFE 8G+256G 版本，官方代号 T811MAN。我不知道和国内在售版本有硬件上的区别，但是照搬掌玩 mini 的刷机教程没有成功过。&lt;/p&gt;
&lt;h1 id=&#34;gsi&#34;&gt;GSI&lt;/h1&gt;
&lt;p&gt;首先，不要碰 GSI。NFE版本暂时没有可以成功刷入的 GSI，我已经试过很多次了，基本都是无限重启。&lt;/p&gt;
&lt;p&gt;看老外群组内的反馈，很多人都能成功刷入，不过都是普通版本的，并不是 NFE 版本。
也有老外给 NFE 版本固件刷到普版上，并没有出现 L1 的 DRM 标识，这说明很有可能这俩硬件层面有细微的变化。&lt;/p&gt;
&lt;p&gt;如果你搞定了可以分享教程给我。&lt;/p&gt;
&lt;p&gt;为了防止变砖，所以我准备了线刷包，这个线刷版本是 &lt;code&gt;iPlay50miniProNFE(T811MAN)_EN_20240520&lt;/code&gt;，OTA 上刷不到这个更新。多次GSI失败救砖后，版本自然升级到了这个版本。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>这是掌玩 mini 的第二篇文章,其他内容可以参考第一篇。</p>
</blockquote>
<h1 id="前言">前言</h1>
<p>上一次写的测评文章我看了一下，2024-02-29，今天08-29，算用了快小半年，体验还不错，也调教的差不多了。</p>
<p>首先本文只针对 掌玩mini 2024 NFE 8G+256G 版本，官方代号 T811MAN。我不知道和国内在售版本有硬件上的区别，但是照搬掌玩 mini 的刷机教程没有成功过。</p>
<h1 id="gsi">GSI</h1>
<p>首先，不要碰 GSI。NFE版本暂时没有可以成功刷入的 GSI，我已经试过很多次了，基本都是无限重启。</p>
<p>看老外群组内的反馈，很多人都能成功刷入，不过都是普通版本的，并不是 NFE 版本。
也有老外给 NFE 版本固件刷到普版上，并没有出现 L1 的 DRM 标识，这说明很有可能这俩硬件层面有细微的变化。</p>
<p>如果你搞定了可以分享教程给我。</p>
<p>为了防止变砖，所以我准备了线刷包，这个线刷版本是 <code>iPlay50miniProNFE(T811MAN)_EN_20240520</code>，OTA 上刷不到这个更新。多次GSI失败救砖后，版本自然升级到了这个版本。</p>
<h1 id="root">Root</h1>
<p>既然 GSI 装不上，一个标准版的Root必不可少。我试了 KernelSU ，刷进去无限重启。但是传统的 Magisk 可以。</p>
<p>按照官方教程使用 <code>fastboot.exe flashing unlock</code> 先解锁 bootloader，这一步会清除所有数据。</p>
<p>然后重启进入正常系统，安装 Magisk 的应用，找到线刷包里的 <code>boot.img</code> 复制进 pad，使用 Magisk 进行补丁。</p>
<p>补丁后的 <code>magisk_patched.img</code> 复制回电脑，使用 <code>fastboot flash boot magisk_patched.img</code> 把刚才的文件刷进 pad，重启即可得到 root 权限。</p>
<p>几乎无门槛。</p>
<p><img alt="Magisk安装成功" decoding="async" height="1920" loading="eager" src="/post/iplay_mini_2024_v2/magisk.webp" width="1200">
</p>
<h1 id="调整最小宽度">调整最小宽度</h1>
<p>掌玩mini最奇怪的一点就是明明大部分时间都在竖屏使用，但是设置页面又像其他平板一样强制左右分栏。默认的最小间距也很奇怪，调整到380+以后正常许多。目前看来380是平板/手机的分界线。</p>
<p><img alt="dpi设置" decoding="async" height="1920" loading="lazy" src="/post/iplay_mini_2024_v2/minwidth.webp" width="1200">
</p>
<p>调整后，Chrome使用平板模式，可以多个 Tab。</p>
<p><img alt="Chrome" decoding="async" height="1920" loading="lazy" src="/post/iplay_mini_2024_v2/chrometab.webp" width="1200">
</p>
<p>通知中心下拉再也不是四个图标，不会出现突出来一个丑死人的问题了。</p>
<p><img alt="通知中心" decoding="async" height="1920" loading="lazy" src="/post/iplay_mini_2024_v2/dropdown.webp" width="1200">
</p>
<p>设置界面也是看着顺眼许多，不再挤成一团。</p>
<p><img alt="设置界面" decoding="async" height="1920" loading="lazy" src="/post/iplay_mini_2024_v2/setting.webp" width="1200">
</p>
<p>另外建议使用手势操作，要不然三大金刚按键会叠加taskbar，很奇怪。
如果要隐藏 Taskbar 可以参考下文。</p>
<h1 id="隐藏-taskbar">隐藏 Taskbar</h1>
<p>使用一个简单的 Magisk 模块，加载后长按底部Taskbar空白区域即可隐藏，每次重启需要重新操作</p>
<p><img alt="隐藏Taskbar" decoding="async" height="1920" loading="lazy" src="/post/iplay_mini_2024_v2/hidetaskbar.webp" width="1200">
</p>
<h1 id="启动器更换">启动器更换</h1>
<p>官方启动器总觉得很奇怪，都优化完了建议后续自己装一个。
这里用了米米推荐的<code>Lawnchair</code>。</p>
<p><img alt="启动器" decoding="async" height="1920" loading="lazy" src="/post/iplay_mini_2024_v2/homepage.webp" width="1200">
</p>
<h1 id="后记">后记</h1>
<p>基本要折腾的点就这么多，原厂 ROM 有原厂的好，就是不用折腾 4G 这些东西。设置界面丑点就丑点吧，日常也不打开它看。再有需要定制的东西用 Root 权限也能搞定，就先这样吧。</p>]]></content:encoded>
    </item>
    <item>
      <title>在线工具站</title>
      <link>https://blog.zjyl1994.com/post/webtools/</link>
      <pubDate>Sun, 11 Aug 2024 21:11:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/webtools/</guid>
      <description>&lt;p&gt;最近一直在忙公司的项目，经常和 JSON，日志系统中输出的文本，excel文件打交道。
所以有不少重复劳动需要弄，比如查看一下 JSON 文本中嵌套的JSON什么的。&lt;/p&gt;
&lt;p&gt;所以借着这个机会把我的早期工具站用 React 重构了，增加了 PWA 技术，现在可以安装到电脑里当WebApp使用，除了整合常用的开发功能，最主要是打开快，All in One。&lt;/p&gt;
&lt;p&gt;由于没有好的设计，所以沿用了之前老工具站（已下线）的 Bootstrap5。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>最近一直在忙公司的项目，经常和 JSON，日志系统中输出的文本，excel文件打交道。
所以有不少重复劳动需要弄，比如查看一下 JSON 文本中嵌套的JSON什么的。</p>
<p>所以借着这个机会把我的早期工具站用 React 重构了，增加了 PWA 技术，现在可以安装到电脑里当WebApp使用，除了整合常用的开发功能，最主要是打开快，All in One。</p>
<p>由于没有好的设计，所以沿用了之前老工具站（已下线）的 Bootstrap5。</p>
<h1 id="首页功能">首页功能</h1>
<p>先看看首页功能：
<img alt="首页" decoding="async" height="1020" loading="eager" src="/post/webtools/index.webp" width="1920">
</p>
<p>我在首页放了比较常用的随机数，时间戳和 UUID 生成。这些都是纯离线的，无需服务器介入。</p>
<h1 id="json处理">JSON处理</h1>
<p>接下来有一个比较好用的工具了：
<img alt="JSON处理" decoding="async" height="1020" loading="lazy" src="/post/webtools/json.webp" width="1920">
</p>
<p>格式化，压缩，转义去转义这些都是传统 JSON 在线工具常见的功能。</p>
<p>比较特色的是&quot;增强格式化&quot;，日常开发中经常遇到 JSON 嵌套 JSON 这种情况，如果比较多的话，一条条人工解析会非常难受，
用增强格式化会尝试解析字符串，如果是 JSON 的话就会嵌套展开。</p>
<p><img alt="JSON处理前" decoding="async" height="121" loading="lazy" src="/post/webtools/json_before.webp" width="977">
</p>
<p>会被一键格式化成如下样子。</p>
<p><img alt="JSON处理后" decoding="async" height="402" loading="lazy" src="/post/webtools/json_after.webp" width="372">
</p>
<p>打印出来的日志里最喜欢 JSON 套 JSON 了，所以这是我相当满意的功能，日常使用频率也很高。</p>
<p>&ldquo;智能处理&quot;也是比较常用的功能，他会按照先单行再多行的顺序尝试去除文本两边的非JSON文本（TRIM），然后尝试格式化，也是处理日志的好帮手。</p>
<p>旁边的功能就是智能处理拆解后的功能了，可以手动处理更复杂的内容。</p>
<p>演示下&quot;智能处理&rdquo;</p>
<p><img alt="JSON智能处理前" decoding="async" height="168" loading="lazy" src="/post/webtools/json_proc_before.webp" width="332">

<img alt="JSON智能处理后" decoding="async" height="423" loading="lazy" src="/post/webtools/json_proc_after.webp" width="311">
</p>
<h1 id="编码处理">编码处理</h1>
<p><img alt="编码处理" decoding="async" height="1020" loading="lazy" src="/post/webtools/encoding.webp" width="1920">
</p>
<p>这个没什么可说的，就是字面意思。八进制 UTF8 解码用于处理 Python 打印出来的 <code>\x123\x456\x789</code> 这样的中文，也是查看日志非常方便的功能。</p>
<h1 id="文本处理">文本处理</h1>
<p><img alt="文本处理" decoding="async" height="1020" loading="lazy" src="/post/webtools/textproc.webp" width="1920">
</p>
<p>文本处理这个模块花的开发时间是最多的，也是根据日常使用不停迭代的结果，非常适合处理日常产生的多行文本。</p>
<p>正则表达式的输入框可以输入正则，根据正则过滤文本，每一行都会视作独立的内容进行处理。正则的&quot;提取&quot;功能输出格式是可以直接复制到 Excel 中的，不会挤在同一个单元格，这点我也做了特殊处理。（手边暂时没有合适的脱敏数据就不演示了，可以自行探索）</p>
<p>下面的替换框就是普通的文本替换，免去了离开去记事本做替换的麻烦。</p>
<p>按钮区的功能就是字面意思，常见的场景比如输出了一系列逗号分割的ID，需要在两边添加引号，生成 WHERE IN 那种格式贴到SQL里，只需要以下几步 <code>逗号切行 -&gt; 加引号 -&gt; 行尾加逗号</code>，然后少少的编辑一下就可以用了。</p>
<p>（虽然我知道文本编辑器有列编辑模式，但是体验下来还是特化成工具更方便一些）</p>
<h1 id="大卡计算">大卡计算</h1>
<p><img alt="大卡计算" decoding="async" height="1020" loading="lazy" src="/post/webtools/kcal.webp" width="1920">
</p>
<p>减脂期非常有用的大卡计算，日常包装食物的&quot;营养成分表&quot;都是以千焦为单位，而且是每份热量。常见食品净含量折算后也不止一份，所以快速计算具体的大卡数量就比较有必要了。</p>
<p>这个功能也支持手机端打开，有了 PWA 加持缓存没过期的时候基本能做到秒开。</p>
<p><img alt="移动端的大卡计算" decoding="async" height="2670" loading="lazy" src="/post/webtools/kcal-m.webp" width="1200">
</p>
<h1 id="结语">结语</h1>
<p>那么，这么好用的工具站哪里可以使用到呢？</p>
<p>站点: <a href="https://utils.zjyl1994.com/" rel="noopener noreferrer" target="_blank">https://utils.zjyl1994.com/</a></p>
<p>源码：https://github.com/zjyl1994/my-web-tools</p>
<p>喜欢的话可以给我 Star，
特别感谢 Rex Zeng 老师帮我优化了部分 React 的写法~</p>]]></content:encoded>
    </item>
    <item>
      <title>掌玩mini 2024 简测</title>
      <link>https://blog.zjyl1994.com/post/iplay_mini_2024/</link>
      <pubDate>Thu, 29 Feb 2024 22:39:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/iplay_mini_2024/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;前情提要：酷比魔方 掌玩mini (iPlay 50 Mini Pro) 2024 NFE 8+256 ¥799&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;缘起&#34;&gt;缘起&lt;/h1&gt;
&lt;p&gt;之前有过一部丐版iPad，当时花了2999买的，主要拿来看Youtube等流媒体，当作出租屋小电视使用，后续由于使用体验实在糟糕逐渐吃灰，所以爱回收变现700多，变现的钱就买了这个。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;官网宣传图&#34; decoding=&#34;async&#34; height=&#34;814&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/iplay_mini_2024/z8veRl.webp&#34; width=&#34;433&#34;&gt;
&lt;/p&gt;
&lt;p&gt;基本配置：MTK G99 / 8G RAM / 256GB UFS2.2 / 8.4&amp;quot; 1920×1200&lt;/p&gt;
&lt;p&gt;卖点: 为数不多的8寸安卓平板，原生 Android 13，有存储卡扩展还能LTE联网。&lt;/p&gt;
&lt;p&gt;现在的安卓平板都越做越大，想找个小尺寸的实在难找，8寸产品线只剩酷比魔方和联想拯救者在做。但是拯救者最便宜的也要2000多，差不多能买2台半这个了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>前情提要：酷比魔方 掌玩mini (iPlay 50 Mini Pro) 2024 NFE 8+256 ¥799</p>
</blockquote>
<h1 id="缘起">缘起</h1>
<p>之前有过一部丐版iPad，当时花了2999买的，主要拿来看Youtube等流媒体，当作出租屋小电视使用，后续由于使用体验实在糟糕逐渐吃灰，所以爱回收变现700多，变现的钱就买了这个。</p>
<p><img alt="官网宣传图" decoding="async" height="814" loading="eager" src="/post/iplay_mini_2024/z8veRl.webp" width="433">
</p>
<p>基本配置：MTK G99 / 8G RAM / 256GB UFS2.2 / 8.4&quot; 1920×1200</p>
<p>卖点: 为数不多的8寸安卓平板，原生 Android 13，有存储卡扩展还能LTE联网。</p>
<p>现在的安卓平板都越做越大，想找个小尺寸的实在难找，8寸产品线只剩酷比魔方和联想拯救者在做。但是拯救者最便宜的也要2000多，差不多能买2台半这个了。</p>
<h1 id="实际体验">实际体验</h1>
<p>2023款的测评网上不少，想了解的可以去看网上的测评视频。
2024款不知道升级了什么，酷比的2.0高科技系统对买NFE版本的人来说，没什么用处（摊手）。这里只说一些实际体验。</p>
<p>首先，8寸平板显示面积比手机大很多，放在包里通勤也很方便。
而且由于是Android系统，所以直接装TVBox之类的影视聚合软件可以获得和电视盒子一样的观影体验。</p>
<p>纯WiFi待机掉电很慢，看视频的话初步估计6-7小时没啥问题。
充电支持 PD 18w，基本常见手机的充电头都可以触发这个档位的快冲。</p>
<p>TypeC的位置不是正中间真的很奇怪，不明白为什么要做在上面。
3.5mm耳机孔的位置更奇怪，做在角上。真的，我很迷惑。如果有机会和酷比的设计师聊聊的话，我肯定要问为什么耳机放在角上……</p>
<p>至于LTE，还没有合适的流量卡，所以LTE场景下的网速和续航暂时不能测，不过MTK的4G续航和速度应该比较平衡。</p>
<p>而且我以为作为一个小厂，不会有安全更新。结果前几天收到了标记为20240222版本的更新，虽然安全更新隔了4个月，但总好过根本没有。</p>
<p><img alt="更新" decoding="async" height="1280" loading="lazy" src="/post/iplay_mini_2024/eMJ18N.webp" width="800">
</p>
<h1 id="一些问题和优化">一些问题和优化</h1>
<h2 id="ui奇怪">UI奇怪</h2>
<p>不知道是不是被定制过，原厂的桌面实在是难用。解决方案很简单，换Nova Launcher。</p>
<p>但是SystemUI看起来灰秃秃的，不知道是做了什么设计，肯定不是屏幕不行，因为看视频的时候没这个感觉。</p>
<p>令人难绷的是，下拉的通知中心一排3个，但是有4个按钮。</p>
<p><img alt="奇怪的UI" decoding="async" height="1280" loading="lazy" src="/post/iplay_mini_2024/gV0wM1.webp" width="800">
</p>
<h2 id="锁屏和唤醒">锁屏和唤醒</h2>
<p>原厂ROM自带“拿起唤醒”，在显示设置中可以开启。</p>
<p><img alt="拿起唤醒" decoding="async" height="1280" loading="lazy" src="/post/iplay_mini_2024/9MaL8b.webp" width="800">
</p>
<p>锁屏没有看到对应的安卓小组件，所以可以借助Nova Launcher的手势功能进行快捷操作。</p>
<p><img alt="手势锁屏" decoding="async" height="1280" loading="lazy" src="/post/iplay_mini_2024/9RB38O.webp" width="800">
</p>
<h2 id="有线耳机音量过大">有线耳机音量过大</h2>
<p>手机拨号盘 <code>*#*#3646633#*#*</code> 进入工程模式，<code>Hardware Testing</code>-&gt;<code>Audio</code>-&gt;<code>Volume</code>-&gt;<code>Audio Playback</code>
按图中设置进行调整，如果觉得声大声小不合适，可以自定义每一档的数值。</p>
<p><img alt="音量调节" decoding="async" height="1280" loading="lazy" src="/post/iplay_mini_2024/YVQyVe.webp" width="800">
</p>
<h1 id="后记">后记</h1>
<p>本来只想写一个怎么解决掌玩mini音量过大问题的帖子，但是忍不住把它完善成一篇简评了。</p>
<p>流量卡已经买到了，LTE也可以测试了。如果用的好，后续还会写一篇介绍的。</p>
<p>现在已经是24年3月1日00:45了，兜兜转转写了几个小时就到了下个月，那就写到这里好了。 :P</p>]]></content:encoded>
    </item>
    <item>
      <title>Chrome Wayland Fcitx 漏字修复</title>
      <link>https://blog.zjyl1994.com/post/chrome-wayland/</link>
      <pubDate>Wed, 14 Feb 2024 20:48:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/chrome-wayland/</guid>
      <description>&lt;p&gt;最近在自用的 Fedora 系统上切换了Wayland显示，输入法就有一些奇怪的漏字现象。&lt;/p&gt;
&lt;p&gt;具体表现为，输入一句汉字后，会有一些拼音的字母跳过输入法直接进入程序。形成类似于 &lt;code&gt;我说&lt;/code&gt; =&amp;gt; &lt;code&gt;我s活&lt;/code&gt; 这样的奇怪句子。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>最近在自用的 Fedora 系统上切换了Wayland显示，输入法就有一些奇怪的漏字现象。</p>
<p>具体表现为，输入一句汉字后，会有一些拼音的字母跳过输入法直接进入程序。形成类似于 <code>我说</code> =&gt; <code>我s活</code> 这样的奇怪句子。</p>
<p>翻了一下 <a href="https://fcitx-im.org/wiki/Using_Fcitx_5_on_Wayland#Chromium_.2F_Electron" rel="noopener noreferrer" target="_blank">ArchWiki</a>，发现已经有现成的修复方案了。</p>
<p>如果你和我一样是 Fcitx5+Wayland 的搭配，编辑Chrome的 <code>.desktop</code> 文件，在命令后添加以下参数即可。
<code>--enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime</code></p>
<p>Chrome和VSCode都可以这么修复。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Docker build 挂梯子</title>
      <link>https://blog.zjyl1994.com/post/docker-proxy/</link>
      <pubDate>Thu, 21 Sep 2023 22:27:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/docker-proxy/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;国内的网络环境，懂得都懂。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Docker build的时候常常需要在镜像内使用包管理器安装一些对应的包，比如&lt;code&gt;gcc&lt;/code&gt;/&lt;code&gt;g++&lt;/code&gt;等等，由于独特的网络环境，不得不使用一些魔法手段。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>国内的网络环境，懂得都懂。</p>
</blockquote>
<p>Docker build的时候常常需要在镜像内使用包管理器安装一些对应的包，比如<code>gcc</code>/<code>g++</code>等等，由于独特的网络环境，不得不使用一些魔法手段。</p>
<p>常用的方法是通过<code>ENV</code>在dockerfile里直接修改<code>HTTP_PROXY</code>,<code>HTTPS_PROXY</code>,<code>GOPROXY</code>等变量为国内特有的地址，
但是对于想开放给世界范围使用的开源项目，镜像写死这些东西会对非CN地区用家造成一些困绕。</p>
<p>在不修改Dockerfile本身的情况下，可以使用<code>build-arg</code>在构建阶段注入环境变量，通过<code>--network host</code>将构建阶段的网路环境与本机打通。
这样就可以借助本地的魔法加速包管理器拉取速度了。</p>
<p>一个简单地例子：
<code>docker build --network host --build-arg http_proxy=http://127.0.0.1:7890 --build-arg https_proxy=http://127.0.0.1:7890 .</code></p>
<p>当打包好镜像后，也可以使用这种方法完成需要拉取外网内容初始化部分的。
<code>docker run --network host -e http_proxy=http://127.0.0.1:7890 -e https_proxy=http://127.0.0.1:7890  ac9e557bd88d</code></p>
]]></content:encoded>
    </item>
    <item>
      <title>AutoHotKey 模拟多媒体控制按键</title>
      <link>https://blog.zjyl1994.com/post/ahk_media/</link>
      <pubDate>Sun, 26 Mar 2023 13:47:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/ahk_media/</guid>
      <description>&lt;p&gt;公司台式机标配的联想键盘属于非常中规中矩的那种，能打字，但是一点额外的媒体控制功能都没有。&lt;/p&gt;
&lt;p&gt;当我们听着音乐的时候，如果同事过来问问题，不能及时暂停可是大问题。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>公司台式机标配的联想键盘属于非常中规中矩的那种，能打字，但是一点额外的媒体控制功能都没有。</p>
<p>当我们听着音乐的时候，如果同事过来问问题，不能及时暂停可是大问题。</p>
<p>所以借助大名鼎鼎的<code>AutoHotKey</code>可以绑定一组快捷键来模拟高级键盘上的多媒体控制功能。</p>
<p>热键定义脚本如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">^!Space::Send {Media_Play_Pause}
</span></span><span class="line"><span class="cl">^!Right::Send {Media_Next}
</span></span><span class="line"><span class="cl">^!Left::Send {Media_Prev}
</span></span><span class="line"><span class="cl">^!Up::Send {Volume_Up}
</span></span><span class="line"><span class="cl">^!Down::Send {Volume_Down}
</span></span><span class="line"><span class="cl">^!AppsKey::Send {Volume_Mute}
</span></span><span class="line"><span class="cl">XButton1::Send {Media_Play_Pause}
</span></span><span class="line"><span class="cl">XButton2::Send {Volume_Mute}
</span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>按键</th>
					<th>功能</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Ctrl+Alt+空格</td>
					<td>播放/暂停</td>
			</tr>
			<tr>
					<td>Ctrl+Alt+方向键右</td>
					<td>下一曲</td>
			</tr>
			<tr>
					<td>Ctrl+Alt+方向键左</td>
					<td>上一曲</td>
			</tr>
			<tr>
					<td>Ctrl+Alt+方向键上</td>
					<td>音量加</td>
			</tr>
			<tr>
					<td>Ctrl+Alt+方向键下</td>
					<td>音量减</td>
			</tr>
			<tr>
					<td>Ctrl+Alt+菜单键</td>
					<td>静音</td>
			</tr>
			<tr>
					<td>鼠标侧键1</td>
					<td>播放/暂停</td>
			</tr>
			<tr>
					<td>鼠标侧键2</td>
					<td>静音</td>
			</tr>
	</tbody>
</table>
<p>至于为什么不使用音乐软件里的快捷键？
我大部分时间都是油管上找工作用BGM，浏览器可是不支持设置快捷键控制媒体的。</p>
<p>通过<code>AutoHotKey</code>触发的都是多媒体键盘上的标准功能按键，Windows上大部分软件都是吃这个设定的。起码Chrome/Firefox都支持。</p>
<p>如果想分享这个功能给其他人，通过<code>Ahk2exe</code>将ahk文件转换为exe就可以轻松的分发了。</p>
<p>亲测了一天，这个多媒体按键真的好用。
Windows 平台上的神级软件真是多，小时候就听说过AutoHotKey，现在真正的帮到了我，很赞。</p>
]]></content:encoded>
    </item>
    <item>
      <title>[23.03.18] 最近的两个小工具</title>
      <link>https://blog.zjyl1994.com/post/recent-work-230318/</link>
      <pubDate>Sat, 18 Mar 2023 23:05:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/recent-work-230318/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;最近搞了两个小玩具，解决了我的一些问题。
能根据自己的需求定制工具，是最幸福的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;文件盒子&#34;&gt;文件盒子&lt;/h1&gt;
&lt;p&gt;&lt;img alt=&#34;filebox&#34; decoding=&#34;async&#34; height=&#34;693&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/recent-work-230318/filebox.webp&#34; width=&#34;699&#34;&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持通过 WebUI 列出文件&lt;/li&gt;
&lt;li&gt;文件的编辑操作通过 WebDAV 进行&lt;/li&gt;
&lt;li&gt;WebUI 可以隐藏文件&lt;/li&gt;
&lt;li&gt;WebDAV 可以设置密码&lt;/li&gt;
&lt;li&gt;Golang 单文件部署无额外依赖&lt;/li&gt;
&lt;li&gt;实测可以部署在 Cloudflare CDN 后面&lt;/li&gt;
&lt;li&gt;文件链接支持过期，防止迅雷等下载工具刷爆服务期流量&lt;/li&gt;
&lt;li&gt;很轻量，实际占用约为 10M&lt;/li&gt;
&lt;li&gt;UPX 压缩完毕后 2.5MB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一款工具主要是为了解决之前 Webls 项目需要 ssh 到服务器的问题。
现在可以通过 WebDAV 在手机/电脑等设备上直接修改文件。&lt;/p&gt;
&lt;h1 id=&#34;简易信息面板&#34;&gt;简易信息面板&lt;/h1&gt;
&lt;p&gt;&lt;img alt=&#34;gostatusd&#34; decoding=&#34;async&#34; height=&#34;556&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/recent-work-230318/gostatusd.webp&#34; width=&#34;447&#34;&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持月流量统计&lt;/li&gt;
&lt;li&gt;单文件无依赖部署&lt;/li&gt;
&lt;li&gt;很轻量，实际内存占用约 11MB&lt;/li&gt;
&lt;li&gt;UPX 压缩后二进制为 5.7MB&lt;/li&gt;
&lt;li&gt;后台持续采集数据，不会阻塞刷新&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一款工具是一个简单地探针应用，没有一些花哨的功能。
目前大部分探针都是PHP的，由于服务器没有PHP环境，所以想看一些信息还是比较麻烦的。简单地升级了一下多年以前的 Gostatusd 项目，配上了新的 UI 就这样了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>最近搞了两个小玩具，解决了我的一些问题。
能根据自己的需求定制工具，是最幸福的。</p>
</blockquote>
<h1 id="文件盒子">文件盒子</h1>
<p><img alt="filebox" decoding="async" height="693" loading="eager" src="/post/recent-work-230318/filebox.webp" width="699">
</p>
<ul>
<li>支持通过 WebUI 列出文件</li>
<li>文件的编辑操作通过 WebDAV 进行</li>
<li>WebUI 可以隐藏文件</li>
<li>WebDAV 可以设置密码</li>
<li>Golang 单文件部署无额外依赖</li>
<li>实测可以部署在 Cloudflare CDN 后面</li>
<li>文件链接支持过期，防止迅雷等下载工具刷爆服务期流量</li>
<li>很轻量，实际占用约为 10M</li>
<li>UPX 压缩完毕后 2.5MB</li>
</ul>
<p>这一款工具主要是为了解决之前 Webls 项目需要 ssh 到服务器的问题。
现在可以通过 WebDAV 在手机/电脑等设备上直接修改文件。</p>
<h1 id="简易信息面板">简易信息面板</h1>
<p><img alt="gostatusd" decoding="async" height="556" loading="lazy" src="/post/recent-work-230318/gostatusd.webp" width="447">
</p>
<ul>
<li>支持月流量统计</li>
<li>单文件无依赖部署</li>
<li>很轻量，实际内存占用约 11MB</li>
<li>UPX 压缩后二进制为 5.7MB</li>
<li>后台持续采集数据，不会阻塞刷新</li>
</ul>
<p>这一款工具是一个简单地探针应用，没有一些花哨的功能。
目前大部分探针都是PHP的，由于服务器没有PHP环境，所以想看一些信息还是比较麻烦的。简单地升级了一下多年以前的 Gostatusd 项目，配上了新的 UI 就这样了。</p>
<p>可以在这里查看详情：</p>
<p><a href="https://github.com/zjyl1994/filebox" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/filebox</a></p>
<p><a href="https://github.com/zjyl1994/gostatusd" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/gostatusd</a></p>
<p>如果好用请给 star。</p>
<p>未来我会发掘更多的需求然后满足自己~</p>]]></content:encoded>
    </item>
    <item>
      <title>VSCode 配置任意位置 F5 调试 Golang</title>
      <link>https://blog.zjyl1994.com/post/vscode-debug-go/</link>
      <pubDate>Wed, 16 Nov 2022 20:44:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/vscode-debug-go/</guid>
      <description>&lt;p&gt;VSCode 安装 Golang 插件后，如果安装了配套的gotools, 实际上已经可以支持调试了。&lt;/p&gt;
&lt;p&gt;但是我们按下F5后会报错，提示 &lt;code&gt;could not launch process: not an executable file&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;原因是你没有给工作区配置 &lt;code&gt;launch.json&lt;/code&gt; 导致VSCode找不到你要运行的程序位置。
默认的设置，你按下F5会尝试运行当前打开的页面，然而大部分情况下我们都不会选择 &lt;code&gt;main&lt;/code&gt; 包的文件而是正在某个文件内部设置断点，
所以仅仅编译当前文件是不够的。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>VSCode 安装 Golang 插件后，如果安装了配套的gotools, 实际上已经可以支持调试了。</p>
<p>但是我们按下F5后会报错，提示 <code>could not launch process: not an executable file</code>。</p>
<p>原因是你没有给工作区配置 <code>launch.json</code> 导致VSCode找不到你要运行的程序位置。
默认的设置，你按下F5会尝试运行当前打开的页面，然而大部分情况下我们都不会选择 <code>main</code> 包的文件而是正在某个文件内部设置断点，
所以仅仅编译当前文件是不够的。</p>
<p>如何解决呢？</p>
<p>在当前项目中创建 <code>.vscode/launch.json</code>，填入以下内容：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;0.2.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;configurations&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Launch Package&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;go&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;request&#34;</span><span class="p">:</span> <span class="s2">&#34;launch&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;auto&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;program&#34;</span><span class="p">:</span> <span class="s2">&#34;${workspaceRoot}&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>保存后即可在任意位置按F5启动调试了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Lazydog OCR</title>
      <link>https://blog.zjyl1994.com/post/lazydogocr/</link>
      <pubDate>Sun, 25 Sep 2022 09:14:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/lazydogocr/</guid>
      <description>&lt;p&gt;日常开发中，我们经常会拿到一个测试同学反馈的截图，圈上了有问题的部分，需要你来排查问题。
你看得到图片上有traceid和JSON入参，但是没有复制给你。你去找他复现，又得折腾人家重新给你抓信息。&lt;/p&gt;
&lt;p&gt;此时使用苹果的同学就跳出来了，可以直接 OCR 图片上的文字出来。&lt;/p&gt;
&lt;p&gt;如果没有苹果怎么办呢？我搜了一下，找到了 Tesseract 这个神奇的库，并把他包装成了一个在线网站。&lt;/p&gt;
&lt;p&gt;最后的成品就是这样的&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;Lazydog OCR&#34; decoding=&#34;async&#34; height=&#34;837&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/lazydogocr/main-page.webp&#34; width=&#34;1112&#34;&gt;
&lt;/p&gt;
&lt;p&gt;提前说明，这种开源的纯离线 OCR 没有大厂提供的付费 OCR 接口效果好，胜在一不用付费，二不用上传，全靠本机的算力进行识别。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>日常开发中，我们经常会拿到一个测试同学反馈的截图，圈上了有问题的部分，需要你来排查问题。
你看得到图片上有traceid和JSON入参，但是没有复制给你。你去找他复现，又得折腾人家重新给你抓信息。</p>
<p>此时使用苹果的同学就跳出来了，可以直接 OCR 图片上的文字出来。</p>
<p>如果没有苹果怎么办呢？我搜了一下，找到了 Tesseract 这个神奇的库，并把他包装成了一个在线网站。</p>
<p>最后的成品就是这样的</p>
<p><img alt="Lazydog OCR" decoding="async" height="837" loading="eager" src="/post/lazydogocr/main-page.webp" width="1112">
</p>
<p>提前说明，这种开源的纯离线 OCR 没有大厂提供的付费 OCR 接口效果好，胜在一不用付费，二不用上传，全靠本机的算力进行识别。</p>
<p>想体验的可以直接访问 <a href="https://ocr.zjyl1994.com" rel="noopener noreferrer" target="_blank">Lazydog OCR</a> 。（名字的意思是懒狗，毕竟谁不想偷懒呢）</p>
<h1 id="使用">使用</h1>
<ol>
<li>打开网站</li>
<li>选择图片文件 / Ctrl + V 粘贴 / 拖拽图片</li>
<li>等待加载 Tesseract 核心</li>
<li>得到识别结果</li>
</ol>
<p>是不是很简单？如果你只想在线识别的话，到这里就可以了。</p>
<h1 id="技术细节">技术细节</h1>
<p><code>Tesseract.js</code> 是开源的识别方案，刚刚迭代到3.0，提高了识别速度。但是官网给的例子还是 2.0 时代的，速度奇慢，几乎不可用。</p>
<p>3.0使用了WASM，还有SIMD的加持，速度就非常的快。为了能私有化部署，我把所有的 CDN 资源拉下放到代码仓库，所以使用的时候直接部署代码仓库即可。</p>
<p>核心功能就是这样的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// 感知是否支持 SIMD
</span></span></span><span class="line"><span class="cl"><span class="nx">simdSupported</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">wasmFeatureDetect</span><span class="p">.</span><span class="nx">simd</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 识别核心
</span></span></span><span class="line"><span class="cl"><span class="nx">Tesseract</span><span class="p">.</span><span class="nx">recognize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">&#34;imgbox&#34;</span><span class="p">),</span> <span class="c1">// 输入一个img、canvas或者其他什么有图片的元素
</span></span></span><span class="line"><span class="cl">    <span class="s1">&#39;eng&#39;</span><span class="p">,</span> <span class="c1">// 使用的识别模型，参考 https://github.com/naptha/tessdata/tree/gh-pages/4.0.0 中的内容 
</span></span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">workerPath</span><span class="o">:</span> <span class="s1">&#39;/assets/core/worker.min.js&#39;</span><span class="p">,</span> <span class="c1">// 加载的 worker位置
</span></span></span><span class="line"><span class="cl">        <span class="c1">// 识别的核心，根据 SIMD 支持选择核心
</span></span></span><span class="line"><span class="cl">        <span class="nx">corePath</span><span class="o">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">simdSupported</span> <span class="o">?</span> <span class="s1">&#39;/assets/core/tesseract-core-simd.wasm.js&#39;</span> <span class="o">:</span> <span class="s1">&#39;/assets/core/tesseract-core.wasm.js&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">langPath</span><span class="o">:</span> <span class="s1">&#39;/assets/lang-data&#39;</span><span class="p">,</span> <span class="c1">// 存放识别模型的路径
</span></span></span><span class="line"><span class="cl">        <span class="nx">logger</span><span class="o">:</span> <span class="nx">m</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">this</span><span class="p">.</span><span class="nx">progressStatus</span> <span class="o">=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">status</span><span class="p">;</span> <span class="c1">// m.status 识别的进度文本
</span></span></span><span class="line"><span class="cl">            <span class="k">this</span><span class="p">.</span><span class="nx">progressValue</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">progress</span> <span class="o">*</span> <span class="mi">100</span><span class="p">);</span> <span class="c1">// m.progress 是 0-1 的浮点型进度
</span></span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">).</span><span class="nx">then</span><span class="p">(({</span> <span class="nx">data</span><span class="o">:</span> <span class="p">{</span> <span class="nx">text</span> <span class="p">}</span> <span class="p">})</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">outputResult</span> <span class="o">=</span> <span class="nx">text</span><span class="p">;</span> <span class="c1">// 最后输出的识别结果就是 text
</span></span></span><span class="line"><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">progressStatus</span> <span class="o">=</span> <span class="s1">&#39;就绪.&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>想增加识别的语言，下载对应的训练数据放到 <code>lang-data</code> ，在语言字符串的位置传入对应的语言代码即可。</p>
<p>训练数据可以在 <code>https://github.com/naptha/tessdata</code> 下载。</p>
<p>整个项目已开源：<code>https://github.com/zjyl1994/lazydogocr</code></p>]]></content:encoded>
    </item>
    <item>
      <title>Golang 泛型初体验</title>
      <link>https://blog.zjyl1994.com/post/multi-worker/</link>
      <pubDate>Fri, 05 Aug 2022 21:53:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/multi-worker/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;最近给公司的几个服务升级到了Go1.18,总算能好好的体验泛型了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1.18泛型更新了一段时间，我们公司的基础构架也上了1.18的底包，可以愉快的玩耍泛型了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>最近给公司的几个服务升级到了Go1.18,总算能好好的体验泛型了。</p>
</blockquote>
<p>1.18泛型更新了一段时间，我们公司的基础构架也上了1.18的底包，可以愉快的玩耍泛型了。</p>
<p>Golang没有函数重载，之前也没有泛型，大部分业务写起来也没什么问题。
但是当你想把某些重复逻辑封装成函数的时候会面临一个问题，强类型。</p>
<p>没有泛型之前，需要针对每个类型都写一份实现。很多时候你只要把值传来传去，并不关心内部的值是什么。比如我最常用的多线程异步执行并获取结果。</p>
<p>没有泛型之前需要这么写：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Go" data-lang="Go"><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">MultiWorker</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">workerNum</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="p">[]</span><span class="kd">interface</span><span class="p">{},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">fn</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="kd">interface</span><span class="p">{})</span><span class="w"> </span><span class="p">(</span><span class="kd">interface</span><span class="p">{},</span><span class="w"> </span><span class="kt">error</span><span class="p">))</span><span class="w"> </span><span class="p">([]</span><span class="kd">interface</span><span class="p">{},</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">resultLock</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">results</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kd">interface</span><span class="p">{},</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">inputs</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">jobCh</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kd">interface</span><span class="p">{},</span><span class="w"> </span><span class="nx">workerNum</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">waitg</span><span class="p">,</span><span class="w"> </span><span class="nx">ctx</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">errgroup</span><span class="p">.</span><span class="nf">WithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">workerNum</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">waitg</span><span class="p">.</span><span class="nf">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">for</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">jobCh</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">fn</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">resultLock</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">results</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">results</span><span class="p">,</span><span class="w"> </span><span class="nx">result</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">resultLock</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">jobCh</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">input</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">close</span><span class="p">(</span><span class="nx">jobCh</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">waitg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">results</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>表面看起来没什么问题，但是实际使用中体验很差，输入的<code>inputs</code>无论什么类型都需要先用for循环转换成<code>[]interface{}</code>才能使用，输出的值也需要进行类型断言才能得到需要的值，一旦中途混了其他类型的数据进去，断言失败会得到一个 <code>panic</code>，这很不好。</p>
<p>现在有了泛型以后，Golang 编译器会在你使用到泛型部分代码的时候进行类型展开，生成对应类型的代码编译进去，对于程序员来说是无感知的。这样下来只会在最终的二进制中膨胀出一点点汇编代码，效率性能也不会有多少损失。</p>
<p>有了泛型以后就可以这么写了：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;context&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;sync&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;golang.org/x/sync/errgroup&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nx">MultiWorker</span><span class="p">[</span><span class="nx">K</span><span class="p">,</span><span class="w"> </span><span class="nx">V</span><span class="w"> </span><span class="kt">any</span><span class="p">](</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">workerNum</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="p">[]</span><span class="nx">K</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">fn</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">K</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">V</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">))</span><span class="w"> </span><span class="p">([]</span><span class="nx">V</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">resultLock</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">results</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="nx">V</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">inputs</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">jobCh</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="nx">K</span><span class="p">,</span><span class="w"> </span><span class="nx">workerNum</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">waitg</span><span class="p">,</span><span class="w"> </span><span class="nx">ctx</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">errgroup</span><span class="p">.</span><span class="nf">WithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">workerNum</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">waitg</span><span class="p">.</span><span class="nf">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">for</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">jobCh</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">fn</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">resultLock</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">results</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">results</span><span class="p">,</span><span class="w"> </span><span class="nx">result</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">resultLock</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">jobCh</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">input</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">close</span><span class="p">(</span><span class="nx">jobCh</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">waitg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">results</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>优势呢，全程带着类型，不需要任何额外的类型断言，如果写错编译期检测都不会过，强壮性得到了极大增强。</p>
<p>同时，泛型也催动了一批新的基础包广泛使用。
<code>github.com/samber/lo</code>提供了对标<code>Lodash</code>的大量函数，都由1.18泛型写成，现在Github已经超过7K star。</p>
<p>我在公司项目的重构中就大量使用了<code>lo</code>这个软件包。<code>Filter</code>过滤数组元素、<code>Map</code>批量处理数据格式转换，都非常方便。
如果打开源码查看，会发现他们的实现都很简单。而且根据Golang的一贯优化，这种超简单的确定性代码最终都会被内联，所以完全不用担心性能问题。</p>
<p>不得不说，这次1.18更新的泛型是一个非常赞的东西，开发愉快度大大提升。</p>
<p>附：使用了泛型的多线程异步执行程序（完整版本）</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;context&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;sync&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;golang.org/x/sync/errgroup&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nx">MultiWorkerDo</span><span class="p">[</span><span class="nx">K</span><span class="w"> </span><span class="kt">any</span><span class="p">](</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">workerNum</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="p">[]</span><span class="nx">K</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">fn</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">K</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">jobCh</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="nx">K</span><span class="p">,</span><span class="w"> </span><span class="nx">workerNum</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">waitg</span><span class="p">,</span><span class="w"> </span><span class="nx">ctx</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">errgroup</span><span class="p">.</span><span class="nf">WithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">workerNum</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">waitg</span><span class="p">.</span><span class="nf">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">for</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">jobCh</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">fn</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">jobCh</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">input</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">close</span><span class="p">(</span><span class="nx">jobCh</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">waitg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nx">MultiWorkerResult</span><span class="p">[</span><span class="nx">K</span><span class="p">,</span><span class="w"> </span><span class="nx">V</span><span class="w"> </span><span class="kt">any</span><span class="p">](</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">workerNum</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="p">[]</span><span class="nx">K</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">fn</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">K</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">V</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">))</span><span class="w"> </span><span class="p">([]</span><span class="nx">V</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">resultLock</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">results</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="nx">V</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">inputs</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">jobCh</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="nx">K</span><span class="p">,</span><span class="w"> </span><span class="nx">workerNum</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">waitg</span><span class="p">,</span><span class="w"> </span><span class="nx">ctx</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">errgroup</span><span class="p">.</span><span class="nf">WithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">workerNum</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">waitg</span><span class="p">.</span><span class="nf">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">for</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">jobCh</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">fn</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">resultLock</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">results</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">results</span><span class="p">,</span><span class="w"> </span><span class="nx">result</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">					</span><span class="nx">resultLock</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">jobCh</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">input</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">close</span><span class="p">(</span><span class="nx">jobCh</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">waitg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">results</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Golang 使用ECDSA进行数字签名</title>
      <link>https://blog.zjyl1994.com/post/ecdsa-golang/</link>
      <pubDate>Wed, 27 Jul 2022 22:26:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/ecdsa-golang/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;数字签名可以检查数据是否遭到篡改&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;起因&#34;&gt;起因&lt;/h1&gt;
&lt;p&gt;我的工作中，开发了一个批量改写和执行 SQL 的数据处理服务。
待执行的 SQL 模板会按照迭代定期发布到服务中，决定了服务最终运行时生成的 SQL。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>数字签名可以检查数据是否遭到篡改</p>
</blockquote>
<h1 id="起因">起因</h1>
<p>我的工作中，开发了一个批量改写和执行 SQL 的数据处理服务。
待执行的 SQL 模板会按照迭代定期发布到服务中，决定了服务最终运行时生成的 SQL。</p>
<p>此处 SQL 模板和数据处理服务的关系，可以类比为固件和硬件。</p>
<p>服务的固件通过 HTTP 接口进行刷入。参考手机厂家的做法，
更新服务都会检查固件包的数字签名，如果固件签名不正确，是无法正确刷入并启动的。</p>
<h1 id="ecdsa">ECDSA</h1>
<p>ECDSA是大名鼎鼎的椭圆曲线签名算法，以256位的私钥尺寸提供堪比RSA3072的安全性。</p>
<p>Golang 标准库中提供了 <code>crypto/ecdsa</code> ，提供了签名，验证和生成公私钥的功能。但是一般情况我们不会使用Go来生成和保存公私钥，更多情况是使用 OpenSSL 进行生成。</p>
<p>OpenSSL 生成公私钥的命令，一般我们使用<code>prime256v1</code>或<code>secp384r1</code>这两种推荐的曲线参数，这里我们选择<code>prime256v1</code>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">openssl ecparam -name prime256v1 -genkey -noout -out priv_key.pem
</span></span><span class="line"><span class="cl">openssl pkey -in priv_key.pem -pubout -out pub_key.pem
</span></span></code></pre></div><p>生成的私钥<code>priv_key.pem</code>，公钥 <code>pub_key.pem</code>，都保存为x509格式的pem证书。</p>
<h1 id="签名">签名</h1>
<p>数字签名，需要先对数据体进行哈希，得到信息摘要后，通过私钥进行签名。
整理后可以得到如下 Golang 函数：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">Sign</span><span class="p">(</span><span class="nx">r</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nx">Reader</span><span class="p">,</span><span class="w"> </span><span class="nx">privKey</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// 加载x509格式的私钥文件</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">block</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">pem</span><span class="p">.</span><span class="nf">Decode</span><span class="p">(</span><span class="nx">privKey</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">block</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;privKey no pem data found&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">pk</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">x509</span><span class="p">.</span><span class="nf">ParseECPrivateKey</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">Bytes</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// 对输入进行哈希获取信息摘要</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">h</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">sha256</span><span class="p">.</span><span class="nf">New</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">Copy</span><span class="p">(</span><span class="nx">h</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">hash</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">h</span><span class="p">.</span><span class="nf">Sum</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// ECDSA 签名</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">sign</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">ecdsa</span><span class="p">.</span><span class="nf">SignASN1</span><span class="p">(</span><span class="nx">rand</span><span class="p">.</span><span class="nx">Reader</span><span class="p">,</span><span class="w"> </span><span class="nx">pk</span><span class="p">,</span><span class="w"> </span><span class="nx">hash</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">base64</span><span class="p">.</span><span class="nx">StdEncoding</span><span class="p">.</span><span class="nf">EncodeToString</span><span class="p">(</span><span class="nx">sign</span><span class="p">),</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h1 id="签名验证">签名验证</h1>
<p>验证时也需要先摘要后进行验证，注意，签名验证使用公钥，此公钥可以公开存放在服务内。整理后可以得到如下 Golang 函数：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">Verify</span><span class="p">(</span><span class="nx">r</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nx">Reader</span><span class="p">,</span><span class="w"> </span><span class="nx">pubKey</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="nx">sign</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">bool</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// 加载 x509 格式的公钥</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">block</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">pem</span><span class="p">.</span><span class="nf">Decode</span><span class="p">(</span><span class="nx">pubKey</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">block</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;pubKey no pem data found&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">genericPublicKey</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">x509</span><span class="p">.</span><span class="nf">ParsePKIXPublicKey</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">Bytes</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">pk</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">genericPublicKey</span><span class="p">.(</span><span class="o">*</span><span class="nx">ecdsa</span><span class="p">.</span><span class="nx">PublicKey</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// 对输入进行哈希获取信息摘要</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">h</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">sha256</span><span class="p">.</span><span class="nf">New</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">Copy</span><span class="p">(</span><span class="nx">h</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">hash</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">h</span><span class="p">.</span><span class="nf">Sum</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// ECDSA 验证</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">bSign</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">base64</span><span class="p">.</span><span class="nx">StdEncoding</span><span class="p">.</span><span class="nf">DecodeString</span><span class="p">(</span><span class="nx">sign</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">ecdsa</span><span class="p">.</span><span class="nf">VerifyASN1</span><span class="p">(</span><span class="nx">pk</span><span class="p">,</span><span class="w"> </span><span class="nx">hash</span><span class="p">,</span><span class="w"> </span><span class="nx">bSign</span><span class="p">),</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h1 id="使用">使用</h1>
<p>一般使用中，我选择通过 1.16 新增的 embed 将 x509 格式的证书包裹在二进制中，考虑到我的使用场景是纯网络服务，并不需要担心二进制在本地被破解，也就不需要对公钥部分进行过多的混淆。</p>
<p>在我的使用场景中，通过专用工具打包固件，再通过<code>Sign</code>函数生成签名。
将固件通过 HTTP 接口POST刷入，签名通过 HTTP Header 传入服务。</p>
<p>服务拿到固件后进行验证签名，签名不通过则返回 HTTP 403 Forbidden。
如果签名通过，则执行后续的解包刷固件功能。</p>
<hr>
<p>这次的博文没有什么技术含量，更多是对工作中常用的工具函数进行总结。</p>
<p>后续我会单独搞一个 wiki 站存放这些东西。</p>
]]></content:encoded>
    </item>
    <item>
      <title>戴尔V3400 Fedora折腾笔记</title>
      <link>https://blog.zjyl1994.com/post/v3400/</link>
      <pubDate>Fri, 27 May 2022 00:09:56 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/v3400/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;赶上京东做活动以旧换新，处理了家里的老电脑和吃灰手机。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我的主力电脑是 联想小新Pro 13 2020 锐龙版本，由于工作离不开真实的 Windows 环境，我的 Linux 开发都被迫挤到18年那部 TMB117 小笔记本上。
很难想象一部18年的赛扬老机器是如何顶着无风扇的降频硬抗开发任务的，在京东看到活动之后就萌生了换机的想法。&lt;/p&gt;
&lt;p&gt;考虑到有一块从下载机上拆下来的闲置大容量SSD硬盘，可选电脑范围就被限制的很窄了，毕竟2022年想找到还有SATA盘位的新笔记本非常难找。
考虑到兼容性、SATA、性能等，最后圈定了 Dell Vostro 3400 i5-1135G7+16G内存+512G固态版本，京东当时的活动价 3599。&lt;/p&gt;
&lt;p&gt;选择理由：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;11代i5 性能保障。（12代i5有大小核设计，按照日常Linux经验，这种新技术头几年体验会很差）&lt;/li&gt;
&lt;li&gt;SATA盘位预留并且有线和安装架无需额外购买（Acer的EX215有预留HDD空间但是排线和架都要另外购买，而且没有提供购买渠道）&lt;/li&gt;
&lt;li&gt;Ubuntu 认证机型，意味着大部分软件功能都应该是好使的，如果不能用可以去Ubuntu里抓驱动过来。&lt;/li&gt;
&lt;/ol&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>赶上京东做活动以旧换新，处理了家里的老电脑和吃灰手机。</p>
</blockquote>
<p>我的主力电脑是 联想小新Pro 13 2020 锐龙版本，由于工作离不开真实的 Windows 环境，我的 Linux 开发都被迫挤到18年那部 TMB117 小笔记本上。
很难想象一部18年的赛扬老机器是如何顶着无风扇的降频硬抗开发任务的，在京东看到活动之后就萌生了换机的想法。</p>
<p>考虑到有一块从下载机上拆下来的闲置大容量SSD硬盘，可选电脑范围就被限制的很窄了，毕竟2022年想找到还有SATA盘位的新笔记本非常难找。
考虑到兼容性、SATA、性能等，最后圈定了 Dell Vostro 3400 i5-1135G7+16G内存+512G固态版本，京东当时的活动价 3599。</p>
<p>选择理由：</p>
<ol>
<li>11代i5 性能保障。（12代i5有大小核设计，按照日常Linux经验，这种新技术头几年体验会很差）</li>
<li>SATA盘位预留并且有线和安装架无需额外购买（Acer的EX215有预留HDD空间但是排线和架都要另外购买，而且没有提供购买渠道）</li>
<li>Ubuntu 认证机型，意味着大部分软件功能都应该是好使的，如果不能用可以去Ubuntu里抓驱动过来。</li>
</ol>
<h1 id="拆机">拆机</h1>
<p>拿到新机，使用 Fedora LiveUSB 进行验证，确定基础功能 OK 后开始拆机。
选择合适尺寸的十字螺丝刀，拧掉后盖能看到的所有螺丝。<strong>注意，靠近屏轴一侧最外的两颗螺丝无法取下，内部有垫片。</strong> 确定完全松开后请用硬质卡片或者拆机使用的撬棒依次打开卡扣，卸下D面。</p>
<p><img alt="拆机图片" decoding="async" height="591" loading="eager" src="/post/v3400/photo_2022-05-27_00-21-15.webp" width="1280">
</p>
<p>此图片来自互联网，可以看到2.5寸硬盘空间上有框架和排线预留，这也是我购买的重要因素之一。</p>
<p>有一个小细节，为了防止你买不到那么小的螺丝，直接在硬盘框架上拧了4颗固定用的螺丝留给你。DELL 过于贴心。</p>
<p><img alt="预留的螺丝" decoding="async" height="1280" loading="lazy" src="/post/v3400/photo_2022-05-27_00-21-24.webp" width="960">
</p>
<p>安装好框架插好排线就可以正常使用了。</p>
<h1 id="fedora-之旅">Fedora 之旅</h1>
<p>最早的 Linux 体验是 TMB117 上 Manjaro+XFCE 的经典组合。考虑到性能变好了，可以愉快的 KDE，于是选择了最新出的 Fedora 36 KDE 版本。</p>
<p>装好之后的成品是这样的：</p>
<p><img alt="桌面" decoding="async" height="1080" loading="lazy" src="/post/v3400/desktop.webp" width="1920">
</p>
<p><img alt="信息" decoding="async" height="586" loading="lazy" src="/post/v3400/screenfetch.webp" width="805">
</p>
<p>（KDE自带的截图就带一圈黑边，估计是窗口管理器加的特效没法被截取下来吧。）</p>
<p>最新的 Fedora 36 已经全面转向 Wayland 了，所以 <code>screenfetch</code> 也没法取到分辨率。不过我可以告诉你这部机是14寸1080p屏幕非IPS，能用，但不会很惊艳。
和小新Pro13那块2.5K的IPS屏幕没得比，但是日常使用看个电影啥的完全OK。</p>
<h1 id="折腾笔记">折腾笔记</h1>
<h2 id="更换软件源">更换软件源</h2>
<p>官方软件源里偶尔会抽到东软的源，dnf经常报错，我一度怀疑是我的操作有问题。
后来经过专家指点是最快源抽到的 Neusoft 镜像有问题。</p>
<p>大名鼎鼎的TUNA源自然是最好的选择，不过听说负载过高，所以我选择换成BFSU的源。</p>
<p>换源教程：https://mirrors.bfsu.edu.cn/help/fedora/</p>
<h2 id="安装常见小工具">安装常见小工具</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dnf install htop screenfetch lm_sensors netcat
</span></span></code></pre></div><h2 id="安装-fcitx-替换-ibus">安装 fcitx 替换 ibus</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dnf install fcitx5 fcitx5-configtool fcitx5-gtk fcitx5-autostart fcitx5-qt fcitx5-chinese-addons  fcitx5-data kcm-fcitx5
</span></span></code></pre></div><p>然后KDE主菜单搜索 <code>输入法选择器</code>，启用fcitx5并重启生效。</p>
<h2 id="开启-intel-硬解">开启 Intel 硬解</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 安装 rpmfusion 软件源获取额外支持</span>
</span></span><span class="line"><span class="cl">sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-<span class="k">$(</span>rpm -E %fedora<span class="k">)</span>.noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-<span class="k">$(</span>rpm -E %fedora<span class="k">)</span>.noarch.rpm
</span></span><span class="line"><span class="cl"><span class="c1"># 安装 Intel 媒体驱动</span>
</span></span><span class="line"><span class="cl">sudo dnf install libva libva-utils libva-intel-driver intel-media-driver ffmpeg
</span></span></code></pre></div><p>此刻你的系统就该支持 Intel 核显硬解视频了，Iris Xe 的显卡游戏性能不好说，看视频还是完全 OK 的。</p>
<h2 id="支持-thetis-的-fido2-key">支持 THETIS 的 FIDO2 Key</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 此段来自 Thetis 的官网说明</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;KERNEL==&#34;hidraw*&#34;, SUBSYSTEM==&#34;hidraw&#34;, MODE=&#34;0664&#34;, GROUP=&#34;plugdev&#34;&#39;</span> <span class="p">|</span> sudo tee /etc/udev/rules.d/thetisu2f.rules
</span></span><span class="line"><span class="cl">sudo udevadm control --reload-rules
</span></span></code></pre></div><h2 id="日常空载占用率过高packagekit导致关机卡顿">日常空载占用率过高，packagekit导致关机卡顿</h2>
<p>如果发现日常使用中风扇狂转，大概率是你加了海外的软件源，然后packagekit在疯狂的拉取更新信息又被RST反复重试导致的。</p>
<p>如何验证呢？如果你的电脑风扇此时正在原地起飞电脑热到爆炸，立刻重启。</p>
<p>在Fedora转圈圈出现时按 <kbd>Esc</kbd></p>
<p>你会发现整个系统在等待 <code>packagekit</code> 读秒，那大概率就是他的问题了。</p>
<p>可以通过 <code>sudo systemctl mask packagekit</code> 屏蔽该服务，重启后即可生效。</p>
<p>已知的影响点：Discover 软件商店的更新会挂掉，不过不影响你正常 <code>dnf update</code> 之类的手动更新方法，只是以后没有人提示你软件包需要更新了而已。</p>
<h2 id="配置-github-代理">配置 Github 代理</h2>
<p>由于众所周知的原因 Github SSH 基本连不上。
于是需要编辑<code>~/.ssh/config</code>增加以下段：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Host github.com
</span></span><span class="line"><span class="cl">    ProxyCommand /usr/bin/netcat -x 127.0.0.1:20170 %h %p
</span></span></code></pre></div><p>其实这一段也是网上复制来的，最重要的问题是：Fedora自带的nc不是netcat,需要手动装了然后修改这里面调用的命令。</p>
<p>其中，20170的端口号需要改成你自己的端口号。</p>
<h1 id="后记">后记</h1>
<p>这部机器被我调教了一个星期不到，基本已经满足我的需求了，通过 Ubuntu 认证的机型对Linux友好程度非常棒，Fedora上适配的也很好，基本免驱动所有功能都OK,包括快捷键。</p>
<p>这部机会作为我的纯 Linux 环境开发使用，这配置应该可以支撑很久了。</p>]]></content:encoded>
    </item>
    <item>
      <title>小工具导航页</title>
      <link>https://blog.zjyl1994.com/post/superutils/</link>
      <pubDate>Sun, 01 May 2022 22:37:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/superutils/</guid>
      <description>&lt;p&gt;上班的时候经常需要使用工具网站，公司环境不方便登录自己的账号进行云同步。
所以我简单的糊了一个 Vue 的小工具导航页。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;成品&#34; decoding=&#34;async&#34; height=&#34;1258&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/superutils/su.webp&#34; width=&#34;1724&#34;&gt;
&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>上班的时候经常需要使用工具网站，公司环境不方便登录自己的账号进行云同步。
所以我简单的糊了一个 Vue 的小工具导航页。</p>
<p><img alt="成品" decoding="async" height="1258" loading="eager" src="/post/superutils/su.webp" width="1724">
</p>
<h1 id="开发">开发</h1>
<p>构想是一个简简单单的页面，可以通过名称进行搜索。对于纯前端筛选，Vue 无疑是最合适的选择，</p>
<p>最基本的页面结构是这样：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;app&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;search-box&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">v-model</span><span class="o">=</span><span class="s">&#34;keyword&#34;</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">&#34;Type keyword to search on name field&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">table</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;</span>Name<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;</span>URL<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">tr</span> <span class="na">v-for</span><span class="o">=</span><span class="s">&#34;item in searchInList()&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>{{item.name}}<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">v-bind:href</span><span class="o">=</span><span class="s">&#34;item.url&#34;</span> <span class="na">target</span><span class="o">=</span><span class="s">&#34;_blank&#34;</span><span class="p">&gt;</span>{{item.url}}<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>然后，搭配简单的 Vue 代码</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">Vue</span><span class="p">.</span><span class="nx">createApp</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">data</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">items</span><span class="o">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="cl">            <span class="nx">keyword</span><span class="o">:</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">mounted</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nx">self</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;data.json&#39;</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="p">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">()).</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">self</span><span class="p">.</span><span class="nx">items</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">sort</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">x</span><span class="p">,</span> <span class="nx">y</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">x</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="nx">y</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span><span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;}</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">x</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="nx">y</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span><span class="k">return</span> <span class="mi">1</span><span class="p">;}</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">});</span>
</span></span><span class="line"><span class="cl">        <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">methods</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">searchInList</span><span class="p">()</span> <span class="p">{</span><span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">item</span> <span class="p">=&gt;</span> <span class="nx">item</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">indexOf</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">keyword</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">())</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="p">)}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}).</span><span class="nx">mount</span><span class="p">(</span><span class="s1">&#39;#app&#39;</span><span class="p">);</span>
</span></span></code></pre></div><p>简单解释一下这些 Vue 在做什么：</p>
<ul>
<li>启动的时候，fetch拉取<code>data.json</code>，下载后读取数据进行按长度排序。</li>
<li>方法：searchInList，过滤包含 keyword 的条目（全转小写，方便搜索）。</li>
</ul>
<h1 id="部署">部署</h1>
<p>由于代码是托管在自己的Gitea服务器上，没法使用传统的CI/CD方案，所以找到个简单的
Webhook方案进行触发。（本想自己写，但是觉得没必要）</p>
<p>这里使用的方案是：https://github.com/adnanh/webhook</p>
<p>Debian 上直接 <code>apt install webhook</code> 可以安装。</p>
<p>默认端口 9000，自己修改systemd单元的Exec段增加 <code>-port</code> 切换到闲置端口。</p>
<p>简单列出一个我使用的配置文档</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">deploysu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">execute-command</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/opt/scripts/deploysu.sh&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">command-working-directory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/opt/scripts&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">trigger-rule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">match</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">value</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">refs/heads/master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">parameter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">payload</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ref</span><span class="w">
</span></span></span></code></pre></div><p>写好后启动webhook服务，此时访问你的 <code>http://server:9000/hooks/deploysu</code> 就会调用你设定的
<code>/opt/scripts/deploysu.sh</code>。</p>
<p>再写一个简简单单的部署脚本。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">rm -rf yp
</span></span><span class="line"><span class="cl">git clone ssh://git@server:22/zjyl1994/yellow_page.git yp
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> yp
</span></span><span class="line"><span class="cl">cp index.html /var/www/su.zjyl1994.com/index.html
</span></span><span class="line"><span class="cl">cp data.json /var/www/su.zjyl1994.com/data.json
</span></span></code></pre></div><p>最后去 Gitea 配置你的 Webhook</p>
<p><img alt="Gitea" decoding="async" height="641" loading="lazy" src="/post/superutils/webhook.webp" width="2351">
</p>
<p>修改后 push 到 master 分支就可以自动更新服务端的部署了。</p>]]></content:encoded>
    </item>
    <item>
      <title>DevTerm 等待与失望</title>
      <link>https://blog.zjyl1994.com/post/devterm/</link>
      <pubDate>Sun, 02 Jan 2022 22:04:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/devterm/</guid>
      <description>&lt;p&gt;&lt;img alt=&#34;Title&#34; decoding=&#34;async&#34; height=&#34;960&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/devterm/title.webp&#34; width=&#34;1280&#34;&gt;

&lt;img alt=&#34;menu&#34; decoding=&#34;async&#34; height=&#34;1280&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/devterm/menu.webp&#34; width=&#34;960&#34;&gt;
&lt;/p&gt;
&lt;p&gt;2020年11月，我在网上看到 ClockworkPi 发布的 DevTerm，在此之前我就听说过这家公司。
他家的 GameShell 之前就想买来着，但是1000多块买一个只能玩游戏的开源掌机实在下不去手。&lt;/p&gt;
&lt;p&gt;2020年12月15号，我在激烈的思想斗争下下单了A04版本。还记得那个时候按照汇率差不多花了1700-1800左右。计划是拿来做一个打字机的，然后页面显示四月前交付，这是我第一次先款预购东西。&lt;/p&gt;
&lt;p&gt;等到2021年3月多，情况有变，全球供应链紧张，论坛帖子发布更新状态，官网等待多天以后变成9月前交付。&lt;/p&gt;
&lt;p&gt;持续等待后，9月，CM3版本交付了。那个时候我看到了少数的真机开箱视频，感觉有点小了。
然后10-11月A06交付了，只剩我这个A04用户持续等待。&lt;/p&gt;
&lt;p&gt;2022年1月1日早晨，我收到了顺丰送来的 DevTerm。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><img alt="Title" decoding="async" height="960" loading="eager" src="/post/devterm/title.webp" width="1280">

<img alt="menu" decoding="async" height="1280" loading="lazy" src="/post/devterm/menu.webp" width="960">
</p>
<p>2020年11月，我在网上看到 ClockworkPi 发布的 DevTerm，在此之前我就听说过这家公司。
他家的 GameShell 之前就想买来着，但是1000多块买一个只能玩游戏的开源掌机实在下不去手。</p>
<p>2020年12月15号，我在激烈的思想斗争下下单了A04版本。还记得那个时候按照汇率差不多花了1700-1800左右。计划是拿来做一个打字机的，然后页面显示四月前交付，这是我第一次先款预购东西。</p>
<p>等到2021年3月多，情况有变，全球供应链紧张，论坛帖子发布更新状态，官网等待多天以后变成9月前交付。</p>
<p>持续等待后，9月，CM3版本交付了。那个时候我看到了少数的真机开箱视频，感觉有点小了。
然后10-11月A06交付了，只剩我这个A04用户持续等待。</p>
<p>2022年1月1日早晨，我收到了顺丰送来的 DevTerm。</p>
<h1 id="组装">组装</h1>
<p>组装 DevTerm 很简单，个人推荐弄一个水口剪。官方塑料件的质量非常之高，基本不需要打磨，只要用心剪就行了。包装里还有三根小螺丝，需要很小的一个十字螺丝刀，主要用途是固定核心板和主板。我没有找到能用的螺丝刀，也就没上，也没影响使用。用的接口是笔记本的DDR3接口，所以固定能力应该没有问题。</p>
<p>唯一的组装问题就是上面扩展插槽保护盖过于紧了，我用小锤子才敲进去，需要巧劲，一定要看前后翻看说明书再装。</p>
<p>个人觉得会出问题的地方。</p>
<ol>
<li>屏幕，网上蛮多人费劲把屏幕卡进去之后才发现接口没有对准留的位置，这个要注意一下。</li>
<li>WIFI天线，这个天线是贴在屏幕背板外侧的，我之前费力贴准到内侧，然后发现扣不上。</li>
<li>顶部扩展口盖子，一定要看好官方的顺序，先插入左侧的，然后右侧敲进去，大力出奇迹。因为左侧是完整的柱状，右侧是倾斜一些的，所以一定要从右侧敲进去。</li>
</ol>
<h1 id="使用">使用</h1>
<h2 id="更新镜像">更新镜像</h2>
<p>A04的镜像是基于Armbian的，所以本质上还是个Debian，但是screenfetch的信息是Ubuntu，具体信息可以看头图。基于Debian，所以可以用apt去装你想要的软件包，deb之类的还算很流行。</p>
<p>拿到手首先需要更新版本，SD卡中安装的是第一代系统。官方的APT源放在Github上，如果你是国内用户再没有魔法手段的话，那你100%连不上，不用试了。我搞了个APT源的镜像，具体使用方法参考前面的博客。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update <span class="o">&amp;&amp;</span> sudo apt install -y devterm-kernel-current-cpi-a04 devterm-gearbox-a04 devterm-fan-daemon-cpi-a04 <span class="o">&amp;&amp;</span> sudo reboot
</span></span></code></pre></div><p>更新系统，重启。</p>
<h2 id="刷键盘固件">刷键盘固件</h2>
<p>官方自带的轨迹球使用体验非常的差，尤其超宽屏横着滚需要滚一年。幸好有论坛大神搞了定制版固件，官方也非常迅速地采纳了优化，当你滚得速度快的时候鼠标移动会很多，再也不用一点一点搓过去了。</p>
<p>首先确定你有<code>/dev/ttyACM0</code>，如果没有多重启几次试试，只有这个在的时候才可以刷。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install dfu-util -y
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">wget https://github.com/clockworkpi/DevTerm/raw/main/Code/devterm_keyboard/bin/DevTerm_keyboard_firmware_v0.3_utils.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">chmod +x  DevTerm_keyboard_firmware_v0.3_utils.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo ./DevTerm_keyboard_firmware_v0.3_utils.sh
</span></span></code></pre></div><p>这些指令来自官方的论坛帖子，我自己试用过，效果提升巨大。
如果你在没有<code>/dev/ttyACM0</code>的时候就刷键盘固件，那你会得到一个失灵的键盘，别紧张长按电源键关机再开就好。</p>
<h2 id="休眠">休眠</h2>
<p>这个设备没有快速休眠按键，想休眠的话没法像笔记本一样合盖。所以可以使用<code>systemctl suspend</code>进行休眠，这样电池消耗会小一些。另外休眠以后键盘无法唤醒，可以通过轻按电源键唤醒。大概率是休眠期间给键盘断电了。</p>
<h2 id="风扇">风扇</h2>
<p>A04版本的风扇用的是GPIO进行控制，有一个Python写的风扇守护程序，温度超过66度的时候会转30秒进行降温，我个人觉得这个可以进行一些优化，首先代码层面，可以读写单独的配置文件，方便萌新来改，直接上手改Python，要不是我程序员出身可能就翻车了。</p>
<p>理论上操控风扇只需要三条指令</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 初始化GPIO</span>
</span></span><span class="line"><span class="cl">gpio mode <span class="m">41</span> out
</span></span><span class="line"><span class="cl"><span class="c1"># 风扇转</span>
</span></span><span class="line"><span class="cl">gpio write <span class="m">41</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 风扇关</span>
</span></span><span class="line"><span class="cl">gpio write <span class="m">41</span> <span class="m">0</span>
</span></span></code></pre></div><p>这个风扇逻辑也是有优化空间的，个人觉得可以弄成温度到多少的时候启动风扇直到某个温度值停止。现在这个启停逻辑只转30秒不太合适。这个小风扇持续转起来还是可以提供稳定降温的，55度吹一阵就可以变成49度。</p>
<h1 id="总结">总结</h1>
<p>电池续航我买的松下电芯，现实7-8h，实际三小时多一些。浏览网页电跑的更快。</p>
<p>长按键除了空格手感都比较软，字母键倒是还可以，但是太小了，只能拿来双手举着打字。
不过这也解决了放在桌子上打字看屏幕累的问题。</p>
<p>armbian使用比较OK，但是桌面不能改壁纸，可能是我没找到地方，apt里软件还不少，但是少数需要自己编译。</p>
<p>如果你是拿来当打字机想做生产力工具的话还是算了，不如买个便宜的商务本把浏览器都删掉装个focuswriter。</p>
<p>如果你是拿来做玩具+大手办，还愿意花这么多钱，那完全OK。官方的质感做的很好，可惜ARM Linux不太适合一般人，最后只能做成极客的Cyberdeck。如果不会折腾的话还是去买GameShell之类的开箱即用玩具。</p>
<p>据说国内买这个的也就几十个人，大部分订单都是外国人订走了，希望他们可以玩出一些花来吧。</p>
<p>等我有空了再折腾折腾，到时候也给官方贡献一些代码。</p>]]></content:encoded>
    </item>
    <item>
      <title>2021</title>
      <link>https://blog.zjyl1994.com/post/2021/</link>
      <pubDate>Sun, 02 Jan 2022 20:05:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/2021/</guid>
      <description>&lt;p&gt;2021 年度总结&lt;/p&gt;
&lt;p&gt;其实我很少做年度总结，今年算是值得记住的一年，换了满意的工作，买了车，减肥也成功了一部分。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>2021 年度总结</p>
<p>其实我很少做年度总结，今年算是值得记住的一年，换了满意的工作，买了车，减肥也成功了一部分。</p>
<h1 id="工作">工作</h1>
<p>2021 年最大的变化莫过于我换了工作，去年的总结里我说迫于生计去了腾讯外包，2020年卷了一圈发现外包并不香，大厂内部也是草台班子。</p>
<p>在去腾讯之前我以为可以学到蛮多东西的，结果进去了，大厂也就那么回事，而且外包员工不是人也是真的，唯一让我怀念的就是腾讯食堂了，怀念腾大13楼的羊排。</p>
<p>3月辞职跑路，在两个月的准备之后进入了面试过三次的0909，也算是圆梦成功了（毕竟每次离职我都收到过0909的面试，也算是老相识了，传说中的创维和中科场地我都以面试者身份进去过）。</p>
<h1 id="生活">生活</h1>
<h2 id="减肥">减肥</h2>
<p>2021年最能吹的就是我减肥20多斤了，运动+节食+戒糖，3个月瘦下来，之前的衣服也能穿了，体力也好了。那个时候最喜欢干的事就是从家走到公司附近，16km往返。平时上班午休也要出去走，每天1w5千步起步。头几个月瘦的是很快的，后续就是保持住，坚持了半年也没反弹。</p>
<p>2022年还是得继续减肥，计划再瘦20多斤。</p>
<h2 id="车">车</h2>
<p>2020年末考了驾照，C1，考了三个月，科目二科目三都是一次过的，吐槽下福永考场那个车坡起无力差点死火，幸好油离配合成功救起来了。</p>
<p>2021年初和朋友去看车，当时计划是看卡罗拉雷凌之类的，顺便路过福特的店，我没想到8个月之后我在这买了车，那个时候还和销售说我要看雷凌搞得销售一脸黑线。</p>
<p>虽然电动车势头很猛，但我最后还是买的油车，上的邻近城市牌照，粤B继续摇看缘分了。
至于不买电动车的原因，不是绿牌不香，实在是电动车没有打动我的地方，我对“林北”加速没那么看重，因为我认识一个开卡罗拉起步窜的比旁边电动的士还快的人。也对智能化无感，我也不想坐在车里看电影，更不喜欢没有实体按键全靠触摸的感觉。对我来说，适度的智能化是OK的，但是空调之类的实体按钮还是要有的，传统的机械手刹一拉噶嘣嘣响也特别有感觉（当然真开了车以后还是很希望有电子手刹+自动驻车的，确实方便）。</p>
<p>深圳这个地方不太适合开车，而且有没有牌不影响，早晚高峰堵的怀疑人生，就算有粤B我也不想上道。更别提我这个外地牌，所以除了刚开始新鲜两天开去上班，其他时间我还是放在停车场，没事就擦擦，就像买了个大手办一样。另外，新手小刮擦比较多，我已经预感到明年保费上涨了。</p>
<p>汽车的保险有能力还是多保一些，大城市豪车真的多，200万三者和400万三者只差不到400块钱，有能力的还是多加点为好。</p>
<h1 id="设备">设备</h1>
<h2 id="mi-11-lite">Mi 11 Lite</h2>
<p>陪伴我3年的 K20 电池老化，所以退休了，继任是小米11青春版，也就那回事，我手机基本就是播放器+微信终端，偶尔拍个照。不打游戏的情况下 780G 真的挺香，续航足够坚持我在外面听歌溜达大半天的。</p>
<h2 id="redmi-buds-3-青春版">Redmi Buds 3 青春版</h2>
<p>只要 99 的入耳式无线耳机，音质我觉得没啥问题，通勤神器，充电一次可以用几天。陪伴了我每天出去运动，要说唯一的问题就是音量有点小，调大了也不太行，安静路上走路听音乐和有声书 OK。个人不喜欢那种 AirPods 带一个棍式的耳机，还是这种小玩意好一些。</p>
<h2 id="iphone-11-pro">iPhone 11 Pro</h2>
<p>阿米转手给我的，我拿了两部 GPD 的 UMPC 来换。iOS 的体验确实流畅，但是好多东西都不是我想要的东西，而且在这个平台上确实会不由自主地买一些 App，导致我卡里的港币越来越少。iOS 需要你的脑电波和苹果在一个频率上，你会觉得这玩意超级好用。我只在某些频段上和苹果脑电波一致，所以香，但是只香一点点。</p>
<p>另外，苹果的照片成像确实 OK。怪不得所有手机相机测评都要拉 iPhone 来陪着打。</p>
<h2 id="devterm">DevTerm</h2>
<p>2020年12月15号我订购了 DevTerm ，在长达12个月的等待之后我终于在2022年1月1日从顺丰手里拿到这部机器。简单来说有点失望，拿来当打字机键盘不太行，屏幕字也小。这部分我会单独写一个文章来讲。</p>
<h1 id="技术">技术</h1>
<h2 id="go">Go</h2>
<p>在腾讯我也写 Go，但是接手的是前人留下来的奇怪代码，我以为是精妙设计的地方问了业务逻辑之后发现他们是在瞎写，彻底贯彻了人和代码只要有一个能跑就行的策略，整个人都麻了，也让我意识到大厂也是草台班子。花了两三个月重构完没多久我就离职了，听之前的同事说接手的同事吐槽代码太烂，我只想说：你还没经历过第一版呢，那时候带我们的老大给我一个服务器账号密码，告诉我代码就在这个服务器里，自己想办法编译起来吧。我好歹给改成了 Go Module，还写了 Makefile。（扶额）</p>
<p>在 0909，公司的代码质量好很多，他的代码结构我到现在自己写东西也在模仿。腾讯的优势是基建好但是组内代码比较烂，0909 的问题就是代码质量 OK，但是云端的基建真的菜，希望公司努力改进吧，这还是能慢慢优化解决的事情。</p>
<h2 id="前端">前端</h2>
<p>对的，我又一次入门 Vue / React 失败了。2021 年我摆脱了 Bootstrap，开始用 Bulma 写页面，算是和现代 UI 设计接轨了一些。写代码写多了更喜欢写原生 JavaScript，现在的浏览器提供的东西基本可以满足需求，不再需要 jQuery 进行差异抹平，fetch 和基本的 DOM 操作就可以满足我的页面了。</p>
<h2 id="开源项目">开源项目</h2>
<p>很惭愧，2021 年没输出什么有价值的开源项目，2022年一定搞一个有价值的东西。</p>
<h1 id="2022目标">2022目标</h1>
<ul>
<li>自驾里程过万</li>
<li>写一个有价值的开源项目</li>
<li>吃遍周边城市</li>
<li>瘦 25 斤</li>
<li>脱单</li>
</ul>
<h1 id="写在最后">写在最后</h1>
<p>2022 年，诸多预言家都说，会有新的一轮疫情爆发，当然这也会是最后一波疫情了，到 2023 疫情就会结束了。希望预言家说的是准确的，疫情快些结束吧。感觉 2019 年底还在昨天的样子，疫情改变了太多人和事。</p>
<p>Take care of youself.</p>
<p>And be well.</p>]]></content:encoded>
    </item>
    <item>
      <title>Clockworkpi APT 镜像源</title>
      <link>https://blog.zjyl1994.com/post/clockwork-proxy/</link>
      <pubDate>Tue, 26 Oct 2021 20:20:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/clockwork-proxy/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Clockworkpi 的 APT 源放在 Github 上，由于众所周知的原因，国内访问稀烂。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2020年12月我预购了DevTerm，A04版本还没有发货，但是A06版本已经有群友拿到了，
面临的一大问题就是官方的APT源在Github上，由于众所周知的缘故，国内访问十分艰难。
就算你侥幸连上了，大概率也是不稳定的。所以我就搞了一个小小的镜像源。&lt;/p&gt;
&lt;p&gt;使用这个镜像源十分简单。只需要你执行以下命令。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo sed -i &#39;s/raw.githubusercontent.com/su.zjyl1994.com/g&#39; /etc/apt/sources.list.d/clockworkpi.list&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;原理就是将 &lt;code&gt;/etc/apt/sources.list.d/clockworkpi.list&lt;/code&gt; 中的 &lt;code&gt;raw.githubusercontent.com&lt;/code&gt; 替换成 &lt;code&gt;su.zjyl1994.com&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;执行完毕后 &lt;code&gt;sudo apt update&lt;/code&gt;，更新软件源，是不是加载速度变快很多了呢？&lt;/p&gt;
&lt;p&gt;此镜像部署在我的服务器上，并且在 Cloudflare 上缓存一小时。基本可以理解为和官方同步的。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>Clockworkpi 的 APT 源放在 Github 上，由于众所周知的原因，国内访问稀烂。</p>
</blockquote>
<p>2020年12月我预购了DevTerm，A04版本还没有发货，但是A06版本已经有群友拿到了，
面临的一大问题就是官方的APT源在Github上，由于众所周知的缘故，国内访问十分艰难。
就算你侥幸连上了，大概率也是不稳定的。所以我就搞了一个小小的镜像源。</p>
<p>使用这个镜像源十分简单。只需要你执行以下命令。</p>
<p><code>sudo sed -i 's/raw.githubusercontent.com/su.zjyl1994.com/g' /etc/apt/sources.list.d/clockworkpi.list</code></p>
<p>原理就是将 <code>/etc/apt/sources.list.d/clockworkpi.list</code> 中的 <code>raw.githubusercontent.com</code> 替换成 <code>su.zjyl1994.com</code>。</p>
<p>执行完毕后 <code>sudo apt update</code>，更新软件源，是不是加载速度变快很多了呢？</p>
<p>此镜像部署在我的服务器上，并且在 Cloudflare 上缓存一小时。基本可以理解为和官方同步的。</p>
<p>以下内容是技术向的，如果你只是想来更换软件源的可以不往下看了。</p>
<p>核心逻辑如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">aptReverseProxy</span><span class="p">(</span><span class="nx">c</span><span class="w"> </span><span class="o">*</span><span class="nx">gin</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">githubRawContent</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">url</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="s">&#34;https://raw.githubusercontent.com&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">proxy</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">httputil</span><span class="p">.</span><span class="nf">NewSingleHostReverseProxy</span><span class="p">(</span><span class="nx">githubRawContent</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">proxy</span><span class="p">.</span><span class="nx">Director</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">req</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">req</span><span class="p">.</span><span class="nx">Header</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nx">Request</span><span class="p">.</span><span class="nx">Header</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">req</span><span class="p">.</span><span class="nx">Host</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">githubRawContent</span><span class="p">.</span><span class="nx">Host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">req</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Scheme</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">githubRawContent</span><span class="p">.</span><span class="nx">Scheme</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">req</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Host</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">githubRawContent</span><span class="p">.</span><span class="nx">Host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">req</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Path</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nx">Request</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">proxy</span><span class="p">.</span><span class="nx">ModifyResponse</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">resp</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Response</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">resp</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Cache-Control&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;public, max-age=3600&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">proxy</span><span class="p">.</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Go 中自带了一个单主机的反代组件，只需要把 Path 修改好，嫁接到 Gin 上就 OK 了。</p>
<p>Director 负责修改向后传递的http请求，只需要把Host、Scheme、Path都改成Github上的就行了。</p>
<p>ModifyResponse 负责修改传给客户端的响应，通过增加 <code>Cache-Control</code> 让 Cloudflare 的 CDN 网络缓存一小时请求内容。</p>
<p>你没有看错，就这么简单。</p>
<p>完整源码在<a href="https://github.com/zjyl1994/suapis/blob/master/service/clockworkpi/proxy.go" rel="noopener noreferrer" target="_blank">这里</a>。可以直接点进去看。</p>]]></content:encoded>
    </item>
    <item>
      <title>重构——在线网站更新手记</title>
      <link>https://blog.zjyl1994.com/post/refactor-2021/</link>
      <pubDate>Wed, 06 Oct 2021 20:04:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/refactor-2021/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;趁着十一假期，我更新了一下网站的设计。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;重构，为了更好的设计。&lt;/p&gt;
&lt;p&gt;我的网站，有三到四个子站组成。最早的设计是通过 VPS 直接提供服务，
在一段时间的迭代中，我选择了 Cloudflare 进行防护和加速，虽然中国大陆地区
使用 Cloudflare 是减速行为。&lt;/p&gt;
&lt;p&gt;现在的网站结构：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;www.zjyl1994.com&lt;/code&gt; 是我的主站，主要负责导航到其他子站点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;blog.zjyl1994.com&lt;/code&gt; 是我的博客站，就是这个站点了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;su.zjyl1994.com&lt;/code&gt; 是我的后端站点，我的所有活动功能都挂在这里。&lt;/p&gt;
&lt;h1 id=&#34;pages&#34;&gt;Pages&lt;/h1&gt;
&lt;p&gt;www 和 blog 都是属于静态网站，www 是纯手写的 html 代码，
blog 的代码是使用 Hugo 生成的，对于这种静态资源站点，我选择了
Cloudflare Pages 进行托管。静态资源依托全球网络可以抵抗任何形式的攻击，
而且可以稳定的被 CDN 进行缓存，进行就近分发。&lt;/p&gt;
&lt;h1 id=&#34;tunnel&#34;&gt;Tunnel&lt;/h1&gt;
&lt;p&gt;Cloudflare 于今年早些时候公布了免费的 Tunnel 计划，原先是收费计划的 Argo Tunnel，
针对回源场景可以免费使用了。使用 Cloudflared 可以直接绕开 caddy、nginx 等反代工具，
让 CF 成为你的 Portal。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>趁着十一假期，我更新了一下网站的设计。</p>
</blockquote>
<p>重构，为了更好的设计。</p>
<p>我的网站，有三到四个子站组成。最早的设计是通过 VPS 直接提供服务，
在一段时间的迭代中，我选择了 Cloudflare 进行防护和加速，虽然中国大陆地区
使用 Cloudflare 是减速行为。</p>
<p>现在的网站结构：</p>
<p><code>www.zjyl1994.com</code> 是我的主站，主要负责导航到其他子站点。</p>
<p><code>blog.zjyl1994.com</code> 是我的博客站，就是这个站点了。</p>
<p><code>su.zjyl1994.com</code> 是我的后端站点，我的所有活动功能都挂在这里。</p>
<h1 id="pages">Pages</h1>
<p>www 和 blog 都是属于静态网站，www 是纯手写的 html 代码，
blog 的代码是使用 Hugo 生成的，对于这种静态资源站点，我选择了
Cloudflare Pages 进行托管。静态资源依托全球网络可以抵抗任何形式的攻击，
而且可以稳定的被 CDN 进行缓存，进行就近分发。</p>
<h1 id="tunnel">Tunnel</h1>
<p>Cloudflare 于今年早些时候公布了免费的 Tunnel 计划，原先是收费计划的 Argo Tunnel，
针对回源场景可以免费使用了。使用 Cloudflared 可以直接绕开 caddy、nginx 等反代工具，
让 CF 成为你的 Portal。</p>
<p>我的服务器是 Ubuntu Server 20.04 LTS 的，所以可以直接参考官方的 <a href="https://pkg.cloudflare.com/" rel="noopener noreferrer" target="_blank">安装教程</a> 。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;deb http://pkg.cloudflare.com/ focal main&#39;</span> <span class="p">|</span> sudo tee /etc/apt/sources.list.d/cloudflare-main.list
</span></span><span class="line"><span class="cl">curl -C - https://pkg.cloudflare.com/pubkey.gpg <span class="p">|</span> sudo apt-key add -
</span></span><span class="line"><span class="cl">sudo apt-get update
</span></span><span class="line"><span class="cl">sudo apt-get install cloudflared
</span></span></code></pre></div><p>安装过后，就可以使用 <code>cloudflared</code> 命令进行后续操作了。</p>
<p>输入 <code>cloudflared tunnel login</code> ，它会给出一个 URL 地址，复制到浏览器里进行授权。</p>
<p>输入 <code>cloudflared tunnel create &lt;NAME&gt;</code> 创建名为 NAME 的隧道，会给定一个 UUID 作为你的隧道标识符。</p>
<p>编辑 <code>~/.cloudflared/config.yaml</code>，参考如下输入：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">http://localhost:&lt;PORT&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tunnel</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;UUID&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">credentials-file</span><span class="p">:</span><span class="w"> </span><span class="l">/root/.cloudflared/&lt;UUID&gt;.json</span><span class="w">
</span></span></span></code></pre></div><p>PORT 替换成后端服务的端口号，UUID 替换成隧道标识符给的 UUID。</p>
<p>此时隧道配置完毕，通过 <code>cloudflared tunnel route dns &lt;Name&gt; &lt;Endpoint&gt;</code>
在 CF 上创建指向 Endpoint 的 DNS 记录。此时，在 DNS 页面可以看到一条 Endpoint 的 CNAME 记录。</p>
<p>输入 <code>cloudflared tunnel run &lt;Name&gt;</code> 启动隧道，此时你就可以访问你的 Endpoint 网站，流量会打到你的内网服务端里。确定没问题后 Ctrl+C 停止运行，我们会用服务的形式启动隧道。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo cloudflared service install
</span></span><span class="line"><span class="cl">sudo systemctl start cloudflared
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> cloudflared
</span></span></code></pre></div><p>此时就可以稳定的访问你的内网服务了。</p>
<h1 id="su-apis">Su APIs</h1>
<p>原本的 Su API 包含了 “订阅合并”、“赛博鱼师傅” 功能，
全新重构的 Su API 在以上功能外扩展了 “文件共享库” 功能。</p>
<h2 id="结构重构">结构重构</h2>
<p>结构重构，旧的 Su 属于传统的 Gin 程序，没有日志写盘，配置文件散落一地。</p>
<p>新的结构由 Gin + Zap 构成，全局状态包含 Debug 、Secret。
根据 Debug 位决定 Zap 的日志输出方式，具体可以 <a href="https://github.com/zjyl1994/suapis/blob/master/vars/setup.go" rel="noopener noreferrer" target="_blank">参考代码</a> 。</p>
<p>全局 vars 包，这是我在公司项目中学到的。将公众变量抽取到一个独立的包里，方便程序全局调取，也方便统一初始化和管理。</p>
<h2 id="订阅合并">订阅合并</h2>
<p>订阅合并部分，增加了 Always Online 类似的功能解决数据源经常挂掉的问题。</p>
<p>实现方式就是访问订阅源进行回源的部分，成功返回之前增加了一个写盘操作，
当访问数据源失败时从磁盘加载上次获取成功的数据。
<a href="https://github.com/zjyl1994/suapis/blob/master/service/sssub/sub.go" rel="noopener noreferrer" target="_blank">参考代码</a> 。</p>
<h2 id="文件共享库">文件共享库</h2>
<p>增加这个共享库的原因是因为我有时候需要在外面获取一些配置文件等信息，
需要在公开电脑上登陆各种网盘的话就会很麻烦。</p>
<p>DigitalOcean 的 Space 是一个 S3 兼容服务，每月5刀提供 250GB 的对象存储。共享 VPS 的 1TB 出站流量。</p>
<p>实现上就是很简单的 ListObject + Presigned 集合。 <a href="https://github.com/zjyl1994/suapis/tree/master/service/s3index" rel="noopener noreferrer" target="_blank">参考代码</a> 。
实际实现中，增加了缓存机制减少每次调用 S3 API 的开销。
虽然 Space 的API调用不收费，但是时间层面还是比较大的开销。</p>
<h2 id="赛博鱼师傅">赛博鱼师傅</h2>
<p>赛博鱼师傅是一个 Telegram Bot 主要提供吃什么选择，旧的实现方式是读取配置文件到内存中，每次加载需要杀掉进程。
新的实现方式是独立读取单独的 <code>eatwhat.txt</code> 文档，更新文档可以实时反馈到吃什么选择器中。</p>]]></content:encoded>
    </item>
    <item>
      <title>Webls - 简单易用的目录列出应用</title>
      <link>https://blog.zjyl1994.com/post/webls/</link>
      <pubDate>Thu, 08 Apr 2021 22:32:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/webls/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;正好有个工具文件上云的需求，就做了这么个工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最近有一个文件扔到服务器上的需求，简简单单用 Golang 糊了一个文件列出工具。&lt;/p&gt;
&lt;p&gt;成品是这样的&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;默认界面&#34; decoding=&#34;async&#34; height=&#34;1108&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/webls/default.webp&#34; width=&#34;2368&#34;&gt;
&lt;/p&gt;
&lt;p&gt;还可以支持 Markdown 展示&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>正好有个工具文件上云的需求，就做了这么个工具。</p>
</blockquote>
<p>最近有一个文件扔到服务器上的需求，简简单单用 Golang 糊了一个文件列出工具。</p>
<p>成品是这样的</p>
<p><img alt="默认界面" decoding="async" height="1108" loading="eager" src="/post/webls/default.webp" width="2368">
</p>
<p>还可以支持 Markdown 展示</p>
<p>只要你的文件夹中有<code>README.md</code>文件，就能渲染出来对应的文件内容。</p>
<p><img alt="有自述文件的情况" decoding="async" height="1122" loading="lazy" src="/post/webls/filewithreadme.webp" width="2316">
</p>
<p>而且使用了 GitHub 风格的渲染器，所以可以支持多种样式</p>
<p><img alt="样式支持" decoding="async" height="1281" loading="lazy" src="/post/webls/markdown.webp" width="2348">
</p>
<p>而且支持中文的路径</p>
<p><img alt="中文路径" decoding="async" height="601" loading="lazy" src="/post/webls/path.webp" width="2337">
</p>
<p>对于带有点的文件和文件夹，和 unix 系列的行为一致，会被隐藏起来。</p>
<p>项目地址: <a href="https://github.com/zjyl1994/webls" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/webls</a></p>
<p>使用方法很简单，把编译好的 webls 文件扔到你需要的地方就可以运行了。</p>
<p>运行后可以在 9496 端口上得到同款界面。同时我还制作了 docker镜像 可以使用。</p>]]></content:encoded>
    </item>
    <item>
      <title>Redmi Note 8 刷机笔记</title>
      <link>https://blog.zjyl1994.com/post/ginkgo/</link>
      <pubDate>Sun, 08 Nov 2020 23:01:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/ginkgo/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;上班实在是忙，没时间折腾技术类的东西。
只能水一篇刷机笔记了，由于这篇实在太水了所以只能放生活分类。（尴尬&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;之前为了刷机玩，买了一部红米的9A，想着就一个摄像头，什么奇怪没有的手机最适合刷机了，不用担心奇怪硬件造成的BUG，结果我想多了，MTK平台一个刷机包没有。这手机7月发布到现在连个第三方官改都找不到。&lt;/p&gt;
&lt;p&gt;后来我就买了这部红米Note8，为了防止之前的教训，我特地去XDA围观了一下这部机的论坛，也看了几大著名的ROM，比如LineageOS、Pixel Experience、Resurrection Remix。他们都官方支持我这部手机，而且Xiaomi.eu也有最终的兜底方案，如果什么都刷不了我还有欧版可以刷，不用担心谷歌服务装不上。&lt;/p&gt;
&lt;h1 id=&#34;刷机&#34;&gt;刷机&lt;/h1&gt;
&lt;p&gt;买到手第一步就是申请解锁，小米解锁需要等待168小时，真是一分钟都少不了，在经历一整个星期的等待后，我解开了红米Note8的BL锁。&lt;/p&gt;
&lt;p&gt;然后就是刷机，我当初选择了 Pixel Experience 作为首选，毕竟谷歌自己出的手机就是安卓标杆，照着改的第三方ROM准没有错。&lt;/p&gt;
&lt;p&gt;标准的刷机流程就是，长按音量下+电源键开机进fastboot，然后用刷机命令刷最新的 &lt;code&gt;twrp recovery&lt;/code&gt; 进去(文后有附地址)。&lt;/p&gt;
&lt;p&gt;注意刷完了不要直接重启，关机，按住音量上+电源键开机直接进入 recovery，要不然你的twrp会被自带的官方rec覆盖掉。可能是小米自己的保护机制。&lt;/p&gt;
&lt;p&gt;进入 recovery 就是install、wipe 然后reboot system。非常正常的刷机方法。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>上班实在是忙，没时间折腾技术类的东西。
只能水一篇刷机笔记了，由于这篇实在太水了所以只能放生活分类。（尴尬</p>
</blockquote>
<h1 id="前言">前言</h1>
<p>之前为了刷机玩，买了一部红米的9A，想着就一个摄像头，什么奇怪没有的手机最适合刷机了，不用担心奇怪硬件造成的BUG，结果我想多了，MTK平台一个刷机包没有。这手机7月发布到现在连个第三方官改都找不到。</p>
<p>后来我就买了这部红米Note8，为了防止之前的教训，我特地去XDA围观了一下这部机的论坛，也看了几大著名的ROM，比如LineageOS、Pixel Experience、Resurrection Remix。他们都官方支持我这部手机，而且Xiaomi.eu也有最终的兜底方案，如果什么都刷不了我还有欧版可以刷，不用担心谷歌服务装不上。</p>
<h1 id="刷机">刷机</h1>
<p>买到手第一步就是申请解锁，小米解锁需要等待168小时，真是一分钟都少不了，在经历一整个星期的等待后，我解开了红米Note8的BL锁。</p>
<p>然后就是刷机，我当初选择了 Pixel Experience 作为首选，毕竟谷歌自己出的手机就是安卓标杆，照着改的第三方ROM准没有错。</p>
<p>标准的刷机流程就是，长按音量下+电源键开机进fastboot，然后用刷机命令刷最新的 <code>twrp recovery</code> 进去(文后有附地址)。</p>
<p>注意刷完了不要直接重启，关机，按住音量上+电源键开机直接进入 recovery，要不然你的twrp会被自带的官方rec覆盖掉。可能是小米自己的保护机制。</p>
<p>进入 recovery 就是install、wipe 然后reboot system。非常正常的刷机方法。</p>
<p>等待开机的时候你需要准备一个魔法网络方便跳过开机的谷歌登录，如果没有请拔掉卡并不连接WIFI，等看到桌面再说。</p>
<p><img alt="主屏幕" decoding="async" height="1280" loading="eager" src="/post/ginkgo/main_screen.webp" width="591">
</p>
<h1 id="调校">调校</h1>
<h2 id="隐藏前摄">隐藏前摄</h2>
<p>默认的设计实在太难看了，白白的屏幕多出来一个摄像头的尖。这个解决起来也很简单，装一个叫NachoNotch的应用就可以了,链接附在文末。</p>
<p><img alt="Nacho Notch" decoding="async" height="1280" loading="lazy" src="/post/ginkgo/nacho_notch.webp" width="591">
</p>
<p>安装完开启Hide Notch！他会在程序最上方画一个黑底的悬浮窗盖住原先的白色，反正实现非常完美。</p>
<p>唯一美中不足的是，会显示一个Nacho Notch正在其他程序上显示的提示，此时右滑提示，选择关闭即可。</p>
<p><img alt="关闭通知" decoding="async" height="1280" loading="lazy" src="/post/ginkgo/disable_notify.webp" width="591">
</p>
<p>唯一的缺点是设置这个APP里没法覆盖，可能是系统安全限制。</p>
<h2 id="快充">快充</h2>
<p>默认刷完 Pixel Experience，他会给你安装PE系列的通用内核，这会导致一些奇怪问题发生。比如本来有的功能就失灵了，比如说快充。使用Ampere这个软件可以看到当前充电的电流，使用官方内核充电时大约在300-500ma，对于一个4000mah的手机来说完全不能接受。</p>
<p>经过我在XDA等论坛不懈搜索，我最终找到了修复方案，刷一个优化过的内核。这个内核就叫 <code>sixteen</code> ，更新的地址在一个Telegram的Channel，<code>Redmi Note 8 / 8T Updates</code>,链接我附在后面了，里面有不少ROM可选。</p>
<p>刷新内核很简单，拿下内核的zip，进入TWRP直接Install就行了，不需要Wipe任何数据。</p>
<p>修复后的Ampere能达到快充标准。</p>
<p><img alt="Ampere修复后" decoding="async" height="1280" loading="lazy" src="/post/ginkgo/ampere.webp" width="591">
</p>
<h1 id="优缺点">优缺点</h1>
<h2 id="优点">优点</h2>
<ol>
<li>原生谷歌体验</li>
<li>待机能力还可以</li>
<li>多摄像头可用（看起来是完全搬了MIUI的相机过来，用手挡镜头进行切换可以看到确实四个摄像头都生效了。）</li>
</ol>
<h2 id="缺点">缺点</h2>
<ol>
<li>录像功能不可用，慢动作、短视频、录像均不可用，虽然你能看到在录在转圈圈跑秒，就是没法存储下来。</li>
</ol>
<p>目前我就找到这一个问题，以后发现再来补充。</p>
<h1 id="最后">最后</h1>
<p>放在最后我要说点和刷机没关系的东西，本来买9A就是为了刷机的，但是没有包，手里的K20正好有包，我就反过来日常用MIUI的9A，K20拿来刷第三方ROM，就这么扭曲的用了差不多4个月。直到我发现第三方ROM下K20的拍摄画质下降太多了，而且9A每次拿出去支付都会卡，这才动了心思要把K20弄回主力机使用。</p>
<p>在此之前我纠结过要不要搞一部可以刷机的手机，最后一路甚至选到了一加8T，但是最后想想不值当，一个拿来刷推看P站的手机完全没必要那么好，最后还是搞了红米Note8。最主要的是，非官方ROM的优化调教很难超过原厂（除非是那些不怎么关注的迷你小厂），相机是重灾区，买一个好手机刷机让一切都变得更糟实在不值当。</p>
<p>我这部Note8为了多用几年选的6+64GB，只要999。相比之前选的一加8T，能买三部Note8，我觉得还是便宜的红米更香一些。</p>
<p>毕竟，2300的差价能吃100份加煎蛋的双排猪脚饭。</p>
<h1 id="相关链接">相关链接</h1>
<p><a href="https://download.pixelexperience.org/ginkgo" rel="noopener noreferrer" target="_blank">Pixel Experience</a></p>
<p><a href="https://drive.google.com/file/d/1XseEI2xJa-Of2dfCgoF2tjyTYSDHY0Pm" rel="noopener noreferrer" target="_blank">Sixteen Kernel</a></p>
<p><a href="https://t.me/rn8updates" rel="noopener noreferrer" target="_blank">Redmi Note 8 / 8T Updates</a></p>
<p><a href="https://dl.twrp.me/ginkgo/" rel="noopener noreferrer" target="_blank">TWRP</a></p>
<p><a href="https://play.google.com/store/apps/details?id=com.xda.nachonotch&hl=en_US&gl=US" rel="noopener noreferrer" target="_blank">Nacho Notch</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Youtube直播转IPTV直播源</title>
      <link>https://blog.zjyl1994.com/post/iptv_proxy_v2/</link>
      <pubDate>Sun, 23 Aug 2020 19:17:23 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/iptv_proxy_v2/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;这篇是升级版！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;Youtube直播转IPTV直播源，基于Youtube-dl，我之前写过一个版本，那个版本因为是自己用的所以安装起来比较麻烦。&lt;/p&gt;
&lt;p&gt;水完博客断断续续到现在还有人过来找我问我怎么安装，所以我花了两个星期断断续续做了一下优化。&lt;/p&gt;
&lt;p&gt;现在这个工具被我命名为 &amp;ldquo;LiveTV!&amp;rdquo; 主要应用场景就是在电视或者其他支持M3U8的软件上直接播放Youtube的直播。&lt;/p&gt;
&lt;p&gt;考虑到国内的网络问题，我还增加了代理模式，让电视不用搞飞机也能看。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;LiveTV&#34; decoding=&#34;async&#34; height=&#34;1640&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/iptv_proxy_v2/index-zh.webp&#34; width=&#34;2343&#34;&gt;
&lt;/p&gt;
&lt;h1 id=&#34;安装教程&#34;&gt;安装教程&lt;/h1&gt;
&lt;p&gt;首先，你需要安装Docker。安装docker的镜像网上很多，如果你是 Ubuntu 之类较新的发行版可以直接使用apt安装。&lt;/p&gt;
&lt;p&gt;如果你在服务器上使用老版本的CentOS7或者Ubuntu14.xx，也可以搜索一下。Docker安装教学应该到处都是。&lt;/p&gt;
&lt;p&gt;以下内容假定你已经安装好了Docker。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>这篇是升级版！</p>
</blockquote>
<h1 id="前言">前言</h1>
<p>Youtube直播转IPTV直播源，基于Youtube-dl，我之前写过一个版本，那个版本因为是自己用的所以安装起来比较麻烦。</p>
<p>水完博客断断续续到现在还有人过来找我问我怎么安装，所以我花了两个星期断断续续做了一下优化。</p>
<p>现在这个工具被我命名为 &ldquo;LiveTV!&rdquo; 主要应用场景就是在电视或者其他支持M3U8的软件上直接播放Youtube的直播。</p>
<p>考虑到国内的网络问题，我还增加了代理模式，让电视不用搞飞机也能看。</p>
<p><img alt="LiveTV" decoding="async" height="1640" loading="eager" src="/post/iptv_proxy_v2/index-zh.webp" width="2343">
</p>
<h1 id="安装教程">安装教程</h1>
<p>首先，你需要安装Docker。安装docker的镜像网上很多，如果你是 Ubuntu 之类较新的发行版可以直接使用apt安装。</p>
<p>如果你在服务器上使用老版本的CentOS7或者Ubuntu14.xx，也可以搜索一下。Docker安装教学应该到处都是。</p>
<p>以下内容假定你已经安装好了Docker。</p>
<h2 id="正常安装">正常安装</h2>
<p>当你在可以直连Youtube的环境下安装时请参考这部分。（比如使用境外VPS或者你本身就在境外）</p>
<p>运行 <code>docker run -p外网端口:9000 -v宿主机目录:/root/data zjyl1994/livetv:1.0</code>，直接启动 LiveTV！</p>
<p>比如说我使用的命令就是 <code>docker run -d -p9500:9000 -v/mnt/data/livetv:/root/data zjyl1994/livetv:1.0</code>。</p>
<p>-p命令指定端口号是必须的，否则你将无法使用这个容器（或许你可以使用host模式开启这个容器，但是会占用9000端口）</p>
<p>-v指定了<code>/mnt/data/livetv</code>作为外部存储目录，这个主要用来存放LiveTV运行产生的数据库和日志文件。</p>
<p>如果不指定外部存储目录，LiveTV将在每次重启时丢失你添加的频道信息。</p>
<h2 id="代理模式安装">代理模式安装</h2>
<p>当你不能直接链接Youtube时，你需要首先有一个飞机。具体怎么弄自己查，此处假定你的飞机地址为 <code>socks5://192.168.1.1:10808</code>。</p>
<p>运行 <code>docker run -p9500:9000 -eHTTP_PROXY=socks5://192.168.1.1:10808 -eHTTPS_PROXY=socks5://192.168.1.1:10808 -v/mnt/d/workspace/livetv/data:/root/data zjyl1994/livetv:1.0</code></p>
<p>此时，LiveTV应该可以联网并正确缓存频道地址了。</p>
<h1 id="使用教程">使用教程</h1>
<p>考虑到不是所有的人都会写配置文件所以我搞了个非常简易的面板。</p>
<p>面板的访问地址是<code>http://你的外网ip:你设定的端口</code>，比如我自己的面板就是<code>http://192.168.1.100:9500</code>。</p>
<p><img alt="LiveTV" decoding="async" height="1640" loading="lazy" src="/post/iptv_proxy_v2/index-zh.webp" width="2343">
</p>
<p>这个面板还有英文版，不过考虑到能读到我这篇文章的读者应该都能懂中文，还是放中文版的图比较好。</p>
<p>使用起来很简单，必须要做的一步就是点击红色设定框中的“自动填充”按钮（也就是URL地址那列的按钮）</p>
<p>原因是设定M3U8需要获取程序运行的外网地址，做自动感知倒是也可以，但还是要给用户修改的权力，毕竟自动感知不可能非常完美。</p>
<p>点击“自动填充”按钮，等待URL地址中的地址发生变化，再点击“储存设定”就可以愉快的使用了。</p>
<p>在蓝色的“直播频道管理员”设定框中可以输入频道名称和Youtube的地址，点击“添加频道”，向LiveTV中增加新的托管频道。
如果你是在VPS或者其他设备上安装，想在电视等不支持飞机的设备上观看，请勾选“代理直播数据流”。</p>
<p>添加成功后下面的表格中会多出你新增的频道，此时复制M3U8那一列的URL丢进电视或者VLC之类的播放器就可以播放了。</p>
<p>如果你是使用Kodi或者其他软件，需要M3U文件，或者你只是单纯的懒得一个一个向电视添加直播源，
可以尝试使用第一行“M3U频道列表”中提供的M3U文件。</p>
<h1 id="备注">备注</h1>
<p>Youtube的链接每6小时失效一次，所以说我做了一个每隔4小时刷新所有频道youtube链接的功能。
Youtube-dl本身比较慢，不适合每次点击都现解析，所以缓存是必不可少的。如果你遭遇断流，可能就是链接失效了。
解决方法也很简单，只要切到别的台再切回来，让电视重新读取最新缓存到的地址即可。</p>
<p>感谢Youtube-dl这么伟大的项目，也祝你的IPTV看的愉快~</p>
<p>（友情提示，操作面板拉到底有一个Log链接，点过去可以查看程序的日志，如果遭遇500可能就是Youtube-dl无法解析，
多半是没有正确配置代理，可以自行排查。想要调节清晰度等请自行修改Youtube-dl的参数，默认是最高画质。）</p>]]></content:encoded>
    </item>
    <item>
      <title>记一次MySQL巨大表查询优化</title>
      <link>https://blog.zjyl1994.com/post/sqloptimize/</link>
      <pubDate>Wed, 01 Jul 2020 23:01:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/sqloptimize/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;今天上班折腾这个搞到晚上9点，记录下来免得后人踩坑&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;今天项目经理过来说我有一张TAPD单已经挂了一个月了，要我解决免得影响考核。我一看，是首页的数据报表接口速度过慢超时。&lt;/p&gt;
&lt;p&gt;这个问题很早就提给我了，那个时候因为其他任务过于着急只能先挂起。现在要过考核所以只能尽快解决。&lt;/p&gt;
&lt;h1 id=&#34;分析&#34;&gt;分析&lt;/h1&gt;
&lt;p&gt;首先查询代码中的数据表名称，前往正式环境的 MySQL 种查看，不看不知道一看吓一跳，2500万行单表，没有索引。
所以解决方法肯定是先加索引。这里我就被坑了，代码中使用了单独的函数生成 where 子句，我尝试把 where 子句中的值加入索引，结果没有效果。&lt;/p&gt;
&lt;p&gt;然后我就尝试打印最后生成的 SQL 语句。结果得到了这样的白痴语句 &lt;code&gt;WHERE a = ? AND b between ? AND ? AND c = ?&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;回想起来之前看过的面经提到，SQL的联合索引在范围检索的时候会慢很多，所以正确的办法应该把所有准确匹配的先放在最左边，MySQL 的联合索引会根据最左原则匹配使用索引。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>今天上班折腾这个搞到晚上9点，记录下来免得后人踩坑</p>
</blockquote>
<h1 id="前言">前言</h1>
<p>今天项目经理过来说我有一张TAPD单已经挂了一个月了，要我解决免得影响考核。我一看，是首页的数据报表接口速度过慢超时。</p>
<p>这个问题很早就提给我了，那个时候因为其他任务过于着急只能先挂起。现在要过考核所以只能尽快解决。</p>
<h1 id="分析">分析</h1>
<p>首先查询代码中的数据表名称，前往正式环境的 MySQL 种查看，不看不知道一看吓一跳，2500万行单表，没有索引。
所以解决方法肯定是先加索引。这里我就被坑了，代码中使用了单独的函数生成 where 子句，我尝试把 where 子句中的值加入索引，结果没有效果。</p>
<p>然后我就尝试打印最后生成的 SQL 语句。结果得到了这样的白痴语句 <code>WHERE a = ? AND b between ? AND ? AND c = ?</code>。</p>
<p>回想起来之前看过的面经提到，SQL的联合索引在范围检索的时候会慢很多，所以正确的办法应该把所有准确匹配的先放在最左边，MySQL 的联合索引会根据最左原则匹配使用索引。</p>
<h1 id="解决">解决</h1>
<p>首先，要调整代码中的 SQL 顺序，使准确匹配在最左侧。<code>WHERE a = ? AND c = ? AND b BETWEEN ? AND ?</code>。</p>
<p>然后，需要在数据库里增加索引 <code>ALTER TABLE table_name ADD INDEX index_name (a,c,b);</code></p>
<p>添加索引这个事，耗时足足4分钟。</p>
<h1 id="效果和总结">效果和总结</h1>
<p>优化前：18秒，远远超出平台规定的5秒限制，只能稍等一会重新请求，第二次会打到缓存上所以说一直这么对付着。</p>
<p>优化后：800ms，相比之前是飞一样的进步。</p>
<p>总结：MySQL的非破坏性大表优化就是加索引，加索引最重要的就是要检查是否会有条件打断你的索引，一旦索引条件被破坏，那么 MySQL 只能全力扫表。
扫表的话，2500万的表确实要扫一阵。我们的机器还是公司内部云的那种MySQL，底层有SSD加持，要是HDD怕是更烂。</p>
<p>当然如果数据表再大的话，就需要根据业务逻辑进行分表拆表了。倾向于根据业务逻辑选择进行分表，如果表还大的话，就需要根据时间进行分表了，那样要改动的逻辑就会很多了。</p>]]></content:encoded>
    </item>
    <item>
      <title>Youtube直播转IPTV源</title>
      <link>https://blog.zjyl1994.com/post/youtube-live-m3u8/</link>
      <pubDate>Sun, 03 May 2020 23:05:19 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/youtube-live-m3u8/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Youtube 是个大宝库！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;Youtube 直播上有很多好东西，比如各大电视台喜欢在油管上直播自己的新闻频道。&lt;/p&gt;
&lt;p&gt;比如海量台湾电视新闻台都在油管上搞了直播，我整理了一下这些。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#三立LIVE新聞HD直播
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://www.youtube.com/watch?v=4ZVUmEUFwaY
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#TVBS新聞 55 頻道
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://www.youtube.com/watch?v=Hu1FkdAOws0
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#東森財經新聞 57
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://www.youtube.com/watch?v=dphWo0r27Z4
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#東森新聞 51 頻道
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://www.youtube.com/watch?v=RaIJ767Bj_M
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#CTI中天新聞HD直播
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://www.youtube.com/watch?v=wUPPkSANpyo
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#中視新聞台 LIVE
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://www.youtube.com/watch?v=3OPNkiqD48g
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#民視新聞直播
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://www.youtube.com/watch?v=XxJKnDLYZz4
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#華視新聞HD
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://www.youtube.com/watch?v=TL8mmew3jb8
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>Youtube 是个大宝库！</p>
</blockquote>
<h1 id="前言">前言</h1>
<p>Youtube 直播上有很多好东西，比如各大电视台喜欢在油管上直播自己的新闻频道。</p>
<p>比如海量台湾电视新闻台都在油管上搞了直播，我整理了一下这些。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">#三立LIVE新聞HD直播
</span></span><span class="line"><span class="cl">https://www.youtube.com/watch?v=4ZVUmEUFwaY
</span></span><span class="line"><span class="cl">#TVBS新聞 55 頻道
</span></span><span class="line"><span class="cl">https://www.youtube.com/watch?v=Hu1FkdAOws0
</span></span><span class="line"><span class="cl">#東森財經新聞 57
</span></span><span class="line"><span class="cl">https://www.youtube.com/watch?v=dphWo0r27Z4
</span></span><span class="line"><span class="cl">#東森新聞 51 頻道
</span></span><span class="line"><span class="cl">https://www.youtube.com/watch?v=RaIJ767Bj_M
</span></span><span class="line"><span class="cl">#CTI中天新聞HD直播
</span></span><span class="line"><span class="cl">https://www.youtube.com/watch?v=wUPPkSANpyo
</span></span><span class="line"><span class="cl">#中視新聞台 LIVE
</span></span><span class="line"><span class="cl">https://www.youtube.com/watch?v=3OPNkiqD48g
</span></span><span class="line"><span class="cl">#民視新聞直播
</span></span><span class="line"><span class="cl">https://www.youtube.com/watch?v=XxJKnDLYZz4
</span></span><span class="line"><span class="cl">#華視新聞HD
</span></span><span class="line"><span class="cl">https://www.youtube.com/watch?v=TL8mmew3jb8
</span></span></code></pre></div><h1 id="解析">解析</h1>
<p>我曾经想过用 Golang 自己写一套 Youtube 解析算法，但是考虑到我可能没有精力随时维护解析算法，所以还是要用现成的第三方库来弄。</p>
<p>Github 有个非常出名的第三方 Youtube 工具，<a href="https://github.com/ytdl-org/youtube-dl" rel="noopener noreferrer" target="_blank">youtube-dl</a>。能在开源世界上拿到 65.6k 的 star 非常不容易，
也侧面证明了这个工具的靠谱程度。通过查看 <code>youtube-dl</code> 的帮助，可以知道 <code>youtube-dl -f best -g {url}</code> 能解析到 Youtube 的 M3U8。</p>
<p>在 Golang 中调用程序并获取输出内容，可以这么做：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">exec</span><span class="p">.</span><span class="nf">LookPath</span><span class="p">(</span><span class="s">&#34;youtube-dl&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">cancelFunc</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span><span class="w"> </span><span class="mi">10</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">defer</span><span class="w"> </span><span class="nf">cancelFunc</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">cmd</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">exec</span><span class="p">.</span><span class="nf">CommandContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;youtube-dl&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;-f&#34;</span><span class="p">,</span><span class="s">&#34;best&#34;</span><span class="p">,</span><span class="s">&#34;-g&#34;</span><span class="p">,</span><span class="s">&#34;${URL}&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">out</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">cmd</span><span class="p">.</span><span class="nf">CombinedOutput</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSpace</span><span class="p">(</span><span class="nb">string</span><span class="p">(</span><span class="nx">out</span><span class="p">)),</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>首先需要检查要调用的程序有没有，免得出问题。<code>exec.LookPath</code> 会检查能否找到程序，在 <code>err == nil</code> 时，证明可以调用到程序。
使用 <code>exec.Command</code> 指定调用的命令行，<code>CombineOutput</code> 会运行并输出程序运行内容。需要考虑的是，运行程序可能会卡住，尤其是
国内的神奇网络，如果没有扶墙，可能会无限卡下去。所以，必须增加超时。通过指定超时 <code>context.WithTimeout(context.Background(), 10*time.Second)</code>，
设定 10 秒的延迟。所以这个时候需要把 <code>exec.Command</code> 换成 <code>exec.CommandContext</code>，超时之后会自动取消。</p>
<h1 id="提供服务">提供服务</h1>
<p>Kodi 之类的 IPTV 客户端，都只能使用 M3U8 地址。所以需要一个 Go 程序提供 Youtube 直播地址转换到 M3U8 功能。用 Gin 可以非常容易的写一个 Web 服务。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">liveHandler</span><span class="p">(</span><span class="nx">c</span><span class="w"> </span><span class="o">*</span><span class="nx">gin</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">liveURL</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nf">Query</span><span class="p">(</span><span class="s">&#34;url&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">liveURL</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">c</span><span class="p">.</span><span class="nf">AbortWithStatus</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusNotFound</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">liveM3U8</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">getYoutubeLiveM3U8</span><span class="p">(</span><span class="nx">liveURL</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">c</span><span class="p">.</span><span class="nf">AbortWithStatus</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusInternalServerError</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">cfg</span><span class="p">.</span><span class="nx">ProxyStream</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">liveProxyM3U8</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">cfg</span><span class="p">.</span><span class="nx">BaseURL</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#34;/p/live.m3u8?url=&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">url</span><span class="p">.</span><span class="nf">QueryEscape</span><span class="p">(</span><span class="nx">liveM3U8</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">c</span><span class="p">.</span><span class="nf">Redirect</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusTemporaryRedirect</span><span class="p">,</span><span class="w"> </span><span class="nx">liveProxyM3U8</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">c</span><span class="p">.</span><span class="nf">Redirect</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusTemporaryRedirect</span><span class="p">,</span><span class="w"> </span><span class="nx">liveM3U8</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h2 id="代理直播流">代理直播流</h2>
<p>正常的油管 M3U8 在电视之类没扶墙的环境是不能播放的，所以代理是很有必要的。如何代理可以参看 RTHK 那篇博文。</p>
<p>这种代理就是简单的读取，转发。只是 M3U8 里面的连接也跟着替换一下，注意必须保持扩展名 <code>.m3u8</code> <code>.ts</code> ，
要不然 VLC 之类的会无法播放。</p>
<h2 id="优化加缓存">优化加缓存</h2>
<p>实际使用的时候，会发现一个很大的问题。比如 Kodi 换台的时候，首先会访问设定的地址，然后调用一次 <code>youtube-dl</code>，
解析个6-7秒返回结果，再加上 Kodi 需要去真实的直播源加载数据，每次换台需要卡顿 8-10s 左右。</p>
<p>考虑到频道列表是固定的，所以可以提前使用 <code>youtube-dl</code> 加载一下。要缓存数据，最好的方法是使用 redis 之类的工具。
不过为了这个简单的缓存引入 redis 实在不值当，所以直接使用内置类型 map 会很方便，高并发环境下需要使用 <code>sync.Map</code> 防止冲突 panic。</p>
<p>具体实现可以参考 <a href="https://github.com/zjyl1994/livetv/blob/master/m3u8cache.go" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/livetv/blob/master/m3u8cache.go</a> 文件。</p>
<h3 id="缓存规则">缓存规则</h3>
<p>在进行 Youtube 地址转换获取 M3U8 的时候，先行访问快取，如果命中快取就直接用快取中获取。获取不到再去用真实的 <code>youtube-dl</code> 获取连接。</p>
<h3 id="冷启动">冷启动</h3>
<p>当程序启动的时候，快取中没有任何数据，所以程序启动时需要逐个获取连接放入快取。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">loadChannelCache</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">channels</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">channelParser</span><span class="p">(</span><span class="nx">cfg</span><span class="p">.</span><span class="nx">ChannelFile</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">channels</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;caching&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">URL</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">liveURL</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">realGetYoutubeLiveM3U8</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">URL</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">urlCache</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">URL</span><span class="p">,</span><span class="w"> </span><span class="nx">liveURL</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">URL</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;cached&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="热更新">热更新</h3>
<p>Youtube 生成的 M3U8 中有过期时间，一般是6小时。如果想要保持随时可看的状态，就得经常更新。
编写一个更新函数，从频道定义文件里获取 Youtube 链接，然后更新一下所有频道的 M3U8。
然后使用 <code>sync.Map.Range</code> 遍历所有的链接，清理掉过期的链接，只要2-3小时抓取一次即可。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">updateURLCache</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">channels</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">channelParser</span><span class="p">(</span><span class="nx">cfg</span><span class="p">.</span><span class="nx">ChannelFile</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">channels</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;caching&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">URL</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">liveURL</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">realGetYoutubeLiveM3U8</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">URL</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">urlCache</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">URL</span><span class="p">,</span><span class="w"> </span><span class="nx">liveURL</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">URL</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;cached&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">urlCache</span><span class="p">.</span><span class="nf">Range</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">k</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="kd">interface</span><span class="p">{})</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">value</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">v</span><span class="p">.(</span><span class="kt">string</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">regex</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">regexp</span><span class="p">.</span><span class="nf">MustCompile</span><span class="p">(</span><span class="s">`/expire/(\d+)/`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">matched</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">regex</span><span class="p">.</span><span class="nf">FindStringSubmatch</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">matched</span><span class="p">)</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">urlCache</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="nx">k</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">expireTime</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nf">Unix</span><span class="p">(</span><span class="nf">string2Int64</span><span class="p">(</span><span class="nx">matched</span><span class="p">[</span><span class="mi">1</span><span class="p">]),</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">().</span><span class="nf">After</span><span class="p">(</span><span class="nx">expireTime</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">urlCache</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="nx">k</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h1 id="安装和使用">安装和使用</h1>
<p>本次的程序使用多配置文件模式，具体可以参考 <a href="https://github.com/zjyl1994/livetv/blob/master/README-zh.md" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/livetv/blob/master/README-zh.md</a>。</p>
<p>这次折腾完了，就可以用 Kodi 看香港台，台湾台，等等各种直播频道，配置好之后就享受观影吧。</p>]]></content:encoded>
    </item>
    <item>
      <title>TVProxy 开发笔记</title>
      <link>https://blog.zjyl1994.com/post/iptv_proxy/</link>
      <pubDate>Tue, 28 Apr 2020 21:06:43 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/iptv_proxy/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;花了两整天时间搞了一个Kodi适用的香港电视直播代理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这篇还是IPTV相关的东西，香港电视台的直播解析可以看前两篇博客，分别写了 RTHK 31/32 和 TVB 无线新闻 / 财经资讯台 的解析方法。&lt;/p&gt;
&lt;p&gt;众所周知 Kodi 是一个集成式的播放平台，本次的主要目的就是改造 TVProxy 让它可以支持 Kodi 的 &lt;code&gt;PVR IPTV Simple Client&lt;/code&gt;，并且准备两种安装方法的必须文件。&lt;/p&gt;
&lt;p&gt;首先，Kodi 的 IPTV 插件需要自行安装。然后可以看到，PVR 插件需要一个M3U文件。在谷歌搜索M3U可以找到大量现成的M3U文件，可以参考他们的格式。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>花了两整天时间搞了一个Kodi适用的香港电视直播代理。</p>
</blockquote>
<p>这篇还是IPTV相关的东西，香港电视台的直播解析可以看前两篇博客，分别写了 RTHK 31/32 和 TVB 无线新闻 / 财经资讯台 的解析方法。</p>
<p>众所周知 Kodi 是一个集成式的播放平台，本次的主要目的就是改造 TVProxy 让它可以支持 Kodi 的 <code>PVR IPTV Simple Client</code>，并且准备两种安装方法的必须文件。</p>
<p>首先，Kodi 的 IPTV 插件需要自行安装。然后可以看到，PVR 插件需要一个M3U文件。在谷歌搜索M3U可以找到大量现成的M3U文件，可以参考他们的格式。</p>
<p>在这里我就直接给出正确格式了：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">#EXTM3U
</span></span><span class="line"><span class="cl">#EXTINF:-1, 电视台名称
</span></span><span class="line"><span class="cl">http://example.com/iptv.m3u8
</span></span></code></pre></div><p>有多个电视台的情况下不停重复2-3行即可。由此可以看出，可以直接让 TVProxy 输出一个预制的M3U，毕竟我们只有写死的四个台。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">m3uHandler</span><span class="p">(</span><span class="nx">c</span><span class="w"> </span><span class="o">*</span><span class="nx">gin</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">m3u</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">`#EXTM3U
</span></span></span><span class="line"><span class="cl"><span class="s">#EXTINF:-1,無綫新聞台
</span></span></span><span class="line"><span class="cl"><span class="s">${baseURL}tvb/inews.m3u8
</span></span></span><span class="line"><span class="cl"><span class="s">#EXTINF:-1,無綫財經資訊台
</span></span></span><span class="line"><span class="cl"><span class="s">${baseURL}tvb/finance.m3u8
</span></span></span><span class="line"><span class="cl"><span class="s">#EXTINF:-1,RTHK 31
</span></span></span><span class="line"><span class="cl"><span class="s">${baseURL}rthk/31.m3u8
</span></span></span><span class="line"><span class="cl"><span class="s">#EXTINF:-1,RTHK 32
</span></span></span><span class="line"><span class="cl"><span class="s">${baseURL}rthk/32.m3u8
</span></span></span><span class="line"><span class="cl"><span class="s">`</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">processedBody</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">ReplaceAll</span><span class="p">(</span><span class="nx">m3u</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;${baseURL}&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">baseURL</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">c</span><span class="p">.</span><span class="nf">Data</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;application/vnd.apple.mpegurl&#34;</span><span class="p">,</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">processedBody</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>让我们启动这个程序，然后在 PVR 中填上 <code>http://127.0.0.1:10086/iptv.m3u</code>，重启 Kodi 后就可以看到对应的频道清单了。</p>
<p><img alt="PVR设置" decoding="async" height="906" loading="eager" src="/post/iptv_proxy/pvr_setting.webp" width="1820">
</p>
<p><img alt="频道列表" decoding="async" height="1080" loading="lazy" src="/post/iptv_proxy/channel_select.webp" width="1920">
</p>
<p><img alt="TVB直播" decoding="async" height="1080" loading="lazy" src="/post/iptv_proxy/tvb_live.webp" width="1920">
</p>
<p>现在就可以舒服的在Kodi上看港台了。</p>
<p>如果你是 HTPC/服务器 可能需要考虑开机自启动的问题。虽然我写了Docker文件，但是不推荐在本地环境适用，仅适用于服务器环境。
注意，RTHK由于被墙所以采用了直接转发，如果部署在服务器上会很消耗流量。请注意自行添加防护手段免得被人爬走导致流量费用爆炸。
Docker部分我就不说了，注意以下参数：</p>
<table>
	<thead>
			<tr>
					<th>环境变量</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>TVPROXY_LISTEN</td>
					<td>决定程序监听哪一个地址上的哪个端口，默认值为&quot;127.0.0.1:10086&quot;</td>
			</tr>
			<tr>
					<td>TVPROXY_BASE_URL</td>
					<td>决定程序对外服务的地址前缀，默认值为&quot;http://127.0.0.1:10086/&quot;</td>
			</tr>
	</tbody>
</table>
<p>TVProxy受<code>HTTP_PROXY</code>这个环境变量影响，所以在本地使用时如果想给程序挂代理直接指定<code>HTTP_PROXY</code>的值即可。</p>
<p>说一下我自己的部署方案：</p>
<p>我有一台类似NUC的无风扇主机，安装Manjaro+Kodi作为下载机和电视。所以TVProxy就装在这台主机上，通过指定<code>HTTP_PROXY</code>走本地的HTTP代理出境。</p>
<p>程序管理使用的是systemd，具体可以查看项目中的service文件。最后，如果你觉得好用请给star，<a href="https://github.com/zjyl1994/tvproxy" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/tvproxy</a> 。
作为个人项目来说，暂时不会接受任何功能需求的issue，要是这些直播源挂了的话倒是可以开issue让我修修，当然不能保证100%可修复，直播源这个东西谁都没有准。</p>
<p>提前祝你观影愉快，尽情享受 IPTV 吧。</p>]]></content:encoded>
    </item>
    <item>
      <title>RTHK直播抓取笔记</title>
      <link>https://blog.zjyl1994.com/post/rthk_m3u8/</link>
      <pubDate>Tue, 28 Apr 2020 17:31:56 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/rthk_m3u8/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;网上提供直播源的香港电视台除了TVB还有RTHK。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;RTHK 是香港唯一的公营电视机构，有31和32两个免费电视台，而且最重要的是他们都提供了线上直播。&lt;/p&gt;
&lt;p&gt;官方的播放地址是 &lt;a href=&#34;https://www.rthk.hk/tv&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;https://www.rthk.hk/tv&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;在线播放截图&#34; decoding=&#34;async&#34; height=&#34;598&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/rthk_m3u8/rthk_live.webp&#34; width=&#34;1012&#34;&gt;
&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>网上提供直播源的香港电视台除了TVB还有RTHK。</p>
</blockquote>
<p>RTHK 是香港唯一的公营电视机构，有31和32两个免费电视台，而且最重要的是他们都提供了线上直播。</p>
<p>官方的播放地址是 <a href="https://www.rthk.hk/tv" rel="noopener noreferrer" target="_blank">https://www.rthk.hk/tv</a></p>
<p><img alt="在线播放截图" decoding="async" height="598" loading="eager" src="/post/rthk_m3u8/rthk_live.webp" width="1012">
</p>
<p>按下 <kbd>F12</kbd> 可以看到视频使用的是jwplayer播放，指向一个内存blob。</p>
<p><img alt="F12结果" decoding="async" height="618" loading="lazy" src="/post/rthk_m3u8/jwplayer.webp" width="1394">
</p>
<p>所以，我们需要转去 Network 标签查看所有网络流量。</p>
<p><img alt="找到M3U8" decoding="async" height="601" loading="lazy" src="/post/rthk_m3u8/network_get_m3u8.webp" width="1353">
</p>
<p>尝试直接访问以下：<code>curl -s https://www.rthk.hk/feeds/dtt/rthktv31_https.m3u8</code></p>
<p>会得到如下输出：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">#EXTM3U
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=344000,RESOLUTION=256x144,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_216_av-p.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=344000,RESOLUTION=256x144,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_216_av-b.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=560000,RESOLUTION=432x240,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_432_av-p.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=560000,RESOLUTION=432x240,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_432_av-b.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=938000,RESOLUTION=640x360,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_810_av-p.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=938000,RESOLUTION=640x360,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_810_av-b.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1424000,RESOLUTION=848x480,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_1296_av-p.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1424000,RESOLUTION=848x480,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_1296_av-b.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2180000,RESOLUTION=1280x720,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_2052_av-p.m3u8?sd=10&amp;rebase=on
</span></span><span class="line"><span class="cl">#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2180000,RESOLUTION=1280x720,CODECS=&#34;avc1.66.30, mp4a.40.2&#34;
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_2052_av-b.m3u8?sd=10&amp;rebase=on
</span></span></code></pre></div><p>当然，你直接拿着这个想像TVB那样直接302是不行的。因为 <code>akamaihd.net</code> 整个在中国大陆不可用，所以说需要代理一下。</p>
<p>这就涉及到了HLS的基本知识，有需要的可以去了解以下。这里需要知道的是，M3U8只是播放清单，真正负责播放的是ts文件。所以代理分为两步，获取真实M3U8内容，然后代理ts文件。</p>
<p>刚才我们获取到的M3U8文件是MasterPlaylist，负责提供多种解析度的。在这里我们选择清晰度最高的 <code>https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/index_2052_av-p.m3u8?sd=10&amp;rebase=on</code> 进行代理。(根据搜索，这个地址早在18年就有人解析出来了，一直没有变，可见十分稳定。只不过因为akamaihd全线被墙，不能播放)</p>
<p>利用curl可以查看到以下内容:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">#EXTM3U
</span></span><span class="line"><span class="cl">#EXT-X-TARGETDURATION:10
</span></span><span class="line"><span class="cl">#EXT-X-ALLOW-CACHE:YES
</span></span><span class="line"><span class="cl">#EXT-X-VERSION:3
</span></span><span class="line"><span class="cl">#EXT-X-MEDIA-SEQUENCE:20055342
</span></span><span class="line"><span class="cl">#EXTINF:10.000,
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/segment20055342_2052_av-p.ts?sd=10
</span></span><span class="line"><span class="cl">#EXTINF:10.000,
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/segment20055343_2052_av-p.ts?sd=10
</span></span><span class="line"><span class="cl">#EXTINF:10.000,
</span></span><span class="line"><span class="cl">https://rthklive1-lh.akamaihd.net/i/rthk31_1@167495/segment20055344_2052_av-p.ts?sd=10
</span></span></code></pre></div><p>我们要做的就是去代理ts文件，并且把M3U8中的地址换成我们自己的代理地址。</p>
<p>首先需要搞一个ts代理，很简单。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">tsProxyHandler</span><span class="p">(</span><span class="nx">c</span><span class="w"> </span><span class="o">*</span><span class="nx">gin</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">remoteUrl</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nf">Query</span><span class="p">(</span><span class="s">&#34;url&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">resp</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">remoteUrl</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">c</span><span class="p">.</span><span class="nf">AbortWithError</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">defer</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">Body</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">c</span><span class="p">.</span><span class="nf">DataFromReader</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">ContentLength</span><span class="p">,</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">),</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">Body</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>就是简单的获取参数，然后去读取数据，记住要原样传递 <code>content-type</code>，正确传输body，其他不重要。</p>
<p>然后，我们来编写M3U8的代理。
M3U8文件是纯文本文件，所有控制信息以 <code>#</code> 开头，所以可以写一个逐行操作的程序，替换掉非 <code>#</code> 开头的URL。
基本代码如下:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">m3u8ProxyHandler</span><span class="p">(</span><span class="nx">m3u8url</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="w"> </span><span class="o">*</span><span class="nx">gin</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">resp</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">getHttpClient</span><span class="p">().</span><span class="nf">Get</span><span class="p">(</span><span class="nx">m3u8url</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">c</span><span class="p">.</span><span class="nf">AbortWithError</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">defer</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">Body</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">bodyBytes</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">ioutil</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">resp</span><span class="p">.</span><span class="nx">Body</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">c</span><span class="p">.</span><span class="nf">AbortWithError</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">bodyString</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">string</span><span class="p">(</span><span class="nx">bodyBytes</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">processedBody</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">m3u8Proc</span><span class="p">(</span><span class="nx">bodyString</span><span class="p">,</span><span class="w"> </span><span class="nx">baseURL</span><span class="o">+</span><span class="s">&#34;i.ts?url=&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">c</span><span class="p">.</span><span class="nf">Data</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">),</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">processedBody</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">m3u8Proc</span><span class="p">(</span><span class="nx">data</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">prefixUrl</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">sb</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nx">Builder</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">scanner</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">bufio</span><span class="p">.</span><span class="nf">NewScanner</span><span class="p">(</span><span class="nx">strings</span><span class="p">.</span><span class="nf">NewReader</span><span class="p">(</span><span class="nx">data</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">scanner</span><span class="p">.</span><span class="nf">Scan</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">l</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">scanner</span><span class="p">.</span><span class="nf">Text</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">l</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;#&#34;</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">l</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">prefixUrl</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nf">QueryEscape</span><span class="p">(</span><span class="nx">l</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;\n&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">String</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>然后就是很简单的组装环节了，要注意的是，VLC 播放器非常吃后缀名这个设定。所以TS文件必须<code>.ts</code>结尾。要不然他会在日志里默默的打上一句&quot;Nothing to play&quot;，然后装傻不去播放。</p>
<p>最后的效果如下：</p>
<p><img alt="VLC播放RTHK32" decoding="async" height="587" loading="lazy" src="/post/rthk_m3u8/play_rthk32.webp" width="855">
</p>
<p>相关的代理我已经封装成库了，可以参看 <a href="https://github.com/zjyl1994/tvproxy" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/tvproxy</a> ,觉得好用给个star啦。</p>]]></content:encoded>
    </item>
    <item>
      <title>TVB直播抓取笔记</title>
      <link>https://blog.zjyl1994.com/post/tvb_m3u8/</link>
      <pubDate>Mon, 27 Apr 2020 16:24:56 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/tvb_m3u8/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;找工作期间闲的无聊想看电视，买了一部NUC作为播放设备，但是没有合适的电视直播源，只能自己动手了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TVB无线新闻台 的直播地址是 &lt;a href=&#34;http://news.tvb.com/live/inews&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;http://news.tvb.com/live/inews&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;TVB无线财经资讯台 的地址是 &lt;a href=&#34;http://news.tvb.com/live/j5_ch85&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;http://news.tvb.com/live/j5_ch85&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;官方直播界面&#34; decoding=&#34;async&#34; height=&#34;624&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/tvb_m3u8/tvb_live.webp&#34; width=&#34;651&#34;&gt;
&lt;/p&gt;
&lt;p&gt;当然这两个直接用网页打开就能看，但是我想在 Kodi 上播放，所以还要处理一下。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>找工作期间闲的无聊想看电视，买了一部NUC作为播放设备，但是没有合适的电视直播源，只能自己动手了。</p>
</blockquote>
<p>TVB无线新闻台 的直播地址是 <a href="http://news.tvb.com/live/inews" rel="noopener noreferrer" target="_blank">http://news.tvb.com/live/inews</a></p>
<p>TVB无线财经资讯台 的地址是 <a href="http://news.tvb.com/live/j5_ch85" rel="noopener noreferrer" target="_blank">http://news.tvb.com/live/j5_ch85</a></p>
<p><img alt="官方直播界面" decoding="async" height="624" loading="eager" src="/post/tvb_m3u8/tvb_live.webp" width="651">
</p>
<p>当然这两个直接用网页打开就能看，但是我想在 Kodi 上播放，所以还要处理一下。</p>
<p>先检查一下源码，一般来说直播都是M3U8格式的，所以在源码里搜以下m3u8可能会有惊喜。</p>
<p><img alt="获取直播地址" decoding="async" height="512" loading="lazy" src="/post/tvb_m3u8/get_source.webp" width="979">
</p>
<p>可以看见，TVB的地址基本没有做特殊处理，只是带了一些控制参数，比如exp看起来就像是过期时间，hmac可能是签名，防止你自己生成这个url的。如果直接使用这个地址进行播放，可能过一阵就会打不开了，因为他们做了过期处理。</p>
<p>所以就需要写一个程序专门提取这个地址，我选择了最熟悉的 Golang。</p>
<p>可以看出来，最重要的URL部分被包裹在 <code>&lt;source src=&quot;</code> 和 <code>&quot; type=&quot;application/x-mpegURL&quot;&gt;</code> 中，所以我们可以写一个正则来直接提取这个URL。</p>
<p>随手写了一个正则:<code>(?&lt;=&lt;source src=).*?(?=type=&quot;application/x-mpegURL&quot;&gt;)</code></p>
<p>让我们测试一下:</p>
<p><img alt="正则测试" decoding="async" height="492" loading="lazy" src="/post/tvb_m3u8/regex.webp" width="678">
</p>
<p>拿到对应的播放URL之后就很简单了，只要起一个HTTP服务器，收到播放请求的时候，用302把播放器跳去官方的播放CDN即可。中间不需要你服务器做什么转发视频流的操作，基本消耗就是一次几KB？</p>
<p>Golang版代码如下:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">parseTVB</span><span class="p">(</span><span class="nx">liveName</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">resp</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;http://news.tvb.com/live/&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">liveName</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">defer</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">Body</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">bBody</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">ioutil</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">resp</span><span class="p">.</span><span class="nx">Body</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">m3u8Regexp</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">regexp</span><span class="p">.</span><span class="nf">MustCompile</span><span class="p">(</span><span class="s">`&lt;source src=&#34;(.*?)&#34; type=&#34;application/x-mpegURL&#34;&gt;`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">match</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">m3u8Regexp</span><span class="p">.</span><span class="nf">FindStringSubmatch</span><span class="p">(</span><span class="nb">string</span><span class="p">(</span><span class="nx">bBody</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">match</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">match</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>这是最基本的 HTTP GET 请求 + 正则运用，没有什么好说的。</p>
<p>经过简单的包装，(代码可以看: <a href="https://github.com/zjyl1994/tvproxy" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/tvproxy</a> )</p>
<p>我们来测试一下:</p>
<p><img alt="VLC播放测试" decoding="async" height="1049" loading="lazy" src="/post/tvb_m3u8/m3u8_test.webp" width="1920">
</p>
<p>可以看到播放的很好。</p>
<p>可惜 <code>akamaihd.net</code> 被墙了，要不然还可以搞一个 RTHK 的直播源。</p>]]></content:encoded>
    </item>
    <item>
      <title>KaiAuth 开发笔记3</title>
      <link>https://blog.zjyl1994.com/post/kai-auth3/</link>
      <pubDate>Wed, 01 Apr 2020 22:40:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/kai-auth3/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;最近升级了一下 KaiAuth，目前版本 v1.1.1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;就在前几天，偶然闲逛 Reddit，发现有外国人也很需要 KaiOS 上的 TOTP 工具。我就推荐了我的 KaiAuth ，
结果外国人并不知道如何OmniSD或者用WebIDE安装这个。我思考了一下，还是需要提交到官方商店。&lt;/p&gt;
&lt;p&gt;考虑到 KaiAuth 早期版都是使用暗码开启高级功能的（其实是我懒得搞菜单），对于上架到官方商店来说，
我不能保证所有用户都会玩暗码这套，所以一个正常一些的菜单还是很重要的。&lt;/p&gt;
&lt;h1 id=&#34;改造&#34;&gt;改造&lt;/h1&gt;
&lt;p&gt;先看看改造后的结果：&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;app_in_launcher&#34; decoding=&#34;async&#34; height=&#34;320&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/kai-auth3/app_in_launcher.webp&#34; width=&#34;240&#34;&gt;

&lt;img alt=&#34;main_interface&#34; decoding=&#34;async&#34; height=&#34;320&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/kai-auth3/main_interface.webp&#34; width=&#34;240&#34;&gt;
&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;scan_qrcode&#34; decoding=&#34;async&#34; height=&#34;320&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/kai-auth3/scan_qrcode.webp&#34; width=&#34;240&#34;&gt;

&lt;img alt=&#34;option_menu&#34; decoding=&#34;async&#34; height=&#34;320&#34; loading=&#34;lazy&#34; src=&#34;https://blog.zjyl1994.com/post/kai-auth3/option_menu.webp&#34; width=&#34;240&#34;&gt;
&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>最近升级了一下 KaiAuth，目前版本 v1.1.1</p>
</blockquote>
<p>就在前几天，偶然闲逛 Reddit，发现有外国人也很需要 KaiOS 上的 TOTP 工具。我就推荐了我的 KaiAuth ，
结果外国人并不知道如何OmniSD或者用WebIDE安装这个。我思考了一下，还是需要提交到官方商店。</p>
<p>考虑到 KaiAuth 早期版都是使用暗码开启高级功能的（其实是我懒得搞菜单），对于上架到官方商店来说，
我不能保证所有用户都会玩暗码这套，所以一个正常一些的菜单还是很重要的。</p>
<h1 id="改造">改造</h1>
<p>先看看改造后的结果：</p>
<p><img alt="app_in_launcher" decoding="async" height="320" loading="eager" src="/post/kai-auth3/app_in_launcher.webp" width="240">

<img alt="main_interface" decoding="async" height="320" loading="lazy" src="/post/kai-auth3/main_interface.webp" width="240">
</p>
<p><img alt="scan_qrcode" decoding="async" height="320" loading="lazy" src="/post/kai-auth3/scan_qrcode.webp" width="240">

<img alt="option_menu" decoding="async" height="320" loading="lazy" src="/post/kai-auth3/option_menu.webp" width="240">
</p>
<h2 id="图标改造">图标改造</h2>
<p>首先，2FA是一个非常大的概念，包括短信验证码、实体安全密钥之类的验证方式。所以说图标上用2FA不太合适，
对于我这个应用来说，TOTP 是非常合适的。所以用 Aesprite 重新画一下图标，改成 TOTP 就好了。</p>
<p>完成品是这样的：</p>
<p><img alt="Icon 56px" decoding="async" height="56" loading="lazy" src="/post/kai-auth3/app_56.webp" width="56">

<img alt="Icon 112px" decoding="async" height="112" loading="lazy" src="/post/kai-auth3/app_112.webp" width="112">
</p>
<h2 id="菜单改造">菜单改造</h2>
<p>考虑到官方没有提供 UI 库，菜单的实现需要自己用 CSS 画。最简单易行的方案应该是用 <code>web activities</code> 方式，
单独开启一个页面。这样页面滚动逻辑可以复用主界面的滚动逻辑。</p>
<p>菜单中提取出来的功能有这些：</p>
<ul>
<li>删除（原先的右软键）</li>
<li>修改名字（新增功能）</li>
<li>查看密钥（新增功能）</li>
<li>从SD卡中导入（旧有*#467678#）</li>
<li>导出到SD卡（旧有*#397678#）</li>
<li>清除现有数据（旧有*#7370#）</li>
</ul>
<p>要注意的是，删除，修改名字，查看密钥在没有选中的代码项时应该禁用。</p>
<h3 id="activity-值传递">Activity 值传递</h3>
<p>这里面用到一个新的功能，<code>MozActivity</code> 的 data 传递。经过查阅代码，我发现了正确用法。</p>
<p>下面是我程序中提取的例子：</p>
<p>调用方：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">menu</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MozActivity</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;com.zjyl1994.kaiauth.Menu&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">data</span><span class="o">:</span> <span class="p">{</span><span class="nx">activeId</span><span class="o">:</span><span class="nx">authcodeActiveItem</span><span class="p">}</span> <span class="c1">// activeId 是你要传递的数据
</span></span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></div><p>接收方：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">navigator</span><span class="p">.</span><span class="nx">mozSetMessageHandler</span><span class="p">(</span><span class="s1">&#39;activity&#39;</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">activityRequest</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">activityHandler</span> <span class="o">=</span> <span class="nx">activityRequest</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">activeId</span> <span class="o">=</span> <span class="nx">activityRequest</span><span class="p">.</span><span class="nx">source</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">activeId</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// activeId 就是传递过来的数据
</span></span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>简单的来说，调用方传递的 <code>data</code> 对象，就是接收方的 <code>activityRequest.source.data</code> 对象。</p>
<h3 id="禁用某些项">禁用某些项</h3>
<p>禁用某些项很简单，只要确保你的这些禁用项靠在一起，然后让按键滚动的时候滚不上去就好了。</p>
<p>具体可以参考 <code>menu.js</code> 中的实现。<a href="https://github.com/zjyl1994/KaiAuth/blob/master/js/menu.js" rel="noopener noreferrer" target="_blank">代码在这里</a></p>
<h2 id="内建功能">内建功能</h2>
<p>我很庆幸 KaiOS 实现了基本的 js 对话框。简单的文字输入框，询问对话框和信息对话框都不需要自己来写了。
而且这些对话框都可以暂停住js的流程等待对话框，比自己实现的方便好用很多。个人建议，没有特殊需求的话，
用它自带这些就可以了。</p>
<h3 id="alert">alert()</h3>
<p>alert 是 js 提供的一个标准的消息对话框。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">alert</span><span class="p">(</span><span class="s1">&#39;在这写消息&#39;</span><span class="p">);</span>
</span></span></code></pre></div><h3 id="confirm">confirm()</h3>
<p>confirm 会弹出一个对话框，询问你提供的文本，在2720上是左软键取消，右软键确认。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">confirm</span><span class="p">(</span><span class="s1">&#39;询问文本&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span><span class="p">(</span><span class="nx">result</span> <span class="o">==</span> <span class="kc">true</span><span class="p">){</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 当用户按下确认的时候执行这里
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="k">else</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 当用户按下取消的时候执行这里
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="prompt">prompt()</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">prompt</span><span class="p">(</span><span class="s1">&#39;提示文本&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="o">!=</span><span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="nx">result</span><span class="o">!=</span><span class="s2">&#34;&#34;</span><span class="p">){</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 当用户输入了非空消息时候执行这里
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="多语言改造">多语言改造</h2>
<p>本次更新，我加了多语言支持。目前KaiAuth支持英文，简体中文，正体中文和广东话。
要注意的是，正体中文并不是简单的把简体中文做繁简转换就行的。这个需要多读正体中文的科技文章，
学习一下正体中文的惯用词。要做多语言一定要下功夫用心来做，要不然不如不做。</p>
<p>同样一个TF卡，简中环境喜欢叫<code>内存卡</code>，正体中文环境喜欢叫<code>記憶卡</code>,廣東話環境喜歡叫<code>記憶咭</code>。</p>
<p>简中环境叫<code>导入文件</code> <code>导出文件</code> 正体中文就叫 <code>載入檔案</code> <code>匯出檔案</code>。</p>
<p>語言這個東西還是很神奇的，要多學習。</p>
<h1 id="提交商店">提交商店</h1>
<p>KaiOS 官方商店提交需要看一下 <a href="https://developer.kaiostech.com/submit-to-kaistore" rel="noopener noreferrer" target="_blank">官方提交文档</a>。
按照要求改造 <code>manifest.webapp</code> 既可。</p>
<p>都做完之后登录 <a href="https://developer.kaiostech.com/devlogin" rel="noopener noreferrer" target="_blank">Submission Portal</a> ,开始提交。
（神奇的是，这个页面不可以用之前注册的kaios防盗账户登录。所以我就重新注册了一个kaios账号。）</p>
<p><img alt="submit_app" decoding="async" height="338" loading="lazy" src="/post/kai-auth3/submit_app.webp" width="1162">
</p>
<p>点击 <code>New Packaged App</code> ，上传 Web IDE 版的应用。然后点击 <code>Test Device</code>，输入你测试设备的 IMEI 代码。
等五六分钟后你测试机的商店里就能搜到你上传的应用了。（当然这个时候别人还是看不到的，要通过审核之后才能显示出来）</p>
<p>我的应用已经提交到了官方商店，四天了，还在审核中（3/27~4/1）。只能说他们效率堪忧。</p>]]></content:encoded>
    </item>
    <item>
      <title>KaiDeploy 开发笔记</title>
      <link>https://blog.zjyl1994.com/post/kaideploy/</link>
      <pubDate>Sun, 22 Mar 2020 23:31:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/kaideploy/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;折腾 KaiOS 的我又搞了一个没什么人会用到的小工具&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;引子&#34;&gt;引子&lt;/h1&gt;
&lt;p&gt;最近折腾完 KaiAuth，就在 KaiOS 的交流群里和他们吹水。总能发现小白玩家连基本的操作都不懂却被迫折腾 adb，然后和 Palemoon 之类的过时火狐战斗，才能把应用装上。（有大把的人连 adb 都找不到）&lt;/p&gt;
&lt;p&gt;Kai OS 脱胎与 Firefox OS，应用要用当年 Firefox 的 Web IDE 才能 sideload 到手机。由于 Mozilla 已经在 2016 年停止开发 Firefox OS， 新版本 Firefox 逐渐拿掉了自己不需要的 Web IDE，这却给我们这些 Kai OS用户造成蛮大的困扰。为了能随时 sideload 新的 app 进手机，必须保留最后一代有 Web IDE 的浏览器。这很不方便，一不小心就自动更新了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>折腾 KaiOS 的我又搞了一个没什么人会用到的小工具</p>
</blockquote>
<h1 id="引子">引子</h1>
<p>最近折腾完 KaiAuth，就在 KaiOS 的交流群里和他们吹水。总能发现小白玩家连基本的操作都不懂却被迫折腾 adb，然后和 Palemoon 之类的过时火狐战斗，才能把应用装上。（有大把的人连 adb 都找不到）</p>
<p>Kai OS 脱胎与 Firefox OS，应用要用当年 Firefox 的 Web IDE 才能 sideload 到手机。由于 Mozilla 已经在 2016 年停止开发 Firefox OS， 新版本 Firefox 逐渐拿掉了自己不需要的 Web IDE，这却给我们这些 Kai OS用户造成蛮大的困扰。为了能随时 sideload 新的 app 进手机，必须保留最后一代有 Web IDE 的浏览器。这很不方便，一不小心就自动更新了。</p>
<h1 id="折腾">折腾</h1>
<h2 id="破解协议">破解协议</h2>
<p>首先，Web IDE 是用 ADB 导出的调试端口进行通讯的，标准端口就是<code>localhost:6000</code>，这是一个很大的突破点。</p>
<p>为了抓到中间的通讯内容，我们需要一个转发器，并且能记录所有中间流过的数据。我选择了 sokit ，毕竟这种东西没必要自己写。</p>
<p>打开 sokit 的转发功能，打开记录到日志，这时候形成了一个 <code>Web IDE &lt;-tcp-&gt; 转发器 &lt;-tcp-&gt; ADB &lt;-USB电缆-&gt; 手机</code> 的连接隧道。</p>
<p>打开 Web IDE ，我们会抓到非常多的数据包，不利于解析。所以我选择了一个更取巧的办法。</p>
<p>Kai OS 曾经有过一款部署工具，叫做 <code>kdeploy</code>，不知道为什么被官方删除了。<a href="https://gitlab.com/suborg" rel="noopener noreferrer" target="_blank">Lux Ferre</a> 大佬开发了一个替代品，<a href="https://gitlab.com/suborg/gdeploy" rel="noopener noreferrer" target="_blank">gdeploy</a>。可以直接通过命令行抓包推送软件包到手机。</p>
<p>现在，通讯隧道变成了  <code>gdeploy &lt;-tcp-&gt; 转发器 &lt;-tcp-&gt; ADB &lt;-USB电缆-&gt; 手机</code> 。完成一次通讯就可以抓到协议报文了。</p>
<h2 id="协议分析">协议分析</h2>
<p>报文太长了，我就不贴在这里了，有兴趣的可以自己抓一下。我这里直接说一下结论，如果你有一定的基础看懂那个报文很简单。</p>
<p>协议体是这样的：<code>json长度:json体</code> (来回的信息都是这样的，最简单的例子就是这样的 <code>2:{}</code>)</p>
<p>首先，打开 TCP 端口，手机会主动上报一条消息，内容是操作系统支持的调试功能。这个对部署应用没有什么意义，可以忽略。</p>
<p>然后，<code>gdeploy</code> 发送：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">31</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;to&#34;</span><span class="p">:</span><span class="s2">&#34;root&#34;</span><span class="p">,</span><span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;listTabs&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>得到手机响应：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">452</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;from&#34;</span><span class="p">:</span><span class="s2">&#34;root&#34;</span><span class="p">,</span><span class="nt">&#34;selected&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span><span class="nt">&#34;tabs&#34;</span><span class="p">:[],</span><span class="nt">&#34;memprofActor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.memprofActor1&#34;</span><span class="p">,</span><span class="nt">&#34;preferenceActor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.preferenceActor2&#34;</span><span class="p">,</span><span class="nt">&#34;actorRegistryActor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.actorRegistryActor3&#34;</span><span class="p">,</span><span class="nt">&#34;settingsActor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.settingsActor4&#34;</span><span class="p">,</span><span class="nt">&#34;webappsActor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.webappsActor5&#34;</span><span class="p">,</span><span class="nt">&#34;deviceActor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.deviceActor6&#34;</span><span class="p">,</span><span class="nt">&#34;directorRegistryActor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.directorRegistryActor7&#34;</span><span class="p">,</span><span class="nt">&#34;heapSnapshotFileActor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.heapSnapshotFileActor8&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>结合上下文，我们发现对于应用部署最有用的就是 <code>webappsActor</code> 这个里面的值，记录下来。</p>
<p>电脑端发送以下报文：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">59</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;to&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.webappsActor5&#34;</span><span class="p">,</span><span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;uploadPackage&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>其中，to 表明要发送给的 <code>actor</code>, type 必须为 <code>uploadPackage</code> 。</p>
<p>手机端响应：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">86</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;actor&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.packageUploadJSONActor9&#34;</span><span class="p">,</span><span class="nt">&#34;from&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.webappsActor5&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>记下来 <code>actor</code> 中的值，后续上传会用到。</p>
<p>电脑端下行以下内容：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">43865</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;to&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.packageUploadJSONActor9&#34;</span><span class="p">,</span><span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;chunk&#34;</span><span class="p">,</span><span class="nt">&#34;chunk&#34;</span><span class="p">:</span><span class="s2">&#34;PK.....&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>通过后面的几条报文，我们可以知道，<code>to</code> 是刚才拿到的 <code>actor</code>， <code>chunk</code> 是分片的 zip 包。（chunk 中的PK头可以很容易的知道这是一个 zip 包）</p>
<p>发送后手机端会响应以下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">79</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;written&#34;</span><span class="p">:</span><span class="mi">20480</span><span class="p">,</span><span class="nt">&#34;_size&#34;</span><span class="p">:</span><span class="mi">102400</span><span class="p">,</span><span class="nt">&#34;from&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.packageUploadJSONActor9&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>其中 <code>written</code> 是本次写入的数据量，<code>_size</code>是整个 <code>uploadActor</code> 接收到的数据量。</p>
<p>多发几次把完整的zip包发过去就可以了</p>
<p>电脑端发送以下 json 通知手机上传完成:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">60</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;to&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.packageUploadJSONActor9&#34;</span><span class="p">,</span><span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;done&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>手机端会回复一个没什么用的 ack 信息：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">48</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;from&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.packageUploadJSONActor9&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>电脑端下达安装指令给 <code>webappActor</code>, 这里的 <code>appId</code> 我查看了 <code>gdeploy</code> 里的实现，一层一层的扒代码进去得到就是个 <code>uuid.v1</code>，所以随便生成一个就好了。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">149</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;appId&#34;</span><span class="p">:</span><span class="s2">&#34;eb4942c0-6b89-11ea-b686-7fba009c94d8&#34;</span><span class="p">,</span><span class="nt">&#34;upload&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.packageUploadJSONActor9&#34;</span><span class="p">,</span><span class="nt">&#34;to&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.webappsActor5&#34;</span><span class="p">,</span><span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;install&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>手机会返回以下响应：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">135</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;appId&#34;</span><span class="p">:</span><span class="s2">&#34;kaiauth.zjyl1994.com&#34;</span><span class="p">,</span><span class="nt">&#34;path&#34;</span><span class="p">:</span><span class="s2">&#34;/data/local/tmp/b2g/eb4942c0-6b89-11ea-b686-7fba009c94d8&#34;</span><span class="p">,</span><span class="nt">&#34;from&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.webappsActor5&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>如果你想在安装完成后和 Web IDE 一样自动打开应用，可以记住 <code>appId</code> 。</p>
<p>电脑端下行以下指令清除用过的 <code>uploadActor</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">62</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;to&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn9.packageUploadJSONActor9&#34;</span><span class="p">,</span><span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;remove&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p><strong>选做部分：</strong></p>
<p>以下内容是通过 Web IDE 安装流程抓取到的：</p>
<p>自动打开应用，<code>manifestURL</code> 的格式是这样的 <code>app://${appId}/manifest.webapp</code>，发给 <code>webappsActor</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">111</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;to&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn0.webappsActor5&#34;</span><span class="p">,</span><span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;launch&#34;</span><span class="p">,</span><span class="nt">&#34;manifestURL&#34;</span><span class="p">:</span><span class="s2">&#34;app://kaiauth.zjyl1994.com/manifest.webapp&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>手机端响应：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">114</span><span class="err">:</span><span class="p">{</span><span class="nt">&#34;from&#34;</span><span class="p">:</span><span class="s2">&#34;server1.conn0.webappsActor5&#34;</span><span class="p">,</span><span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;appOpen&#34;</span><span class="p">,</span><span class="nt">&#34;manifestURL&#34;</span><span class="p">:</span><span class="s2">&#34;app://kaiauth.zjyl1994.com/manifest.webapp&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>此时手机上会自动弹出你刚才安装的App界面。</p>
<h2 id="实现踩坑">实现踩坑</h2>
<p>考虑到需要免除运行库，所以 <code>Golang</code> 就是最简单的解决方案了。</p>
<h3 id="打包-zip">打包 Zip</h3>
<p>网上能找到的 <code>Golang</code> 打包 zip 代码或多或少都有一些问题, 多数问题出现在他会把绝对路径压制进 zip。所以我修改了一个版本的 zip 打包方法，考虑到没必要留存zip包，所以说直接内存操作又快又方便，毕竟 Kai OS 应用也没见过超过 10 MB 的。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">zipToMem</span><span class="p">(</span><span class="nx">source</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">data</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">buf</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">new</span><span class="p">(</span><span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">archive</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">zip</span><span class="p">.</span><span class="nf">NewWriter</span><span class="p">(</span><span class="nx">buf</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">source</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">filepath</span><span class="p">.</span><span class="nf">Abs</span><span class="p">(</span><span class="nx">source</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">info</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nf">Stat</span><span class="p">(</span><span class="nx">source</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">info</span><span class="p">.</span><span class="nf">IsDir</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;source not dir&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">filepath</span><span class="p">.</span><span class="nf">Walk</span><span class="p">(</span><span class="nx">source</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">path</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">info</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">FileInfo</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">info</span><span class="p">.</span><span class="nf">IsDir</span><span class="p">()</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">info</span><span class="p">.</span><span class="nf">Name</span><span class="p">()</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&#34;.git&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="nx">filepath</span><span class="p">.</span><span class="nx">SkipDir</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">header</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">zip</span><span class="p">.</span><span class="nf">FileInfoHeader</span><span class="p">(</span><span class="nx">info</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">header</span><span class="p">.</span><span class="nx">Name</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">TrimPrefix</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span><span class="w"> </span><span class="nx">source</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">info</span><span class="p">.</span><span class="nf">IsDir</span><span class="p">()</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">header</span><span class="p">.</span><span class="nx">Name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">header</span><span class="p">.</span><span class="nx">Name</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">filepath</span><span class="p">.</span><span class="nf">ToSlash</span><span class="p">(</span><span class="nx">header</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">info</span><span class="p">.</span><span class="nf">IsDir</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">header</span><span class="p">.</span><span class="nx">Name</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s">&#34;/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">header</span><span class="p">.</span><span class="nx">Method</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">zip</span><span class="p">.</span><span class="nx">Deflate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">header</span><span class="p">.</span><span class="nx">Name</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">TrimPrefix</span><span class="p">(</span><span class="nx">header</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;/&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="o">*</span><span class="nx">verboseFlag</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">header</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">writer</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">archive</span><span class="p">.</span><span class="nf">CreateHeader</span><span class="p">(</span><span class="nx">header</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">info</span><span class="p">.</span><span class="nf">IsDir</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">file</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">defer</span><span class="w"> </span><span class="nx">file</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">Copy</span><span class="p">(</span><span class="nx">writer</span><span class="p">,</span><span class="w"> </span><span class="nx">file</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">archive</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">buf</span><span class="p">.</span><span class="nf">Bytes</span><span class="p">(),</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>简单来说就是通过 <code>filepath.Walk</code> 遍历文件夹里的文件，然后对路径做处理，考虑到 Windows 和 *nix 系列的路径分隔符不一样，所以统一用 <code>filepath.ToSlash</code> 转换到 <code>/</code>，然后方便处理。</p>
<p>最后的结果就是打开 zip 包，所有文件都是相对路径模式存在。这样就正确了！</p>
<p>如果你的zip处理不正确，不是相对路径的，Kai OS 那边只会提示安装失败，并不会给你详细原因。</p>
<h3 id="协议解析和组装">协议解析和组装</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">readJSON</span><span class="p">(</span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">bufio</span><span class="p">.</span><span class="nx">Reader</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="o">*</span><span class="nx">simplejson</span><span class="p">.</span><span class="nx">Json</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">strLen</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">r</span><span class="p">.</span><span class="nf">ReadString</span><span class="p">(</span><span class="nx">delimByte</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">strLen</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSuffix</span><span class="p">(</span><span class="nx">strLen</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;:&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">bJSON</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="nf">atoi</span><span class="p">(</span><span class="nx">strLen</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">ReadFull</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="nx">bJSON</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">simplejson</span><span class="p">.</span><span class="nf">NewJson</span><span class="p">(</span><span class="nx">bJSON</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">writeJSON</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span><span class="w"> </span><span class="nx">json</span><span class="w"> </span><span class="o">*</span><span class="nx">simplejson</span><span class="p">.</span><span class="nx">Json</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">bJSON</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">json</span><span class="p">.</span><span class="nf">MarshalJSON</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">buf</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">bytes</span><span class="p">.</span><span class="nf">NewBufferString</span><span class="p">(</span><span class="nf">itoa</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">bJSON</span><span class="p">)))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">buf</span><span class="p">.</span><span class="nf">WriteByte</span><span class="p">(</span><span class="nx">delimByte</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">buf</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="nx">bJSON</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="nx">buf</span><span class="p">.</span><span class="nf">Bytes</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>为了方便解析，所以输入输出都是 <code>simplejson</code> 对象。组装协议很简单，读取协议使用了 <code>bufio.ReadString</code> 实现了截取冒号前字符串的功能，要注意：截取到的字符串是包含冒号的，需要处理掉。</p>
<p>不得不说这个协议设计的还是很简单明了的，如果可以的话甚至可以用 <code>telnet</code> 进行操作，基本不需要二进制操作。一般来说我设计协议都是固定字节的包头，决定后面要读取多长的数据。这种文本协议解析速度肯定没有二进制协议快，但是胜在简单明了。</p>
<h3 id="chunk转换">chunk转换</h3>
<p>本次最大的问题就在于，直接使用 Go 的 <code>json.Marshal</code> 处理字节数组，会处理成 Base64 ，如果协议是我设计的我肯定不会在 json 里直接写二进制，但是手机端的处理程序是固定的，只能针对性的修改。</p>
<p>通过查阅 <a href="https://www.json.org/json-zh.html" rel="noopener noreferrer" target="_blank">https://www.json.org/json-zh.html</a> ，可以知道 JSON 的字符串需要转义一定的内容。</p>
<p><img alt="JSON string" decoding="async" loading="eager" src="https://www.json.org/img/string.png">
</p>
<p>考虑到 ASCII 中有不少控制字符，最好的办法就是只显示可见字符。</p>
<p>可见字符的 ASCII 范围是 32~126，不可见字符用 <code>\u00FF</code> 表示。（FF是字节的十六进制值）</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">jsonEncodeBytes</span><span class="p">(</span><span class="nx">byteArray</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="nx">json</span><span class="p">.</span><span class="nx">RawMessage</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">sb</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nx">Builder</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`&#34;`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">byteArray</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">switch</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="mi">8</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`\b`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="mi">9</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`\t`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="mi">10</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`\n`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="mi">12</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`\f`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="mi">13</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`\r`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="mi">34</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`\&#34;`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="mi">92</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`\\`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">if</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">32</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="mi">126</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteByte</span><span class="p">(</span><span class="nx">b</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`\u00`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteByte</span><span class="p">(</span><span class="nx">hextable</span><span class="p">[</span><span class="nx">b</span><span class="o">&gt;&gt;</span><span class="mi">4</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteByte</span><span class="p">(</span><span class="nx">hextable</span><span class="p">[</span><span class="nx">b</span><span class="o">&amp;</span><span class="mh">0x0f</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">`&#34;`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">result</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">json</span><span class="p">.</span><span class="nf">RawMessage</span><span class="p">(</span><span class="nx">sb</span><span class="p">.</span><span class="nf">String</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">result</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>这样就能得到手机端需要的 Json 里面跑二进制流了。</p>
<h1 id="一些废话">一些废话</h1>
<p>Kai OS 还是很有折腾潜力的，3月14日的时候 Mozilla 已经和 Kaiostech 达成协议。由 Mozilla 给 Kai OS 升级 Gecko 内核，
新闻在这里 <a href="https://www.kaiostech.com/press/kaios-technologies-and-mozilla-partner-to-enable-a-healthy-mobile-internet-for-everyone/" rel="noopener noreferrer" target="_blank">KaiOS Technologies and Mozilla partner to enable a healthy mobile internet for everyone</a>。</p>
<p>重点在于，升级后的 Kai OS 速度会得到一系列更强的 Buff，毕竟现在的 Kai OS Gecko 还停留在 2016 的 48 版本。2017 年大把的特性更新还上了量子火狐，现在 Kai OS 一点没吃到。就是不知道未来的 HMD 能不能给 Nokia 2720 Flip 升级新内核了，估计很难。</p>
<p><img alt="KaiOS-Mozilla" decoding="async" height="631" loading="lazy" src="/post/kaideploy/KaiOS-Mozilla-Features.webp" width="1201">
</p>
<p>最后，完整的代码在这里: <a href="https://github.com/zjyl1994/kaideploy" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/kaideploy</a></p>
<p>我已经借助 bat 批处理文件和 WinRAR 自解压做好了一个 自带 ADB 的 OmniSD 一键安装器，个人测试还是很方便使用的。</p>]]></content:encoded>
    </item>
    <item>
      <title>Introducing KaiAuth</title>
      <link>https://blog.zjyl1994.com/post/introducing_kaiauth/</link>
      <pubDate>Fri, 20 Mar 2020 23:10:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/introducing_kaiauth/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;KaiAuth is a simple alternative of Google Authenticator on KaiOS&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt=&#34;KaiAuth logo&#34; decoding=&#34;async&#34; height=&#34;112&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/introducing_kaiauth/logo.webp&#34; width=&#34;112&#34;&gt;
&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not a native speaker of English so there will be some mistakes in my sayings.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>KaiAuth is a simple alternative of Google Authenticator on KaiOS</p>
</blockquote>
<p><img alt="KaiAuth logo" decoding="async" height="112" loading="eager" src="/post/introducing_kaiauth/logo.webp" width="112">
</p>
<p>I&rsquo;m not a native speaker of English so there will be some mistakes in my sayings.</p>
<h1 id="install">Install</h1>
<h2 id="webide">WebIDE</h2>
<ol>
<li>Download <a href="https://github.com/zjyl1994/KaiAuth/archive/master.zip" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/KaiAuth/archive/master.zip</a> .</li>
<li>Extract this zip to a folder.</li>
<li>Click &ldquo;Open Packaged App&rdquo; and select the folder from the previous step.</li>
<li>Click &ldquo;Install and Run&rdquo; button.</li>
</ol>
<h2 id="omnisd">OmniSD</h2>
<ol>
<li>Open <a href="https://github.com/zjyl1994/KaiAuth/releases/latest" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/KaiAuth/releases/latest</a> and download <strong>kaiauth.zip</strong>.</li>
<li>Copy this zip file to the apps directory of the memory card.</li>
<li>Open <strong>OmniSD</strong> and install this zip.</li>
</ol>
<p>When you do the right thing by following the steps above,KaiAuth will appear in your phone menu.</p>
<h1 id="use">Use</h1>
<h2 id="basic-functions">Basic functions</h2>
<p><img alt="Icon in menu" decoding="async" height="320" loading="lazy" src="/post/introducing_kaiauth/logo_in_menu.webp" width="240">
</p>
<h3 id="main-interface">Main interface</h3>
<p><img alt="Main interface" decoding="async" height="320" loading="lazy" src="/post/introducing_kaiauth/main_interface.webp" width="240">
</p>
<p>Although these screenshots are in Chinese, don&rsquo;t worry, English versions are now available.</p>
<p>You can see a lot of TOTP codes on the screen, you can use the navigation keys to scroll them up, down, left and right.</p>
<p>There are two soft keys at the bottom, the left one means to add a new code, and the right one means to delete the currently selected code.</p>
<p>Before deleting, you will be asked if you want to delete, so you do n’t need to worry about deleting an account profile mistakenly.</p>
<p>Press the soft key named Add on the left,the application will switch to the scan QR code page.</p>
<h3 id="scan-qr-code-interface">Scan QR code interface</h3>
<p><img alt="QR code scan" decoding="async" height="320" loading="lazy" src="/post/introducing_kaiauth/scan_qrcode.webp" width="240">
</p>
<p>To use the QR-code scanning feature, the application requires permission for using the camera. A dialog will popup to ask you whether you are going to grant it or not.</p>
<p>Place the QR-code into the camera&rsquo;s sight. If any valid QR-codes are identified, the application will automatically extract information contained in the QR-code then generate a new profile from the information. The new profile will then be shown in the main interface of the app.</p>
<h2 id="advanced-features">Advanced Features</h2>
<p>Based on what I actually need, I have made some advanced features. They can be activated by pressing keys in some special sequences.
Please enter these codes by sequence when you are opening KaiAuth. Entering these codes in the phone dialer has no effects at all.</p>
<h3 id="7370">*#7370#</h3>
<p>Designed for very old Nokia phones. Will clean the data stored in the app. Very useful when you have messed up your code list and had to re-import the correct data.</p>
<h3 id="0000">*#0000#</h3>
<p>Also useful when you are using an old Nokia. The version of the KaiAuth app will be shown after typing the sequence above.</p>
<h3 id="467678">*#467678#</h3>
<p>Will automatically import profiles from a file <code>kaiauth.json</code> located in the SD card. Useful when you are transferring an existing profile list from another device, or restore profiles from a backup you previously made.</p>
<h3 id="397678">*#397678#</h3>
<p>Will export current profiles stored in the application into a file named <code>kaiauth.json</code> in the SD card. Used for backing up or transferring profiles to a new device.</p>
<p><strong>WARNING:</strong> The file contains sensitive information, please keep it in a safe place. Anyone who gets the file can get access to your account by using profiles stored in the file!</p>
<h1 id="why-i-made-it">Why i made it</h1>
<p>I bought a Nokia 2720 Flip, which runs KaiOS. I have many accounts protected with Google Authenticator. I hope that I can still query these codes and log in to my account on my computer when I am away from my smartphone, so I go to the Internet to search for related tools.</p>
<p>Unfortunately, I didn&rsquo;t find related tools, some of them are only rough implementation of web pages, which need to use cursor to operate, which is very uncomfortable. Some don’t even have a basic feature to scan QR codes. It ’s very difficult to enter the two-step verification key accurately on the T9 keyboard of old phones. So I developed this KaiAuth myself, hoping that it can help someone who needs it.</p>
<p>If you like my work, please give me star on GitHub. <a href="https://github.com/zjyl1994/KaiAuth" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/KaiAuth</a></p>
<p>Thanks for the help of Google Translate, I hope you can understand my bad English.</p>
<p>Finally, I would like to say great thanks for the correction on semantics and words to the article from <a href="https://twitter.com/Carina__Nebula" rel="noopener noreferrer" target="_blank">Carina Nebula</a>!</p>
<p>Easter egg in logo: 86 is the international calling code of China, 0755 is the calling code of Shenzhen.</p>]]></content:encoded>
    </item>
    <item>
      <title>KaiAuth 开发笔记</title>
      <link>https://blog.zjyl1994.com/post/kai-auth2/</link>
      <pubDate>Wed, 11 Mar 2020 12:43:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/kai-auth2/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;KaiAuth 是一款 KaiOS 上的谷歌验证器替代品&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;12月初的时候，我用原生 JavaScript 实现了第一版的 KaiAuth。具体可以看之前的博文。&lt;/p&gt;
&lt;p&gt;如果没有耐心看下去也可以去 &lt;a href=&#34;https://github.com/zjyl1994/KaiAuth/releases/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;https://github.com/zjyl1994/KaiAuth/releases/&lt;/a&gt; 直接下载最新版编译好的应用。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>KaiAuth 是一款 KaiOS 上的谷歌验证器替代品</p>
</blockquote>
<p>12月初的时候，我用原生 JavaScript 实现了第一版的 KaiAuth。具体可以看之前的博文。</p>
<p>如果没有耐心看下去也可以去 <a href="https://github.com/zjyl1994/KaiAuth/releases/" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/KaiAuth/releases/</a> 直接下载最新版编译好的应用。</p>
<h2 id="改进目标">改进目标</h2>
<p>首先，需要确认改进点，旧版本的 KaiAuth 是这样的：</p>
<p><img alt="kaiauth1" decoding="async" height="320" loading="eager" src="/post/kai-auth2/kaiauth1.webp" width="240">
</p>
<p>目测可以看到：没有添加删除，没有其他功能，只有简单的看两步验证代码和一个方向键滚动屏幕。</p>
<p>添加删除账号需要修改 <code>data/data.json</code> 然后重新部署。</p>
<p>期望的改进点是，可以手机自己独立增加删除账号，所以引入左右软键。左边是<code>Add</code>，右侧是<code>Delete</code></p>
<p><img alt="kaiauth2" decoding="async" height="320" loading="lazy" src="/post/kai-auth2/kaiauth2.webp" width="240">
</p>
<p>（上面的是已经做了多语言的截图，感谢 LiarOnce 的贡献）</p>
<p>考虑到添加需要扫描二维码，所以说需要实现一套扫码的页面和逻辑。</p>
<p><img alt="KaiAuth2 QRCode Scan" decoding="async" height="320" loading="lazy" src="/post/kai-auth2/k2-cam.webp" width="240">
</p>
<p>扫码功能界面就长这样。扫描到二维码会自动添加并返回主列表。</p>
<h2 id="实现">实现</h2>
<h3 id="存储">存储</h3>
<p>首先，KaiAuth 第一版使用 <code>fetch('data/data.json')</code> 加载软件内的 JSON ，没有办法实现动态修改。在第二版中，使用 <code>localStorage</code> 存储，可以方便的修改。</p>
<p>列表数据采用JSON存储，格式如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;id&#34;</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Example Account 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;secret&#34;</span><span class="p">:</span> <span class="s2">&#34;2DMCERI7V4OPLHYBWNLQ4WASIIH4XZTP&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;id&#34;</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Example Account 2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;secret&#34;</span><span class="p">:</span> <span class="s2">&#34;MYCX4VGRCNXXCVXEDC2ZLPM6MZORO55Z&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;id&#34;</span><span class="p">:</span><span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Example Account 3&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;secret&#34;</span><span class="p">:</span> <span class="s2">&#34;JBSWY3DPEHPK3PXP&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;id&#34;</span><span class="p">:</span><span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Example Account 4&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;secret&#34;</span><span class="p">:</span> <span class="s2">&#34;LL7HWMUW77Z75J3D&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;id&#34;</span><span class="p">:</span><span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Example Account 5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;secret&#34;</span><span class="p">:</span> <span class="s2">&#34;WUABTBGATYBBEURE&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p><code>localStorage</code>只能存储字符串，所以要保存成JSON格式。通过<code>authcodes = JSON.parse(window.localStorage.getItem(&quot;authcodes&quot;));</code>可以拿到保存的列表。</p>
<h3 id="删除">删除</h3>
<p>删除功能比较简单，只要在列表上绑定<code>data-id</code>，然后获取到当前<code>active</code>的那条数据的id，去authcodes数组里删掉即可，然后刷新UI。</p>
<h3 id="扫码增加">扫码增加</h3>
<p>扫码功能是本次的难点，首先，扫码库我选择<code>jsQR</code>。</p>
<p>通过查看官方Demo：https://cozmo.github.io/jsQR/</p>
<p>可以获得以下代码：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">navigator</span><span class="p">.</span><span class="nx">mediaDevices</span><span class="p">.</span><span class="nx">getUserMedia</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">audio</span><span class="o">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">video</span><span class="o">:</span> <span class="p">{</span> <span class="nx">facingMode</span><span class="o">:</span> <span class="s2">&#34;environment&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">stream</span><span class="p">)</span> <span class="p">{</span>  
</span></span><span class="line"><span class="cl">    <span class="nx">video</span><span class="p">.</span><span class="nx">srcObject</span> <span class="o">=</span> <span class="nx">stream</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">video</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s2">&#34;playsinline&#34;</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">video</span><span class="p">.</span><span class="nx">play</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>video对象是<code>&lt;video&gt;</code>标签，用户看到的内容就是摄像头获取的视频流。</p>
<p>考虑到方便用户使用，扫描到二维码的时候就应该获取结果，无需用户做更多操作。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">capture</span><span class="p">(){</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">video</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="nx">video</span><span class="p">.</span><span class="nx">HAVE_ENOUGH_DATA</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">video</span><span class="p">.</span><span class="nx">videoHeight</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">video</span><span class="p">.</span><span class="nx">videoWidth</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="nx">canvasContext</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">video</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="kd">var</span> <span class="nx">imageData</span> <span class="o">=</span> <span class="nx">canvasContext</span><span class="p">.</span><span class="nx">getImageData</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="kd">var</span> <span class="nx">code</span> <span class="o">=</span> <span class="nx">jsQR</span><span class="p">(</span><span class="nx">imageData</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span> <span class="nx">imageData</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span> <span class="nx">imageData</span><span class="p">.</span><span class="nx">height</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nx">inversionAttempts</span><span class="o">:</span> <span class="s2">&#34;dontInvert&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">});</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span><span class="p">(</span><span class="nx">code</span><span class="p">){</span>
</span></span><span class="line"><span class="cl">                <span class="nx">activityHandler</span><span class="p">.</span><span class="nx">postResult</span><span class="p">(</span><span class="nx">code</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><p>每隔200ms触发一次<code>capture</code>，检测<code>video</code>的状态，当<code>video</code>有数据的时候，通过<code>canvas</code>把<code>video</code>的当前帧画过去，这样可以获取照片数据，交给<code>jsQR</code>进行检测。当<code>jsQR</code>读取到内容后，<code>code</code>里就会有二维码的内容了。</p>
<h3 id="多activity">多Activity</h3>
<p>和Android一样，Kai OS也是有activity设定的。</p>
<p>activity需要在<code>manifest.webapp</code>中提前注册。首先需要指定<code>origin</code>,一个简单的例子如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;origin&#34;</span><span class="p">:</span> <span class="s2">&#34;app://kaiauth.zjyl1994.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;activities&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;com.zjyl1994.kaiauth.addCode&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;href&#34;</span><span class="p">:</span> <span class="s2">&#34;./add.html&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;disposition&#34;</span><span class="p">:</span> <span class="s2">&#34;inline&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;returnValue&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>指定<code>href</code>为activity的页面，<code>disposition</code>为inline（inline模式下显示为弹出层，也可以选择window，会新开一个页面），<code>returnValue</code>控制是否可以读到返回值。如果你需要activity返回一些数据，可以设置为true。</p>
<h4 id="启动activity">启动Activity</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">Activity</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MozActivity</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;前面注册的activety名字&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="nx">Activity</span><span class="p">.</span><span class="nx">onsuccess</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">ActivityResult</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">result</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>以下代码执行后，会调用系统自带的函数启动新activity，结果会返回到ActivityResult里。唯一美中不足的就是切换activity会短暂的出现一次应用的Splash（闪屏，指启动的loading页面），尚不清楚是特性还是bug。</p>
<p>ActivityResult 可以是Object，所以可以承载很丰富的数据。</p>
<h4 id="返回数据">返回数据</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">activityHandler</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">navigator</span><span class="p">.</span><span class="nx">mozSetMessageHandler</span><span class="p">(</span><span class="s1">&#39;activity&#39;</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">activityRequest</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">activityHandler</span> <span class="o">=</span> <span class="nx">activityRequest</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 返回错误信息，通过onerror接收
</span></span></span><span class="line"><span class="cl"><span class="nx">activityHandler</span><span class="p">.</span><span class="nx">postError</span><span class="p">(</span><span class="s1">&#39;No QR Code&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 返回成功数据，通过onsuccess接收
</span></span></span><span class="line"><span class="cl"><span class="nx">activityHandler</span><span class="p">.</span><span class="nx">postResult</span><span class="p">(</span><span class="s1">&#39;any object here&#39;</span><span class="p">)</span>
</span></span></code></pre></div><p>调用post后会自动关闭activity。</p>
<h3 id="shortcodes">Shortcodes</h3>
<p>Shortcode是老式按键手机最喜欢用的功能，通过数字按键得一系列特定组合，可以实现很多隐藏功能。</p>
<p>KaiAuth得隐藏功能如下：</p>
<table>
	<thead>
			<tr>
					<th>Shortcodes</th>
					<th>功能</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>*#0000#</td>
					<td>查看版本信息</td>
			</tr>
			<tr>
					<td>*#7370#</td>
					<td>清空 localstorage，这会丢失所有的认证数据</td>
			</tr>
			<tr>
					<td>*#467678#</td>
					<td>导入SD卡中的kaiauth.json文件</td>
			</tr>
			<tr>
					<td>*#397678#</td>
					<td>导出当前数据到SD卡kaiauth.json中</td>
			</tr>
	</tbody>
</table>]]></content:encoded>
    </item>
    <item>
      <title>2020</title>
      <link>https://blog.zjyl1994.com/post/2020/</link>
      <pubDate>Sun, 02 Feb 2020 15:11:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/2020/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;失业在家半年了，刚开始是自己想休息，然后马上家里的事就来了，一通瞎忙，折腾完了就到了过年，现在过完年又闹肺炎。这个庚子年不消停啊。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总结我的 2019 ，就是折腾。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>失业在家半年了，刚开始是自己想休息，然后马上家里的事就来了，一通瞎忙，折腾完了就到了过年，现在过完年又闹肺炎。这个庚子年不消停啊。</p>
</blockquote>
<p>总结我的 2019 ，就是折腾。</p>
<p>我知道 2020 年 2 月份再回顾 2019 太晚了，所以也不打算过多的回顾。2019 年，年中拿到一线城市户口，9 月遭遇裁员，经济寒冬，闲下来的我帮着家里装修监工一通搞。我的 2019 也就这么几样事。</p>
<p>眼看到了 2020，一月份有多乱大家都看到了。
我自己的 2019 愿望也就实现了一个，独立游戏一直也没开工，可能是我实在太懒，不过大概率这个锅要甩给编不出好故事的我。
看了太多魔幻新闻，我是想不到什么可以安全写出来而不会被上名单的故事。</p>
<p>没了收入最大的问题就是买东西不痛快了，后期很多时候都不能出去浪了，出去坐动车也舍不得买一等座了。</p>
<p>就看这个经济形势，2020 的企业肯定不会好过，新一轮裁员风暴就在眼前。也不知道这波延期开工后会有多少公司在招人，一切听天由命好了。</p>
<p>幸好当年上班的我自控能力，存了点生活费，短期内还衣食无忧。不过要是继续保持这个状态，给座金山都能吃没了。</p>
<p>希望 2020 的 IT 公司都能好好的生存下去，给我们普通程序员一条生路好了。</p>
<p>另外，今天是完全回文日，亚洲欧洲美洲的写法里都是哦，本世纪内唯一一次。 (2020-02-02/02-02-2020)</p>
]]></content:encoded>
    </item>
    <item>
      <title>Authy 导出密钥</title>
      <link>https://blog.zjyl1994.com/post/authy-export/</link>
      <pubDate>Sat, 07 Dec 2019 21:40:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/authy-export/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Authy 也可以导出密钥，而且现在很简单了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Authy 是一款挺好用的 2FA 生成器，问题在于它没有 KaiOS 版本，所以我昨天自己写了一个生成器。详情可以见上一篇文章。&lt;/p&gt;
&lt;p&gt;Authy 官方没有给密钥导出方案（说是为了安全考虑不提供这个），网上的 js 方案都是从chrome版的authy中读取，还不能用。我花了很久找到了这个现成的方案。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>Authy 也可以导出密钥，而且现在很简单了</p>
</blockquote>
<p>Authy 是一款挺好用的 2FA 生成器，问题在于它没有 KaiOS 版本，所以我昨天自己写了一个生成器。详情可以见上一篇文章。</p>
<p>Authy 官方没有给密钥导出方案（说是为了安全考虑不提供这个），网上的 js 方案都是从chrome版的authy中读取，还不能用。我花了很久找到了这个现成的方案。</p>
<p><a href="https://github.com/alexzorin/authy" rel="noopener noreferrer" target="_blank">https://github.com/alexzorin/authy</a></p>
<p>可以用 <code>go get github.com/alexzorin/authy</code> 下载这个包，然后使用 <code>go run $GOPATH/src/github.com/alexzorin/authy/cmd/authy-export/authy-export.go</code> 运行。</p>
<p><strong>注意</strong> 不要用 Git Bash 运行这个程序，否则到输入密码部分会直接崩溃。建议用 CMD 或者 PowerShell 运行。</p>
<p>运行起来之后，首先要求输入的是 Authy 的电话区号，不带+号的。</p>
<p>然后输入手机号，它会联系 Authy 服务器，此时你的 Authy 设备上会有个登录提示，按照提示同意即可。</p>
<p>然后会显示输入 backup password，输入你的密码后，就会显示出你所有的 token。</p>
<p>输出的都是 <code>otp://</code> 开头的 2FA URI，你就可以把这些 OTP 导入其他 2FA 验证器了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>KaiOS 折腾开发笔记</title>
      <link>https://blog.zjyl1994.com/post/kai-auth/</link>
      <pubDate>Sat, 07 Dec 2019 15:23:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/kai-auth/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;11 月末冒险过关去 HK 买了一部诺基亚 2720 Flip&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;2019年9月5号，HMD 发布了 Nokia 2720 Flip，一部翻盖的半智能机。&lt;/p&gt;
&lt;p&gt;个人对翻盖机还是很喜欢的，之前纯为了情怀买过一个飞利浦的真功能机，结果只支持 GSM，2019 年 GSM 已经不行了，很多时候信号都没有。而这部 2720 Flip，支持4G、VoLTE、热点分享，瞬间种草各种想买。&lt;/p&gt;
&lt;p&gt;当时说，2019 年第四季度大陆发售，结果转眼间从 10 月等到 12 月，每天跑去诺基亚中国的微博下蹲守也不见发布，正好有朋友约了我过关买东西，我就去丰泽拎了一部港版 2720 Flip 回来。&lt;/p&gt;
&lt;p&gt;淘宝上很多卖这个的，基本都是 799 RMB，但是丰泽的官网上显示只要 798 HKD，考虑到港币的汇率，实际到手价格也就710 RMB。（不过丰泽只有黑色版本，也不知道为啥没有白的可以买）&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>11 月末冒险过关去 HK 买了一部诺基亚 2720 Flip</p>
</blockquote>
<h1 id="前言">前言</h1>
<p>2019年9月5号，HMD 发布了 Nokia 2720 Flip，一部翻盖的半智能机。</p>
<p>个人对翻盖机还是很喜欢的，之前纯为了情怀买过一个飞利浦的真功能机，结果只支持 GSM，2019 年 GSM 已经不行了，很多时候信号都没有。而这部 2720 Flip，支持4G、VoLTE、热点分享，瞬间种草各种想买。</p>
<p>当时说，2019 年第四季度大陆发售，结果转眼间从 10 月等到 12 月，每天跑去诺基亚中国的微博下蹲守也不见发布，正好有朋友约了我过关买东西，我就去丰泽拎了一部港版 2720 Flip 回来。</p>
<p>淘宝上很多卖这个的，基本都是 799 RMB，但是丰泽的官网上显示只要 798 HKD，考虑到港币的汇率，实际到手价格也就710 RMB。（不过丰泽只有黑色版本，也不知道为啥没有白的可以买）</p>
<p><img alt="2720-opened" decoding="async" height="480" loading="eager" src="/post/kai-auth/2720-opened.webp" width="720">
</p>
<h1 id="系统">系统</h1>
<p>KaiOS 用起来，有种 S40 和 S60 的混合体，可能年轻人都没用过这东西了吧？</p>
<p><img decoding="async" height="336" loading="lazy" src="/post/kai-auth/launcher.webp" width="503">
</p>
<p>在香港的时候，连上丰泽的 WiFi ，桌面上有 Youtube、Twitter、WhatsApp，甚至有 Google 助理。</p>
<p>但是我拎回来，插上大陆的电话卡，这些东西全都消失了，甚至自带的小游戏都没了。</p>
<p>KaiStore 也还行，可以直接访问没有被墙掉。但是里面可用的应用非常少，你可以理解为商店只是个摆设。</p>
<h1 id="折腾">折腾</h1>
<p>作为一个备用机，我主要的需求就是密码管理器和两步验证，密码管理器我现在有在线版的可以用，但是两步验证，之前只能用智能手机。Authy 必须要谷歌框架，而一般的国内手机很少有这种东西。所以我对 TOTP 应用比较有需求。</p>
<p>KaiOS 是一个 HTML5 驱动的半智能手机，基于停止开发的 Firefox OS 二次开发，也就是说你只要会前端技术就可以给这个写 App。要开发，首先要打开开发者模式。输入<code>*#*#33284#*#*</code>，直接进入调试模式，可以看到状态栏上有个 DEBUG 图标。</p>
<p>电脑上要安装 ADB，没有的可以直接在<a href="https://developer.android.com/studio/releases/platform-tools.html#download" rel="noopener noreferrer" target="_blank">这里</a>下载。装好 ADB，就可以尝试连接了。（我是不太知道为什么 KaiOS 要用 ADB 链接）使用<code>adb forward tcp:6000 localfilesystem:/data/local/debugger-socket</code>命令一键连接。</p>
<p>电脑上的调试器，可以使用 Pale Moon，虽然都推荐 Firefox 59 ESR 版本，但是因为这个东西每次都要自动升级，就很麻烦。所以推荐用不会强行自动升级的 Pale Moon 28.6.1，找不到可以直接点<a href="https://rm-eu.palemoon.org/release/palemoon-28.6.1.win64.installer.exe" rel="noopener noreferrer" target="_blank">这里</a>下载。</p>
<p><img alt="palemoon" decoding="async" height="699" loading="lazy" src="/post/kai-auth/palemoon.webp" width="406">
</p>
<p><img decoding="async" height="581" loading="lazy" src="/post/kai-auth/webide.webp" width="1031">
</p>
<h2 id="kaiauth">KaiAuth</h2>
<p>KaiOS 的开发说简单也简单，说复杂也复杂，可能是我没有相关的前端经验。先来看我的最终成品：</p>
<p><img decoding="async" height="320" loading="lazy" src="/post/kai-auth/kaiauth.webp" width="240">
</p>
<p>首先要理解一个概念，KaiOS 并没有官方的 UI 库和控件，你可以理解为一个全屏的 Firefox 浏览器，你所看到的一切都是用 HTML 和 CSS 一点点画出来的。（也就是上图除去最上面一行状态栏，其他都是要自己实现）</p>
<p>我首先看了 KaiOS 的官方开发者网站，推荐的是 <a href="https://github.com/AdrianMachado/KaiUI" rel="noopener noreferrer" target="_blank">KaiUI</a> ，但是这个并不能用，依赖遭到破坏不能顺利安装。</p>
<p>后来找到了一系列的 react 工具链，但是折腾很久都不会弄。 （项目创建工具：<a href="https://github.com/kaios-design/create-kaios-app" rel="noopener noreferrer" target="_blank">create-kaios-app</a> ，配套的 UI 库：<a href="https://github.com/kaios-design/kaid" rel="noopener noreferrer" target="_blank">Kaid</a> ）</p>
<p>最后，我的办法最后就是用原生 Javascript 写。参考了 OmniSD 的代码，总共花了不到一晚上就写好了。</p>
<h3 id="界面">界面</h3>
<p>KaiOS 的界面有一定的设计指导，大部分界面都由三个区域组成。标题栏就是最上面蓝色的 KaiAuth 那一行，最下面的是软键，由于我这个应用非常特殊，不需要软键。所以我就把软键区改成了验证码倒计时提示。</p>
<p>中间的区域，是最重要的信息区。KaiAuth 中，这个区域是一个列表，上部大大的验证码，下面是验证码对应的名字。这个列表可以用方向键上下滚动。</p>
<p>这部分就是简单的 HTML + CSS写成，重要的是用<code>position: fixed;</code>把对应的元素固定到该去的位置。</p>
<h3 id="按键">按键</h3>
<p>KaiOS 的按键需要你自己使用 js 进行响应，包括软键，因为软键也是你自己画的。 具体的实例可以参考官方的<a href="https://developer.kaiostech.com/core-developer-topics/dpad-navigation" rel="noopener noreferrer" target="_blank">教程</a>。官方的五维导航键例子就是这样的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;keydown&#39;</span><span class="p">,</span> <span class="nx">handleKeydown</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">handleKeydown</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">switch</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="s1">&#39;ArrowUp&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">      <span class="nx">nav</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="s1">&#39;ArrowDown&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">      <span class="nx">nav</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="s1">&#39;ArrowRight&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">      <span class="nx">nav</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="s1">&#39;ArrowLeft&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">      <span class="nx">nav</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">nav</span> <span class="p">(</span><span class="nx">move</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">currentIndex</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">.</span><span class="nx">tabIndex</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">next</span> <span class="o">=</span> <span class="nx">currentIndex</span> <span class="o">+</span> <span class="nx">move</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">items</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.items&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">targetElement</span> <span class="o">=</span> <span class="nx">items</span><span class="p">[</span><span class="nx">next</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="nx">targetElement</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>ArrowUp</code>、<code>ArrowDown</code>、<code>ArrowRight</code>、<code>ArrowLeft</code> 是导航键的四个方向，<code>Enter</code> 是方向键中间按下。<code>SoftRight</code>、<code>SoftLeft</code>是左右软键。其他按钮就直接和键盘上印的一样了，123 就是 123。</p>
<h3 id="totp">TOTP</h3>
<p>谷歌验证器用的数字生成算法，就是标准的 TOTP 算法，每隔 30 秒一个切换。这个也不需要自己来实现，有专门的 <a href="https://otplib.yeojz.dev/" rel="noopener noreferrer" target="_blank">otplib</a> 库可以使用。要注意，otplib 是针对 Node.js 环境用的，KaiOS 是标准的 Gecko 48 环境，不是 Node ，所以要使用 otplib-browser 版本。下面是 TOTP 的例子：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">secert</span> <span class="o">=</span> <span class="s1">&#39;WUABTBGATYBBEURE&#39;</span><span class="p">;</span> <span class="c1">// TOTP 中的密钥
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">code</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">otplib</span><span class="p">.</span><span class="nx">authenticator</span><span class="p">.</span><span class="nx">generate</span><span class="p">(</span><span class="nx">secert</span><span class="p">);</span> <span class="c1">// 生成的验证码
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">remain</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">otplib</span><span class="p">.</span><span class="nx">authenticator</span><span class="p">.</span><span class="nx">timeRemaining</span><span class="p">();</span> <span class="c1">// 验证码剩余的时间
</span></span></span></code></pre></div><p>最初的实现，是每秒刷新一次，但是这样会导致滚动列表时的闪烁问题。所以，现在每秒更新的是 remain 这个数值，当 remain 为 30 时，重新生成一波验证码并显示。</p>
<p>TOTP标准的添加方法是扫描二维码，但是没有合适的资料可以借鉴，短期内无法实现。</p>
<p>从原理上来说二维码会解析出一个 otpauth 开头的 uri，解析后就能得到 secert 和其他设置信息。</p>
<p><img decoding="async" height="228" loading="lazy" src="/post/kai-auth/qrcode.webp" width="228">
</p>
<p>解码出来，就是：<code>otpauth://totp/otplib-website:otplib-demo-user?secret=WUABTBGATYBBEURE&amp;period=30&amp;digits=6&amp;algorithm=SHA1&amp;issuer=otplib-website</code></p>
<h3 id="数据源">数据源</h3>
<p>数据的存储同网页，可以用 localStorage 实现。但是本应用中不太一样，因为目前还不能支持扫码添加账号， 而且 KaiOS 并没有提供文件选择器直接提取文件，所以说最简单的办法就是把数据文件直接写入 app 内部，等后期有时间再做改进。KaiOS 可以读取 SD 卡，但是需要特别提请授权，可以查看 <a href="https://developer.kaiostech.com/api/device-storage/devicestorage" rel="noopener noreferrer" target="_blank">API 手册</a> 学习。</p>
<p>要访问本应用包内的数据，KaiOS 并没有提供特别的 API ，所以还是要使用传统的 XHR 或者 fetch。为了简单我选择了 fetch。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;data/data.json&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="p">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">out</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// out 就是已经处理好的js对象了
</span></span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><h3 id="方向键滚屏">方向键滚屏</h3>
<p>方向键滚屏其实是这中间比较重要的一件事，如果不滚动，方向键一路滚动选择条往下，最后会走出屏幕。这样会导致屏幕区域外的看不到，影响使用体验。所以需要记录滚动位置，点亮对应的 Item，核心代码就是这样的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">selectIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">mainlist</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;authcodes&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">selectItemByIndex</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">[].</span><span class="nx">forEach</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="nx">mainlist</span><span class="p">.</span><span class="nx">children</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nx">activeElem</span> <span class="o">=</span> <span class="nx">mainlist</span><span class="p">.</span><span class="nx">children</span><span class="p">[</span><span class="nx">selectIndex</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="nx">activeElem</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">activeElem</span><span class="p">.</span><span class="nx">scrollIntoViewIfNeeded</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;keydown&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">switch</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="s1">&#39;ArrowUp&#39;</span><span class="o">:</span> <span class="c1">//scroll up
</span></span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="s1">&#39;ArrowLeft&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">            <span class="nx">selectIndex</span><span class="o">--</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">selectIndex</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="nx">selectIndex</span> <span class="o">=</span> <span class="nx">authcodes</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="nx">selectItemByIndex</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="s1">&#39;ArrowDown&#39;</span><span class="o">:</span> <span class="c1">//scroll down
</span></span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="s1">&#39;ArrowRight&#39;</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">            <span class="nx">selectIndex</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">selectIndex</span> <span class="o">&gt;</span> <span class="p">(</span><span class="nx">authcodes</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span> <span class="nx">selectIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="nx">selectItemByIndex</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p><code>scrollIntoViewIfNeeded</code> 是一个非标的方法，是 WebKit 世界的东西。因为 KaiOS 的渲染引擎是 Gecko，所以这个其实是没有用的。所以我们要单独找一个 Polyfill 来实现这个功能。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// scrollIntoView polyfill
</span></span></span><span class="line"><span class="cl"><span class="c1">// from https://gist.github.com/KilianSSL/774297b76378566588f02538631c3137
</span></span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">Element</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">scrollIntoViewIfNeeded</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Element</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">scrollIntoViewIfNeeded</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">centerIfNeeded</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">centerIfNeeded</span> <span class="o">=</span> <span class="nx">arguments</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span> <span class="o">?</span> <span class="kc">true</span> <span class="o">:</span> <span class="o">!!</span><span class="nx">centerIfNeeded</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nx">parent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">parentComputedStyle</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">getComputedStyle</span><span class="p">(</span><span class="nx">parent</span><span class="p">,</span> <span class="kc">null</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="nx">parentBorderTopWidth</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">parentComputedStyle</span><span class="p">.</span><span class="nx">getPropertyValue</span><span class="p">(</span><span class="s1">&#39;border-top-width&#39;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">            <span class="nx">parentBorderLeftWidth</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">parentComputedStyle</span><span class="p">.</span><span class="nx">getPropertyValue</span><span class="p">(</span><span class="s1">&#39;border-left-width&#39;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">            <span class="nx">overTop</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">offsetTop</span> <span class="o">-</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">offsetTop</span> <span class="o">&lt;</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">scrollTop</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">overBottom</span> <span class="o">=</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">offsetTop</span> <span class="o">-</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">offsetTop</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientHeight</span> <span class="o">-</span> <span class="nx">parentBorderTopWidth</span><span class="p">)</span> <span class="o">&gt;</span> <span class="p">(</span><span class="nx">parent</span><span class="p">.</span><span class="nx">scrollTop</span> <span class="o">+</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">clientHeight</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="nx">overLeft</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">offsetLeft</span> <span class="o">-</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">offsetLeft</span> <span class="o">&lt;</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">scrollLeft</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">overRight</span> <span class="o">=</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">offsetLeft</span> <span class="o">-</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">offsetLeft</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientWidth</span> <span class="o">-</span> <span class="nx">parentBorderLeftWidth</span><span class="p">)</span> <span class="o">&gt;</span> <span class="p">(</span><span class="nx">parent</span><span class="p">.</span><span class="nx">scrollLeft</span> <span class="o">+</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">clientWidth</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="nx">alignWithTop</span> <span class="o">=</span> <span class="nx">overTop</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">overBottom</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">((</span><span class="nx">overTop</span> <span class="o">||</span> <span class="nx">overBottom</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">centerIfNeeded</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">parent</span><span class="p">.</span><span class="nx">scrollTop</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">offsetTop</span> <span class="o">-</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">offsetTop</span> <span class="o">-</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">clientHeight</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">-</span> <span class="nx">parentBorderTopWidth</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientHeight</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">((</span><span class="nx">overLeft</span> <span class="o">||</span> <span class="nx">overRight</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">centerIfNeeded</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">parent</span><span class="p">.</span><span class="nx">scrollLeft</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">offsetLeft</span> <span class="o">-</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">offsetLeft</span> <span class="o">-</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">clientWidth</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">-</span> <span class="nx">parentBorderLeftWidth</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">clientWidth</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">((</span><span class="nx">overTop</span> <span class="o">||</span> <span class="nx">overBottom</span> <span class="o">||</span> <span class="nx">overLeft</span> <span class="o">||</span> <span class="nx">overRight</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">centerIfNeeded</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">this</span><span class="p">.</span><span class="nx">scrollIntoView</span><span class="p">(</span><span class="nx">alignWithTop</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>简单来说，就是计算边距然后判断是否在可视范围内，不在就滚动到对应的位置。一般的 <code>scrollIntoView</code> 就只能做到上对齐，但是<code>scrollIntoViewIfNeeded</code>可以做到根据距离对齐最近的一边。这样列表的滚动体验和传统的S60手机接近了。</p>
<h3 id="安装和使用">安装和使用</h3>
<ol>
<li>
<p>下载 KaiAuth 项目</p>
</li>
<li>
<p>编辑 <code>data/data.json</code>，把你的名字和密钥写进去。（注意，显示的顺序就是你JSON中写的顺序）</p>
</li>
<li>
<p>打开 WebIDE，加载 KaiAuth 项目</p>
</li>
<li>
<p>按照前文所述开启手机调试模式</p>
</li>
<li>
<p>点击 三角形 播放键直接下载到手机中</p>
</li>
</ol>
<p>这样一个能用的 KaiAuth 就装好了。</p>
<h1 id="后记">后记</h1>
<h2 id="感受">感受</h2>
<p>KaiOS 本身可玩性不足，而且 2720 价格很高。但是它可以使用 HTML5 编写 app，前提是你有足够的前端经验。</p>
<p>由于官方没有给任何内置的UI或者控件库，所以有种给你个 GDI 句柄自己画界面的感觉，甚至要自己处理消息循环响应控件按键，感觉非常的原始。</p>
<p>KaiAuth 为了实现简单，其实是不可用的状态，因为数据从 app 内部加载。如果未来有时间的话，我会给它更新扫描二维码添加和导入导出功能。这样不仅仅需要实现拍照，也要自己实现多 activity 切换，而且导入导出也要自己实现一套文件选择逻辑。目前看来，并没有相关的第三方库可以用。想要实现这些功能，基础控件和页面切换还是要自己整的。</p>
<h2 id="相关链接">相关链接</h2>
<ul>
<li><a href="https://www.nokia.com/phones/zh_int/nokia-2720" rel="noopener noreferrer" target="_blank">诺基亚 2720 官方主页</a></li>
<li><a href="https://www.fortress.com.hk/zt/product/2720-flip-mobile-phone/p/BP_11914043" rel="noopener noreferrer" target="_blank">丰泽 2720 信息页面</a></li>
<li><a href="https://developer.kaiostech.com/" rel="noopener noreferrer" target="_blank">KaiOS 官方开发者门户</a></li>
<li><a href="https://developer.kaiostech.com/getting-started/build-your-first-package-app/sample-code" rel="noopener noreferrer" target="_blank">官方的 ToDo Demo</a></li>
<li><a href="https://sites.google.com/view/bananahackers" rel="noopener noreferrer" target="_blank">BananaHackers</a></li>
<li><a href="https://developer.android.com/studio/releases/platform-tools.html#download" rel="noopener noreferrer" target="_blank">独立版 ADB</a></li>
<li><a href="https://rm-eu.palemoon.org/release/palemoon-28.6.1.win64.installer.exe" rel="noopener noreferrer" target="_blank">Pale Moon 28.6.1 下载</a></li>
<li><a href="https://github.com/AdrianMachado/KaiUI" rel="noopener noreferrer" target="_blank">KaiUI</a></li>
<li><a href="https://github.com/kaios-design/create-kaios-app" rel="noopener noreferrer" target="_blank">create-kaios-app</a></li>
<li><a href="https://github.com/kaios-design/kaid" rel="noopener noreferrer" target="_blank">Kaid</a></li>
<li><a href="https://developer.kaiostech.com/core-developer-topics/dpad-navigation" rel="noopener noreferrer" target="_blank">官方五维导航键教程</a></li>
<li><a href="https://otplib.yeojz.dev/" rel="noopener noreferrer" target="_blank">otplib</a></li>
<li><a href="https://developer.kaiostech.com/api/device-storage/devicestorage" rel="noopener noreferrer" target="_blank">存储 API 手册</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Hugo 博客折腾记</title>
      <link>https://blog.zjyl1994.com/post/hugo-nginx-githook/</link>
      <pubDate>Wed, 06 Nov 2019 22:42:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/hugo-nginx-githook/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;五一期间折腾了一下服务器的 Docker 化，踩了好多的坑，写文记录下来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;别小看这篇文章，我已经折腾两天了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;起因&#34;&gt;起因&lt;/h1&gt;
&lt;p&gt;本来想搞个第二博客写点非技术性的东西，经过一系列纠结，选定了 WordPress + Docker-Compose 的部署方案，本来教程都快写完了，但是 WordPress 死活拉不起来，昨天整整调试了一天，最后放弃。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>五一期间折腾了一下服务器的 Docker 化，踩了好多的坑，写文记录下来。</p>
</blockquote>
<blockquote>
<p>别小看这篇文章，我已经折腾两天了</p>
</blockquote>
<h1 id="起因">起因</h1>
<p>本来想搞个第二博客写点非技术性的东西，经过一系列纠结，选定了 WordPress + Docker-Compose 的部署方案，本来教程都快写完了，但是 WordPress 死活拉不起来，昨天整整调试了一天，最后放弃。</p>
<p>后来选定了 Hugo + GitHub Web hook + Caddy 这种备选方案，因为我现在的博客就是这么搭建的。由于我不想开源，所以选了 GitHub 的私有仓库，一切都很美好。但是 Caddy 的 Git 插件有 bug，并不能处理 私有仓库这种 <code>git@github.com:username/example.git</code> 的地址，相关讨论 issue 在这里： <a href="https://github.com/abiosoft/caddy-git/issues/106" rel="noopener noreferrer" target="_blank">https://github.com/abiosoft/caddy-git/issues/106</a> 。简单来说就是标准库更新之后这个插件处理 ssh 式仓库地址的功能就炸了，虽然有人提 fix，但是开发者没有 merge。所以只能用最原始的办法了：Nginx+部署脚本+Web hook handler。</p>
<h1 id="教程">教程</h1>
<h2 id="nginx-安装">Nginx 安装</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 先安装 epel 源，才能安装 nginx</span>
</span></span><span class="line"><span class="cl">yum install epel-release
</span></span><span class="line"><span class="cl"><span class="c1"># 装 nginx</span>
</span></span><span class="line"><span class="cl">yum install nginx
</span></span><span class="line"><span class="cl"><span class="c1"># 启动nginx+自启</span>
</span></span><span class="line"><span class="cl">systemctl start nginx
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> nginx
</span></span></code></pre></div><p>安装后需要简单的配置，主要就是网站的 root 指定到 <code>/var/www</code> 目录中，当然你也可以指定到其他地方。</p>
<h2 id="部署脚本">部署脚本</h2>
<p>部署脚本是最重要的，去 Github 拉数据，送到 hugo 编译，把编译后的内容放到 nginx 的 web root 目录中。</p>
<p>我的部署脚本是这样的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">cd</span> story_site
</span></span><span class="line"><span class="cl">git pull --recurse-submodules
</span></span><span class="line"><span class="cl">hugo --cleanDestinationDir --destination<span class="o">=</span>/var/www
</span></span></code></pre></div><p>写完部署脚本可以试一下，执行完毕之后应该就可以在浏览器中看到对应的博客页面了。</p>
<h2 id="自动更新">自动更新</h2>
<p>现在的更新是每次搞完了手动调用 <code>deploy.sh</code> , 自动更新非常有必要。（因为 Caddy 版可以做到这一切）</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wget https://github.com/adnanh/webhook/releases/download/2.6.10/webhook-linux-amd64.tar.gz
</span></span><span class="line"><span class="cl">tar -xvf webhook*.tar.gz
</span></span><span class="line"><span class="cl">mv webhook-linux-amd64/webhook /usr/local/bin
</span></span><span class="line"><span class="cl">rm -rf webhook-linux-amd64*
</span></span></code></pre></div><p>新建一个hook描述文件 <code>webhook.json</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;redeploy-webhook&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;execute-command&#34;</span><span class="p">:</span> <span class="s2">&#34;/root/deploy.sh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;command-working-directory&#34;</span><span class="p">:</span> <span class="s2">&#34;/root&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>然后运行  <code>webhook -hooks webhook.json -port=10086</code> 端口写什么都可以</p>
<p>然后去 GitHub 上设置 Webhook 就可以了。</p>
<p>Webhook 地址就是这样的 <code>http://ipaddress:10086/hooks/redeploy-webhook</code></p>
<h1 id="结语">结语</h1>
<p>折腾了两天，我本身没想到搞一个第二博客会这么麻烦，中间 debug 耗费太多时间。现在看来 PHP 的 WordPress 已经不适用于轻量级网站了，Caddy 也有 bug，等待修复把。（服务器上的 Caddy 还留着，等官方修复了）</p>
]]></content:encoded>
    </item>
    <item>
      <title>10月乱想</title>
      <link>https://blog.zjyl1994.com/post/201910-think/</link>
      <pubDate>Fri, 25 Oct 2019 23:16:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/201910-think/</guid>
      <description>&lt;p&gt;不知不觉十月份就要结束了，经济形势越来越差，很多公司都在裁员过冬。&lt;/p&gt;
&lt;p&gt;很不幸的是，我也被裁了。&lt;/p&gt;
&lt;p&gt;其实一切都是可以预料到的，从去年年底福利缩水，聚餐减少，到今年一次团建没有，外加老板各种电话出去要债，技术部门停止招新。
不知道他们有没有，我反正做好了被裁员的准备。全组在 Java 背景的组长领导下集体转向 SpringBoot 做困兽挣扎，我这个写 Go 的留下来一点用处没有。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>不知不觉十月份就要结束了，经济形势越来越差，很多公司都在裁员过冬。</p>
<p>很不幸的是，我也被裁了。</p>
<p>其实一切都是可以预料到的，从去年年底福利缩水，聚餐减少，到今年一次团建没有，外加老板各种电话出去要债，技术部门停止招新。
不知道他们有没有，我反正做好了被裁员的准备。全组在 Java 背景的组长领导下集体转向 SpringBoot 做困兽挣扎，我这个写 Go 的留下来一点用处没有。</p>
<p>说起来也是造化弄人，中途公司接了一个微信二次开发的，如果做好了也能缓解一下经济困难，硬着头皮捡起多年不写的 C# WinForm 肝了一个星期，
等我搞完了测试都过了，结果正式上线前一天微信接口让腾讯封了。也许公司注定赚不到这份钱。</p>
<p>2019.9.16 拿到离职证明，拿到裁员协议，和 HR 告别，背上包，离开公司。</p>
<p>舒舒服服的玩了一个月，也该准备准备迎接新生活了。</p>
<p>（另外，物价一直在涨，村里吃粉都加一块钱了，猪脚饭叉烧饭之类的的更是要加两三元。）</p>
<p>2019.10.25 睡不着的夜晚</p>
]]></content:encoded>
    </item>
    <item>
      <title>GPD MicroPC 使用感受</title>
      <link>https://blog.zjyl1994.com/post/gpdmicropc/</link>
      <pubDate>Tue, 27 Aug 2019 23:29:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/gpdmicropc/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;四月还是五月的一天，无聊的我在地铁上刷今日头条，突然看到一个文章，说 GPD 又出新玩具了（准确的说是生产力工具）。一看价格也不贵，京东众筹 1999。于是买了一台，算是了却心愿。&lt;/p&gt;
&lt;p&gt;说起来我对袖珍电脑有兴趣不是一两天了，一个是大学上课带电脑偷偷玩东西，电脑巨大无比不好藏，而且真的很重。大学用的昭阳 K29，虽然是台 12 寸的笔记本，但是由于巨大的 9 芯电池，
重量不输一般的 15 寸笔记本，每天上课十分健身。所以一直以来都想要小型化的电脑。&lt;/p&gt;
&lt;p&gt;上班后，在北京的公司里接了一个做嵌入式设备的项目，然后做完了需要出现场调试设备。那个设备本质上是个工控机，但是没有屏幕。需要用串口线连进去 tty 控制，所以大笔记本也是麻烦。
那个时候就看到了 GPD Pocket ，没记错的话 3300，咬着牙买的。那个时候每个月到手也就6k，为了这个分期+吃土三个月。体验嘛，真的很方便，现场设备很小挂在墙上，
笔记本的话需要拖着很难搞。而这种便携设备只需要放在盒子里，连好线敲就是了。&lt;/p&gt;
&lt;p&gt;那个嵌入式项目结束后，pocket 就一直在吃灰。究其原因，铝合金外壳，漂亮，但是烫手，ATOM 处理器真的很热。而且性能对于电脑软件真的稀烂，幸好我只用 XShell。Chrome总会无端崩溃，
估计也是性能的锅。而且，只有一个标准尺寸 USB 口，接了鼠标就没法弄U盘，为了 pocket 还买了个宜丽客马克鼠，就为了不占住 USB 口。（顺带一提，我的马克鼠也在吃灰）&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<p>四月还是五月的一天，无聊的我在地铁上刷今日头条，突然看到一个文章，说 GPD 又出新玩具了（准确的说是生产力工具）。一看价格也不贵，京东众筹 1999。于是买了一台，算是了却心愿。</p>
<p>说起来我对袖珍电脑有兴趣不是一两天了，一个是大学上课带电脑偷偷玩东西，电脑巨大无比不好藏，而且真的很重。大学用的昭阳 K29，虽然是台 12 寸的笔记本，但是由于巨大的 9 芯电池，
重量不输一般的 15 寸笔记本，每天上课十分健身。所以一直以来都想要小型化的电脑。</p>
<p>上班后，在北京的公司里接了一个做嵌入式设备的项目，然后做完了需要出现场调试设备。那个设备本质上是个工控机，但是没有屏幕。需要用串口线连进去 tty 控制，所以大笔记本也是麻烦。
那个时候就看到了 GPD Pocket ，没记错的话 3300，咬着牙买的。那个时候每个月到手也就6k，为了这个分期+吃土三个月。体验嘛，真的很方便，现场设备很小挂在墙上，
笔记本的话需要拖着很难搞。而这种便携设备只需要放在盒子里，连好线敲就是了。</p>
<p>那个嵌入式项目结束后，pocket 就一直在吃灰。究其原因，铝合金外壳，漂亮，但是烫手，ATOM 处理器真的很热。而且性能对于电脑软件真的稀烂，幸好我只用 XShell。Chrome总会无端崩溃，
估计也是性能的锅。而且，只有一个标准尺寸 USB 口，接了鼠标就没法弄U盘，为了 pocket 还买了个宜丽客马克鼠，就为了不占住 USB 口。（顺带一提，我的马克鼠也在吃灰）</p>
<h1 id="入手">入手</h1>
<p>我也不知道为什么我就突然想买这个。因为我已经没有需要出现场的活了，而且我还买了宏基的11寸笔记本专门做移动的活。两个USB口就很够用，而且大键盘也不错。至少比pocket的奇怪键位舒服很多。
但是，就是心里非常长草，最后决定还是拔掉。所以京东众筹我就参与了，等了一个多月，6月多发货。从南山科技园那边发到这边，中通居然要交换到深圳市总集散中心，也不知道是什么奇怪的玩法。</p>
<p>合盖状态是这样的：</p>
<p><img alt="合盖状态" decoding="async" height="600" loading="eager" src="/post/gpdmicropc/closed.webp" width="800">
</p>
<p>然后打开它是这样的：</p>
<p><img alt="开盖状态" decoding="async" height="600" loading="lazy" src="/post/gpdmicropc/opend.webp" width="800">
</p>
<p>你没有看错，后面插了一排的线。GPD 现在终于弄明白了自己的定位，Windows 机器就是生产力，而不是简单的做一些Windows掌机。
这次主打的买点是工程师必备电脑，接口齐全的令人发指，这也是我买它的原因。</p>
<p><img alt="各种接口" decoding="async" height="606" loading="lazy" src="/post/gpdmicropc/port.webp" width="1200">
</p>
<p>这个小电脑有以下接口：</p>
<ul>
<li>USB Type-A x3</li>
<li>RS232 串口 x1</li>
<li>HDMI x1</li>
<li>USB Type-C x1</li>
<li>RJ45 x1</li>
<li>MicroSD 读卡器 x1</li>
</ul>
<p>是的你没有看错，不是MicroHDMI，是全尺寸的HDMI。RJ45网线口也是标准大小，3个标准尺寸USB端口完全不需要OTG线之类的累赘，还有个RS232端口可以用来处理该死的串口交换机。几乎不需要任何转接头。</p>
<h1 id="简单测评和调教">简单测评和调教</h1>
<p>老套的参数我不想细说，这东西百度 GPD MicroPC 一抓一大把。简单说一下，京东众筹的这版是 8G 内存+ N4100 的，国外版众筹只有 4GB 版本。大内存应对Chrome之类的巨无霸应用还不算吃力。</p>
<p>键盘使用起来呢，肯定没有pocket那种大键盘舒服，但是作为一个应急本，应该没人想在上面用键盘敲长篇大论吧。而且这次的键位还算正常，不像pocket经常按错。</p>
<p>然后这次的散热异常给力，背面巨大的散热窗口，我都能看到风扇本体。外加风扇开关，可以选择关闭让它被动散热，也可以选择让风扇转起来。根据我听声估测得有800-1000转吧，反正高负载的时候风扇确实很有效。</p>
<p>屏幕，不同于pocket，这次GPD MicroPC的不是触屏，导致某些操作就得去操作右上角的那个触摸板，手感还不错，但是我习惯于Mac式的滚屏，我的linux主机和Windows机都改过滚动方向。
到这里就很不适应，中途也搞了很多方法，结果发现它这个触摸板是作为PS/2鼠标接进去的，滚动在BIOS里就处理了。等了一个星期官方发布了翻转滚动的BIOS，刷进去就好了。<strong>痛点解决</strong></p>
<p>还是滚屏相关的，不同于pocket没法手持只能桌子上用，这次的 MicroPC 非常适合端起来使用，我经常使用它在床上举着上网侧着上网。这个时候双指触摸板滚屏就很不现实。解决方案也有，
但这次不是官方的方案了，使用XMouseButton映射鼠标键中键，设置为按下后配合单指触摸板滑动上下滚屏，目前体验超好。<strong>痛点解决</strong></p>
<p>目前来说 GPD MicroPC 是满足了我的需求。从前面的图片你们可以看出，我装了xshell装了vscode和office全家桶。体验嘛，应急打字，修服务器都不在话下。我可以安心的把我的SSH密钥放进去，然后从不离身。随时保持对服务器的控制。像是自己私钥这种东西，放在公司电脑里是很难令人放心的。</p>
<p>我在公司尝试使用这东西办公了一天，期间chrome三四个标签页，除了偶发标签页崩溃其他还好（估计还是cpu孱弱）。然后vscode写Golang的代码，后台Go的工具链频繁分析语法给智能提示啥的，完全没问题。
使用的时候，HDMI接显示器，键盘鼠标一插，再插上电源，完全体的移动办公室。不需要任何转接头。</p>
<h1 id="优缺点和值不值得买">优缺点和值不值得买</h1>
<h2 id="优点">优点</h2>
<ul>
<li>小巧，一巴掌大，6寸屏幕，同事都说像文曲星。</li>
<li>标准 Windows 10 专业版，这东西居然给了套正版win10专业版（虽然绑定机器的）</li>
<li>接口过于齐全，之前买的转换器都没用了。</li>
<li>可自己更换SSD，用的NGFF标准接口。客服群里有人改了512G的，不过发热有点感人。</li>
<li>运行轻度应用毫无压力（你要是指望用这东西P图压片子就不要想了）</li>
<li>PD2.0 TypeC充电口，随便找个PD2的快充就行了，甚至手机充电器支持PD2也成。（推荐Anker超极充，还能当充电宝用，就是原装线太短了）</li>
</ul>
<h2 id="缺点">缺点</h2>
<ul>
<li>电池，差不多4-5小时，也可能和我的应用状态有关，反正没有Pocket电池耐用。</li>
<li>屏幕只有720P，6寸720p有点难受，而且某些exe应用对720px高的电脑不友好，有些沙雕专业软件为了768屏幕做的
最下面一排按钮看不到，需要拖拽。</li>
<li>重，虽然相比正常的电脑轻便很多，但是依然是通勤出门的累赘。（不过是个出差小帮手，可以扔进行李箱或者肩挎包，不占多大空间）</li>
</ul>
<h2 id="值不值得买">值不值得买</h2>
<p>这种小众产品值不值得买还是要看个人，我在B站看测评的时候一帮人都在纠结游戏性能。想想也是，这帮学生每天就是玩游戏自然是游戏最重要，不能打游戏的都是垃圾。
只有上班狗才知道，当你出现场调试设备，站在船上摇摇晃晃，举着一个全尺寸的15.6寸高性能游戏本是多沙比的一幕。
这种小东西作为工作用机，能运行厂商提供的各种诡异土制MFC exe，有串口有网口，只要1999。相比拿着自己心爱的笔记本在河上飘飘荡荡还要举着练臂力改设备的配置文件，
肯定是这种东西超值的很。simple but it works 就够了。</p>
<p>当然，你要是没有特殊需求买了玩玩也是可以的，小尺寸Windows设备我也就见到GPD还在做了，而且主打的都是各色掌机，像Win2配置更高，但是要4k+。这种工作本真的不多见。</p>
<p>如果你是想拿来玩游戏，那么千万别买，这东西不是你的菜，虽然有人尝试用这玩意跑动GTA了，但是那个帧率惨不忍睹你真的能玩下去？有这个钱不如赶紧去买个Switch体验下真正的游戏。</p>]]></content:encoded>
    </item>
    <item>
      <title>OhMyPushBot ———— Telegram 推送 Bot</title>
      <link>https://blog.zjyl1994.com/post/ohmypushbot/</link>
      <pubDate>Sat, 06 Jul 2019 23:41:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/ohmypushbot/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;前几天做的小玩具&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;ohmypushbot&#34;&gt;OhMyPushBot&lt;/h1&gt;
&lt;p&gt;Telegram 是个有趣的聊天工具，最好玩的就是它十分开放 Bot 系统。相比于微信公众号这种层层审核还一堆限制，或者 QQ、微信 机器人这种强行扒官方协议的灰色接口，
Telegram 提供的 API 非常简单。你可以用 Bot 管理 Channel，制作好玩的 Bot，这一切只需要和 BotFather 聊聊天，就可以设置并创建一个Bot，完全不需要等待审核。&lt;/p&gt;
&lt;p&gt;制作这个 Bot，最大的用处就是方便一些自动化脚本通知事件结束。一般的场景下，自动化脚本要发送通知，多数使用发送邮件的方案，但是发送邮件需要使用专用的工具。
而且一般的邮件需要自己检查邮件情况，早些年有过黑莓的PushMail服务，几百元一个月的服务费也不是一般人玩得起的。现在好了，Telegram 作为一个免费 IM 工具，
完善的客户端支持（Linux、Windows上都有，各大手机平台也都有），比较实时的推送速度，简直是事件通知最佳的选择。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>前几天做的小玩具</p>
</blockquote>
<h1 id="ohmypushbot">OhMyPushBot</h1>
<p>Telegram 是个有趣的聊天工具，最好玩的就是它十分开放 Bot 系统。相比于微信公众号这种层层审核还一堆限制，或者 QQ、微信 机器人这种强行扒官方协议的灰色接口，
Telegram 提供的 API 非常简单。你可以用 Bot 管理 Channel，制作好玩的 Bot，这一切只需要和 BotFather 聊聊天，就可以设置并创建一个Bot，完全不需要等待审核。</p>
<p>制作这个 Bot，最大的用处就是方便一些自动化脚本通知事件结束。一般的场景下，自动化脚本要发送通知，多数使用发送邮件的方案，但是发送邮件需要使用专用的工具。
而且一般的邮件需要自己检查邮件情况，早些年有过黑莓的PushMail服务，几百元一个月的服务费也不是一般人玩得起的。现在好了，Telegram 作为一个免费 IM 工具，
完善的客户端支持（Linux、Windows上都有，各大手机平台也都有），比较实时的推送速度，简直是事件通知最佳的选择。</p>
<h1 id="如何使用">如何使用</h1>
<p>我已经部署了一个实例，挂在我的服务器上做转发。访问 <a href="https://t.me/ohmypushbot" rel="noopener noreferrer" target="_blank">OhMyPushBot</a>，先和 Bot 聊一下天。</p>
<ol>
<li>和 Bot 聊天，点击 <code>start</code> 按钮</li>
<li>点击命令或者手动输入 <code>/url</code> 命令</li>
<li>Bot 会发送给你一个 URL，像是 <a href="https://ohmypushbot.zjyl1994.com/send?chatid=1234567890&sign=xxxxxxxxxxxxxxxxxxx" rel="noopener noreferrer" target="_blank">https://ohmypushbot.zjyl1994.com/send?chatid=1234567890&sign=xxxxxxxxxxxxxxxxxxx</a></li>
<li>给刚才的 URL 发送一个 HTTP POST 请求，请求Body中的消息会被投递到您和 Bot 的聊天窗口中</li>
<li>消息体正文支持 Markdown 格式，请求成功后接口返回 <code>ok</code> ，否则返回错误消息</li>
</ol>
<h1 id="markdown-支持">Markdown 支持</h1>
<p>消息体支持的 <a href="https://core.telegram.org/bots/api#markdown-style" rel="noopener noreferrer" target="_blank">Markdown 特性</a> ，原文是英文的，简单翻译如下：</p>
<pre>
*粗体*
_斜体_
[行内超链接](http://www.example.com/)
[行内提及用户](tg://user?id=123456789)
`行内代码块`
```代码块
预先格式化过的代码块
```
</pre>
<h1 id="如何实现">如何实现</h1>
<p>原生的 Telegram Bot API 非常好用，只需要你拿到合法的 Bot Token，调用 HTTP 格式的接口，就可以发送消息，也可以用 Webhook 接收用户发来的消息。</p>
<p>这个 Bot 主要使用了两个 API，<code>sendMessage</code> 和 <code>setWebhook</code>,使用方法可以参见 <a href="https://core.telegram.org/bots/api" rel="noopener noreferrer" target="_blank">Telegram Bot API 文档</a>。</p>
<p>首先我先使用 <code>setWebhook</code> 挂了一个 Webhook 到 Telegram 服务器，当 Bot 收到消息的时候，Telegram 会主动调用我们的接口，解析 JSON，
可以得到当前聊天的 ChatID，调用 <code>sendMessage</code> 通过 ChatID 可以发送消息到特定的对话中。
具体实现可以参见开源代码的 <a href="https://github.com/zjyl1994/telegram-push-bot/blob/master/service.go" rel="noopener noreferrer" target="_blank">service.go</a>。</p>
<p>为了防止无聊的人随机生成 ChatID 调用接口发送垃圾消息，我增加了一个简单的参数验证，接口的 sign 部分就是 ChatID 根据 BotToken 进行 HMAC-MD5 之后的结果。
如果不知道的人想伪造 sign，从密码学角度来说几乎不可能。而且 HMAC 的 Key 是 BotToken，每个Bot都不一样，所以十分的安全。</p>
<h1 id="开源代码">开源代码</h1>
<p>为了方便各位审计我的代码，我把代码托管到了 GitHub 上，地址是：<a href="https://github.com/zjyl1994/telegram-push-bot" rel="noopener noreferrer" target="_blank">https://github.com/zjyl1994/telegram-push-bot</a>
欢迎各位大佬和神仙拍砖。</p>]]></content:encoded>
    </item>
    <item>
      <title>几种报表SQL常用的时间分组写法</title>
      <link>https://blog.zjyl1994.com/post/datechar_for_report/</link>
      <pubDate>Sat, 22 Jun 2019 13:50:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/datechar_for_report/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;写了小一个月报表相关的SQL，把最容易忘的记下来&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最近在公司的任务多数是写各种报表，SQL最长的格式化完了有30行，各种 JOIN 和 UNION ALL 拼 N 张表里的数据。其中最常用到的就是按照时间分组。&lt;/p&gt;
&lt;p&gt;假设有如下表&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;员工名(VARCHAR)&lt;/th&gt;
					&lt;th&gt;交易金额(INT)&lt;/th&gt;
					&lt;th&gt;时间(DATETIME)&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;张三&lt;/td&gt;
					&lt;td&gt;66&lt;/td&gt;
					&lt;td&gt;2017-06-23 19:34:00&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;张三&lt;/td&gt;
					&lt;td&gt;166&lt;/td&gt;
					&lt;td&gt;2017-06-23 19:36:00&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;张三&lt;/td&gt;
					&lt;td&gt;566&lt;/td&gt;
					&lt;td&gt;2017-06-23 20:34:00&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;张三&lt;/td&gt;
					&lt;td&gt;934&lt;/td&gt;
					&lt;td&gt;2017-06-24 21:34:00&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;李四&lt;/td&gt;
					&lt;td&gt;661&lt;/td&gt;
					&lt;td&gt;2017-07-01 10:36:00&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;李四&lt;/td&gt;
					&lt;td&gt;83&lt;/td&gt;
					&lt;td&gt;2017-06-25 19:34:00&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;李四&lt;/td&gt;
					&lt;td&gt;23&lt;/td&gt;
					&lt;td&gt;2017-06-26 19:34:00&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果想查某个时间段里每个人的总交易金额报表，大概的 SQL 写完应该是这样的&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;staff_name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SUM&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;money&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;DATE_FORMAT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%Y-%m-%d&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;date_char&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;table_name&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;开始时间&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AND&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;结束时间&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;GROUP&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;staff_name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;date_char&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中最重要的部分就是 date_char ,它决定了时间分组的粒度。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>写了小一个月报表相关的SQL，把最容易忘的记下来</p>
</blockquote>
<p>最近在公司的任务多数是写各种报表，SQL最长的格式化完了有30行，各种 JOIN 和 UNION ALL 拼 N 张表里的数据。其中最常用到的就是按照时间分组。</p>
<p>假设有如下表</p>
<table>
	<thead>
			<tr>
					<th>员工名(VARCHAR)</th>
					<th>交易金额(INT)</th>
					<th>时间(DATETIME)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>张三</td>
					<td>66</td>
					<td>2017-06-23 19:34:00</td>
			</tr>
			<tr>
					<td>张三</td>
					<td>166</td>
					<td>2017-06-23 19:36:00</td>
			</tr>
			<tr>
					<td>张三</td>
					<td>566</td>
					<td>2017-06-23 20:34:00</td>
			</tr>
			<tr>
					<td>张三</td>
					<td>934</td>
					<td>2017-06-24 21:34:00</td>
			</tr>
			<tr>
					<td>李四</td>
					<td>661</td>
					<td>2017-07-01 10:36:00</td>
			</tr>
			<tr>
					<td>李四</td>
					<td>83</td>
					<td>2017-06-25 19:34:00</td>
			</tr>
			<tr>
					<td>李四</td>
					<td>23</td>
					<td>2017-06-26 19:34:00</td>
			</tr>
	</tbody>
</table>
<p>如果想查某个时间段里每个人的总交易金额报表，大概的 SQL 写完应该是这样的</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">`</span><span class="n">staff_name</span><span class="o">`</span><span class="p">,</span><span class="k">SUM</span><span class="p">(</span><span class="o">`</span><span class="n">money</span><span class="o">`</span><span class="p">),(</span><span class="n">DATE_FORMAT</span><span class="p">(</span><span class="o">`</span><span class="n">time</span><span class="o">`</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;%Y-%m-%d&#39;</span><span class="p">))</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">date_char</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="k">table_name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="o">`</span><span class="n">time</span><span class="o">`</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s2">&#34;开始时间&#34;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="o">`</span><span class="n">time</span><span class="o">`</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="s2">&#34;结束时间&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">staff_name</span><span class="p">,</span><span class="n">date_char</span><span class="w">
</span></span></span></code></pre></div><p>其中最重要的部分就是 date_char ,它决定了时间分组的粒度。</p>
<p>常用的时间分组SQL见下表（使用时须将fieldName替换成真实数据库里的字段）</p>
<table>
	<thead>
			<tr>
					<th>粒度</th>
					<th>SQL</th>
					<th>date_char输出格式</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>半小时</td>
					<td><code>(DATE_FORMAT(fieldName - INTERVAL MINUTE(fieldName)%30 MINUTE, '%Y-%m-%d %H:%i:00'))</code></td>
					<td>2019-06-22 13:30:00</td>
			</tr>
			<tr>
					<td>小时</td>
					<td><code>(DATE_FORMAT(fieldName, '%Y-%m-%d %H:00:00'))</code></td>
					<td>2019-06-22 13:00:00</td>
			</tr>
			<tr>
					<td>天</td>
					<td><code>(DATE_FORMAT(fieldName, '%Y-%m-%d'))</code></td>
					<td>2019-06-22</td>
			</tr>
			<tr>
					<td>周</td>
					<td><code>(DATE_FORMAT(fieldName, '%Y(%u)'))</code></td>
					<td>2019(25)</td>
			</tr>
			<tr>
					<td>月</td>
					<td><code>(DATE_FORMAT(fieldName, '%Y-%m'))</code></td>
					<td>2019-06</td>
			</tr>
			<tr>
					<td>年</td>
					<td><code>(DATE_FORMAT(fieldName, '%Y'))</code></td>
					<td>2019</td>
			</tr>
	</tbody>
</table>
<p>使用的时候直接把上面的 SQL 复制出来，修改字段名，然后加上 AS date_char 就好了。</p>
<p>我司早期的报表查询都是用 DATE_FORMAT 把时间转成字符串然后再切割指定长度的（比如天就切10位，月就切7位，年切4位这样的），但是字符串运算永远比直接算数慢，所以说还是我这种方法比较靠谱。</p>
<p>另外要注意，做这种数据报表的时候一定要用 WHERE 提前圈定数据范围，缩小进入 GROUP BY 运算的数据量，要不然一个SQL得跑好久。</p>]]></content:encoded>
    </item>
    <item>
      <title>服务器 Docker 化笔记</title>
      <link>https://blog.zjyl1994.com/post/dockerize/</link>
      <pubDate>Sun, 05 May 2019 23:50:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/dockerize/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;五一期间折腾了一下服务器的 Docker 化，踩了好多的坑，写文记录下来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;起因&#34;&gt;起因&lt;/h1&gt;
&lt;p&gt;起因很简单，五一去玩了一圈，结果到处都是人，所以在家猫着总得折腾点什么。
在公司写东西日常使用 Docker，目前对 Docker 的操作也非常熟悉了，所以总想着把我的服务器 Docker 化&lt;/p&gt;
&lt;p&gt;先说一下我的基础配置，Kimsufi 的 KS-7 主机，i3 处理器，8G 内存，2T 硬盘，100M 带宽。
内存很充裕，就可以放心的折腾 Docker 环境。事实上我最后弄完几个容器，实际服务器占用也就 500M - 700M 左右。&lt;/p&gt;
&lt;h1 id=&#34;重装服务器&#34;&gt;重装服务器&lt;/h1&gt;
&lt;p&gt;由于我的主机已经用了一阵，所以服务器里很多已经抛弃的东西，也有很多正在用的东西。首先要备份数据到其他地方，我还有其它的服务器，所以把有用的数据 rsync 送到别的服务器上。&lt;/p&gt;
&lt;p&gt;Kimsufi 登入管理面板，重新安装最新版的 CentOS 7。配置 SSH Key，升级最新软件包和内核。Kimsufi 安装的时候要注意，一定要勾选 &lt;code&gt;use distribution kernel&lt;/code&gt; 选项。
如果没有勾选这个选项，Kimsufi 会给你安装定制版的内核，我不太清楚定制版内核会带来什么优势，但是目前来看只会造成麻烦。所以一定要使用发行版自带的内核。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>五一期间折腾了一下服务器的 Docker 化，踩了好多的坑，写文记录下来。</p>
</blockquote>
<h1 id="起因">起因</h1>
<p>起因很简单，五一去玩了一圈，结果到处都是人，所以在家猫着总得折腾点什么。
在公司写东西日常使用 Docker，目前对 Docker 的操作也非常熟悉了，所以总想着把我的服务器 Docker 化</p>
<p>先说一下我的基础配置，Kimsufi 的 KS-7 主机，i3 处理器，8G 内存，2T 硬盘，100M 带宽。
内存很充裕，就可以放心的折腾 Docker 环境。事实上我最后弄完几个容器，实际服务器占用也就 500M - 700M 左右。</p>
<h1 id="重装服务器">重装服务器</h1>
<p>由于我的主机已经用了一阵，所以服务器里很多已经抛弃的东西，也有很多正在用的东西。首先要备份数据到其他地方，我还有其它的服务器，所以把有用的数据 rsync 送到别的服务器上。</p>
<p>Kimsufi 登入管理面板，重新安装最新版的 CentOS 7。配置 SSH Key，升级最新软件包和内核。Kimsufi 安装的时候要注意，一定要勾选 <code>use distribution kernel</code> 选项。
如果没有勾选这个选项，Kimsufi 会给你安装定制版的内核，我不太清楚定制版内核会带来什么优势，但是目前来看只会造成麻烦。所以一定要使用发行版自带的内核。</p>
<h2 id="升级内核">升级内核</h2>
<p>升级内核很简单，因为有秋水逸冰大佬写的<a href="https://teddysun.com/489.html" rel="noopener noreferrer" target="_blank">一键 BBR 脚本</a>。他的一键 BBR 脚本本质上就是给你的服务器升级到 4.12 以上版本带 BBR 的内核。所以说拿来使用非常方便。</p>
<p><code>wget --no-check-certificate https://github.com/teddysun/across/raw/master/bbr.sh &amp;&amp; chmod +x bbr.sh &amp;&amp; ./bbr.sh</code></p>
<p>一条命令，按照提示按y，最后会重启应用最新的内核。（目前能安装的最新内核是 Linux 5.0）</p>
<h1 id="配置-docker-环境">配置 Docker 环境</h1>
<h2 id="安装-docker-ce">安装 Docker CE</h2>
<p>安装 Docker CE 并配置自启动</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">yum install -y yum-utils device-mapper-persistent-data lvm2
</span></span><span class="line"><span class="cl">yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
</span></span><span class="line"><span class="cl">yum install docker-ce docker-ce-cli containerd.io
</span></span><span class="line"><span class="cl">systemctl start docker
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> docker
</span></span></code></pre></div><p>运行完上面这些命令的话，Docker 环境也应该装好了。如果你想试试 Docker 的话，可以输入 <code>docker run hello-world</code>，看一下 Docker 的 Hello World。</p>
<h2 id="容器规划">容器规划</h2>
<p>一个好的服务器环境需要提前规划。</p>
<table>
	<thead>
			<tr>
					<th>容器</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>caddy</td>
					<td>Web 服务器，反代所有 Web 服务</td>
			</tr>
			<tr>
					<td>portainer</td>
					<td>图形化的 Docker 管理面板</td>
			</tr>
			<tr>
					<td>mariadb</td>
					<td>MySQL 服务器 (个人倾向 mariadb 分支)</td>
			</tr>
			<tr>
					<td>phpmyadmin</td>
					<td>图形化的 MySQL 管理面板</td>
			</tr>
			<tr>
					<td>registry</td>
					<td>Docker 镜像的私有仓库</td>
			</tr>
			<tr>
					<td>blog</td>
					<td>我的博客服务</td>
			</tr>
	</tbody>
</table>
<h2 id="配置-docker-的网络环境">配置 Docker 的网络环境</h2>
<p>要想 Docker 容器之间愉快的互联，需要设置一个 Docker 网络。(本文中用 xxcloud 作为网络名)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker network create xxcloud
</span></span></code></pre></div><h2 id="配置-portainer">配置 Portainer</h2>
<p>Portainer 是一个非常好用的 Docker 面板，要启动这个非常简单。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -d  --restart<span class="o">=</span>always -v /var/run/docker.sock:/var/run/docker.sock --name portainer --network xxcloud portainer/portainer
</span></span></code></pre></div><p>（由于我打算使用 Caddy 做反代，所以此处不暴露端口给外部，如果需要的话，自己添加 -p 参数映射外网端口）</p>
<p>非常神奇的是，Docker 容器中的面板可以操作 Docker 自身，只需要映射 docker.sock 文件到容器中就行了。</p>
<h2 id="配置-caddy">配置 Caddy</h2>
<h3 id="安装-caddy">安装 Caddy</h3>
<p>Caddy 是一个非常好用的 Web 服务器，支持自动 ACME 申请 Let&rsquo;s Encrypt 的 TLS 证书。很多东西都需要强制 HTTPS，如果使用传统的 Nginx，你需要去申请，然后拿到证书回来安装证书，每隔 90 天再手动去换证书。如果你不想手动换证书，市面能买到的证书也差不多需要1年一换。</p>
<p>使用 Caddy 就很简单了，给一个邮箱名字然后它自己会去处理这些麻烦事。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /home/data/caddy
</span></span><span class="line"><span class="cl">docker run -d -p 80:80 -p 443:443 --restart<span class="o">=</span>always --name caddy  --network xxcloud -e <span class="nv">ACME_AGREE</span><span class="o">=</span><span class="nb">true</span> -v /home/data/caddy/caddyfile:/etc/Caddyfile -v /home/data/caddy/srv:/srv -v /home/data/caddy/certs:/root/.caddy abiosoft/caddy
</span></span></code></pre></div><p>Caddy 需要映射 80 和 443 到外网，因为它负责反代所有服务，作为整个的入口。</p>
<p>映射功能说明：</p>
<table>
	<thead>
			<tr>
					<th>实体机上的路径</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>/home/data/caddy/caddyfile</td>
					<td>配置文件 Caddyfile</td>
			</tr>
			<tr>
					<td>/home/data/caddy/srv</td>
					<td>存放网站文件</td>
			</tr>
			<tr>
					<td>/home/data/caddy/certs</td>
					<td>存放 Caddy 获取到的证书文件</td>
			</tr>
	</tbody>
</table>
<p>80 和 443 都需要自己在 Firewalld 里打开端口，具体如何可以自己搜索。</p>
<h3 id="配置-caddy-1">配置 Caddy</h3>
<p>修改 Caddyfile：(注意，你需要把 example.com 换成你自己的域名，具体Caddy写法可以参考Caddy教程)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Caddyfile" data-lang="Caddyfile"><span class="line"><span class="cl"><span class="gh">portainer.example.com</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">tls</span> <span class="s">email@example.com</span>
</span></span><span class="line"><span class="cl">	<span class="k">gzip</span>
</span></span><span class="line"><span class="cl">	<span class="k">proxy</span> <span class="s">/</span> <span class="n">http://portainer</span><span class="p">:</span><span class="mi">9000</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="安装-mariadb">安装 MariaDB</h2>
<p>日常开发中 MySQL 是非常重要的一个东西，我个人倾向于 MariaDB。MySQL 被 Oracle 收购以后，MySQL 的原作者在最后一个自己的版本上起了新的分支，开发了MariaDB。MariaDB 和 MySQL 完全兼容，所有的教程都能用，而且也不用怕 Oracle 搞事。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /home/data/mysql
</span></span><span class="line"><span class="cl">docker run --name mariadb  --restart<span class="o">=</span>always --network xxcloud -v /home/data/mysql:/var/lib/mysql -e <span class="nv">MYSQL_ROOT_PASSWORD</span><span class="o">=</span>REPLACE_IT_WITH_YOU_ROOT_PASSWORD -d mariadb:latest
</span></span></code></pre></div><p>运行完上面的命令，MariaDB 就拉起来了。MariaDB 的数据会存在 /home/data/mysql 里。</p>
<p><strong>注意：</strong>  MYSQL_ROOT_PASSWORD 这个密码一定要换成自己的，而且，这个仅限于第一次运行时设置。如果你的 /home/data/mysql 里有现成的 MySQL 数据，那么容器会使用现成数据里的密码。</p>
<h2 id="phpmyadmin">phpMyAdmin</h2>
<h3 id="安装-phpmyadmin">安装 phpMyAdmin</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /home/data/phpmyadmin/themes
</span></span><span class="line"><span class="cl">docker run --name phpmyadmin  --restart<span class="o">=</span>always --network xxcloud -e <span class="nv">PMA_HOST</span><span class="o">=</span>mariadb -e <span class="nv">PMA_ABSOLUTE_URI</span><span class="o">=</span>https://phpmyadmin.example.com -d -v /home/data/phpmyadmin/config.user.inc.php:/etc/phpmyadmin/config.user.inc.php -v /home/data/phpmyadmin/themes:/var/www/html/themes phpmyadmin/phpmyadmin
</span></span></code></pre></div><p>官方教程十分的坑，并没有说明白实际的运行位置。官方文档说主题需要映射 <code>/www/themes</code>，实际上这个主题文件在 <code>/var/www/html/themes</code>，我费了好大劲才进入容器查到位置。（它使用的是sh，也不在/bin里，只能来回试）</p>
<p>映射功能说明：</p>
<table>
	<thead>
			<tr>
					<th>实体机上的路径</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>/home/data/phpmyadmin/config.user.inc.php</td>
					<td>配置文件</td>
			</tr>
			<tr>
					<td>/home/data/phpmyadmin/themes</td>
					<td>放主题</td>
			</tr>
	</tbody>
</table>
<h3 id="配置-phpmyadmin">配置 phpMyAdmin</h3>
<h4 id="持久化高级设定">持久化高级设定</h4>
<p>phpMyAdmin 的高级设定想要持久化，需要在 MySQL 里建立数据库。新建一个叫 phpmyadmin 的数据库，使用 phpMyAdmin 源码包中的 <code>sql/create_tables.sql</code> 初始化数据表。</p>
<p>编辑 <code>/home/data/phpmyadmin/config.user.inc.php</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;blowfish_secret&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;/Cwzlc:SHQ9}sxGN/0;iuOX1:H3QSbpy&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$i</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;auth_type&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;cookie&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;host&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;mariadb&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;compress&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;AllowNoPassword&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;controlhost&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;controlport&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;controluser&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;phpmyadmin&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;controlpass&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;REPLACE_IT_WITH_YOU_ROOT_PASSWORD&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;pmadb&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;phpmyadmin&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;bookmarktable&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__bookmark&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;relation&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__relation&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;table_info&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__table_info&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;table_coords&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__table_coords&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;pdf_pages&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__pdf_pages&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;column_info&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__column_info&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;history&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__history&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;table_uiprefs&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__table_uiprefs&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;tracking&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__tracking&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;userconfig&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__userconfig&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;recent&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__recent&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;favorite&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__favorite&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;users&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__users&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;usergroups&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__usergroups&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;navigationhiding&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__navigationhiding&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;savedsearches&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__savedsearches&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;central_columns&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__central_columns&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;designer_settings&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__designer_settings&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;Servers&#39;</span><span class="p">][</span><span class="nv">$i</span><span class="p">][</span><span class="s1">&#39;export_templates&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;pma__export_templates&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$cfg</span><span class="p">[</span><span class="s1">&#39;DefaultLang&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;zh&#39;</span><span class="p">;</span>
</span></span></code></pre></div><p>其中 <code>controluser</code> 是你给 phpMyAdmin 存储数据时开的数据库账户，
<code>controlpass</code> 是账户的密码，
<code>pmadb</code> 是数据库名。</p>
<p>要注意的是 <code>blowfish_secret</code> 需要你生成一个自己的密语，如果你不知道如何生成，可以点<a href="https://phpsolved.com/phpmyadmin-blowfish-secret-generator/" rel="noopener noreferrer" target="_blank">这里</a>，每次刷新你都会得到一个新的随机密语。</p>
<h4 id="配置主题">配置主题</h4>
<p>phpMyAdmin 的主题可以在 <a href="https://www.phpmyadmin.net/themes/" rel="noopener noreferrer" target="_blank">https://www.phpmyadmin.net/themes/</a> 这里下载，下载之后可以在 phpMyAdmin 首页主题处切换。</p>
<p><strong>注意：</strong> 下载后的主题需要解压后变成文件夹放在themes文件夹里。由于有 php 文件，所以最好使用官网上的主题，安全性比较高。</p>
<h3 id="通过-caddy-映射外网访问">通过 Caddy 映射外网访问</h3>
<p>编辑前文提到的caddyfile，增加如下内容</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Caddyfile" data-lang="Caddyfile"><span class="line"><span class="cl"><span class="gh">phpmyadmin.example.com</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">tls</span> <span class="s">email@example.com</span>
</span></span><span class="line"><span class="cl">	<span class="k">gzip</span>
</span></span><span class="line"><span class="cl">	<span class="k">proxy</span> <span class="s">/</span> <span class="n">http://phpmyadmin</span><span class="p">:</span><span class="mi">80</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="配置私有-docker-仓库">配置私有 Docker 仓库</h2>
<h3 id="安装-docker-registry">安装 Docker Registry</h3>
<p>安装私有 Docker Registry 很简单，它本身也是一个镜像。</p>
<p>我希望的 Docker Registry 是 HTTPS + 密码保护 的安全私有仓库，所以说首先需要生成密码文件。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /home/data/auth
</span></span><span class="line"><span class="cl">docker run --entrypoint htpasswd registry:2 -Bbn testuser testpassword &gt; /home/data/auth/registry.htpasswd
</span></span></code></pre></div><p>你需要替换上述命令中的 <code>testuser</code> 和 <code>testpassword</code> 为你自己的用户名和密码。</p>
<p>然后用命令 <code>docker container stop registry</code> 停止刚才的 registry 容器</p>
<p>现在，可以拉起真正使用的 registry 了。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /home/data/registry
</span></span><span class="line"><span class="cl">docker run -d --restart<span class="o">=</span>always --name registry --network xxcloud -v /home/data/registry:/var/lib/registry -v /home/data/auth/registry.htpasswd:/auth/htpasswd -e <span class="s2">&#34;REGISTRY_AUTH=htpasswd&#34;</span> -e <span class="s2">&#34;REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm&#34;</span> -e <span class="nv">REGISTRY_AUTH_HTPASSWD_PATH</span><span class="o">=</span>/auth/htpasswd registry:2
</span></span></code></pre></div><p>实体机上的 <code>/home/data/registry</code>，就是存储仓库数据的地方，建议放在空间大的目录下。</p>
<h3 id="配置-caddy-2">配置 Caddy</h3>
<p>编辑 Caddyfile</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Caddyfile" data-lang="Caddyfile"><span class="line"><span class="cl"><span class="gh">registry.example.com</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">tls</span> <span class="s">email@example.com</span>
</span></span><span class="line"><span class="cl">	<span class="k">proxy</span> <span class="s">/</span> <span class="n">http://registry</span><span class="p">:</span><span class="mi">5000</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">transparent</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>特别注意：</strong> 反代 registry 的时候一定要增加 <code>transparent</code> 这个属性，要不然 push 镜像的时候会无限 retry。（我在这上面坑了好久）</p>
<h2 id="配置博客">配置博客</h2>
<p>配置博客可以单独写一篇文章，就不在这写了</p>
<h1 id="后记">后记</h1>
<p>很久没有写过这么长的文章了，我是一个金鱼记忆的人 （毕竟是咸鱼），如果不记下来肯定会忘记。</p>
<p>写这篇博客一个是为了记录我遇到的问题踩到的坑，如果能帮到后来人更好。</p>
<p>服务器 Docker 化之后整洁很多，数据都在 <code>/home/data</code> 里，一目了然，尝鲜新的东西只需要docker拉下来，也不用配置更多更麻烦的东西。而且自制的一些东西只要本地 Docker 能跑起来，push 到线上环境100%也能跑起来，这就是 Docker 的优势。总的来说利大于弊。</p>
<p>而且，我在配置完所有内容后，服务器实际占用空间也就500MB - 700MB。当然 Docker 还是需要大内存的，1G 以下的服务器就不要这么折腾了。</p>]]></content:encoded>
    </item>
    <item>
      <title>Alpine with CGO</title>
      <link>https://blog.zjyl1994.com/post/alpine-cgo/</link>
      <pubDate>Mon, 08 Apr 2019 22:17:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/alpine-cgo/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;今天折腾这个一天，记录下来免得未来踩坑&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;alpine-with-cgo&#34;&gt;Alpine with CGO&lt;/h1&gt;
&lt;p&gt;Golang 很棒，静态编译十分方便。但是，它也不是 100% 静态编译的，因为它需要依赖glibc （ 标准C运行库 ）。&lt;/p&gt;
&lt;p&gt;而 Docker 最常用的 Apline 镜像，使用的是 musl 库，并不能愉快的运行 Go 程序&lt;/p&gt;
&lt;p&gt;网上大部分教程都是教你，&lt;code&gt;CGO_ENABLED=0 go build -a -installsuffix cgo&lt;/code&gt;，使用纯 Go 编译，不用 CGO 链接 glibc ，问题就可以解决了。&lt;/p&gt;
&lt;p&gt;但是最麻烦的问题是，你需要引入 C/C++ 库的时候，你并不能禁用 CGO 。&lt;/p&gt;
&lt;p&gt;幸好 Alpine 有包管理器，所以我们可以很愉快的安装所需要的库。（我之前尝试手动安装glic和libstdc++，很难弄）&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>今天折腾这个一天，记录下来免得未来踩坑</p>
</blockquote>
<h1 id="alpine-with-cgo">Alpine with CGO</h1>
<p>Golang 很棒，静态编译十分方便。但是，它也不是 100% 静态编译的，因为它需要依赖glibc （ 标准C运行库 ）。</p>
<p>而 Docker 最常用的 Apline 镜像，使用的是 musl 库，并不能愉快的运行 Go 程序</p>
<p>网上大部分教程都是教你，<code>CGO_ENABLED=0 go build -a -installsuffix cgo</code>，使用纯 Go 编译，不用 CGO 链接 glibc ，问题就可以解决了。</p>
<p>但是最麻烦的问题是，你需要引入 C/C++ 库的时候，你并不能禁用 CGO 。</p>
<p>幸好 Alpine 有包管理器，所以我们可以很愉快的安装所需要的库。（我之前尝试手动安装glic和libstdc++，很难弄）</p>
<p>默认的 Golang 官方编译环境中，没有我们所需的 GCC 和 G++ 编译环境，所以要先安装 build-base 配置编译环境。</p>
<p>运行镜像我们也使用 Alpine ，由于 Alpine 极为精简，并没有常用的时区、证书等，会导致不可预料的错误。所以我们需要安装这些东西：</p>
<table>
	<thead>
			<tr>
					<th>包名</th>
					<th>用处</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>ca-certificates</td>
					<td>[可选] CA 证书，方便使用 TLS</td>
			</tr>
			<tr>
					<td>tzdata</td>
					<td>[可选] 时区配置，方便 GORM 等需要处理时间的场景</td>
			</tr>
			<tr>
					<td>libc6-compat</td>
					<td>[必选] C 标准库</td>
			</tr>
			<tr>
					<td>libgcc</td>
					<td>[必选] GCC 相关库，CGO 编译的程序会依赖</td>
			</tr>
			<tr>
					<td>libstdc++</td>
					<td>[必选] C++ 标准库</td>
			</tr>
	</tbody>
</table>
<p>完整版 Dockerfile 如下：</p>
<p>（使用这个 Dockerfile 时需要将 <code>github.com/zjyl1994/app</code> 替换为自己的包路径）</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">golang:1.12.2-alpine3.9</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apk --no-cache add build-base<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> . /code<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> mkdir -p /usr/local/go/src/github.com/zjyl1994 <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    ln -s /code /usr/local/go/src/github.com/zjyl1994/app <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="nb">cd</span> /usr/local/go/src/github.com/zjyl1994/app <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="nv">CGO_ENABLED</span><span class="o">=</span><span class="m">1</span> go build -a<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">alpine:latest</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apk --no-cache add tzdata ca-certificates libc6-compat libgcc libstdc++<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>builder /usr/local/go/src/github.com/zjyl1994/app/app /app/app<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;/app/app&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>解决Alpine缺少字体的问题</title>
      <link>https://blog.zjyl1994.com/post/alpine-fontconfig/</link>
      <pubDate>Mon, 11 Feb 2019 16:20:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/alpine-fontconfig/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;2019-02-11 农历己亥年上班第一天&lt;/p&gt;
&lt;p&gt;好久没有写博客了，我这个博客是Hugo静态编译的，写一次还是蛮复杂的，而且上班以后基本没有什么值得写的东西，所以博客基本都在长草。&lt;/p&gt;
&lt;p&gt;立一个FLAG，2019 博客不长草。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;我现在的这家公司基本都在用现代化的开发工具，所有程序写完都在docker环境下运行，我们公司dalao钦定的底包就是alpine。后续的问题都是Alpine没有自带默认字体造成的。如果你想快速知道怎么解决请直接看最后。&lt;/p&gt;
&lt;h1 id=&#34;问题&#34;&gt;问题&lt;/h1&gt;
&lt;p&gt;我遇到的问题是这样的：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.lang.NullPointerException at sun.awt.FontConfiguration.getVersion(FontConfiguration.java:1264)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;Java报错&#34; decoding=&#34;async&#34; height=&#34;451&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/alpine-fontconfig/javaexception.webp&#34; width=&#34;680&#34;&gt;
&lt;/p&gt;
&lt;p&gt;场景是公司买的快速开发框架jeeplus，自带一个数据导入功能，需要下载excel模板，当下载模板时触发NPE。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>2019-02-11 农历己亥年上班第一天</p>
<p>好久没有写博客了，我这个博客是Hugo静态编译的，写一次还是蛮复杂的，而且上班以后基本没有什么值得写的东西，所以博客基本都在长草。</p>
<p>立一个FLAG，2019 博客不长草。</p>
</blockquote>
<h1 id="前言">前言</h1>
<p>我现在的这家公司基本都在用现代化的开发工具，所有程序写完都在docker环境下运行，我们公司dalao钦定的底包就是alpine。后续的问题都是Alpine没有自带默认字体造成的。如果你想快速知道怎么解决请直接看最后。</p>
<h1 id="问题">问题</h1>
<p>我遇到的问题是这样的：</p>
<p><code>java.lang.NullPointerException at sun.awt.FontConfiguration.getVersion(FontConfiguration.java:1264)</code></p>
<p><img alt="Java报错" decoding="async" height="451" loading="eager" src="/post/alpine-fontconfig/javaexception.webp" width="680">
</p>
<p>场景是公司买的快速开发框架jeeplus，自带一个数据导入功能，需要下载excel模板，当下载模板时触发NPE。</p>
<h1 id="追踪问题">追踪问题</h1>
<p>要解决一个问题最好的办法就是去看调用栈。</p>
<p>通过看调用栈，我们可以知道问题出在Excel组件上。</p>
<p>Excel组件试图根据字体大小自动调整单元格宽度，当寻找fontconfig时发现根本找不到，返回一个Null。</p>
<p>拿着<code>java.lang.NullPointerException at sun.awt.FontConfiguration.getVersion(FontConfiguration.java:1264)</code>在谷歌搜索，可以看到很多github的issue，第一条连接 <a href="https://github.com/docker-library/openjdk/issues/73" rel="noopener noreferrer" target="_blank">NullPointerException in Alpine JRE 8 Font #73</a> 就证明了我的猜想，确实是Alpine环境缺少对应的包。此问题影响所有使用官方Alpine的Java环境。</p>
<h1 id="解决方案">解决方案</h1>
<p>在 Dockerfile 中进行修改，增加如下内容:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">ENV</span> LANG en_US.UTF-8<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apk add --update ttf-dejavu fontconfig <span class="o">&amp;&amp;</span> rm -rf /var/cache/apk/*<span class="err">
</span></span></span></code></pre></div><p>通过添加fontconfig和一款字体dejavu，可以完美解决问题。</p>
<p>不过最好的方法还是封装一个镜像，替换掉官方的底包。流水线上每次打包都安装ttf-dejavu还是挺慢的。</p>]]></content:encoded>
    </item>
    <item>
      <title>Ubuntu 18.04 必做操作</title>
      <link>https://blog.zjyl1994.com/post/ubuntu-source/</link>
      <pubDate>Fri, 12 Oct 2018 07:00:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/ubuntu-source/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;新开一个栏目，专门记录开发中常见的命令和操作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;新装完Ubuntu Server之后要做几个操作，才能让他更好用:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更新软件源&lt;/li&gt;
&lt;li&gt;安装必备软件&lt;/li&gt;
&lt;li&gt;设置系统代理&lt;/li&gt;
&lt;/ol&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>新开一个栏目，专门记录开发中常见的命令和操作</p>
</blockquote>
<p>新装完Ubuntu Server之后要做几个操作，才能让他更好用:</p>
<ol>
<li>更新软件源</li>
<li>安装必备软件</li>
<li>设置系统代理</li>
</ol>
<p>首先我们要做的事是更新软件源，默认软件源在美国，拜天朝国情所赐，你科学上网依然很慢。</p>
<p>这里选用中科大的源，USTC的源国内各个地方速度都不错。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vi /etc/apt/sources.list
</span></span></code></pre></div><p>打开的文件按I进入插入模式，在最前面加上如下内容:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">deb https://mirrors.ustc.edu.cn/ubuntu/ bionic main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb https://mirrors.ustc.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb https://mirrors.ustc.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb https://mirrors.ustc.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb https://mirrors.ustc.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
</span></span><span class="line"><span class="cl">deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
</span></span></code></pre></div><p>然后<kbd>ESC</kbd>:wq保存，再输入 <code>sudo apt-get update</code> 使修改的源生效。</p>
<p>下一步是安装常用软件，开发机上git和基础的编译工具得装，要不然缺一些什么以后麻烦。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt-get install -y git build-essential
</span></span></code></pre></div><p>设置系统级代理很简单，只需要设置两个环境变量：</p>
<p>http_proxy 和 https_proxy，为了保险 HTTP_PROXY 和 HTTPS_PROXY 也最好设置上。</p>
<p>格式如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">http_proxy</span><span class="o">=</span>http://127.0.0.1.15000
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">https_proxy</span><span class="o">=</span>http://127.0.0.1.15000
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HTTP_PROXY</span><span class="o">=</span>http://127.0.0.1.15000
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HTTPS_PROXY</span><span class="o">=</span>http://127.0.0.1.15000
</span></span></code></pre></div><p>经过这一番设置基本上 Ubuntu 就可用了，根据需要安装 Python 、 Go 或者 PHP 环境使用即可。</p>]]></content:encoded>
    </item>
    <item>
      <title>Ringbuffer环形缓冲区</title>
      <link>https://blog.zjyl1994.com/post/ringbuffer/</link>
      <pubDate>Sun, 05 Aug 2018 11:53:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/ringbuffer/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;环形缓冲区是很常用的数据结构，用途很广泛。我目前遇到的使用场景就是TCP分片转回TCP流，一个线程读取TCP分片拆包，写入缓存区，另一个线程从缓存区中读取，互相不影响。&lt;/p&gt;
&lt;p&gt;为此Google了一番，发现了kfifo这个Linux内核的实现，十分精妙。&lt;/p&gt;
&lt;h1 id=&#34;原理&#34;&gt;原理&lt;/h1&gt;
&lt;h2 id=&#34;基本原理&#34;&gt;基本原理&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;kfifo&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;char&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;buffer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;    &lt;span class=&#34;cm&#34;&gt;/* the buffer holding the data */&lt;/span&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;    &lt;span class=&#34;cm&#34;&gt;/* the size of the allocated buffer */&lt;/span&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;    &lt;span class=&#34;cm&#34;&gt;/* data is added at offset (in % size) */&lt;/span&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;out&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;    &lt;span class=&#34;cm&#34;&gt;/* data is extracted from off. (out % size) */&lt;/span&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;spinlock_t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;lock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;    &lt;span class=&#34;cm&#34;&gt;/* protects concurrent modifications */&lt;/span&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;先看一下基本定义，buffer是数据缓存区，size是缓存区长度，in/out是输入输出指针位置（%size后就是真实指针了）。&lt;/p&gt;
&lt;p&gt;size需要检测是否为2的次幂，不是的话需要升到2的次幂。方便后续计算。（可以看内核的roundup_pow_of_two实现）&lt;/p&gt;
&lt;p&gt;in、out每次操作都+对应的读写长度，通过取模运算回落size区间。&lt;/p&gt;
&lt;p&gt;data := kfifo.buffer[kfifo.out % kfifo.size:kfifo.in % kfifo.size]&lt;/p&gt;
&lt;p&gt;data就是有效数据的缓存区区间。&lt;/p&gt;
&lt;h2 id=&#34;快速取模&#34;&gt;快速取模&lt;/h2&gt;
&lt;p&gt;kfifo使用了二进制的位运算实现了2的幂取模运算，并且利用了无符号数的溢出做回绕。很精妙的设计。&lt;/p&gt;
&lt;p&gt;首先，缓存区尺寸要取整为2的幂次，这样可以利用位运算进行取模。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;M mod N = M &amp;amp; (N-1)&lt;/code&gt;,当N为2的幂次时有效。&lt;/p&gt;
&lt;p&gt;取模运算对于计算机来说运算速度也是很慢的（虽然你感觉不出来），而位运算就是分分钟的事了。&lt;/p&gt;
&lt;h2 id=&#34;回绕&#34;&gt;回绕&lt;/h2&gt;
&lt;p&gt;无符号数溢出后，就会变成0从头开始，借助这个特性可以绕开逻辑判断。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<p>环形缓冲区是很常用的数据结构，用途很广泛。我目前遇到的使用场景就是TCP分片转回TCP流，一个线程读取TCP分片拆包，写入缓存区，另一个线程从缓存区中读取，互相不影响。</p>
<p>为此Google了一番，发现了kfifo这个Linux内核的实现，十分精妙。</p>
<h1 id="原理">原理</h1>
<h2 id="基本原理">基本原理</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">struct</span> <span class="n">kfifo</span> <span class="p">{</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*</span><span class="n">buffer</span><span class="p">;</span>    <span class="cm">/* the buffer holding the data */</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">size</span><span class="p">;</span>    <span class="cm">/* the size of the allocated buffer */</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">in</span><span class="p">;</span>    <span class="cm">/* data is added at offset (in % size) */</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">out</span><span class="p">;</span>    <span class="cm">/* data is extracted from off. (out % size) */</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">spinlock_t</span> <span class="o">*</span><span class="n">lock</span><span class="p">;</span>    <span class="cm">/* protects concurrent modifications */</span>   
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>先看一下基本定义，buffer是数据缓存区，size是缓存区长度，in/out是输入输出指针位置（%size后就是真实指针了）。</p>
<p>size需要检测是否为2的次幂，不是的话需要升到2的次幂。方便后续计算。（可以看内核的roundup_pow_of_two实现）</p>
<p>in、out每次操作都+对应的读写长度，通过取模运算回落size区间。</p>
<p>data := kfifo.buffer[kfifo.out % kfifo.size:kfifo.in % kfifo.size]</p>
<p>data就是有效数据的缓存区区间。</p>
<h2 id="快速取模">快速取模</h2>
<p>kfifo使用了二进制的位运算实现了2的幂取模运算，并且利用了无符号数的溢出做回绕。很精妙的设计。</p>
<p>首先，缓存区尺寸要取整为2的幂次，这样可以利用位运算进行取模。</p>
<p><code>M mod N = M &amp; (N-1)</code>,当N为2的幂次时有效。</p>
<p>取模运算对于计算机来说运算速度也是很慢的（虽然你感觉不出来），而位运算就是分分钟的事了。</p>
<h2 id="回绕">回绕</h2>
<p>无符号数溢出后，就会变成0从头开始，借助这个特性可以绕开逻辑判断。</p>
<h1 id="go版本实现">Go版本实现</h1>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">RingBuffer</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">data</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">size</span><span class="w"> </span><span class="kt">uint</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">in</span><span class="w">   </span><span class="kt">uint</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">out</span><span class="w">  </span><span class="kt">uint</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">NewRingBuffer</span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="kt">uint</span><span class="p">)</span><span class="w"> </span><span class="nx">RingBuffer</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// if size not pow of 2 round up it</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">))</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">size</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">size</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">size</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">size</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">size</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">size</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="mi">4</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">size</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">size</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="mi">8</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">size</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">size</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="mi">16</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">size</span><span class="o">++</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">rb</span><span class="w"> </span><span class="nx">RingBuffer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">size</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">rb</span><span class="p">.</span><span class="nx">data</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="nx">size</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">rb</span><span class="p">.</span><span class="nx">in</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">rb</span><span class="p">.</span><span class="nx">out</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">rb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">rb</span><span class="w"> </span><span class="o">*</span><span class="nx">RingBuffer</span><span class="p">)</span><span class="w"> </span><span class="nf">Write</span><span class="p">(</span><span class="nx">p</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">n</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">lData</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">uint</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">p</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">lData</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">min</span><span class="p">(</span><span class="nx">lData</span><span class="p">,</span><span class="w"> </span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="o">-</span><span class="nx">rb</span><span class="p">.</span><span class="nx">in</span><span class="o">+</span><span class="nx">rb</span><span class="p">.</span><span class="nx">out</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">l</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">min</span><span class="p">(</span><span class="nx">lData</span><span class="p">,</span><span class="w"> </span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="o">-</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">in</span><span class="o">&amp;</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="o">-</span><span class="mi">1</span><span class="p">)))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">copy</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">rb</span><span class="p">.</span><span class="nx">in</span><span class="o">&amp;</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="o">-</span><span class="mi">1</span><span class="p">):],</span><span class="w"> </span><span class="nx">p</span><span class="p">[:</span><span class="nx">l</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">copy</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">data</span><span class="p">[:</span><span class="nx">lData</span><span class="o">-</span><span class="nx">l</span><span class="p">],</span><span class="w"> </span><span class="nx">p</span><span class="p">[</span><span class="nx">l</span><span class="p">:])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">rb</span><span class="p">.</span><span class="nx">in</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nx">lData</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="nx">lData</span><span class="p">),</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">rb</span><span class="w"> </span><span class="o">*</span><span class="nx">RingBuffer</span><span class="p">)</span><span class="w"> </span><span class="nf">Read</span><span class="p">(</span><span class="nx">p</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">n</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">lData</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">uint</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">p</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">lData</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">min</span><span class="p">(</span><span class="nx">lData</span><span class="p">,</span><span class="w"> </span><span class="nx">rb</span><span class="p">.</span><span class="nx">in</span><span class="o">-</span><span class="nx">rb</span><span class="p">.</span><span class="nx">out</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">l</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">min</span><span class="p">(</span><span class="nx">lData</span><span class="p">,</span><span class="w"> </span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="o">-</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">out</span><span class="o">&amp;</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="o">-</span><span class="mi">1</span><span class="p">)))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">copy</span><span class="p">(</span><span class="nx">p</span><span class="p">,</span><span class="w"> </span><span class="nx">rb</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">rb</span><span class="p">.</span><span class="nx">out</span><span class="o">&amp;</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="o">-</span><span class="mi">1</span><span class="p">):</span><span class="nx">rb</span><span class="p">.</span><span class="nx">out</span><span class="o">&amp;</span><span class="p">(</span><span class="nx">rb</span><span class="p">.</span><span class="nx">size</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span><span class="o">+</span><span class="nx">l</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">copy</span><span class="p">(</span><span class="nx">p</span><span class="p">[</span><span class="nx">l</span><span class="p">:],</span><span class="w"> </span><span class="nx">rb</span><span class="p">.</span><span class="nx">data</span><span class="p">[:</span><span class="nx">lData</span><span class="o">-</span><span class="nx">l</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">rb</span><span class="p">.</span><span class="nx">out</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nx">lData</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="nx">lData</span><span class="p">),</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nb">min</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span><span class="w"> </span><span class="nx">y</span><span class="w"> </span><span class="kt">uint</span><span class="p">)</span><span class="w"> </span><span class="kt">uint</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">x</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">y</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">x</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">y</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h1 id="linux内核的kfifo原版实现">Linux内核的Kfifo原版实现</h1>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">struct</span> <span class="n">kfifo</span> <span class="p">{</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*</span><span class="n">buffer</span><span class="p">;</span>    <span class="cm">/* the buffer holding the data */</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">size</span><span class="p">;</span>    <span class="cm">/* the size of the allocated buffer */</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">in</span><span class="p">;</span>    <span class="cm">/* data is added at offset (in % size) */</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">out</span><span class="p">;</span>    <span class="cm">/* data is extracted from off. (out % size) */</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">spinlock_t</span> <span class="o">*</span><span class="n">lock</span><span class="p">;</span>    <span class="cm">/* protects concurrent modifications */</span>   
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">struct</span> <span class="n">kfifo</span> <span class="o">*</span><span class="nf">kfifo_alloc</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">size</span><span class="p">,</span> <span class="kt">gfp_t</span> <span class="n">gfp_mask</span><span class="p">,</span> <span class="kt">spinlock_t</span> <span class="o">*</span><span class="n">lock</span><span class="p">)</span>   
</span></span><span class="line"><span class="cl"><span class="p">{</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*</span><span class="n">buffer</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">kfifo</span> <span class="o">*</span><span class="n">ret</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/*  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * round up to the next power of 2, since our &#39;let the indices  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * wrap&#39; tachnique works only in this case.  
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span>   
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">size</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span> <span class="p">{</span>   
</span></span><span class="line"><span class="cl">        <span class="nf">BUG_ON</span><span class="p">(</span><span class="n">size</span> <span class="o">&gt;</span> <span class="mh">0x80000000</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">        <span class="n">size</span> <span class="o">=</span> <span class="nf">roundup_pow_of_two</span><span class="p">(</span><span class="n">size</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">    <span class="p">}</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="n">buffer</span> <span class="o">=</span> <span class="nf">kmalloc</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">gfp_mask</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">buffer</span><span class="p">)</span>   
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nf">ERR_PTR</span><span class="p">(</span><span class="o">-</span><span class="n">ENOMEM</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="n">ret</span> <span class="o">=</span> <span class="nf">kfifo_init</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">gfp_mask</span><span class="p">,</span> <span class="n">lock</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">IS_ERR</span><span class="p">(</span><span class="n">ret</span><span class="p">))</span>   
</span></span><span class="line"><span class="cl">        <span class="nf">kfree</span><span class="p">(</span><span class="n">buffer</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">ret</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">unsigned</span> <span class="kt">int</span> <span class="nf">__kfifo_put</span><span class="p">(</span><span class="k">struct</span> <span class="n">kfifo</span> <span class="o">*</span><span class="n">fifo</span><span class="p">,</span>   
</span></span><span class="line"><span class="cl">             <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*</span><span class="n">buffer</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">len</span><span class="p">)</span>   
</span></span><span class="line"><span class="cl"><span class="p">{</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">l</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="n">len</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">len</span><span class="p">,</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">-</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">in</span> <span class="o">+</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">out</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/*  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * Ensure that we sample the fifo-&gt;out index -before- we  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * start putting bytes into the kfifo.  
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="nf">smp_mb</span><span class="p">();</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/* first put the data starting from fifo-&gt;in to buffer end */</span>   
</span></span><span class="line"><span class="cl">    <span class="n">l</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">len</span><span class="p">,</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">-</span> <span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">in</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)));</span>   
</span></span><span class="line"><span class="cl">    <span class="nf">memcpy</span><span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">buffer</span> <span class="o">+</span> <span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">in</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)),</span> <span class="n">buffer</span><span class="p">,</span> <span class="n">l</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/* then put the rest (if any) at the beginning of the buffer */</span>   
</span></span><span class="line"><span class="cl">    <span class="nf">memcpy</span><span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">buffer</span><span class="p">,</span> <span class="n">buffer</span> <span class="o">+</span> <span class="n">l</span><span class="p">,</span> <span class="n">len</span> <span class="o">-</span> <span class="n">l</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/*  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * Ensure that we add the bytes to the kfifo -before-  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * we update the fifo-&gt;in index.  
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="nf">smp_wmb</span><span class="p">();</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">in</span> <span class="o">+=</span> <span class="n">len</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">len</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl"><span class="p">}</span>  
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="kt">unsigned</span> <span class="kt">int</span> <span class="nf">__kfifo_get</span><span class="p">(</span><span class="k">struct</span> <span class="n">kfifo</span> <span class="o">*</span><span class="n">fifo</span><span class="p">,</span>   
</span></span><span class="line"><span class="cl">             <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*</span><span class="n">buffer</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">len</span><span class="p">)</span>   
</span></span><span class="line"><span class="cl"><span class="p">{</span>   
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">l</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="n">len</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">len</span><span class="p">,</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">in</span> <span class="o">-</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">out</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/*  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * Ensure that we sample the fifo-&gt;in index -before- we  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * start removing bytes from the kfifo.  
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="nf">smp_rmb</span><span class="p">();</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/* first get the data from fifo-&gt;out until the end of the buffer */</span>   
</span></span><span class="line"><span class="cl">    <span class="n">l</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">len</span><span class="p">,</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">-</span> <span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">out</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)));</span>   
</span></span><span class="line"><span class="cl">    <span class="nf">memcpy</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">buffer</span> <span class="o">+</span> <span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">out</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">fifo</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)),</span> <span class="n">l</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/* then get the rest (if any) from the beginning of the buffer */</span>   
</span></span><span class="line"><span class="cl">    <span class="nf">memcpy</span><span class="p">(</span><span class="n">buffer</span> <span class="o">+</span> <span class="n">l</span><span class="p">,</span> <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">buffer</span><span class="p">,</span> <span class="n">len</span> <span class="o">-</span> <span class="n">l</span><span class="p">);</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="cm">/*  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * Ensure that we remove the bytes from the kfifo -before-  
</span></span></span><span class="line"><span class="cl"><span class="cm">     * we update the fifo-&gt;out index.  
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="nf">smp_mb</span><span class="p">();</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="n">fifo</span><span class="o">-&gt;</span><span class="n">out</span> <span class="o">+=</span> <span class="n">len</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">len</span><span class="p">;</span>   
</span></span><span class="line"><span class="cl"><span class="p">}</span>   
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>宏碁TMB117测评</title>
      <link>https://blog.zjyl1994.com/post/tmb117/</link>
      <pubDate>Tue, 17 Jul 2018 22:45:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/tmb117/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;2018-07-17夜 深圳 狂风暴雨配闪电&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;买了个摸鱼用的笔记本，宏碁TravelMate B117，使用体验还挺好。&lt;/p&gt;
&lt;p&gt;我的需求很简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能运行Manjaro&lt;/li&gt;
&lt;li&gt;电池耐用&lt;/li&gt;
&lt;li&gt;尺寸小&lt;/li&gt;
&lt;li&gt;便宜&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;选来选去就选到了这个笔记本。基本配置是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intel 奔腾 N3710 处理器&lt;/li&gt;
&lt;li&gt;4GB 内存 / 128GB 闪存&lt;/li&gt;
&lt;li&gt;11.6英寸1366*768屏幕&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最重要的是：这个本子我买的时候特价只要2399，现在2499也不是很贵。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;京东购买页&#34; decoding=&#34;async&#34; height=&#34;558&#34; loading=&#34;eager&#34; src=&#34;https://blog.zjyl1994.com/post/tmb117/jdpage.webp&#34; width=&#34;1224&#34;&gt;
&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>2018-07-17夜 深圳 狂风暴雨配闪电</p>
</blockquote>
<h1 id="前言">前言</h1>
<p>买了个摸鱼用的笔记本，宏碁TravelMate B117，使用体验还挺好。</p>
<p>我的需求很简单：</p>
<ol>
<li>能运行Manjaro</li>
<li>电池耐用</li>
<li>尺寸小</li>
<li>便宜</li>
</ol>
<p>选来选去就选到了这个笔记本。基本配置是这样的：</p>
<ul>
<li>Intel 奔腾 N3710 处理器</li>
<li>4GB 内存 / 128GB 闪存</li>
<li>11.6英寸1366*768屏幕</li>
</ul>
<p>最重要的是：这个本子我买的时候特价只要2399，现在2499也不是很贵。</p>
<p><img alt="京东购买页" decoding="async" height="558" loading="eager" src="/post/tmb117/jdpage.webp" width="1224">
</p>
<p>购买链接：https://item.jd.com/3119258.html</p>
<p><img alt="真机实拍" decoding="async" height="1368" loading="lazy" src="/post/tmb117/laptop.webp" width="1024">
</p>
<p>搭配的45W充电器，内置48Wh电池，充电速度还算可以。</p>
<p><img alt="充电器及指示灯" decoding="async" height="1368" loading="lazy" src="/post/tmb117/charger.webp" width="1024">
</p>
<p>PS：这个指示灯在电脑合盖休眠时会呼吸闪烁，不过并没有多烦人。</p>
<p>PS2：这个电脑无风扇的！而且是固态的！也就是说，没噪音！</p>
<p>PS3：N3710自带AES-NI加持。（Intel的硬件AES加速方案）</p>
<p>PS4：Intel自己家的蓝牙无线方案，Linux免驱。</p>
<p>PSP：没有显卡，Intel核显，Linux免驱。</p>
<h1 id="配置环境">配置环境</h1>
<h2 id="安装manjaro">安装Manjaro</h2>
<p>买这个本子最重要的用途就是使用实体机安装Manjaro了，首先先去下载XFCE版的Manjaro。</p>
<p>下载完Manjaro的ISO镜像后，需要使用Rufus的DD模式进行U盘制作。</p>
<p>由于这个电脑预装了一个WIndows10家庭版，所以先要把它请出去。首先需要去Win10里重启电脑到UEFI设置界面，然后关闭UEFI的SecureBoot功能。这样才能在UEFI模式下启动Manjaro安装程序。</p>
<p>**注意：**默认情况下是不给你操作SecureBoot的选项的，默认Enabled。你需要设置Supervisor Password之后才可以激活SecureBoot选项。如果怕忘了密码可以选择关闭SecureBoot后再取消Supervisor Password。（设置的时候新密码和确认新密码直接回车就是取消）</p>
<p>安装过程不再赘述，网上有不少Manjaro安装教程。相比ArchLinux本身，Manjaro有自己的图形化安装界面简单太多了。要注意，N3710这颗处理器居然是有AES-NI加持的，也就是说加密性能会有Intel的定向优化加持。所以说全盘加密等特性想开就开，不用担心影响性能。</p>
<p>日常使用情况，4GB内存占用1.3G左右，主要是浏览器。开机通过free命令查看内存消耗，大约350MB左右。（XFCE环境）</p>
<h2 id="安装基本开发工具">安装基本开发工具</h2>
<p>我这个本子主要做golang开发，所以说golang环境和vscode是必不可少的。由于这个电脑CPU的孱弱性能，不建议上来就用Goland之类的巨型IDE，这种IDE还是在正规的台式开发机上使用为好。</p>
<ul>
<li>安装Go环境  <code>pacman -S go</code></li>
<li>安装Yaourt <code>pacman -S yaourt</code></li>
<li>安装Vscode <code>yaourt visual studio code binary</code> 选第一个aur上的包就可以了</li>
</ul>
<h2 id="安装fcitx">安装Fcitx</h2>
<p>中文输入法很烦人，我也是折腾到现在才把输入法搞定。</p>
<p>首先要装一堆输入法的依赖：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pacman -S fcitx fcitx-configtool fcitx-qt4 fcitx-qt5 fcitx-gtk2 fcitx-gtk3
</span></span></code></pre></div><p>然后根据你的喜好安装对应的输入法，我个人比较喜欢sunpinyin，这是早年用Ubuntu养成的习惯。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pacman -S fcitx-sunpinyin
</span></span></code></pre></div><p>重启之后，你就可以使用<kbd>Ctrl</kbd> + <kbd>space</kbd>激活输入法了。</p>
<h2 id="配置代理">配置代理</h2>
<p>大陆程序员都懂这步该怎么搞。</p>
<p>我只想提醒一点，你需要把<code>HTTP_PROXY</code> 、<code>HTTPS_PROXY</code>、 <code>http_proxy</code>、 <code>https_proxy</code> 都给设置上。有的应用吃大写，有的应用吃小写，xfce并没有提供统一设置的地方。</p>
<h1 id="总结">总结</h1>
<p>宏碁的这款TravelMate B117适合于作为开发备用机使用，用来开发和测试一些需要真机Linux支持的应用，便宜够用，这就够了。</p>
<p>官方声称拥有防泼溅键盘，但日常生活中还是不要让它沾水。</p>
<p>机器本身自带AES-NI，全盘加密的应用场景不会太慢。</p>
<p>不俗的电池使用时间，日常打打代码测试一下离了电源也能坚持5h以上。（这还是代码开了自动提示，后台程序总在那扫来扫去的成绩。Windows用户可以参考官方给的9h续航时间）</p>
<p>性能上，如果你会折腾电脑有信心保证不被全家桶占领的话，这个电脑还是可以做一些日常应用的。打个字上个网完全无压力。</p>
<p>无风扇，固态硬盘，噪声近乎于0，你晚上在屋里写东西也不会很吵。</p>
<p>最主要的是便宜，2399就能买到正规老牌笔记本厂商出品的真正x86电脑，而且做工对得起售价，手感不错。</p>
<p>毕竟，2399要什么自行车。（N3160处理器 + 32GB闪存版本最低能卖到1799，和手机差不多价了）</p>]]></content:encoded>
    </item>
    <item>
      <title>逃离北京，移居深圳</title>
      <link>https://blog.zjyl1994.com/post/beijing2shenzhen/</link>
      <pubDate>Mon, 09 Jul 2018 22:14:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/beijing2shenzhen/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;很早之前就应该写这篇文章了，毕竟已经来这边两个多月了，是时候总结一下这次的北京大逃离了。&lt;/p&gt;
&lt;p&gt;（长篇唠唠叨叨流水账预警）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;引子&#34;&gt;引子&lt;/h1&gt;
&lt;p&gt;刚毕业的时候有两个选择，北京or深圳。家里建议我选择北京，毕竟离家近，都是北方生活可以很好的习惯。最主要的是冬天有暖气，回家还方便。再加上之前去过一次北京稍稍有点认识地方，毕业就去了北京找工作。具体找工作怎么经历按下不提，最后在16年9月份在北京站住脚了。隔了几个月，因为之前经历了城中村，加上老板给涨了一下薪水，觉得应该提高一下居住水平，我就和同事搬到了五环边上的清河地区一起合租。房租从之前的单间1000每月涨到2500一个次卧。&lt;/p&gt;
&lt;p&gt;经历了北京的低端人口大清理，我在清河地区的房子再续租已经从5200一个月涨到了6800一个月，再加上糟糕的天气和公司里的各种斗争搞事，我觉得不应该在这样下去了。所以2018过年期间下了决心，必须走。（压垮我的最后一根稻草应该是涨薪的时候只给涨了900块钱，和糊弄人没什么区别，本来基数就少，涨一次抠抠搜搜连房租涨幅都追不上）&lt;/p&gt;
&lt;h1 id=&#34;离职&#34;&gt;离职&lt;/h1&gt;
&lt;p&gt;要离职肯定要提前申请，折腾快一个月的交接。三个人分别接收了我不同领域的任务，然后开了离职证明。（有个小插曲：离职证明网上下载的模板，领导给盖完公章说我应该没事看看文化方面的知识，全因为离职证明的那段话没空两格? 拜托那是百度搜到的模板诶，不知道老板讨这个嘴上面子有什么意思，可能是小孩子脾气？）&lt;/p&gt;
&lt;p&gt;因为是裸辞，拿着我的所有东西离开的时候还是蛮爽的。开开心心的走出办公楼坐上回家的公交。就这么在家颓废了一个月。当然，到了出信用卡账单的时候就很方，要交最后一次房租，要还一个月放纵的生活费，也是一笔超大的开支。毕竟每个月都是动态平衡，虽然攒不下钱但不至于负债。没了工作只能用之前的存款生抗，也是没办法。&lt;/p&gt;
&lt;h1 id=&#34;启程&#34;&gt;启程&lt;/h1&gt;
&lt;p&gt;定下要走了，收拾了一大堆东西，能拿走的都拿走了。拿不走的一部分送给室友一部分拜托室友等有地址了帮忙邮到广东。去深圳有好多种方法，我和家里人一起去他们帮我拿着大量的行李，肯定不能坐飞机也不能坐高铁，毕竟没有这么大的储物空间。最后买了北京西到深圳的硬卧，在车上折腾了一天一夜。硬卧车可以放很多行李，至少我的两只行李箱两只双肩背和两袋子锅碗瓢盆都能装得下。（不要考虑动卧，动卧只有床底下能放少量扁的行李，亲身体会。）&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>很早之前就应该写这篇文章了，毕竟已经来这边两个多月了，是时候总结一下这次的北京大逃离了。</p>
<p>（长篇唠唠叨叨流水账预警）</p>
</blockquote>
<h1 id="引子">引子</h1>
<p>刚毕业的时候有两个选择，北京or深圳。家里建议我选择北京，毕竟离家近，都是北方生活可以很好的习惯。最主要的是冬天有暖气，回家还方便。再加上之前去过一次北京稍稍有点认识地方，毕业就去了北京找工作。具体找工作怎么经历按下不提，最后在16年9月份在北京站住脚了。隔了几个月，因为之前经历了城中村，加上老板给涨了一下薪水，觉得应该提高一下居住水平，我就和同事搬到了五环边上的清河地区一起合租。房租从之前的单间1000每月涨到2500一个次卧。</p>
<p>经历了北京的低端人口大清理，我在清河地区的房子再续租已经从5200一个月涨到了6800一个月，再加上糟糕的天气和公司里的各种斗争搞事，我觉得不应该在这样下去了。所以2018过年期间下了决心，必须走。（压垮我的最后一根稻草应该是涨薪的时候只给涨了900块钱，和糊弄人没什么区别，本来基数就少，涨一次抠抠搜搜连房租涨幅都追不上）</p>
<h1 id="离职">离职</h1>
<p>要离职肯定要提前申请，折腾快一个月的交接。三个人分别接收了我不同领域的任务，然后开了离职证明。（有个小插曲：离职证明网上下载的模板，领导给盖完公章说我应该没事看看文化方面的知识，全因为离职证明的那段话没空两格? 拜托那是百度搜到的模板诶，不知道老板讨这个嘴上面子有什么意思，可能是小孩子脾气？）</p>
<p>因为是裸辞，拿着我的所有东西离开的时候还是蛮爽的。开开心心的走出办公楼坐上回家的公交。就这么在家颓废了一个月。当然，到了出信用卡账单的时候就很方，要交最后一次房租，要还一个月放纵的生活费，也是一笔超大的开支。毕竟每个月都是动态平衡，虽然攒不下钱但不至于负债。没了工作只能用之前的存款生抗，也是没办法。</p>
<h1 id="启程">启程</h1>
<p>定下要走了，收拾了一大堆东西，能拿走的都拿走了。拿不走的一部分送给室友一部分拜托室友等有地址了帮忙邮到广东。去深圳有好多种方法，我和家里人一起去他们帮我拿着大量的行李，肯定不能坐飞机也不能坐高铁，毕竟没有这么大的储物空间。最后买了北京西到深圳的硬卧，在车上折腾了一天一夜。硬卧车可以放很多行李，至少我的两只行李箱两只双肩背和两袋子锅碗瓢盆都能装得下。（不要考虑动卧，动卧只有床底下能放少量扁的行李，亲身体会。）</p>
<h1 id="落脚">落脚</h1>
<p>到了深圳，就是出来打车到之前订的酒店。有个秘诀，就是酒店在酒店集团自己的APP里选7天长租比那些旅游软件里选住一个星期便宜不少。一口气订了一个星期的房子，1000出头，每天100多点，周六日贵一些。相比直接订238一晚便宜多了。</p>
<p>落脚之后就是找房子，深圳有个好处。做IT的科技公司就南山科技园那一片，以那个地方为目的地找合适的地铁线路还是比较好找的。这个时候建议在高德地图上搜公寓，大把的农民房可供选择。深圳的农民房真是很惊人，有的都有电梯，还能修到将近10层，楼道里打扫的干干净净。相比之下北京城中村里强行续的三四层脏乱破小楼简直就是违章建筑。这种聚集区还是很好找的，高德地图打开，放大，名叫xx村，然后地图上一块一块的建筑就是了。有个显著特征，这种楼都是方的。你看旁边的小区楼都是有奇怪造型的，最次也是长方形的，就这种农民房是正方形的密密麻麻一片。</p>
<p>最后我是找到了1800一个月的农民房单间，这边基本都是这个价格，有的有电梯的贵一些，也不会贵太多。上班不用发愁，有地铁还是很方便的。选的好的话通勤时间可以压在一小时以内，毕竟深圳比北京小太多了。建议地点，西乡、坪洲、翻身、灵芝、洪浪北。这几个地方我都去实地考察过确实有不少我说的这样的房子。</p>
<p>（然后有个朋友能在南山附近租到2000多一点的酒店式公寓，也不知道怎么做到的。）</p>
<h1 id="上班">上班</h1>
<p>确定住处之后就是海投简历然后等待offer，这个没什么难度，就是不停地投简历跑面试。最后确定一家offer就可以了。不过这边好多就是让你回去等消息然后没下文，即使有回信处理速度也很慢。我都上班一个多月了拉钩上还有还有反馈不合适的面试，也是没谁。根据我的测算，这些公司至少要半个月~一个月来给你回信，而且大概率是不给明确拒绝的。（再吐槽一下，拉钩上某家招PHP的来回骚扰我，那个时候我写的还是面Go岗，等我搭话了去面试了，到现在都没明确回复结果（我都上班一个多月了），简直就是逗人玩。）</p>
<h1 id="一些杂项">一些杂项</h1>
<ol>
<li>深圳通地铁站就能办，有押金卡和纪念卡。有机器卖，不要去人工那边买。人工那边大概率买到带图案的纪念卡，50块钱一张简直抢钱一样。</li>
<li>北京西到深圳有好几种，普通卧铺是到深圳站（罗湖），动卧到深圳北站，是高铁站。如果行李很多不要买动卧，一个是贵第二个没地方放行李。动卧一张1500，普通硬卧一张450。区别就是动卧晚上上车早上到，硬卧多消耗一个白天。（感谢铁道部慢车也给提速了）</li>
<li>深圳有两个口岸比较方便，罗湖口岸就在火车站边上，福田口岸在4号线终点。这俩都有地铁不用淋雨就能到HK那边，当然先要办港澳通行证。深圳户口可以办那种一星期一次的签证，有频繁往来需求会方便很多。</li>
<li>深圳这边便利店有的是，而且卖速食面。和北京那种不知道放了多久的便利店盒饭比还是看着现煮的速食面更放心，而且拌面的XO酱也不错。</li>
<li>选择一个好的房子可以解决很多问题，有的房东会在楼下放快递篮，还有的快递可以给你放到附近的快递点让你晚上回来自己拿，蛮方便的。这边很多个人商店也是24小时的，我不止一次晚上12点下去买个饮料什么的，都开着。</li>
<li>敲定房子之前可以先看看第二天早上从你选定的房子出门吃早餐到进地铁站需要多久，大概有个数再交钱签合同。免得到时候发现地铁和早餐摊堵爆需要早早出来排队。这边早餐多数是肠粉，肉蛋肠4~6元都有可能，也不算太贵。</li>
<li>深圳有入户补贴，应届生现在秒批，像我这种非应届的就需要慢慢跑手续办了。办成了本科给打1w5的补贴，研究生博士生更多。也许这是唯一一个面对普通人开放户口的一线了，等过几年户籍人数差不多了可能也要走上北上封闭的路子。所以说要搞还是得尽早。人才引进落户至少需要一个月的社保才能提交申请。</li>
<li>如果之前是北上广这种一线，转移公积金可以去公积金中心填表转移。不需要回北京开这个那个的证明，在深圳连续交6个月社保公积金就能在深圳直接转过来，据往上消息需要你之前城市的公积金是封存状态。</li>
<li>社保转移很难，需要上家减员的时候做省外减员，而且钱提不出来只能给你转年限，而且这个年限只是退休时候有用，对你在深圳办事没用。一般上家减员都不会给你特地选省外的，就很麻烦。所以说这个需要等未来社保联网再转了。总结下来，麻烦得很不建议现在搞。</li>
<li>换城市是一笔很大的开支，包括车票，临时居住的宾馆，租房的押金，等等一大堆事。需要你至少准备1w块钱免得到时候资金周转不开。一般来说从落脚到上班开第一次工资要2个月，这两个月的生活费也不是小数目。</li>
<li>提前咨询退房是否需要本人，如果有宽带一定要提前转给室友或者在当地的人。自用宽带的话能提前消了最好。宽带不支持异地销号，而且必须机主本人持身份证回归属地办理，我为了这个特地回了一趟北京简直日了狗了。你要是到期不销号，按月收钱你还用不上就很亏。</li>
<li>一般来说，从你提交辞呈到交接完可以走，是一个月。算上放松心情一个月，一个月启程落脚，一个月找工作，一个月上班。从你这边有想法要走到在深圳站住脚需要4个月，如果你不需要放松心情也需要3个月时间。期间这3个月是没有人给你发薪的，全要靠自己积蓄了，多准备一些钱是没错的。</li>
<li>深圳很热，所以说你需要关注一下电费价格，农民房有1.3一度的电也有1.8一度的电，要选最便宜那个，要不然开会空调你电费就爆了。</li>
<li>去广州城际不需要对号入座，从罗湖站买票80块钱就可以到广州东，当天车票随便上那一趟就走了，除了贵其他的蛮不错的。高铁必须按照车次来，75块钱，城际不需要。</li>
<li>羊城通全广东除了东莞深圳都能用，办一张押金也就18，很划算的。</li>
</ol>]]></content:encoded>
    </item>
    <item>
      <title>区块链是什么，我来告诉你</title>
      <link>https://blog.zjyl1994.com/post/blockchain/</link>
      <pubDate>Wed, 02 May 2018 21:26:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/blockchain/</guid>
      <description>&lt;h1 id=&#34;区块链是什么&#34;&gt;区块链是什么&lt;/h1&gt;
&lt;p&gt;区块链，当今最火热的技术热点。为什么这么火呢，三个字：“比特币”。&lt;/p&gt;
&lt;p&gt;首先，我们要知道区块链是什么，根据&lt;a href=&#34;https://zh.wikipedia.org/wiki/%E5%8C%BA%E5%9D%97%E9%93%BE&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;维基百科的描述&lt;/a&gt;来看，是这样的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;区块链&lt;/strong&gt;（英语：blockchain 或 block chain）是用分布式数据库识别、传播和记载信息的智能化对等网络，也称为价值互联网。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;用简单的话来说，区块链就是一个全球一起写的一本书。这本书有可能是账本，也可能是别的什么东西。&lt;/p&gt;
&lt;p&gt;区块链有以下几个优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不可修改，数据写进区块就不能更改了。&lt;/li&gt;
&lt;li&gt;单向性，你只能在最后一个块后面加新的块，而不能在之前区块中间加新块。&lt;/li&gt;
&lt;li&gt;众生平等，任何人只要有合适的客户端就可以入网，根本没有限制。谁能出新块完全取决于共识算法。&lt;/li&gt;
&lt;li&gt;去中心化，每个人都是客户端，每个人也都是服务端，每个人都拿着所有人的账本，所以这是完全的去中心化。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;区块链的去中心化是真正的去中心化，没有管理员，唯一一种有效的攻击手段就是拿到51%以上的算力，这在现在这么多人参与的情况下几乎是不可能的。而且，这种攻击仅仅能拿来控制新出块的内容，并不能改动之前的老块。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="区块链是什么">区块链是什么</h1>
<p>区块链，当今最火热的技术热点。为什么这么火呢，三个字：“比特币”。</p>
<p>首先，我们要知道区块链是什么，根据<a href="https://zh.wikipedia.org/wiki/%E5%8C%BA%E5%9D%97%E9%93%BE" rel="noopener noreferrer" target="_blank">维基百科的描述</a>来看，是这样的：</p>
<blockquote>
<p><strong>区块链</strong>（英语：blockchain 或 block chain）是用分布式数据库识别、传播和记载信息的智能化对等网络，也称为价值互联网。</p>
</blockquote>
<p>用简单的话来说，区块链就是一个全球一起写的一本书。这本书有可能是账本，也可能是别的什么东西。</p>
<p>区块链有以下几个优势：</p>
<ol>
<li>不可修改，数据写进区块就不能更改了。</li>
<li>单向性，你只能在最后一个块后面加新的块，而不能在之前区块中间加新块。</li>
<li>众生平等，任何人只要有合适的客户端就可以入网，根本没有限制。谁能出新块完全取决于共识算法。</li>
<li>去中心化，每个人都是客户端，每个人也都是服务端，每个人都拿着所有人的账本，所以这是完全的去中心化。</li>
</ol>
<p>区块链的去中心化是真正的去中心化，没有管理员，唯一一种有效的攻击手段就是拿到51%以上的算力，这在现在这么多人参与的情况下几乎是不可能的。而且，这种攻击仅仅能拿来控制新出块的内容，并不能改动之前的老块。</p>
<h1 id="区块">区块</h1>
<p>想了解区块链的本质，我们需要知道什么是区块。</p>
<p>一个区块分为区块头和数据两部分。</p>
<h2 id="区块头">区块头</h2>
<p>区块头包含当前块的一些描述数据，一般来说有以下内容</p>
<ol>
<li>上一区块的整体HASH</li>
<li>当前区块的生成时间</li>
<li>当前区块中数据的HASH</li>
<li>负责让大家猜的随机数NONCE</li>
</ol>
<p>HASH就是哈希算法，是一种数据摘要，MD5/SHA1之类的就算是HASH的一种。一个合格的HASH算法能做到只要修改原始数据一比特，哈希得到的结果都会面目全非，这就是密码学中的雪崩效应，具体不多讲感兴趣的可以搜一下雪崩效应。而且，两份不同数据得到同样哈希结果也是几乎不可能的，这是哈希的抗碰撞性。而且，哈希是不可逆的（谁发明了这种任意数据压缩成256bit还能解压回去的技术，今年的图灵奖就是你的了。）</p>
<h2 id="区块数据">区块数据</h2>
<p>区块数据就是承载区块链的实际应用了。如果你在里面记录历史事件，这就是历史链；如果你用来记录交易账目，这就是金融链；如果你用来记录溯源信息，这就是溯源链。能做什么全看你的想象力了。</p>
<h1 id="链">链</h1>
<p>区块链之所以是区块链，主要是这条链。</p>
<p>区块头中包含上一块的HASH，意味着你的区块不能提前生成，只有知道上一个区块的HASH才行。</p>
<p>而且你也不能修改链上的某个块，因为你修改了区块数据，HASH会因为雪崩效应变得面目全非。后面的链就不知道前面的链是哪个了，这条链也就断了。在实际应用中，生效的永远是最长的那条链。也就是说，想要修改数据，你需要同时修改该块和以后的所有数据，还得让全世界都承认你的修改（让你的链比别人都长），这简直就是不可能的事情。</p>
<h1 id="共识">共识</h1>
<p>为什么，你的块提交了别人就要收下并信任呢？这就涉及到了节点之间的共识问题。当前主流的就是比特币的工作量证明。原理简单来说可以理解为：求一个特别难的密码学题目。其中最适合的就是计算哈希。区块链用的是SHA256算法，可以把任意数据计算成256bit。举个简单的例子，给定一个HASH值，要求区块头计算的HASH小于这个值，这个区块才会生效。你要做的就是猜出来NONCE（实际上真实的链有一套自己的难度计算体系控制出块速率，动态调节HASH的门槛）。</p>
<p>因为哈希的单向性，任何人都可以验证你的答案对不对。但是你没法反推，只能一点一点的改变数据去计算HASH做碰撞（这也是大家诟病的地方，因为除了作为工作量证明以外，算出来的数真的没有什么用处）。这就是数字时代的挖矿。</p>
<p>你的结果传给全网，他们计算后发现答案正确，就把你的这一块加入了他的区块链里。这个操作是全世界同步的。为什么你不能修改链就在这，你做不到那么短时间内计算好N多块的哈希和答案，也就没法让其他人也按照你的要求修改链了。</p>
<p>中本聪设计了一套动态调整出块难度的算法，使得出块时间维持在10分钟左右。就是通过调节HASH门槛来实现的。</p>
<p>当两个人同时拿到了正确答案怎么办？主要还是看谁快。你早拿到答案早广播，他们早采纳。谁先到6个块以上就采取谁的链，以最长的为准，这就是共识算法。</p>
<h1 id="后记">后记</h1>
<p>事实上，区块链的应用场景十分有限。他出块速度慢，VISA的全球支付网络每秒钟都要处理几万笔交易，用区块链每块几MB完全不够用的，等10分钟出一个块，一个块存不了几笔交易，那就非常耽误事。你等着你的数据同步到全世界最少也得等1小时再去确认是不是真正的写链了，这对金融行业也是不太现实的事。而且计算哈希很巧妙，从数学上证明了安全，但是算力白白浪费也是一种浪费。</p>
<p>目前区块链我觉得做的最好的应用场景还是电子货币，银行银行之间，银行客户之间，客户客户之间拿来交换数据，让密码学保证数据安全性和不可篡改性。当然，要做好这些还是需要修改链的出块速度和块大小，否则永远不能愉快的商用。</p>
<p>想象一下，现在比特币上用的区块链拿来做VISA体系的话，你刷卡之后交易得排队等着确认，然后全世界每10分钟确认几万笔，你买一件衣服可能确认交易要等2个月，你会用嘛？</p>]]></content:encoded>
    </item>
    <item>
      <title>30行不到自动下载Tumblr小姐姐</title>
      <link>https://blog.zjyl1994.com/post/tumbler_spider/</link>
      <pubDate>Thu, 05 Apr 2018 22:31:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/tumbler_spider/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Tumblr是个好东西，上面有很多漂亮小姐姐。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在Tumblr的海洋里遨游，总会有几个对胃口的博客。如果你想下载里面的图片，应该怎么下呢？
普通人肯定是手动一张一张另存为啦。但是你要知道有的博客里会有几千张甚至几万张照片哦，你不会想一个一个手动来吧。 XD&lt;/p&gt;
&lt;p&gt;这个时候就要请出Python了！&lt;/p&gt;
&lt;p&gt;Python不用我过多介绍了吧，就是一个编程语言。嗯，这个东西很简单，要学的话教程网上很多，有英文基础就可以了（其实关键词就那几个，不会英文也能学啦。&lt;/p&gt;
&lt;p&gt;要爬Tumblr的图片，首先要知道Tumblr的图片地址，正常来说需要我们解析网页然后抓取HTML中的jpg路径。但是，Tumblr这个好心的家伙居然给了API！&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>Tumblr是个好东西，上面有很多漂亮小姐姐。</p>
</blockquote>
<p>在Tumblr的海洋里遨游，总会有几个对胃口的博客。如果你想下载里面的图片，应该怎么下呢？
普通人肯定是手动一张一张另存为啦。但是你要知道有的博客里会有几千张甚至几万张照片哦，你不会想一个一个手动来吧。 XD</p>
<p>这个时候就要请出Python了！</p>
<p>Python不用我过多介绍了吧，就是一个编程语言。嗯，这个东西很简单，要学的话教程网上很多，有英文基础就可以了（其实关键词就那几个，不会英文也能学啦。</p>
<p>要爬Tumblr的图片，首先要知道Tumblr的图片地址，正常来说需要我们解析网页然后抓取HTML中的jpg路径。但是，Tumblr这个好心的家伙居然给了API！</p>
<p>它的API分为两个版本，V2需要注册，功能更多。但是V1并不需要注册，只需要在Tumblr的地址后面加上/api/read就可以获取到XML格式的信息了。给你们看一下他输出的XML：</p>
<ol>
<li><img alt="XML内容" decoding="async" height="1042" loading="eager" src="/post/tumbler_spider/tumblr-xml.webp" width="1920">
</li>
</ol>
<p>就是这样，你要的图片路径都有，还有很多附加信息。</p>
<p>拿到了XML文档，我们就该想一下怎么解析了，首先要了解一下什么是XPath。</p>
<p>XPath是一种用来访问XML节点信息的描述语言，可以根据你的需要找到所有数据。算是一种小小的DSL，具体内容可以谷歌，或者去Bing搜一下。</p>
<p>在这个程序里我用到了4条XPath，分别是：</p>
<ol>
<li>/tumblr/posts/@start</li>
<li>/tumblr/posts/@total</li>
<li>/tumblr/posts/post/photoset/photo[*]/photo-url[1]/text()</li>
<li>/tumblr/posts/post/photo-url[1]/text()</li>
</ol>
<p>通过查看XML结构我们得知，tumblr节点下的posts有两个属性，start和total，这两个查看API文档可以知道是翻页查询时使用的参数，所以分别用1、2提取。</p>
<p>然后可以观察图片的路径，可以看到位于post节点下的photo-url节点中，这时候使用photo-url[1]就可以提取结果集中的第一项了。至于为什么只要第一个photo-url？那是因为别的都是小分辨率的，只有第一个是分辨率最大的那个。所以说我们有了第4个xpath，至于为什么有第3个xpath，观察某些多图片博客的xml可以看到他有个photoset在外面，所以我们用3把它也兼容掉。</p>
<p>通过3、4拿到对应的文件路径之后我们就可以下载了，要注意34的结果集要做一次去重，防止匹配到同一张图片哦，剩下的用Python3自带的urllib下载就行了。</p>
<p>下面是完整代码：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">urllib</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">lxml</span> <span class="kn">import</span> <span class="n">etree</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 配置信息</span>
</span></span><span class="line"><span class="cl"><span class="n">tumblrUsername</span> <span class="o">=</span> <span class="s2">&#34;megane-megane-megane&#34;</span> <span class="c1"># 你想下载的Tumblr博客用户名</span>
</span></span><span class="line"><span class="cl"><span class="n">saveToPath</span> <span class="o">=</span> <span class="s2">&#34;D:/needtest/tumblr/pics&#34;</span> <span class="c1"># 下载到的文件夹路径</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 程序本体</span>
</span></span><span class="line"><span class="cl"><span class="n">postOffset</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="n">total</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="n">postOffset</span> <span class="o">&lt;</span> <span class="n">total</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">baseUrl</span> <span class="o">=</span> <span class="s2">&#34;https://</span><span class="si">%s</span><span class="s2">.tumblr.com/api/read?type=photo&amp;filter=text&amp;num=50&amp;start=</span><span class="si">%s</span><span class="s2">&#34;</span> <span class="o">%</span> <span class="p">(</span><span class="n">tumblrUsername</span><span class="p">,</span><span class="n">postOffset</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">baseUrl</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">xml</span> <span class="o">=</span> <span class="n">etree</span><span class="o">.</span><span class="n">fromstring</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">start</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">xml</span><span class="o">.</span><span class="n">xpath</span><span class="p">(</span><span class="s1">&#39;/tumblr/posts/@start&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span>    <span class="c1"># 起始条目</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">xml</span><span class="o">.</span><span class="n">xpath</span><span class="p">(</span><span class="s1">&#39;/tumblr/posts/@total&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span>    <span class="c1"># 总条目</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;[</span><span class="si">%s</span><span class="s2">]</span><span class="si">%d</span><span class="s2">/</span><span class="si">%d</span><span class="s2">&#34;</span> <span class="o">%</span> <span class="p">(</span><span class="n">tumblrUsername</span><span class="p">,</span><span class="n">start</span><span class="p">,</span><span class="n">total</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">picList</span> <span class="o">=</span> <span class="n">xml</span><span class="o">.</span><span class="n">xpath</span><span class="p">(</span><span class="s1">&#39;/tumblr/posts/post/photoset/photo[*]/photo-url[1]/text()&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">picList</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">xml</span><span class="o">.</span><span class="n">xpath</span><span class="p">(</span><span class="s1">&#39;/tumblr/posts/post/photo-url[1]/text()&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">postOffset</span> <span class="o">=</span> <span class="n">start</span> <span class="o">+</span> <span class="mi">50</span>
</span></span><span class="line"><span class="cl">    <span class="n">picList</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">picList</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">pic</span> <span class="ow">in</span> <span class="n">picList</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">filename</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">saveToPath</span><span class="p">,</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">pic</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">pic</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">urlretrieve</span><span class="p">(</span><span class="n">pic</span><span class="p">,</span><span class="n">filename</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="n">urllib</span><span class="o">.</span><span class="n">error</span><span class="o">.</span><span class="n">HTTPError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</span></span></code></pre></div><p>这是运行效果图：</p>
<p><img alt="运行效果" decoding="async" height="1042" loading="lazy" src="/post/tumbler_spider/tumblr-code.webp" width="1920">
</p>
<p><img alt="最终效果" decoding="async" height="1042" loading="lazy" src="/post/tumbler_spider/tumblr-pics.webp" width="1920">
</p>
<p>这算是简易版的了，下载速度慢可以通过多线程来解决，不过这样的话就不止30行了，有能力可以自己动手改造一下。</p>]]></content:encoded>
    </item>
    <item>
      <title>爱沙尼亚电子公民计划</title>
      <link>https://blog.zjyl1994.com/post/e-residency/</link>
      <pubDate>Mon, 26 Mar 2018 22:11:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/e-residency/</guid>
      <description>&lt;p&gt;最近一直在研究爱沙尼亚的电子公民计划，感觉很有意思，把我整理的这些东西写出来，希望对后人有用。&lt;/p&gt;
&lt;p&gt;首先，爱沙尼亚电子公民是2014年发布的一个项目。爱沙尼亚这个国家曾经是苏联加盟国，本来是个独立国家，40年被苏联强行吞并，90年获得独立，然后现在是欧盟和北约成员国。爱沙尼亚的国旗也很好看，蓝黑白三色旗，辨识度很高很独特。爱沙尼亚域名后缀.ee，国际区号+372，首都塔林。&lt;/p&gt;
&lt;p&gt;爱沙尼亚有发达的线上政务系统，用电子身份证可以在线办理很多很多的业务。甚至开公司都不用出门，直接线上就能办理。也正是因为有这个系统，才能搞电子公民这套东西。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>最近一直在研究爱沙尼亚的电子公民计划，感觉很有意思，把我整理的这些东西写出来，希望对后人有用。</p>
<p>首先，爱沙尼亚电子公民是2014年发布的一个项目。爱沙尼亚这个国家曾经是苏联加盟国，本来是个独立国家，40年被苏联强行吞并，90年获得独立，然后现在是欧盟和北约成员国。爱沙尼亚的国旗也很好看，蓝黑白三色旗，辨识度很高很独特。爱沙尼亚域名后缀.ee，国际区号+372，首都塔林。</p>
<p>爱沙尼亚有发达的线上政务系统，用电子身份证可以在线办理很多很多的业务。甚至开公司都不用出门，直接线上就能办理。也正是因为有这个系统，才能搞电子公民这套东西。</p>
<p>爱沙尼亚的电子公民卡就长这样，还得配个专用的读卡器</p>
<p><img alt="电子公民卡" decoding="async" loading="eager" src="https://e-resident.gov.ee/wp-content/themes/eas/images/data/smartid.png">
</p>
<p>实际上使用的效果是这样的</p>
<p><img alt="电子公民卡使用方法" decoding="async" loading="lazy" src="https://e-resident.gov.ee/wp-content/uploads/2018/03/bg03.jpg">
</p>
<p>刚才你看到的图片都来自爱沙尼亚电子公民的项目主页，https://e-resident.gov.ee/ (上了HTTPS，而且还是个Wordpress站，很赶潮流)。</p>
<p>电子公民身份能做什么呢：首先是开公司，然后是开相关的银行账号。并且可以在线享用一系列的远程政府服务。</p>
<p>这个电子公民身份就是鼓励世界各地的人成立爱沙尼亚的公司，以他们国家公司身份进行经营。当然最后要的肯定是你给他们政府交税了，我查了一下20%的税。而且只有分红和开工资的时候抽税，钱放在账户里扩大经营花掉的话是不会收税的。<strong>但是这个电子公民身份证没有照片也不能作为线下环境的身份证明，也不是绿卡或入境许可。你去爱沙尼亚还是要申请签证去。</strong></p>
<h1 id="如何申请">如何申请</h1>
<p>要申请电子公民，首先你需要有护照，护照在户口所在地的出入境管理局就能办，费用80-100左右。</p>
<p>然后去https://apply.gov.ee/ 提交申请，注意是纯英文的，中间会让你填写上传护照图片和个人照片。</p>
<p>申请这个需要交100欧元的费用，支持VISA和MasterCard，不支持银联。</p>
<p>申请之后爱沙尼亚的边境管理部门会审核你的资料，审核通过后需要去大使馆领取电子公民卡和读卡器。</p>
<h1 id="开公司需要什么">开公司需要什么</h1>
<p>开当地公司你需要有当地地址、还有一系列法律文书要办，有很多Provider可以提供这些服务。我在网上搜到的就是LeapIN他家，网址是https://www.leapin.eu ，还有很多其他的Provider可以选择，看电子公民主页上的信息就知道了。https://e-resident.gov.ee/run-a-company/</p>
<p>#费用如何</p>
<p>以下数据翻译自LeapIN的文档，别人家可能更便宜也可能更贵。（原文 <a href="https://www.leapin.eu/faq/cost-and-fees" rel="noopener noreferrer" target="_blank">https://www.leapin.eu/faq/cost-and-fees</a> ），他家官方文档说可以刷信用卡支付这些费用。</p>
<h2 id="一次性费用">一次性费用</h2>
<ol>
<li>
<p>电子公民卡100欧元+去北京大使馆拿卡的路费</p>
</li>
<li>
<p>190欧元的公司注册手续费（LeapIN代收）</p>
</li>
<li>
<p>公司在线注册费用25欧元（也是LeapIN代收）</p>
</li>
<li>
<p>开银行账号的钱（他家支持三种账号方式，见下文）</p>
</li>
</ol>
<h2 id="每月费用">每月费用</h2>
<ol>
<li>LeapIN的月费，（收入小于1000欧的刚起步公司统一49欧元一个月，超过1000欧元按照价格表算钱，价格表在下面）</li>
<li>2欧元的银行卡卡费</li>
</ol>
<h1 id="leapin的套餐">LeapIN的套餐</h1>
<p>以下内容翻译自https://www.leapin.eu/pricing 这个页面，私人公司就是咱们的有限责任公司了，他们叫OÜ</p>
<h1 id="59欧元-app开发者">59欧元 APP开发者</h1>
<ol>
<li>一个私人公司</li>
<li>银行账号和付款卡</li>
<li>账号、税、规则他们都包了</li>
<li>LeapIN的自助在线服务</li>
<li>支持全球的App商店</li>
</ol>
<h1 id="79欧元-专业版">79欧元 专业版</h1>
<ol>
<li>一个私人公司</li>
<li>银行账号和付款卡</li>
<li>账号、税、规则他们都包了</li>
<li>LeapIN的自助在线服务</li>
<li>高级销售收据（Invoice，可能叫发票更贴切）</li>
<li>VAT号码（增值税税号）</li>
</ol>
<h2 id="99欧元-saas提供者">99欧元 SaaS提供者</h2>
<ol>
<li>一个私人公司</li>
<li>银行账号和付款卡</li>
<li>账号、税、规则他们都包了</li>
<li>LeapIN的自助在线服务</li>
<li>支持支付网关和批量开收据</li>
<li>VAT号码</li>
</ol>
<h1 id="银行账号">银行账号</h1>
<p>银行账号有两种，一种是传统的银行账号，比如LHV，他就需要去爱沙尼亚当地进行面签，当然他也是最好的选择。支持支付网关集成，便宜功能全，还有LeapIN的内部集成。</p>
<p>还有一种是电子帐号，主要是TransferWise和Holvi。</p>
<p>TransferWise支持多种钱，并且可以远程开户。开这个户可以得到这些（后面的各种code实在不会翻译）</p>
<ul>
<li>欧洲 IBAN</li>
<li>英国账号 和 sort code</li>
<li>澳洲账号 和 BSB code</li>
<li>美国账号 和 routing number</li>
</ul>
<p>Holvi只支持欧元，也可以远程开户，适合在欧洲地区和欧洲客户结算欧元使用。35欧元每月的服务费。</p>
<p>不同银行间的区别请看这个 <a href="https://www.leapin.eu/faq/banking#what-are-the-differences-between-banking" rel="noopener noreferrer" target="_blank">https://www.leapin.eu/faq/banking#what-are-the-differences-between-banking</a> 很长，翻译不下去了。</p>
<p>简单来说就是TranfserWise支持英镑欧元和美刀，Holvi只支持欧元，LHV支持15种货币（美元除外）。</p>
<p>LHV和TransferWise支持用SWIFT方式转账，Holvi不支持。</p>
<p>LHV支持与LeapIN实时交换数据，其他需要自己倒。</p>
<p>TransferWise没有银行卡，Holvi免费拿，LHV2欧元一个月。</p>
<p>LHV支持各种支付网关（毕竟传统银行账号），Holvi支持一部分，TransferWise不支持（在线卖东西就不方便收钱了）。</p>
<p>LeapIN他家支持多个账号，你可以先用远程能开户的先做业务，等需要传统账号的时候再切到LHV上也可以。</p>
<p>文档里还说，如果是USD的业务推荐用TransferWise，其他的推荐LHV。</p>
<hr>
<p>我最近一直在研究这个，中文世界里很少有讲电子公民和如何开爱沙尼亚公司的，我就简单翻译了一下LeapIN的文档贴在这里给大家一个方便。</p>
<p>爱沙尼亚公司税不多（20%），而且电子公民身份开公司很简单，这种Provider都给包办了。自由职业什么的需要公司身份进行收款或申请什么的话，这个应该是很好的选择。在家动动鼠标就把公司开了然后收款。</p>
<p>就是不知道TransferWise和Holvi的账号能不能拿来收Google Play开发者或者Steam开发者的收入，如果可以的话搞一个真挺好的。起码不会被每年5万美刀的限额控制住，也算是离岸了。</p>]]></content:encoded>
    </item>
    <item>
      <title>使用UWF保护电脑免遭全家桶</title>
      <link>https://blog.zjyl1994.com/post/ufw/</link>
      <pubDate>Tue, 27 Feb 2018 22:48:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/ufw/</guid>
      <description>&lt;p&gt;春节回家，几乎是修了好几天电脑。没有办法，现在国产全家桶实在是太厉害了，你随便搜个想要的软件，直接安装就会中各种全家桶。&lt;/p&gt;
&lt;p&gt;我回家在我爸的电脑里发现了各种播放器各种压，桌面被各种助手占据，悬浮窗五六个，还有时候被装上奇怪的输入法，安全软件也不止一份。&lt;/p&gt;
&lt;p&gt;对于我这种电脑从业人士来说，要想避开那些捆绑安装还是很轻松的，但是家里的普通人们就不一定了。他们习惯于缺少功能，就去百度搜一下，点击高速下载，然后中各种下载器。下载器下载的安装包装完推荐的各种勾也不取消直接点立即体验来打开所需要的应用。这不是教的问题，谁能指望五六十岁的长辈们和我们年轻人一样对各种电脑问题如此精通呢？&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我想到了机房常用的还原卡。小学机房就有还原卡，做任何操作只要重启就会还原到老师刚装好的状态。但是还原卡第一是要买硬件产品，第二是笔记本没法装。经过苦苦搜索终于找到了适合Windows10的软件还原卡方法，不需要额外花钱买硬件，通吃所有设备。这一切只需要你有Windows10企业版或者教育版。（企业版实测通过，教育版据说也有这个功能）（Windows其他版本升级到企业版只需要安装企业版密钥并进行激活即可。）&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>春节回家，几乎是修了好几天电脑。没有办法，现在国产全家桶实在是太厉害了，你随便搜个想要的软件，直接安装就会中各种全家桶。</p>
<p>我回家在我爸的电脑里发现了各种播放器各种压，桌面被各种助手占据，悬浮窗五六个，还有时候被装上奇怪的输入法，安全软件也不止一份。</p>
<p>对于我这种电脑从业人士来说，要想避开那些捆绑安装还是很轻松的，但是家里的普通人们就不一定了。他们习惯于缺少功能，就去百度搜一下，点击高速下载，然后中各种下载器。下载器下载的安装包装完推荐的各种勾也不取消直接点立即体验来打开所需要的应用。这不是教的问题，谁能指望五六十岁的长辈们和我们年轻人一样对各种电脑问题如此精通呢？</p>
<p>为了解决这个问题，我想到了机房常用的还原卡。小学机房就有还原卡，做任何操作只要重启就会还原到老师刚装好的状态。但是还原卡第一是要买硬件产品，第二是笔记本没法装。经过苦苦搜索终于找到了适合Windows10的软件还原卡方法，不需要额外花钱买硬件，通吃所有设备。这一切只需要你有Windows10企业版或者教育版。（企业版实测通过，教育版据说也有这个功能）（Windows其他版本升级到企业版只需要安装企业版密钥并进行激活即可。）</p>
<p>Windows10企业版有一个很好的功能，就是Unified Write Filter，中文名为“统一写入过滤器”。简单的工作原理就是把对要保护硬盘的写入暂存，而不是真实的写入硬盘本身。也就是说，这是一个内核级的软还原卡。建议用这个功能的时候物理内存大一些，免得后期使用过卡。</p>
<p>首先你需要先激活Win10企业版，然后在控制面板-&gt;程序和功能-&gt;启用和关闭Windows功能-&gt;设备锁定中选中统一写入筛选器。</p>
<p><img alt="统一写入筛选器的位置" decoding="async" height="492" loading="eager" src="/post/ufw/ufw1.webp" width="484">
</p>
<p>然后按Win+X弹出高级功能菜单，选择“Windows PowerShell（管理员）”。打开拥有管理员权限的PS后，输入如下命令：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">uwfmgr</span> <span class="kd">filter</span><span class="w"> </span><span class="nb">enable</span>	<span class="c"># 开启UWF过滤器</span>
</span></span><span class="line"><span class="cl"><span class="n">uwfmgr</span> <span class="n">volume</span> <span class="n">protect</span> <span class="n">c:</span> <span class="c"># 保护C盘</span>
</span></span></code></pre></div><p>按照上面的指令操作后，重启电脑。你的C盘就处于UWF保护下了，无论你做什么操作C盘都不会被污染，中全家桶之后只要重启就又是完好如初的状态。</p>
<p>要想解除UWF，你还是需要打开“Windows PowerShell（管理员）”，输入以下命令：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">uwfmgr</span> <span class="n">volume</span> <span class="n">unprotect</span> <span class="n">c:</span> <span class="c"># 取消C盘的保护</span>
</span></span><span class="line"><span class="cl"><span class="n">uwfmgr</span> <span class="kd">filter</span><span class="w"> </span><span class="nb">disable</span>	<span class="c"># 开启UWF过滤器</span>
</span></span></code></pre></div><p>经过这样一系列设置，你就可以放心大胆的把电脑交给长辈了，永远也不会中什么乱七八糟的卫士，压缩软件和桌面助手。因为每次重启C盘都会被重置到你设置保护的那个时刻。即使卫士什么的学聪明了躲到其他盘里，也会因为C盘没有对应的自动启动方式而无法自启动祸害电脑。</p>
<p>说真的，国产软件的名声就是被这些外门邪道害了。买别人捆绑的推广，一时间装机量上去了数据好看了，实际上用户发现一个删一个，想靠捆绑安装建立用户忠诚度简直是天方夜谭。追这些虚的数据有啥用，商业上要的是忠诚肯花钱的死忠用户，而不是被捆绑搞得一肚子气的敌对用户。</p>]]></content:encoded>
    </item>
    <item>
      <title>Golang采集系统信息</title>
      <link>https://blog.zjyl1994.com/post/psutil/</link>
      <pubDate>Wed, 31 Jan 2018 15:58:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/psutil/</guid>
      <description>&lt;p&gt;一直以来就想做一个可以显示服务器信息的集中式监控页面。经过两三天艰苦努力终于放弃(前端实现地图上展示信息太难了。&lt;/p&gt;
&lt;p&gt;虽然对应的前端做不出来，但是对应的信息采集后端已经写好并且可以使用，为了不浪费我的成果，在这贴出来。你们谁要是写个对应的前端的话，请务必通知我。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>一直以来就想做一个可以显示服务器信息的集中式监控页面。经过两三天艰苦努力终于放弃(前端实现地图上展示信息太难了。</p>
<p>虽然对应的前端做不出来，但是对应的信息采集后端已经写好并且可以使用，为了不浪费我的成果，在这贴出来。你们谁要是写个对应的前端的话，请务必通知我。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;encoding/json&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;flag&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;fmt&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;github.com/shirou/gopsutil/cpu&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;github.com/shirou/gopsutil/disk&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;github.com/shirou/gopsutil/host&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;github.com/shirou/gopsutil/load&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;github.com/shirou/gopsutil/mem&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;github.com/shirou/gopsutil/net&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;log&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;net/http&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;time&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">StatusServer</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Percent</span><span class="w">  </span><span class="nx">StatusPercent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">CPU</span><span class="w">      </span><span class="p">[]</span><span class="nx">CPUInfo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Mem</span><span class="w">      </span><span class="nx">MemInfo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Swap</span><span class="w">     </span><span class="nx">SwapInfo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Load</span><span class="w">     </span><span class="o">*</span><span class="nx">load</span><span class="p">.</span><span class="nx">AvgStat</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Network</span><span class="w">  </span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">InterfaceInfo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">BootTime</span><span class="w"> </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Uptime</span><span class="w">   </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">StatusPercent</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">CPU</span><span class="w">  </span><span class="kt">float64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Disk</span><span class="w"> </span><span class="kt">float64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Mem</span><span class="w">  </span><span class="kt">float64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Swap</span><span class="w"> </span><span class="kt">float64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">CPUInfo</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ModelName</span><span class="w"> </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Cores</span><span class="w">     </span><span class="kt">int32</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">MemInfo</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Total</span><span class="w">     </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Used</span><span class="w">      </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Available</span><span class="w"> </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">SwapInfo</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Total</span><span class="w">     </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Used</span><span class="w">      </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Available</span><span class="w"> </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">InterfaceInfo</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Addrs</span><span class="w">    </span><span class="p">[]</span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ByteSent</span><span class="w"> </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ByteRecv</span><span class="w"> </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">port</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">flag</span><span class="p">.</span><span class="nf">String</span><span class="p">(</span><span class="s">&#34;port&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;:58000&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;HTTP listen port&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">flag</span><span class="p">.</span><span class="nf">Parse</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">getInfo</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="o">*</span><span class="nx">port</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">log</span><span class="p">.</span><span class="nf">Fatalln</span><span class="p">(</span><span class="s">&#34;ListenAndServe: &#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">getInfo</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Access-Control-Allow-Origin&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;*&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Add</span><span class="p">(</span><span class="s">&#34;Access-Control-Allow-Headers&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;Content-Type&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;application/json&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="nf">infoMiniJSON</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">infoMiniJSON</span><span class="p">()</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">mem</span><span class="p">.</span><span class="nf">VirtualMemory</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">s</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">mem</span><span class="p">.</span><span class="nf">SwapMemory</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">c</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">cpu</span><span class="p">.</span><span class="nf">Info</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">cc</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">cpu</span><span class="p">.</span><span class="nf">Percent</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">,</span><span class="w"> </span><span class="kc">false</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">d</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">disk</span><span class="p">.</span><span class="nf">Usage</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">n</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">host</span><span class="p">.</span><span class="nf">Info</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">nv</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nf">IOCounters</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">l</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">load</span><span class="p">.</span><span class="nf">Avg</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nf">Interfaces</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">new</span><span class="p">(</span><span class="nx">StatusServer</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Load</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">l</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Uptime</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n</span><span class="p">.</span><span class="nx">Uptime</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">BootTime</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n</span><span class="p">.</span><span class="nx">BootTime</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Percent</span><span class="p">.</span><span class="nx">Mem</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">UsedPercent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Percent</span><span class="p">.</span><span class="nx">CPU</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">cc</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Percent</span><span class="p">.</span><span class="nx">Swap</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">UsedPercent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Percent</span><span class="p">.</span><span class="nx">Disk</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">d</span><span class="p">.</span><span class="nx">UsedPercent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">CPU</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="nx">CPUInfo</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">c</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">ci</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">c</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ss</span><span class="p">.</span><span class="nx">CPU</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">ModelName</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">ci</span><span class="p">.</span><span class="nx">ModelName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ss</span><span class="p">.</span><span class="nx">CPU</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">Cores</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">ci</span><span class="p">.</span><span class="nx">Cores</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Mem</span><span class="p">.</span><span class="nx">Total</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">Total</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Mem</span><span class="p">.</span><span class="nx">Available</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">Available</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Mem</span><span class="p">.</span><span class="nx">Used</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">Used</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Swap</span><span class="p">.</span><span class="nx">Total</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Total</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Swap</span><span class="p">.</span><span class="nx">Available</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Free</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Swap</span><span class="p">.</span><span class="nx">Used</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Used</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Network</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">InterfaceInfo</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">nv</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="kd">var</span><span class="w"> </span><span class="nx">ii</span><span class="w"> </span><span class="nx">InterfaceInfo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ii</span><span class="p">.</span><span class="nx">ByteSent</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">BytesSent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ii</span><span class="p">.</span><span class="nx">ByteRecv</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">BytesRecv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Network</span><span class="p">[</span><span class="nx">v</span><span class="p">.</span><span class="nx">Name</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">ii</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nx">ii</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">ss</span><span class="p">.</span><span class="nx">Network</span><span class="p">[</span><span class="nx">v</span><span class="p">.</span><span class="nx">Name</span><span class="p">];</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">ii</span><span class="p">.</span><span class="nx">Addrs</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">Addrs</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">vv</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">Addrs</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">ii</span><span class="p">.</span><span class="nx">Addrs</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">vv</span><span class="p">.</span><span class="nx">Addr</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">ss</span><span class="p">.</span><span class="nx">Network</span><span class="p">[</span><span class="nx">v</span><span class="p">.</span><span class="nx">Name</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">ii</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">json</span><span class="p">.</span><span class="nf">Marshal</span><span class="p">(</span><span class="nx">ss</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nb">string</span><span class="p">(</span><span class="nx">b</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>运行起来后，用浏览器访问被采集机器的58000端口就可以看到机器的信息了。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;Percent&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;CPU&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Disk&#34;</span><span class="p">:</span> <span class="mf">11.68627166041493</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Mem&#34;</span><span class="p">:</span> <span class="mf">21.34952360680132</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Swap&#34;</span><span class="p">:</span> <span class="mf">1.498353300558446</span>
</span></span><span class="line"><span class="cl">	<span class="p">},</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;CPU&#34;</span><span class="p">:</span> <span class="p">[{</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;ModelName&#34;</span><span class="p">:</span> <span class="s2">&#34;Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Cores&#34;</span><span class="p">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">	<span class="p">}],</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;Mem&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Total&#34;</span><span class="p">:</span> <span class="mi">2097467392</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Used&#34;</span><span class="p">:</span> <span class="mi">447799296</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Available&#34;</span><span class="p">:</span> <span class="mi">1649668096</span>
</span></span><span class="line"><span class="cl">	<span class="p">},</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;Swap&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Total&#34;</span><span class="p">:</span> <span class="mi">2145382400</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Used&#34;</span><span class="p">:</span> <span class="mi">32145408</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;Available&#34;</span><span class="p">:</span> <span class="mi">2113236992</span>
</span></span><span class="line"><span class="cl">	<span class="p">},</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;Load&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;load1&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;load5&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;load15&#34;</span><span class="p">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">	<span class="p">},</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;Network&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;ens32&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;Addrs&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;192.168.1.68/24&#34;</span><span class="p">,</span> <span class="s2">&#34;fe80::20c:29ff:fed6:1bea/64&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;ByteSent&#34;</span><span class="p">:</span> <span class="mi">10643267063</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;ByteRecv&#34;</span><span class="p">:</span> <span class="mi">3625202751</span>
</span></span><span class="line"><span class="cl">		<span class="p">},</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&#34;lo&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;Addrs&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;127.0.0.1/8&#34;</span><span class="p">,</span> <span class="s2">&#34;::1/128&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;ByteSent&#34;</span><span class="p">:</span> <span class="mi">8310060</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;ByteRecv&#34;</span><span class="p">:</span> <span class="mi">8310060</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="p">},</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;BootTime&#34;</span><span class="p">:</span> <span class="mi">1512095971</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;Uptime&#34;</span><span class="p">:</span> <span class="mi">5290007</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>gopsutil可以获取更多信息，具体可以去看<a href="https://godoc.org/github.com/shirou/gopsutil" rel="noopener noreferrer" target="_blank">gopsutil文档</a>。</p>]]></content:encoded>
    </item>
    <item>
      <title>博客自动部署系统Shell版</title>
      <link>https://blog.zjyl1994.com/post/blogdeployshell/</link>
      <pubDate>Thu, 25 Jan 2018 11:28:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/blogdeployshell/</guid>
      <description>&lt;p&gt;这是用bash脚本+Crontab结合的自动部署，只依赖md5deep包。&lt;/p&gt;
&lt;p&gt;建议设置2-3分钟一次，以免影响性能。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>这是用bash脚本+Crontab结合的自动部署，只依赖md5deep包。</p>
<p>建议设置2-3分钟一次，以免影响性能。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nv">RESULT</span><span class="o">=</span><span class="sb">`</span>md5deep -x /tmp/blogs.md5 -r /path/to/blog/root/ <span class="p">|</span> wc -l<span class="sb">`</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="nv">$RESULT</span> -ne <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    hugo -s /path/to/blog/root/ -d /tmp/blog
</span></span><span class="line"><span class="cl">    rsync -avz --delete /tmp/blog/ rsync@example.com:/path/to/you/www/root
</span></span><span class="line"><span class="cl">    rm -rf /tmp/blog
</span></span><span class="line"><span class="cl">    md5deep -lr /path/to/blog/root/ &gt; /tmp/blogs.md5
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>博客自动部署系统</title>
      <link>https://blog.zjyl1994.com/post/blogdeploy/</link>
      <pubDate>Thu, 25 Jan 2018 01:32:00 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/blogdeploy/</guid>
      <description>&lt;p&gt;静态网站生成器是一个很好用的东西，问题就是他没有控制面板，就会带来一个问题。&lt;/p&gt;
&lt;p&gt;你每次写完文章都要手动去ssh里build一次网站，对于能懒就懒的我来说，这么弄不行。&lt;/p&gt;
&lt;p&gt;传统的实现方式是Git+Hook，这种就需要用Git，很不方便。
我选择的方案是 Nextcloud 直接编辑文件，inofity监听文件变化进行更新。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>静态网站生成器是一个很好用的东西，问题就是他没有控制面板，就会带来一个问题。</p>
<p>你每次写完文章都要手动去ssh里build一次网站，对于能懒就懒的我来说，这么弄不行。</p>
<p>传统的实现方式是Git+Hook，这种就需要用Git，很不方便。
我选择的方案是 Nextcloud 直接编辑文件，inofity监听文件变化进行更新。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">watchdog.observers</span> <span class="kn">import</span> <span class="n">Observer</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">watchdog.events</span> <span class="kn">import</span> <span class="n">FileSystemEventHandler</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">lastmodify</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Watcher</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">DIRECTORY_TO_WATCH</span> <span class="o">=</span> <span class="s2">&#34;/path/to/folder/blogs/&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">DELAY_SECOND</span> <span class="o">=</span> <span class="mi">5</span>
</span></span><span class="line"><span class="cl">    <span class="n">COMMAND_RUN</span> <span class="o">=</span> <span class="s2">&#34;/path/to/script/blog.sh&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">observer</span> <span class="o">=</span> <span class="n">Observer</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">global</span> <span class="n">lastmodify</span>
</span></span><span class="line"><span class="cl">        <span class="n">event_handler</span> <span class="o">=</span> <span class="n">Handler</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">observer</span><span class="o">.</span><span class="n">schedule</span><span class="p">(</span><span class="n">event_handler</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">DIRECTORY_TO_WATCH</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">observer</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="n">lastmodify</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="k">if</span> <span class="p">(</span><span class="n">lastmodify</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">DELAY_SECOND</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                        <span class="n">os</span><span class="o">.</span><span class="n">system</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">COMMAND_RUN</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                        <span class="n">lastmodify</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">                <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">except</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">observer</span><span class="o">.</span><span class="n">stop</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">observer</span><span class="o">.</span><span class="n">join</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Handler</span><span class="p">(</span><span class="n">FileSystemEventHandler</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@staticmethod</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">on_any_event</span><span class="p">(</span><span class="n">event</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">global</span> <span class="n">lastmodify</span>
</span></span><span class="line"><span class="cl">        <span class="n">lastmodify</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">w</span> <span class="o">=</span> <span class="n">Watcher</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">w</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>博客2.0</title>
      <link>https://blog.zjyl1994.com/post/blogrebuild/</link>
      <pubDate>Wed, 24 Jan 2018 13:48:56 +0800</pubDate>
      <guid>https://blog.zjyl1994.com/post/blogrebuild/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Hugo 是个不错的静态网站生成器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;早前博客都用的Wordpress，然后做迁移，数据就没了，我还没备份。年年新建&lt;/p&gt;
&lt;p&gt;中间过渡的时候用过一阵Blogger，但是好多国内朋友都没法访问，就很皮。&lt;/p&gt;
&lt;p&gt;重新配了一个Hugo的博客，挂在洛杉矶的服务器上，就当是从头开始了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>Hugo 是个不错的静态网站生成器</p>
</blockquote>
<p>早前博客都用的Wordpress，然后做迁移，数据就没了，我还没备份。年年新建</p>
<p>中间过渡的时候用过一阵Blogger，但是好多国内朋友都没法访问，就很皮。</p>
<p>重新配了一个Hugo的博客，挂在洛杉矶的服务器上，就当是从头开始了。</p>]]></content:encoded>
    </item>
  </channel>
</rss>
