<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>小华</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>https://blog.mangege.com/favicon.ico</icon>
  <id>https://blog.mangege.com/</id>
  <link href="https://blog.mangege.com/" rel="alternate"/>
  <link href="https://blog.mangege.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 小华</rights>
  <subtitle>瞎折腾的垃圾佬</subtitle>
  <title>笑话小华</title>
  <updated>2026-04-14T15:05:49.626Z</updated>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="hexo" scheme="https://blog.mangege.com/tags/hexo/"/>
    <category term="mermaid" scheme="https://blog.mangege.com/tags/mermaid/"/>
    <category term="plugin" scheme="https://blog.mangege.com/tags/plugin/"/>
    <content>
      <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>最近 AI 生成文字时配合 Mermaid 绘制图表非常方便，想让博客支持 Mermaid 渲染。</p><p>调研发现现有方案 <code>hexo-filter-mermaid-diagrams</code> 已停止维护，无法兼容新版 Hexo。</p><h2 id="解决方案：hexo-markdown-mermaid"><a href="#解决方案：hexo-markdown-mermaid" class="headerlink" title="解决方案：hexo-markdown-mermaid"></a>解决方案：hexo-markdown-mermaid</h2><p>我开发了一个轻量级的 Hexo 插件 <a href="https://github.com/mangege/hexo-markdown-mermaid">hexo-markdown-mermaid</a>，采用浏览器端渲染，零依赖，即装即用。</p><h3 id="核心特点"><a href="#核心特点" class="headerlink" title="核心特点"></a>核心特点</h3><ol><li><strong>零依赖</strong> - 无需 Puppeteer，仅通过 CDN 引入 Mermaid.js</li><li><strong>轻量级</strong> - 核心代码仅约 20 行</li><li><strong>配置灵活</strong> - 支持自定义 Mermaid 版本和配置参数</li></ol><h3 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h3><p>插件通过 Hexo 的 <code>injector</code> 在页面底部注入 Mermaid.js 和初始化代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">hexo.<span class="property">extend</span>.<span class="property">injector</span>.<span class="title function_">register</span>(<span class="string">&#x27;body_end&#x27;</span>, <span class="string">`</span></span><br><span class="line"><span class="string">&lt;script src=&quot;https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js&quot;&gt;&lt;/script&gt;</span></span><br><span class="line"><span class="string">&lt;script&gt;</span></span><br><span class="line"><span class="string">  mermaid.initialize(&#123;startOnLoad: true&#125;);</span></span><br><span class="line"><span class="string">&lt;/script&gt;</span></span><br><span class="line"><span class="string">`</span>, <span class="string">&#x27;default&#x27;</span>);</span><br></pre></td></tr></table></figure><p>Mermaid 默认使用 <code>.mermaid</code> 选择器查找图表元素，而 Hexo 默认输出格式为 <code>&lt;pre class=&quot;mermaid&quot;&gt;</code>，因此无需额外配置即可自动渲染。</p><h3 id="与语法高亮的兼容"><a href="#与语法高亮的兼容" class="headerlink" title="与语法高亮的兼容"></a>与语法高亮的兼容</h3><p>如果使用 highlight.js，需要在 <code>_config.yml</code> 中排除 mermaid 语言：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">highlight:</span></span><br><span class="line">  <span class="attr">exclude_languages:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mermaid</span></span><br></pre></td></tr></table></figure><p>原因：highlight.js 会预编译代码块为 HTML，生成 <code>&lt;pre&gt;&lt;code class=&quot;highlight mermaid&quot;&gt;</code>，而 Mermaid 默认查找 <code>&lt;pre class=&quot;mermaid&quot;&gt;</code>。通过排除让 highlight.js 跳过 mermaid 块，保留原始格式供 Mermaid 渲染。</p><h2 id="实现细节"><a href="#实现细节" class="headerlink" title="实现细节"></a>实现细节</h2><p>最初考虑过预编译方案（mermaid-cli），但依赖 Puppeteer 较重，不适合简单需求。</p><p>最终采用浏览器端渲染，通过 CDN 引入 Mermaid.js，配合 <code>hexo.extend.injector</code> 注入页面。</p><p>关键点：</p><ol><li>使用 <code>hexo.config.mermaid</code> 读取用户配置</li><li><code>startOnLoad: true</code> 让 Mermaid 页面加载时自动渲染</li></ol><h2 id="高度定制化"><a href="#高度定制化" class="headerlink" title="高度定制化"></a>高度定制化</h2><p>如果插件无法满足需求，可以不安装此插件，直接使用 Hexo 的 <a href="https://hexo.io/docs/plugins">Script</a> 功能。适用场景：</p><ul><li>自定义 Mermaid 配置（如 theme、securityLevel、fontFamily）</li><li>修改选择器（如 <code>querySelector: &#39;pre code.mermaid&#39;</code>）</li><li>完全自定义渲染逻辑</li></ul><p>在 <code>scripts/</code> 目录下创建自定义脚本：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">hexo.<span class="property">extend</span>.<span class="property">injector</span>.<span class="title function_">register</span>(<span class="string">&#x27;body_end&#x27;</span>, <span class="string">`</span></span><br><span class="line"><span class="string">&lt;script src=&quot;https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js&quot;&gt;&lt;/script&gt;</span></span><br><span class="line"><span class="string">&lt;script&gt;</span></span><br><span class="line"><span class="string">  mermaid.initialize(&#123;</span></span><br><span class="line"><span class="string">    startOnLoad: true,</span></span><br><span class="line"><span class="string">    theme: &#x27;dark&#x27;,</span></span><br><span class="line"><span class="string">    securityLevel: &#x27;loose&#x27;,</span></span><br><span class="line"><span class="string">    fontFamily: &#x27;Arial&#x27;</span></span><br><span class="line"><span class="string">  &#125;);</span></span><br><span class="line"><span class="string">&lt;/script&gt;</span></span><br><span class="line"><span class="string">`</span>, <span class="string">&#x27;default&#x27;</span>);</span><br></pre></td></tr></table></figure><h2 id="安装使用"><a href="#安装使用" class="headerlink" title="安装使用"></a>安装使用</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-markdown-mermaid</span><br></pre></td></tr></table></figure><p>配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">highlight:</span></span><br><span class="line">  <span class="attr">exclude_languages:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mermaid</span></span><br><span class="line"></span><br><span class="line"><span class="attr">mermaid:</span></span><br><span class="line">  <span class="attr">version:</span> <span class="number">11</span></span><br><span class="line">  <span class="attr">startOnLoad:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">theme:</span> <span class="string">default</span></span><br></pre></td></tr></table></figure><p>详细文档见 <a href="https://github.com/mangege/hexo-markdown-mermaid">GitHub</a>。</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2026-03-26-1800/</id>
    <link href="https://blog.mangege.com/tech/2026-03-26-1800/"/>
    <published>2026-03-26T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>最近 AI 生成文字时配合 Mermaid 绘制图表非常方便，想让博客支持 Mermaid 渲染。</p>
<p>调研发现现有方案]]>
    </summary>
    <title>Hexo 博客系统的 Mermaid 插件：hexo-markdown-mermaid</title>
    <updated>2026-04-14T15:05:49.626Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="linux" scheme="https://blog.mangege.com/tags/linux/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <category term="backend" scheme="https://blog.mangege.com/tags/backend/"/>
    <category term="docker" scheme="https://blog.mangege.com/tags/docker/"/>
    <category term="networking" scheme="https://blog.mangege.com/tags/networking/"/>
    <content>
      <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>之前尝试过基于 OpenWrt + OpenClash 搭建旁路由，但 OpenWrt 系统本身较为臃肿，且 OpenClash 配置复杂，稳定性一般。后来也关注过 Sing-box，虽然功能新颖但变动较快，稳定性有待验证。因此，决定基于 OpenClash 的核心 Mihomo，手动搭建一套精简、高效的旁路由方案。</p><p>特别是在 Vibe Coding 等 AI 辅助开发模式下，AI 需要自动全局代理，以避免手动解决 npm 包安装、Docker 镜像拉取以及调用大语言模型 API 时的连通性问题。手动开发时可能偶尔需要代理，但 AI 自动开发时对网络连通性要求极高，一个稳定可靠的旁路由方案能极大提升开发效率。</p><p>采用 Docker + Macvlan + Mihomo + TUN 的组合，相较于在 OpenWrt 上再搭建服务，具有以下优势：</p><ul><li><strong>更轻量</strong>：无需完整的 OpenWrt 系统，仅需 Docker 容器运行 Mihomo。</li><li><strong>启动更快</strong>：容器化部署，启动和重启速度显著提升。</li><li><strong>更稳定</strong>：Mihomo 作为 Clash 核心，经过多年发展，稳定性有保障。</li></ul><h2 id="架构图"><a href="#架构图" class="headerlink" title="架构图"></a>架构图</h2><pre><code class="highlight mermaid">flowchart LR    subgraph Terminal[终端设备]        T1[电脑/手机]    end    subgraph Host[宿主机]        subgraph Docker[Docker]            M[Mihomo容器]        end    end    subgraph Network[网络出口]        Direct[直连]        Proxy[代理]    end    T1 --&gt;|设置网关为 192.168.1.11| M    M --&gt; Rule[规则引擎]    Rule --&gt;|中国域名/IP| Direct    Rule --&gt;|其他| Proxy    Direct --&gt; Internet[互联网]    Proxy --&gt; Internet</code></pre><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><ol><li><strong>一台支持 Docker 的 Linux 主机</strong>（物理机或虚拟机均可）</li><li><strong>确保系统支持 macvlan 网络驱动</strong>（通常 Linux 内核 3.9+ 都支持）</li><li><strong>准备配置文件 <code>config.yaml</code></strong>（下文提供模板）</li></ol><h2 id="创建-Macvlan-网络"><a href="#创建-Macvlan-网络" class="headerlink" title="创建 Macvlan 网络"></a>创建 Macvlan 网络</h2><p>首先，需要创建一个 macvlan 网络，让容器能够获得独立的局域网 IP。假设你的主机网卡为 <code>eth0</code>，网段为 <code>192.168.1.0/24</code>，网关为 <code>192.168.1.1</code>。</p><p><strong>开启 IP 转发</strong>：在创建网络之前，确保宿主机已开启 IP 转发功能，这是旁路由正常工作的必要条件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 临时开启（重启后失效）</span></span><br><span class="line">sysctl -w net.ipv4.ip_forward=1</span><br><span class="line"></span><br><span class="line"><span class="comment"># 永久开启</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;net.ipv4.ip_forward=1&quot;</span> &gt; /etc/sysctl.d/99-ipforward.conf</span><br><span class="line">sysctl -p /etc/sysctl.d/99-ipforward.conf</span><br></pre></td></tr></table></figure><p>执行以下命令创建 macvlan 网络：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker network create -d macvlan \</span><br><span class="line">  --subnet=192.168.1.0/24 \</span><br><span class="line">  --gateway=192.168.1.1 \</span><br><span class="line">  -o parent=eth0 \</span><br><span class="line">  macnet</span><br></pre></td></tr></table></figure><p><strong>注意</strong>：请根据你的实际网络环境修改 <code>--subnet</code>、<code>--gateway</code> 和 <code>-o parent</code> 参数。</p><p><strong>macvlan 网络限制（可选）</strong>：默认情况下，宿主机与 macvlan 容器之间无法直接通信。如果宿主机也需要使用旁路由代理，需要额外创建一个 macvlan shim 网络：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建宿主机使用的 macvlan 接口</span></span><br><span class="line">ip <span class="built_in">link</span> add macvlan-shim <span class="built_in">link</span> eth0 <span class="built_in">type</span> macvlan mode bridge</span><br><span class="line">ip addr add 192.168.1.10/24 dev macvlan-shim</span><br><span class="line">ip <span class="built_in">link</span> <span class="built_in">set</span> macvlan-shim up</span><br><span class="line">ip route add 192.168.1.11/32 dev macvlan-shim</span><br></pre></td></tr></table></figure><p>这样宿主机就可以通过 192.168.1.10 访问容器的 192.168.1.11 了。如果宿主机不需要代理，可以跳过此步骤。</p><h2 id="准备配置文件"><a href="#准备配置文件" class="headerlink" title="准备配置文件"></a>准备配置文件</h2><p>创建一个目录用于存放配置文件，例如 <code>/root/mihomo</code>。在该目录下创建 <code>config.yaml</code> 文件。以下是一个精简的配置模板（示例配置），包含 <code>proxy-providers</code> 示例，请替换为自己的配置信息。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Mihomo Minimal Transparent Proxy Config</span></span><br><span class="line"><span class="comment"># Suitable for Docker macvlan + TUN model</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># 代理配置（示例）</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">proxies:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&quot;SS1&quot;</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">ss</span></span><br><span class="line">    <span class="attr">server:</span> <span class="string">aaa.mangege.com</span>  <span class="comment"># 替换为你的代理服务器地址</span></span><br><span class="line">    <span class="attr">port:</span> <span class="number">443</span>                <span class="comment"># 替换为你的代理端口</span></span><br><span class="line">    <span class="attr">cipher:</span> <span class="string">chacha20-ietf-poly1305</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">&quot;aaa&quot;</span>          <span class="comment"># 替换为你的代理密码</span></span><br><span class="line">    <span class="attr">udp:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">udp-over-tcp:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">udp-over-tcp-version:</span> <span class="number">2</span></span><br><span class="line">    <span class="attr">ip-version:</span> <span class="string">ipv4</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># 代理提供者（示例）</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">proxy-providers:</span></span><br><span class="line">  <span class="attr">my-provider:</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">http</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">./providers/my-provider.yaml</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">https://example.com/provider.yaml</span>  <span class="comment"># 替换为你的订阅链接</span></span><br><span class="line">    <span class="attr">interval:</span> <span class="number">3600</span></span><br><span class="line">    <span class="attr">health-check:</span></span><br><span class="line">      <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">url:</span> <span class="string">http://www.gstatic.com/generate_204</span></span><br><span class="line">      <span class="attr">interval:</span> <span class="number">300</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># 基础配置</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">mixed-port:</span> <span class="number">7890</span></span><br><span class="line"><span class="attr">redir-port:</span> <span class="number">7892</span>       <span class="comment"># TCP 透明代理</span></span><br><span class="line"><span class="attr">tproxy-port:</span> <span class="number">7893</span>      <span class="comment"># UDP 透明代理</span></span><br><span class="line"><span class="attr">ipv6:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">allow-lan:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">unified-delay:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">tcp-concurrent:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># 外部控制</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">external-controller:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:9090</span></span><br><span class="line"><span class="attr">external-ui:</span> <span class="string">ui</span></span><br><span class="line"><span class="attr">external-ui-url:</span> <span class="string">&quot;https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># Geo 数据</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">geodata-mode:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">geox-url:</span></span><br><span class="line">  <span class="attr">geoip:</span> <span class="string">&quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat&quot;</span></span><br><span class="line">  <span class="attr">geosite:</span> <span class="string">&quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat&quot;</span></span><br><span class="line">  <span class="attr">mmdb:</span> <span class="string">&quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb&quot;</span></span><br><span class="line">  <span class="attr">asn:</span> <span class="string">&quot;https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># 其他设置</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">client-fingerprint:</span> <span class="string">chrome</span></span><br><span class="line"></span><br><span class="line"><span class="attr">profile:</span></span><br><span class="line">  <span class="attr">store-selected:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">store-fake-ip:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">sniffer:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">sniff:</span></span><br><span class="line">    <span class="attr">HTTP:</span></span><br><span class="line">      <span class="attr">ports:</span> [<span class="number">80</span>, <span class="number">8080</span><span class="number">-8880</span>]</span><br><span class="line">      <span class="attr">override-destination:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">TLS:</span></span><br><span class="line">      <span class="attr">ports:</span> [<span class="number">443</span>, <span class="number">8443</span>]</span><br><span class="line">    <span class="attr">QUIC:</span></span><br><span class="line">      <span class="attr">ports:</span> [<span class="number">443</span>, <span class="number">8443</span>]</span><br><span class="line">  <span class="attr">skip-domain:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;Mijia Cloud&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;+.push.apple.com&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># TUN 设置（关键）</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">tun:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">stack:</span> <span class="string">gvisor</span></span><br><span class="line">  <span class="attr">mtu:</span> <span class="number">1500</span></span><br><span class="line">  <span class="attr">dns-hijack:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;any:53&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;tcp://any:53&quot;</span></span><br><span class="line">  <span class="attr">auto-route:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">auto-redirect:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">auto-detect-interface:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">fake-ip-range:</span> <span class="number">198.18</span><span class="number">.0</span><span class="number">.1</span><span class="string">/16</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># DNS 设置</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">dns:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">ipv6:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">enhanced-mode:</span> <span class="string">fake-ip</span></span><br><span class="line">  <span class="attr">fake-ip-filter:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;*&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;+.lan&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;+.local&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;+.market.xiaomi.com&quot;</span></span><br><span class="line">  <span class="attr">default-nameserver:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">tls://223.5.5.5</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">tls://223.6.6.6</span></span><br><span class="line">  <span class="attr">fallback:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">tls://1.1.1.1</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">tls://8.8.8.8</span></span><br><span class="line">  <span class="attr">nameserver:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">https://doh.pub/dns-query</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">https://dns.alidns.com/dns-query</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># 代理组</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">proxy-groups:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&quot;DEFAULT&quot;</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">select</span></span><br><span class="line">    <span class="attr">proxies:</span> [<span class="string">SS1</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="comment"># 规则</span></span><br><span class="line"><span class="comment"># -----------------------------</span></span><br><span class="line"><span class="attr">rules:</span></span><br><span class="line">  <span class="comment"># 局域网IP直连</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">GEOIP,lan,DIRECT,no-resolve</span></span><br><span class="line">  <span class="comment"># 中国域名直连（通过GeoSite规则）</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">GEOSITE,CN,DIRECT</span></span><br><span class="line">  <span class="comment"># 中国IP直连（通过GeoIP规则）</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">GEOIP,CN,DIRECT</span></span><br><span class="line">  <span class="comment"># Tailscale相关域名直连（可选）</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">DOMAIN-SUFFIX,ts.net,DIRECT</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">DOMAIN-SUFFIX,tailscale.io,DIRECT</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">DOMAIN-SUFFIX,tailscale.com,DIRECT</span></span><br><span class="line">  <span class="comment"># 其他所有流量走代理组DEFAULT（即使用SS1代理）</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">MATCH,DEFAULT</span></span><br></pre></td></tr></table></figure><h3 id="重要提示"><a href="#重要提示" class="headerlink" title="重要提示"></a>重要提示</h3><ul><li>将上述配置中的代理信息（<code>server</code>、<code>port</code>、<code>password</code> 等）替换为你自己的。</li><li>如果使用 <code>proxy-providers</code>，请确保 <code>url</code> 指向有效的订阅链接。</li><li>根据需要调整 <code>tun</code>、<code>dns</code>、<code>rules</code> 等配置。</li><li>更多配置选项请参考 Mihomo 官方文档：<ul><li><a href="https://wiki.metacubex.one/example/conf/">配置示例</a></li><li><a href="https://wiki.metacubex.one/config/">完整配置说明</a></li></ul></li></ul><h2 id="启动容器"><a href="#启动容器" class="headerlink" title="启动容器"></a>启动容器</h2><p>使用以下命令启动 Mihomo 容器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --name mihomo \</span><br><span class="line">  --restart always \</span><br><span class="line">  --network macnet \</span><br><span class="line">  --ip 192.168.1.11 \</span><br><span class="line">  -v /root/mihomo/config.yaml:/root/.config/mihomo/config.yaml \</span><br><span class="line">  metacubex/mihomo:latest</span><br></pre></td></tr></table></figure><p><strong>注意</strong>：</p><ul><li><code>--ip</code> 参数指定容器的 IP 地址，请确保该 IP 在你的局域网网段内且未被占用。</li><li><code>-v</code> 参数将主机上的配置文件映射到容器内，请确保路径正确。</li><li><code>--network macnet</code> 使用之前创建的 macvlan 网络。</li></ul><h2 id="配置说明"><a href="#配置说明" class="headerlink" title="配置说明"></a>配置说明</h2><ul><li><strong>Macvlan 网络</strong>：使容器获得独立 IP，如同物理设备接入局域网。</li><li><strong>TUN 模式</strong>：启用 TUN 接口实现透明代理，对应用透明。</li><li><strong>DNS 劫持</strong>：通过 <code>dns-hijack</code> 劫持 DNS 查询，防止 DNS 泄露。</li><li><strong>规则分流</strong>：采用“中国站点与中国IP走直连，否则走代理”的策略。具体规则：局域网IP直连、中国域名直连（GeoSite）、中国IP直连（GeoIP）、Tailscale域名直连，其他所有流量走代理组。</li><li><strong>代理组</strong>：配置一个名为 DEFAULT 的 select 代理组，包含 SS1 代理。规则中的 MATCH,DEFAULT 将所有未匹配的流量导向此代理组，实现代理功能。</li><li><strong>客户端配置</strong>：需要在要使用旁路由的机器上手动配置静态IP和DNS服务器地址，将它们都设置为 Mihomo 容器的IP地址（如 192.168.1.11），以确保流量正确通过旁路由。<ul><li><strong>Windows</strong>：网络设置 → 更改适配器选项 → 右键以太网&#x2F;WiFi → 属性 → IPv4 → 手动设置IP和DNS</li><li><strong>macOS</strong>：系统偏好设置 → 网络 → 高级 → TCP&#x2F;IP → 手动配置 → DNS → 添加 192.168.1.11</li><li><strong>Linux</strong>：<code>nmcli</code> 或编辑 <code>/etc/resolv.conf</code>，添加 <code>nameserver 192.168.1.11</code></li></ul></li><li><strong>安全性建议</strong>：<ol><li>配置文件权限：<code>chmod 600 /root/mihomo/config.yaml</code></li><li>防火墙设置：确保 Mihomo 端口（7890、7892、7893、9090）不被外部访问</li><li>定期更新：定期更新镜像以获取安全补丁</li><li>日志监控：定期检查日志，发现异常访问</li></ol></li><li><strong>性能调优</strong>：<ol><li>MTU 调整：在 config.yaml 的 tun 部分调整 MTU 值（通常 1500 或 1400）</li><li>连接数限制：根据服务器性能调整 <code>max-concurrent</code> 参数</li><li>缓存优化：启用 <code>store-selected</code> 和 <code>store-fake-ip</code> 以提升性能</li><li>DNS 缓存：调整 DNS 缓存时间以减少查询次数</li></ol></li></ul><h2 id="验证配置"><a href="#验证配置" class="headerlink" title="验证配置"></a>验证配置</h2><p>配置完成后，需要验证旁路由是否工作正常。</p><h3 id="基础测试"><a href="#基础测试" class="headerlink" title="基础测试"></a>基础测试</h3><ol><li><p><strong>检查容器状态</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker ps | grep mihomo</span><br></pre></td></tr></table></figure></li><li><p><strong>测试 DNS 解析</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nslookup google.com 192.168.1.11</span><br></pre></td></tr></table></figure></li><li><p><strong>测试网络连通性</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 从客户端 ping 网关</span></span><br><span class="line">ping 192.168.1.11</span><br><span class="line"></span><br><span class="line"><span class="comment"># 从客户端测试代理</span></span><br><span class="line">curl -x socks5://192.168.1.11:7890 https://httpbin.org/ip</span><br></pre></td></tr></table></figure></li></ol><h3 id="流量测试"><a href="#流量测试" class="headerlink" title="流量测试"></a>流量测试</h3><ol><li><p><strong>国内网站直连测试</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -I https://www.baidu.com</span><br></pre></td></tr></table></figure></li><li><p><strong>国外网站代理测试</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -I https://www.google.com</span><br></pre></td></tr></table></figure></li><li><p><strong>查看 Mihomo 日志确认分流</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker logs mihomo -f | grep -E <span class="string">&quot;DIRECT|REJECT|匹配&quot;</span></span><br></pre></td></tr></table></figure></li></ol><h3 id="性能测试"><a href="#性能测试" class="headerlink" title="性能测试"></a>性能测试</h3><ol><li><p><strong>速度测试</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 使用 speedtest-cli 测试</span></span><br><span class="line">pip install speedtest-cli</span><br><span class="line">speedtest-cli --server 12345  <span class="comment"># 替换为实际服务器ID</span></span><br></pre></td></tr></table></figure></li><li><p><strong>延迟测试</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ping -c 10 192.168.1.11</span><br></pre></td></tr></table></figure></li></ol><h2 id="故障排查"><a href="#故障排查" class="headerlink" title="故障排查"></a>故障排查</h2><h3 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h3><ol><li><p><strong>容器无法启动</strong>：</p><ul><li>检查配置文件语法：<code>docker logs mihomo</code></li><li>确认 macvlan 网络已创建：<code>docker network ls</code></li><li>检查 IP 地址冲突：<code>ping 192.168.1.11</code></li></ul></li><li><p><strong>客户端无法上网</strong>：</p><ul><li>确认客户端网关和 DNS 都设置为 <code>192.168.1.11</code></li><li>检查 Mihomo 日志：<code>docker logs mihomo -f</code></li><li>测试 DNS 解析：<code>nslookup google.com 192.168.1.11</code></li></ul></li><li><p><strong>速度慢或不稳定</strong>：</p><ul><li>检查代理服务器状态</li><li>调整 MTU 值（在 config.yaml 的 tun 部分修改）</li><li>查看容器资源使用：<code>docker stats mihomo</code></li></ul></li></ol><h3 id="日志查看"><a href="#日志查看" class="headerlink" title="日志查看"></a>日志查看</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看实时日志</span></span><br><span class="line">docker logs mihomo -f</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看最近100行日志</span></span><br><span class="line">docker logs mihomo --<span class="built_in">tail</span> 100</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看特定时间后的日志</span></span><br><span class="line">docker logs mihomo --since 2026-03-25T21:00:00</span><br></pre></td></tr></table></figure><h3 id="配置验证"><a href="#配置验证" class="headerlink" title="配置验证"></a>配置验证</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 检查容器状态</span></span><br><span class="line">docker ps | grep mihomo</span><br><span class="line"></span><br><span class="line"><span class="comment"># 检查网络连接</span></span><br><span class="line">docker <span class="built_in">exec</span> mihomo ping -c 3 192.168.1.1</span><br><span class="line"></span><br><span class="line"><span class="comment"># 检查 DNS 服务</span></span><br><span class="line">docker <span class="built_in">exec</span> mihomo nslookup google.com</span><br></pre></td></tr></table></figure><h2 id="维护"><a href="#维护" class="headerlink" title="维护"></a>维护</h2><h3 id="容器管理"><a href="#容器管理" class="headerlink" title="容器管理"></a>容器管理</h3><ol><li><p><strong>重启容器</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker restart mihomo</span><br></pre></td></tr></table></figure></li><li><p><strong>停止容器</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker stop mihomo</span><br></pre></td></tr></table></figure></li><li><p><strong>删除容器</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">rm</span> -f mihomo</span><br></pre></td></tr></table></figure></li></ol><h3 id="配置更新"><a href="#配置更新" class="headerlink" title="配置更新"></a>配置更新</h3><ol><li><p><strong>修改配置文件</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 编辑配置文件</span></span><br><span class="line">vim /root/mihomo/config.yaml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 重启容器使配置生效</span></span><br><span class="line">docker restart mihomo</span><br></pre></td></tr></table></figure></li><li><p><strong>配置热重载</strong>（如果支持）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 通过 API 重载配置</span></span><br><span class="line">curl -X PUT http://127.0.0.1:9090/configs -d <span class="string">&#x27;&#123;&quot;path&quot;:&quot;/root/.config/mihomo/config.yaml&quot;&#125;&#x27;</span></span><br></pre></td></tr></table></figure></li></ol><h3 id="镜像更新"><a href="#镜像更新" class="headerlink" title="镜像更新"></a>镜像更新</h3><ol><li><p><strong>拉取最新镜像</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull metacubex/mihomo:latest</span><br></pre></td></tr></table></figure></li><li><p><strong>重建容器</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 停止并删除旧容器</span></span><br><span class="line">docker stop mihomo</span><br><span class="line">docker <span class="built_in">rm</span> mihomo</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用新镜像重新创建容器</span></span><br><span class="line">docker run -d \</span><br><span class="line">  --name mihomo \</span><br><span class="line">  --restart always \</span><br><span class="line">  --network macnet \</span><br><span class="line">  --ip 192.168.1.11 \</span><br><span class="line">  -v /root/mihomo/config.yaml:/root/.config/mihomo/config.yaml \</span><br><span class="line">  metacubex/mihomo:latest</span><br></pre></td></tr></table></figure></li></ol><h3 id="备份与恢复"><a href="#备份与恢复" class="headerlink" title="备份与恢复"></a>备份与恢复</h3><ol><li><p><strong>备份配置</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 备份配置文件</span></span><br><span class="line"><span class="built_in">cp</span> /root/mihomo/config.yaml /root/mihomo/config.yaml.backup</span><br><span class="line"></span><br><span class="line"><span class="comment"># 备份容器</span></span><br><span class="line">docker <span class="built_in">export</span> mihomo &gt; mihomo-backup.tar</span><br></pre></td></tr></table></figure></li><li><p><strong>恢复配置</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 恢复配置文件</span></span><br><span class="line"><span class="built_in">cp</span> /root/mihomo/config.yaml.backup /root/mihomo/config.yaml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 重启容器</span></span><br><span class="line">docker restart mihomo</span><br></pre></td></tr></table></figure></li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过 Docker + Macvlan + Mihomo + TUN 的组合，我们搭建了一个轻量、稳定且高效的旁路由方案。相比传统的 OpenWrt 方案，它更加精简，启动更快，维护也更简单。只需将终端设备的网关指向旁路由的 IP（如 <code>192.168.1.11</code>），即可享受全局代理服务。</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2026-03-25-2120/</id>
    <link href="https://blog.mangege.com/tech/2026-03-25-2120/"/>
    <published>2026-03-25T05:20:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>之前尝试过基于 OpenWrt + OpenClash 搭建旁路由，但 OpenWrt 系统本身较为臃肿，且 OpenClash]]>
    </summary>
    <title>基于 Mihomo + Docker 实现旁路由</title>
    <updated>2026-04-14T15:05:49.626Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="linux" scheme="https://blog.mangege.com/tags/linux/"/>
    <category term="frontend" scheme="https://blog.mangege.com/tags/frontend/"/>
    <category term="backend" scheme="https://blog.mangege.com/tags/backend/"/>
    <content>
      <![CDATA[<p>FFI 全称 Foreign Function Interface .<br>主要解决在 Node.js 里用 JS 调用 C&#x2F;C++ 写的动态库的问题.<br><a href="https://www.npmjs.com/package/ffi-napi">https://www.npmjs.com/package/ffi-napi</a></p><ol><li>在安装 ffi 之前,请先安装好 node-gyp 相关的依赖,具体请看官方安装说明 <a href="https://github.com/nodejs/node-gyp">https://github.com/nodejs/node-gyp</a> .</li><li>Node.js 12 及以上的版本,请安装 ffi-napi 包,而不是 ffi 包. 原理请看 N-API 介绍: <a href="https://xcoder.in/2017/07/01/nodejs-addon-history/">https://xcoder.in/2017/07/01/nodejs-addon-history/</a></li><li>请安装 ref-napi 包,而不是 ref 包,参数是指针类型时需要. 其它像 struct, union, array 请找对应的 napi 的包装. <a href="https://github.com/node-ffi-napi">https://github.com/node-ffi-napi</a></li></ol><p>先看 FFI官方给的示例:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> ffi = <span class="built_in">require</span>(<span class="string">&#x27;ffi-napi&#x27;</span>);</span><br><span class="line"> </span><br><span class="line"><span class="keyword">var</span> libm = ffi.<span class="title class_">Library</span>(<span class="string">&#x27;libm&#x27;</span>, &#123;</span><br><span class="line">  <span class="string">&#x27;ceil&#x27;</span>: [ <span class="string">&#x27;double&#x27;</span>, [ <span class="string">&#x27;double&#x27;</span> ] ]</span><br><span class="line">&#125;);</span><br><span class="line">libm.<span class="title function_">ceil</span>(<span class="number">1.5</span>); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure><p>示例是调用 C 的 math 相关的库的 ceil 函数. 在 Mac 或 Linux 下,我们可以通过 <code>man ceil</code> 看函数的 C 签名.</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">NAME</span><br><span class="line">     ceil -- round to smallest integral value not less than x</span><br><span class="line"></span><br><span class="line">SYNOPSIS</span><br><span class="line">     #include &lt;math.h&gt;</span><br><span class="line"></span><br><span class="line">     double</span><br><span class="line">     ceil(double x);</span><br><span class="line"></span><br><span class="line">     long double</span><br><span class="line">     ceill(long double x);</span><br><span class="line"></span><br><span class="line">     float</span><br><span class="line">     ceilf(float x);</span><br><span class="line"></span><br><span class="line">DESCRIPTION</span><br><span class="line">     The ceil() functions return the smallest integral value greater than or</span><br><span class="line">     equal to x.</span><br></pre></td></tr></table></figure><p>可以看到 ceil 函数返回一个 double 值,且需要一个 double 值. </p><p><a href="https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial">https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial</a> 手册描述 <code>ffi.Library</code> 的函数签名如下:</p><p><code>ffi.Library(libraryFile, { functionSymbol: [ returnType, [ arg1Type, arg2Type, ... ], ... ]);</code></p><p>所以示例代码调用 libm 的 ceil 意思就很明白.</p><p>之所以需要 ref 包,是因为在调用指针等 JS 里没有的类型时,需要用它来构建参数的值.</p><p>拿 <a href="https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial">FFI 手册</a> 里的代码来举例</p><p><code>sqlite3_open</code> 等函数的签名请看 <a href="https://www.sqlite.org/capi3ref.html#sqlite3_open">https://www.sqlite.org/capi3ref.html#sqlite3_open</a></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> ref = <span class="built_in">require</span>(<span class="string">&#x27;ref&#x27;</span>);</span><br><span class="line"><span class="keyword">var</span> ffi = <span class="built_in">require</span>(<span class="string">&#x27;ffi&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// typedef</span></span><br><span class="line"><span class="keyword">var</span> sqlite3 = ref.<span class="property">types</span>.<span class="property">void</span>; <span class="comment">// we don&#x27;t know what the layout of &quot;sqlite3&quot; looks like</span></span><br><span class="line"><span class="keyword">var</span> sqlite3Ptr = ref.<span class="title function_">refType</span>(sqlite3);</span><br><span class="line"><span class="keyword">var</span> sqlite3PtrPtr = ref.<span class="title function_">refType</span>(sqlite3Ptr);</span><br><span class="line"><span class="keyword">var</span> stringPtr = ref.<span class="title function_">refType</span>(ref.<span class="property">types</span>.<span class="property">CString</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// binding to a few &quot;libsqlite3&quot; functions...</span></span><br><span class="line"><span class="keyword">var</span> libsqlite3 = ffi.<span class="title class_">Library</span>(<span class="string">&#x27;libsqlite3&#x27;</span>, &#123;</span><br><span class="line">  <span class="string">&#x27;sqlite3_open&#x27;</span>: [ <span class="string">&#x27;int&#x27;</span>, [ <span class="string">&#x27;string&#x27;</span>, sqlite3PtrPtr ] ],</span><br><span class="line">  <span class="string">&#x27;sqlite3_close&#x27;</span>: [ <span class="string">&#x27;int&#x27;</span>, [ sqlite3Ptr ] ],</span><br><span class="line">  <span class="string">&#x27;sqlite3_exec&#x27;</span>: [ <span class="string">&#x27;int&#x27;</span>, [ sqlite3Ptr, <span class="string">&#x27;string&#x27;</span>, <span class="string">&#x27;pointer&#x27;</span>, <span class="string">&#x27;pointer&#x27;</span>, stringPtr ] ],</span><br><span class="line">  <span class="string">&#x27;sqlite3_changes&#x27;</span>: [ <span class="string">&#x27;int&#x27;</span>, [ sqlite3Ptr ]]</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// now use them:</span></span><br><span class="line"><span class="keyword">var</span> dbPtrPtr = ref.<span class="title function_">alloc</span>(sqlite3PtrPtr);</span><br><span class="line">libsqlite3.<span class="title function_">sqlite3_open</span>(<span class="string">&quot;test.sqlite3&quot;</span>, dbPtrPtr);</span><br><span class="line"><span class="keyword">var</span> dbHandle = dbPtrPtr.<span class="title function_">deref</span>();</span><br></pre></td></tr></table></figure><p>函数签名我们可以用 ref.types 来构建,也可以直接写成 string 字符,对于复杂的指针类型,我们其实可以直接用 <code>&#39;pointer&#39;</code> 就行了.但是使用 ref.alloc 创建指针等复杂参数值时,就必须得 ref.types 方式来构建了.</p><p><code>ref.alloc</code> 函数返回的是指针,如果是需要传值,需要调用再调用 <code>deref</code> 方法. 从手册里的下文示例代码可以看出 <code>alloc</code> 返回的是指针类型.</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> intPtr = ref.<span class="title function_">refType</span>(<span class="string">&#x27;int&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> libmylibrary = ffi.<span class="title class_">Library</span>(<span class="string">&#x27;libmylibrary&#x27;</span>, &#123; ...,</span><br><span class="line">  <span class="string">&#x27;manipulate_number&#x27;</span>: [ <span class="string">&#x27;void&#x27;</span>, [ intPtr ] ]</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> outNumber = ref.<span class="title function_">alloc</span>(<span class="string">&#x27;int&#x27;</span>); <span class="comment">// allocate a 4-byte (32-bit) chunk for the output data</span></span><br><span class="line">libmylibrary.<span class="title function_">manipulate_number</span>(outNumber);</span><br><span class="line"><span class="keyword">var</span> actualNumber = outNumber.<span class="title function_">deref</span>();</span><br></pre></td></tr></table></figure><p>异步调用函数示例代码如下:</p><p><a href="https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial#async-library-calls">https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial#async-library-calls</a></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> libmylibrary = ffi.<span class="title class_">Library</span>(<span class="string">&#x27;libmylibrary&#x27;</span>, &#123;</span><br><span class="line">  <span class="string">&#x27;mycall&#x27;</span>: [ <span class="string">&#x27;int&#x27;</span>, [ <span class="string">&#x27;int&#x27;</span> ] ]</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">libmylibrary.<span class="property">mycall</span>.<span class="title function_">async</span>(<span class="number">1234</span>, <span class="keyword">function</span> (<span class="params">err, res</span>) &#123;&#125;);</span><br></pre></td></tr></table></figure><p>开始主题,调用 win32 api . 以 SystemParametersInfo 函数为例.</p><p>首先找到 SystemParametersInfo 的函数原型:</p><p><a href="https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/um/WinUser.h">https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/um/WinUser.h</a></p><p>在上文件搜索,没有 SystemParametersInfo 函数,只有 SystemParametersInfo 常量,根据是否为 UNICODE 模式,决定是调用 SystemParametersInfoA(ANSI) 还是 SystemParametersInfoW (WideChar) .因为我们在 Node.js 里调用没有引入头文件编译,所以我们任意选择一个即可,本文选择用 SystemParametersInfoA 来做示例.</p><p>SystemParametersInfoA 函数的文档请看: <a href="https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-systemparametersinfoa">https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-systemparametersinfoa</a></p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">BOOL <span class="title">SystemParametersInfoA</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">  UINT  uiAction,</span></span></span><br><span class="line"><span class="params"><span class="function">  UINT  uiParam,</span></span></span><br><span class="line"><span class="params"><span class="function">  PVOID pvParam,</span></span></span><br><span class="line"><span class="params"><span class="function">  UINT  fWinIni</span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>;</span><br></pre></td></tr></table></figure><p>1,2,4 参数为数字类型, 3 参数为指针.</p><p>示例是调用此方法检测系统是否进入屏保状态, 我们定位到 SPI_GETSCREENSAVERRUNNING 关键的那一行,告诉我们参数3需要为一个 bool 的指针,如果检测屏保已经启动,会把指针的值设为 true .</p><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Determines whether a screen saver <span class="keyword">is</span> currently running <span class="keyword">on</span> the <span class="keyword">window</span> station <span class="keyword">of</span> the calling process. The pvParam parameter must <span class="type">point</span> <span class="keyword">to</span> a <span class="type">BOOL</span> variable that receives <span class="keyword">TRUE</span> <span class="keyword">if</span> a screen saver <span class="keyword">is</span> currently running, <span class="keyword">or</span> <span class="keyword">FALSE</span> otherwise. Note that <span class="keyword">only</span> the interactive <span class="keyword">window</span> station, WinSta0, can have a screen saver running.</span><br></pre></td></tr></table></figure><p>uiParam,fWinIni 看文档的说明,在调用  SPI_GETSCREENSAVERRUNNING 时,我们传 0 即可.</p><p>所以最终的 js 调用代码如下.</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> ffi = <span class="built_in">require</span>(<span class="string">&#x27;ffi-napi&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> ref = <span class="built_in">require</span>(<span class="string">&#x27;ref-napi&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> user32 = ffi.<span class="title class_">Library</span>(<span class="string">&#x27;user32.dll&#x27;</span>, &#123;</span><br><span class="line">    <span class="title class_">SystemParametersInfoA</span>: [<span class="string">&#x27;bool&#x27;</span>, [<span class="string">&#x27;uint&#x27;</span>, <span class="string">&#x27;uint&#x27;</span>, <span class="string">&#x27;pointer&#x27;</span>, <span class="string">&#x27;uint&#x27;</span>]]</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 0x0072 SPI_GETSCREENSAVERRUNNING, 从头文件或文档里查,因不是 C++ 项目,所以只能以直接写值,否则可以写 SPI_GETSCREENSAVERRUNNING ,然后 C++ 的预处理器会帮忙替换.</span></span><br><span class="line"><span class="keyword">let</span> checkResult = user32.<span class="title class_">SystemParametersInfoA</span>(<span class="number">0x0072</span>, <span class="number">0</span>, isEnable, <span class="number">0</span>)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`checkResult <span class="subst">$&#123;checkResult&#125;</span> isEnable <span class="subst">$&#123;isEnable.deref()&#125;</span>`</span>)</span><br></pre></td></tr></table></figure><p>使用 ffi 调用 win32 api 示例即完成了,如需调用其它函数,只需要找到相关的头文件和文档说明按照上代码的方式编写即可.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2019-12-26-1/</id>
    <link href="https://blog.mangege.com/tech/2019-12-26-1/"/>
    <published>2019-12-26T02:13:16.000Z</published>
    <summary>
      <![CDATA[<p>FFI 全称 Foreign Function Interface .<br>主要解决在 Node.js 里用 JS 调用 C&#x2F;C++ 写的动态库的问题.<br><a]]>
    </summary>
    <title>Node.js 使用 FFI 调用 win32 API</title>
    <updated>2026-04-14T15:05:49.626Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="backend" scheme="https://blog.mangege.com/tags/backend/"/>
    <content>
      <![CDATA[<p>瞎折腾建站为了节省成本,只能买些小品牌服务商的 VPS. 小品牌不放心,数据得自己好好备份.</p><p>MySQL 备份方式有很多,比如直接 Dump ,但小 VPS 空间少,带宽少,拉到本地速度慢.除非是数据库数据比较小,不然不推荐用 Dump 来备份.<br>增量备份还可以考虑 XtraBackup ,但维护起来比较麻烦,而且也占用本地空间.</p><p>个人看来,使用从库应该是备份 VPS 的 MySQL 的最佳方案.</p><ul><li>占用空间小(binlog 需要占用一些空间)</li><li>增量同步,比较实时(只要网络与IO跟的上,从库基本上没有延迟.个人测试,国外主库,国内从库,有1小时的延迟的样子)</li><li>稳定,搭建后维护成本低.</li></ul><p>为什么选择 Docker 来搭建从库,而不是直接安装 MySQL 到主机上?<br>有时机器上面已经安装了 MySQL ,再搭建一个 MySQL 做从库,需要改 MySQL 配置文件,添加开机自启脚本,麻烦,容易出错.而 Docker 可以完美的解决这些问题.</p><p>使用 Docker ,你可以在两台不同的 VPS 互相做主从备份,或在家里的电脑,自己的工作电脑建立从库(不是 24 小时开机的话, 主库的 binlog 保存久一点), 而不影响主机上原有的 MySQL 的使用.</p><p>本教程基于 Percona 编译的 MySQL 版本, 主从库版本请一致.<br>本文对于 Docker 和 MySQL 的主从配置写的不是很详细,需要自己有一定的基础.</p><p>参考链接:</p><ul><li><a href="https://www.percona.com/blog/2016/03/30/docker-mysql-replication-101/">https://www.percona.com/blog/2016/03/30/docker-mysql-replication-101/</a></li><li><a href="https://hub.docker.com/_/percona">https://hub.docker.com/_/percona</a></li><li><a href="https://www.percona.com/doc/percona-xtrabackup/LATEST/backup_scenarios/full_backup.html">https://www.percona.com/doc/percona-xtrabackup/LATEST/backup_scenarios/full_backup.html</a></li></ul><h4 id="1-安装-Docker"><a href="#1-安装-Docker" class="headerlink" title="1 安装 Docker"></a>1 安装 Docker</h4><p>请参考官网链接 <a href="https://docs.docker.com/install/">https://docs.docker.com/install/</a></p><h4 id="2-拉镜像"><a href="#2-拉镜像" class="headerlink" title="2 拉镜像"></a>2 拉镜像</h4><p>请根据自己的版本需求修改 tag .</p><p><code>docker pull percona:8.0</code></p><h4 id="3-备份主库数据库"><a href="#3-备份主库数据库" class="headerlink" title="3 备份主库数据库"></a>3 备份主库数据库</h4><p>使用 XtraBackup 备份主库数据(不用停机), 然后把备份文件打包,下载到从库机器上面 (速度慢就套层 CF CDN).</p><p><code>xtrabackup --backup --target-dir=/data/backups/</code></p><p>具体教程 <a href="https://www.percona.com/doc/percona-xtrabackup/LATEST/backup_scenarios/full_backup.html">https://www.percona.com/doc/percona-xtrabackup/LATEST/backup_scenarios&#x2F;full_backup.html</a></p><h4 id="4-准备从库数据库文件"><a href="#4-准备从库数据库文件" class="headerlink" title="4 准备从库数据库文件"></a>4 准备从库数据库文件</h4><p>在从库机器上面解压数据库备份文件,然后执行准备命令. 执行了 prepare ,这个时候 MySQL 就可以直接基于此目录的数据文件启动了.</p><p><code>xtrabackup --prepare --target-dir=/data/backups/</code></p><h4 id="6-创建从库数据库配置文件"><a href="#6-创建从库数据库配置文件" class="headerlink" title="6 创建从库数据库配置文件"></a>6 创建从库数据库配置文件</h4><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">vi /data/confs/slavedb.cnf</span><br><span class="line"># Config Settings:</span><br><span class="line">[mysqld]</span><br><span class="line">server-id=2</span><br></pre></td></tr></table></figure><p>注意主库也要配置一个不同的 server-id , 并开启 binlog .</p><h4 id="5-修改数据库文件用户-ID"><a href="#5-修改数据库文件用户-ID" class="headerlink" title="5 修改数据库文件用户 ID"></a>5 修改数据库文件用户 ID</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> -R 1001:1001 /data/backups/</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> -R 1001:1001 /data/confs/</span><br></pre></td></tr></table></figure><p>之所以改成 1001 ,是因为 Percona 的 MySQL Docker 镜像启动 MySQL 用的用户 mysql 的 id 是 1001 . 而 archlinux 的 mysql user id 是 89 .所以只能直接改 user id .</p><p>其它 MySQL 镜像的 user id 也许不一样, 可以先启动一个 MySQL Docker 实例,然后再使用 <code>docker exec -it mysql-slave bash</code> 启动一个 shell, 再在 shell 执行 <code>id</code> 命令,即可找到正确的 user id .</p><p>如果文件权限不改对,启动 MySQL 会报错的. 可以使用 <code>docker logs mysql-slave</code> 查看报错信息.</p><h4 id="6-创建主库-MySQL-用户"><a href="#6-创建主库-MySQL-用户" class="headerlink" title="6 创建主库 MySQL 用户"></a>6 创建主库 MySQL 用户</h4><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;repl&#x27;</span>@<span class="string">&#x27;%&#x27;</span> IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;mysqlpass&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">REPLICATION</span> SLAVE <span class="keyword">ON</span> *.* <span class="keyword">TO</span> <span class="string">&#x27;repl&#x27;</span>@<span class="string">&#x27;%&#x27;</span>;</span><br></pre></td></tr></table></figure><p>把密码修改成你的复杂密码,在 mysql 8, 这密码的复杂度是创建不了用户的.</p><h4 id="7-启动从库"><a href="#7-启动从库" class="headerlink" title="7 启动从库"></a>7 启动从库</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker run --name mysql-slave -d -v /data/confs:/etc/mysql/conf.d -v /data/backups:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=<span class="string">&#x27;your_master_root_password&#x27;</span> -d percona:8.0</span><br><span class="line">docker logs mysql-slave <span class="comment"># 查看是否有报错,有报错解决报错</span></span><br><span class="line">docker <span class="built_in">exec</span> -it mysql-slave bash <span class="comment"># 启动 shell, 之前输入 mysql -uroot -pyour_master_root_password 连接 mysql 进行管理.</span></span><br></pre></td></tr></table></figure><p>不同的 Docker 镜像, -v 参数有可能不同,请以自己的镜像文档为准.  <a href="https://hub.docker.com/_/percona">https://hub.docker.com/_/percona</a></p><h4 id="8-配置从库"><a href="#8-配置从库" class="headerlink" title="8 配置从库"></a>8 配置从库</h4><p>在上个步骤已经启动一个 MySQL shell ,现在我们需要配置从库连接到主库即可.</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">change master to master_host=&quot;mysql master ip&quot;,master_port=3306,master_user=&quot;repl&quot;,master_password=&quot;mysqlpass&quot;,master_log_file=&quot;mysql-bin.000007&quot;,master_log_pos=155;</span><br><span class="line"></span><br><span class="line">start slave;</span><br><span class="line"></span><br><span class="line">show slave status;</span><br></pre></td></tr></table></figure><p>master_log_file 与 master_log_pos 查看数据库备份文件夹的 xtrabackup_info 的 binlog_pos 即可.</p><p>到这一步,整个从库已经配置成功,可以使用 show slave status 查看从库状态. 在主库上面使用 show master status 与从库对比,查看同步延迟.</p><p>最后,记得设置 docker 服务为开机自启.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2019-02-21-2/</id>
    <link href="https://blog.mangege.com/tech/2019-02-21-2/"/>
    <published>2019-02-20T23:49:30.000Z</published>
    <summary>
      <![CDATA[<p>瞎折腾建站为了节省成本,只能买些小品牌服务商的 VPS. 小品牌不放心,数据得自己好好备份.</p>
<p>MySQL 备份方式有很多,比如直接 Dump ,但小 VPS 空间少,带宽少,拉到本地速度慢.除非是数据库数据比较小,不然不推荐用 Dump]]>
    </summary>
    <title>使用 Docker 搭建 MySQL 从库备份 VPS 数据库</title>
    <updated>2026-04-14T15:05:49.626Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="linux" scheme="https://blog.mangege.com/tags/linux/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <content>
      <![CDATA[<p>Onda v820w 平板使用的是 Intel Z3735F CPU,本质上就是一台普通 PC .<br>只是麻烦的地方有两点,一是只支持 EFI 启动, 二是 EFI 只支持 32 位.</p><p>准备工具:  </p><ol><li>otg 线</li><li>usb hub</li><li>usb 键盘与鼠标</li><li>U 盘 多个</li></ol><p>平板本身就支持 Android 和 Win 双启动.添加 Linux 系统有两种安装方式,一种是安装到 SD 卡. 一种是把 Win 分区删除掉重新分区,分 8G 左右给 Linux . Win 用 12 G.</p><p>本文主要介绍把 Win 重新分区后再安装 Archlinux . 从安装的简易程度来说, Debian 应该是优选的发行版本,是唯数不多的还支持 32 bit CPU 的发行版本.</p><p>Archlinux 已经不支持 32 bit CPU, 平板的CPU是 64 bit,但其 UEFI 只支持 32 bit .所以 archlinux 的 usb 启动盘只能手动创建,而使用 <code>cp archlinux.iso /dev/sdb</code> 的创建的启动盘在普通电脑能启动,但这平板是不行的.</p><h2 id="制作-archlinux-启动盘"><a href="#制作-archlinux-启动盘" class="headerlink" title="制作 archlinux 启动盘"></a>制作 archlinux 启动盘</h2><ol><li><p>使用 fdisk 等你熟悉的磁盘分区工具进行分区, gpt 分区表, 一个 Microsoft basic data 分区,分区格式为 fat32.</p><p> 使用 <code>mkfs.vfat -F 32 -n ARCH /dev/sdb1</code> 命令进行格式化时,把分区 Label 设置为 ARCH ,后面 grub 配置文件是根据 LABEL 查找根目录的.注意要把命令的设备路径改成你的.</p></li><li><p>提取 iso 文件到 U盘,参考 <a href="https://wiki.archlinux.org/index.php/USB_flash_installation_media#Using_manual_formatting">https://wiki.archlinux.org/index.php/USB_flash_installation_media#Using_manual_formatting</a></p></li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">mkdir</span> -p /mnt/&#123;iso,usb&#125;</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">mount -o loop archlinux-version-x86_64.iso /mnt/iso</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">mount /dev/sdXn /mnt/usb</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">cp</span> -a /mnt/iso/* /mnt/usb</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">sync</span></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">umount /mnt/iso</span></span><br></pre></td></tr></table></figure><ol start="3"><li>安装 32 bit UEFI 启动文件, 参考 <a href="https://wiki.archlinux.org/index.php/ASUS_x205ta#Creating_bootia32.efi">https://wiki.archlinux.org/index.php/ASUS_x205ta#Creating_bootia32.efi</a> <a href="https://wiki.archlinux.org/index.php/GRUB/Tips_and_tricks#GRUB_standalone">https://wiki.archlinux.org/index.php/GRUB/Tips_and_tricks#GRUB_standalone</a></li></ol><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># 把 <span class="regexp">/run/m</span>edia<span class="regexp">/outman/</span>ARCH/ 路径替换你的u盘的挂载路径.</span><br><span class="line">grub-mkstandalone -d <span class="regexp">/usr/</span>lib<span class="regexp">/grub/i</span>386-efi<span class="regexp">/ -O i386-efi --modules=&quot;part_gpt part_msdos&quot; --locales=&quot;en@quot&quot; --themes=&quot;&quot; -o &quot;/</span>run<span class="regexp">/media/</span>outman<span class="regexp">/ARCH/</span>EFI<span class="regexp">/boot/</span>bootia32.efi<span class="string">&quot; &quot;</span>boot<span class="regexp">/grub/g</span>rub.cfg=./grub.cfg<span class="string">&quot; -v</span></span><br></pre></td></tr></table></figure><p>创建 &#x2F;run&#x2F;media&#x2F;outman&#x2F;ARCH&#x2F;EFI&#x2F;boot&#x2F;grub.cfg 文件,记得路径是你的路径.<br>&lt;FS-LABEL&gt; 替换成 ARCH (之前的 U 盘的分区 Label)</p><figure class="highlight stata"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">insmod part_gpt</span><br><span class="line">insmod part_msdos</span><br><span class="line">insmod fat</span><br><span class="line">insmod efi_gop</span><br><span class="line">insmod efi_uga</span><br><span class="line">insmod video_bochs</span><br><span class="line">insmod video_cirrus</span><br><span class="line">insmod font</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> loadfont <span class="string">&quot;$&#123;prefix&#125;/fonts/unicode.pf2&quot;</span> ; then</span><br><span class="line">  insmod gfxterm</span><br><span class="line">  <span class="keyword">set</span> gfxmode=<span class="string">&quot;1024x768x32;auto&quot;</span></span><br><span class="line">  terminal_input console</span><br><span class="line">  terminal_output gfxterm</span><br><span class="line">fi</span><br><span class="line"></span><br><span class="line">menuentry <span class="string">&quot;Arch Linux archiso x86_64&quot;</span> &#123;</span><br><span class="line">  <span class="keyword">set</span> gfxpayload=<span class="keyword">keep</span></span><br><span class="line">  <span class="keyword">search</span> --<span class="keyword">no</span>-floppy --<span class="keyword">set</span>=root --<span class="keyword">label</span> &lt;FS-<span class="keyword">LABEL</span>&gt;</span><br><span class="line">  linux /<span class="keyword">arch</span>/<span class="keyword">boot</span>/x86_64/vmlinuz archisobasedir=<span class="keyword">arch</span> archisolabel=&lt;FS-<span class="keyword">LABEL</span>&gt; add_efi_memmap</span><br><span class="line">  initrd /<span class="keyword">arch</span>/<span class="keyword">boot</span>/x86_64/archiso.img</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">menuentry <span class="string">&quot;UEFI Shell x86_64 v2&quot;</span> &#123;</span><br><span class="line">  <span class="keyword">search</span> --<span class="keyword">no</span>-floppy --<span class="keyword">set</span>=root --<span class="keyword">label</span> &lt;FS-<span class="keyword">LABEL</span>&gt;</span><br><span class="line">  chainloader /EFI/shellx64_v2.efi</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">menuentry <span class="string">&quot;UEFI Shell x86_64 v1&quot;</span> &#123;</span><br><span class="line">  <span class="keyword">search</span> --<span class="keyword">no</span>-floppy --<span class="keyword">set</span>=root --<span class="keyword">label</span> &lt;FS-<span class="keyword">LABEL</span>&gt;</span><br><span class="line">  chainloader /EFI/shellx64_v1.efi</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样, archlinux 的启动盘已经制作完成.</p><h2 id="调整分区-重装-Win"><a href="#调整分区-重装-Win" class="headerlink" title="调整分区,重装 Win"></a>调整分区,重装 Win</h2><ul><li>推荐使用 rufus 制作 win 启动盘.</li><li>官网 Win 10 驱动下载地址: <a href="http://www.onda.cn/Tablet_Supports.aspx?id=323">http://www.onda.cn/Tablet_Supports.aspx?id=323</a></li><li>rufus 重装 win 时,把原来的 win 分区删除掉,然后分 12G 给 Win 差不多了.</li></ul><h2 id="安装-archlinux-系统"><a href="#安装-archlinux-系统" class="headerlink" title="安装 archlinux 系统"></a>安装 archlinux 系统</h2><ol><li>接入 otg , usb hub ,键盘 .</li><li>重启或开机不停按 ESC , 之后进 BOOT MANAGE ,选择你的 U 盘启动.</li><li>如果没有看到 U 盘,可以尝试使用 BOOT FROM FILE,选择 EFI&#x2F;boot&#x2F;bootia32.efi 文件,这样会进入一个 grub shell,之后可以使用 <code>configfile (hd0,gpt1)/EFI/boot/grub.cfg</code>(带自动补全,自己补对目录) 来启动自己写的 grub 配置文件,高手可以直接在 grub shell 里敲命令启动.</li><li>稍等几十秒启动完成后, 使用 <code>wifi-menu</code> 连接 wifi ,自带无线驱动,连上就可以用.</li><li>之后就是普通的 archlinux 安装流程,我这就不细写了,只写一些需要注意的地方.</li></ol><p>Linux 分区分两个,一个 boot 分区(同时是EFI分区), 一个根目录分区.</p><p>下面是我的分区表信息,供参考<br>&#x2F;dev&#x2F;mmcblk1p18 是 boot 分区<br>&#x2F;dev&#x2F;mmcblk1p19 是 linux 根目录分区</p><figure class="highlight tap"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">Disk /dev/mmcblk1: 28.9 GiB,<span class="number"> 31037849600 </span>bytes,<span class="number"> 60620800 </span>sectors</span><br><span class="line">Units: sectors of<span class="number"> 1 </span>*<span class="number"> 512 </span>=<span class="number"> 512 </span>bytes</span><br><span class="line">Sector size (logical/physical):<span class="number"> 512 </span>bytes /<span class="number"> 512 </span>bytes</span><br><span class="line">I/O size (minimum/optimal):<span class="number"> 512 </span>bytes /<span class="number"> 512 </span>bytes</span><br><span class="line">Disklabel type: gpt</span><br><span class="line"></span><br><span class="line">Device             Start      End  Sectors  Size Type</span><br><span class="line">/dev/mmcblk1p1       <span class="number"> 40 </span> <span class="number"> 131111 </span> <span class="number"> 131072 </span>  64M EFI System</span><br><span class="line">/dev/mmcblk1p2   <span class="number"> 131112 </span> <span class="number"> 262183 </span> <span class="number"> 131072 </span>  64M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p3   <span class="number"> 262184 </span> <span class="number"> 294951 </span>  <span class="number"> 32768 </span>  16M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p4   <span class="number"> 294952 </span> <span class="number"> 327719 </span>  <span class="number"> 32768 </span>  16M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p5   <span class="number"> 327720 </span> <span class="number"> 360487 </span>  <span class="number"> 32768 </span>  16M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p6   <span class="number"> 360488 </span> <span class="number"> 393255 </span>  <span class="number"> 32768 </span>  16M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p7   <span class="number"> 393256 </span> <span class="number"> 524327 </span> <span class="number"> 131072 </span>  64M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p8   <span class="number"> 524328 </span> <span class="number"> 589863 </span>  <span class="number"> 65536 </span>  32M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p9   <span class="number"> 589864 </span> <span class="number"> 622631 </span>  <span class="number"> 32768 </span>  16M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p10  <span class="number"> 622632 </span> <span class="number"> 655399 </span>  <span class="number"> 32768 </span>  16M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p11  <span class="number"> 655400 </span> <span class="number"> 688167 </span>  <span class="number"> 32768 </span>  16M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p12  <span class="number"> 688168 </span><span class="number"> 2785319 </span><span class="number"> 2097152 </span>   1G Microsoft basic data</span><br><span class="line">/dev/mmcblk1p13 <span class="number"> 2785320 </span><span class="number"> 3309607 </span> <span class="number"> 524288 </span> 256M Microsoft basic data</span><br><span class="line">/dev/mmcblk1p14 <span class="number"> 3309608 </span><span class="number"> 5406759 </span><span class="number"> 2097152 </span>   1G Microsoft basic data</span><br><span class="line">/dev/mmcblk1p15 <span class="number"> 5406760 </span>20086823<span class="number"> 14680064 </span>   7G Microsoft basic data</span><br><span class="line">/dev/mmcblk1p16<span class="number"> 20088832 </span>20121599   <span class="number"> 32768 </span>  16M Microsoft reserved</span><br><span class="line">/dev/mmcblk1p17<span class="number"> 20121600 </span>45254655<span class="number"> 25133056 </span>  12G Microsoft basic data</span><br><span class="line">/dev/mmcblk1p18<span class="number"> 45254656 </span>45778943  <span class="number"> 524288 </span> 256M EFI System</span><br><span class="line">/dev/mmcblk1p19<span class="number"> 45778944 </span>60620766<span class="number"> 14841823 </span> 7.1G Linux filesystem</span><br></pre></td></tr></table></figure><figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 格式化分区</span></span><br><span class="line">mkfs.vfat -F <span class="number">32</span> -n ARCHBOOT <span class="regexp">/dev/mm</span>cblk1p18</span><br><span class="line">mkfs.ext4 -E lazy_itable_init <span class="regexp">/dev/mm</span>cblk1p19</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">挂载分区</span><br><span class="line">mount /dev/mmcblk1p19 /mnt</span><br><span class="line"><span class="built_in">mkdir</span> /mnt/boot</span><br><span class="line">mount /dev/mmcblk1p18 /mnt/boot</span><br></pre></td></tr></table></figure><p>arch-root 后安装完后,执行下面命令.</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># wifi 用</span></span><br><span class="line">pacman -S wpa_supplicant dialog # wifi-menu</span><br><span class="line"><span class="comment"># 启动用</span></span><br><span class="line">pacman -S grub efibootmgr</span><br><span class="line"><span class="comment"># 安装启动 /boot/ 目录是挂载的 EFI 分区</span></span><br><span class="line">grub-install <span class="attribute">--target</span>=i386-efi <span class="attribute">--efi-directory</span>=/boot/ <span class="attribute">--bootloader-id</span>=GRUB</span><br><span class="line">grub-mkconfig -o /boot/grub/grub.cfg</span><br></pre></td></tr></table></figure><p>之后就是重启,然后不停按 ESC 键,之后在 EFI 启动项里,可以看到 GRUB 启动项,选择启动即可,三系统安装完成.</p><h2 id="一些坑"><a href="#一些坑" class="headerlink" title="一些坑"></a>一些坑</h2><h4 id="1-重新刷-Android-进不了-DNX-Mode"><a href="#1-重新刷-Android-进不了-DNX-Mode" class="headerlink" title="1. 重新刷 Android 进不了 DNX Mode ?"></a>1. 重新刷 Android 进不了 DNX Mode ?</h4><p>按 ESC 选 SCU 进 BIOS 设置,进 BOOT 的 TAB , 关闭 Quick boot .</p><p>然后官方的文档是说等显示 DNX Mode 才释放三个按键.事实上,同时按 音量上下加开机键 后,出现字就释放开机键,还保持音量上下键不放,等进入 DNX Mode 再释放.</p><h4 id="2-Linux-终端下旋转屏幕-无X"><a href="#2-Linux-终端下旋转屏幕-无X" class="headerlink" title="2. Linux 终端下旋转屏幕 (无X)"></a>2. Linux 终端下旋转屏幕 (无X)</h4><p><a href="https://askubuntu.com/questions/237963/how-do-i-rotate-my-display-when-not-using-an-x-server">https://askubuntu.com/questions/237963/how-do-i-rotate-my-display-when-not-using-an-x-server</a></p><p><code>echo 1 &gt; /sys/class/graphics/fbcon/rotate</code></p><figure class="highlight basic"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="symbol">0 </span>- Normal rotation</span><br><span class="line"><span class="symbol">1 </span>- Rotate clockwise</span><br><span class="line"><span class="symbol">2 </span>- Rotate upside down</span><br><span class="line"><span class="symbol">3 </span>- Rotate counter-clockwise</span><br></pre></td></tr></table></figure><p>X 环境下使用 xrandr</p><h4 id="3-触屏驱动"><a href="#3-触屏驱动" class="headerlink" title="3. 触屏驱动"></a>3. 触屏驱动</h4><p><a href="https://github.com/onitake/gsl-firmware">https://github.com/onitake/gsl-firmware</a></p><h4 id="4-只安装-Win-或-linux"><a href="#4-只安装-Win-或-linux" class="headerlink" title="4. 只安装 Win 或 linux"></a>4. 只安装 Win 或 linux</h4><p>其实就是普通的电脑一样,删除掉所有分区重新分区即可.</p><p><a href="http://www.ondaforum.com/topic/3544-guide-remove-android-and-install-only-windows-8-or-10-v820w/">http://www.ondaforum.com/topic/3544-guide-remove-android-and-install-only-windows-8-or-10-v820w/</a></p><h4 id="5-不错的参考"><a href="#5-不错的参考" class="headerlink" title="5. 不错的参考"></a>5. 不错的参考</h4><p><a href="https://hhuysqt.github.io/ubuntu-tablet/">https://hhuysqt.github.io/ubuntu-tablet/</a></p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2019-01-26-1/</id>
    <link href="https://blog.mangege.com/tech/2019-01-26-1/"/>
    <published>2019-01-26T00:32:15.000Z</published>
    <summary>
      <![CDATA[<p>Onda v820w 平板使用的是 Intel Z3735F CPU,本质上就是一台普通 PC .<br>只是麻烦的地方有两点,一是只支持 EFI 启动, 二是 EFI 只支持 32 位.</p>
<p>准备工具:  </p>
<ol>
<li>otg]]>
    </summary>
    <title>在Onda v820w 平板安装三系统(Linux, Win, Android)</title>
    <updated>2026-04-14T15:05:49.626Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="frontend" scheme="https://blog.mangege.com/tags/frontend/"/>
    <content>
      <![CDATA[<p>本文主要讲解使用 <a href="https://github.com/mikel/mail">mail</a> 库解析邮件所碰到的坑.</p><p>邮件格式本身的解析由 mail 库.由于邮件格式标准<a href="https://github.com/mikel/mail/tree/2-6-stable/reference">过多且过于复杂</a>,鉴于个人能力有限,所以就不讲解邮件相关的标准的.需要自己先阅读相关资料.比如 RFC 822, multipart 等方面的资料.</p><p>坑主要分两大类:</p><ol><li>编码 (修炼的必经之路)</li><li>邮件非常见格式解析 (主要是苹果设备发出来的邮件)</li></ol><ul><li>正文只有图片(只包含附件 part, 无 text part 或 html part)</li><li>正文有多个文本段(multi text part)</li><li>multipart 再包含 multipart</li></ul><h3 id="mail-基础技巧"><a href="#mail-基础技巧" class="headerlink" title="mail 基础技巧"></a>mail 基础技巧</h3><ol><li>查看 <a href="https://github.com/mikel/mail">mail 官方文档</a></li><li><code>Mail.new(str)</code> 的 str 变量,需要为 RFC 822 标准格式</li><li>gmail 邮件详情页的 “显示原始邮件” ,下载下来的 <a href="https://productforums.google.com/d/msg/gmail/NLxxg-5jk_o/7Mg88jSz7vQJ">original_msg.txt</a> 文件,是 RFC 822 标准,调试时可以直接下载此文件来调试. </li><li>iamp 抓取时, <code>imap.uid_fetch(uid, [&#39;RFC822&#39;])[0]</code> 这样可以拿到 RFC 822 格式的内容. <a href="https://github.com/mikel/mail/blob/2-6-stable/lib/mail/network/retriever_methods/imap.rb#L86">参考来源</a></li></ol><h3 id="编码"><a href="#编码" class="headerlink" title="编码"></a>编码</h3><p>编码这个坑与编程语言无关,它是我们修炼必经的路.</p><ul><li>世界上有多种字符,比如英文,简体中文,繁体中文.</li><li>一种字符有可能有多种编码,比如简体中文有 GB2312, GBK, GB18030 . <a href="http://www.qqxiuzi.cn/bianma/zifuji.php">参考来源</a></li><li>一种编码有可能有多种实现,比如 Unicode 编码有 UTF-8, UTF-16, UTF-32 多种实现. <a href="https://zh.wikipedia.org/wiki/Unicode">参考来源</a></li></ul><p>代码示例:</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># &quot;中&quot; 字不同编码的十六进制值. http://blog.bigbinary.com/2011/07/20/ruby-pack-unpack.html</span></span><br><span class="line">puts <span class="string">&quot;中&quot;</span>.encode(<span class="string">&#x27;UTF-8&#x27;</span>).unpack(<span class="string">&#x27;H*&#x27;</span>) <span class="comment"># e4b8ad</span></span><br><span class="line">puts <span class="string">&quot;中&quot;</span>.encode(<span class="string">&#x27;UTF-32&#x27;</span>).unpack(<span class="string">&#x27;H*&#x27;</span>) <span class="comment"># 0000feff00004e2d </span></span><br><span class="line">puts <span class="string">&quot;中&quot;</span>.encode(<span class="string">&#x27;GBK&#x27;</span>).unpack(<span class="string">&#x27;H*&#x27;</span>) <span class="comment"># d6d0</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 字节数组</span></span><br><span class="line">puts <span class="string">&quot;中&quot;</span>.encode(<span class="string">&#x27;UTF-8&#x27;</span>).bytes.inspect <span class="comment"># [228, 184, 173]</span></span><br><span class="line">puts <span class="string">&quot;中&quot;</span>.encode(<span class="string">&#x27;UTF-32&#x27;</span>).bytes.inspect <span class="comment"># [0, 0, 254, 255, 0, 0, 78, 45]</span></span><br><span class="line">puts <span class="string">&quot;中&quot;</span>.encode(<span class="string">&#x27;GBK&#x27;</span>).bytes.inspect <span class="comment"># [214, 208]</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Base64</span></span><br><span class="line"><span class="keyword">require</span> <span class="string">&quot;base64&quot;</span></span><br><span class="line">puts <span class="title class_">Base64</span>.encode64(<span class="string">&#x27;中&#x27;</span>.encode(<span class="string">&#x27;GBK&#x27;</span>)) <span class="comment"># 1tA=</span></span><br><span class="line">puts <span class="title class_">Base64</span>.encode64(<span class="string">&#x27;中&#x27;</span>.encode(<span class="string">&#x27;UTF-8&#x27;</span>)) <span class="comment"># 5Lit</span></span><br></pre></td></tr></table></figure><p>从上示例可以看出,同一个字符,用不同编码时,其二进制数据值有可能不一样.</p><p>那么编码的主要问题是什么? 请看代码示例:</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">require</span> <span class="string">&quot;base64&quot;</span></span><br><span class="line">str = <span class="title class_">Base64</span>.decode64(<span class="string">&#x27;1tA=&#x27;</span>) <span class="comment"># 解码 &quot;中&quot; 的 GBK 编码的 base64 值.</span></span><br><span class="line">puts str.encoding <span class="comment"># ASCII-8BIT, 相当于是一个字节数组(byte array, 1byte = 8bit)</span></span><br><span class="line">puts str.bytes.inspect <span class="comment"># [214, 208] , 等于上示例的 &quot;中&quot;.encode(&#x27;GBK&#x27;).bytes.inspect .也就是说变量的在内存里的二进制值还是 GBK 编码.</span></span><br><span class="line">puts str <span class="comment"># 打印出乱码. 因为终端一般设置的编码为 UTF-8 ,如果想要此语句不显示成乱码,把终端编码改成 GBK 即可.记得改回 UTF-8.</span></span><br><span class="line"></span><br><span class="line">puts str.force_encoding(<span class="string">&#x27;GBK&#x27;</span>).bytes.inspect <span class="comment"># [214, 208], 字节还是 GBK 未变</span></span><br><span class="line">puts str.force_encoding(<span class="string">&#x27;GBK&#x27;</span>).encode(<span class="string">&#x27;UTF-8&#x27;</span>) <span class="comment"># 中 force_encoding 只是改变变量的编码元信息. encode 把变量的字节从 GBK 变成 UTF-8 . 这样打印就不乱码了.</span></span><br><span class="line"></span><br><span class="line">puts str.force_encoding(<span class="string">&#x27;BIG5&#x27;</span>).bytes.inspect <span class="comment"># [214, 208], 字节还是 GBK 未变</span></span><br><span class="line">puts str.force_encoding(<span class="string">&#x27;BIG5&#x27;</span>).encode(<span class="string">&#x27;UTF-8&#x27;</span>) <span class="comment"># 笢 同样的字节数据,在繁体 BIG5 编码里有效且是另外一个字符.</span></span><br><span class="line"></span><br><span class="line">str.encode(<span class="string">&#x27;UTF-8&#x27;</span>) <span class="comment"># 报错.因为不知道字节的编码信息,有可能默认编码转换映射是从 ASCII to UTF-8(待考证)</span></span><br></pre></td></tr></table></figure><p>把一个字符转成对应编码的字节不难,把一个已知编码信息的字节转成对应字符也不难.</p><p>难在把一个不知道编码信息的二进制数据,转成对应的字符. <code>[214, 208]</code> 是 GBK 编码里的 “中” 字,同时也是 BIG5 编码里面的 “笢” 字.</p><p>上示例就是演示了邮件里碰到的编码问题,当正文经过 base64 编码后.收到邮件后, base64 解码出来的二进制数据,到底是 GBK, 还是 BIG5 ,还是其它编码?</p><p>所幸的是,大部分情况我们都不需要靠猜, 一般邮件正文 part 都有这样一段 header 信息. 其 Content-Type 的 charset 就告诉了我们这段 base64 解码后的二进制是什么编码.</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">MIME-Version: 1.0</span><br><span class="line">Content-Type: text/html; charset=&quot;utf-8&quot;</span><br><span class="line">Content-Transfer-Encoding: base64</span><br></pre></td></tr></table></figure><p>代码示例:</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">require</span> <span class="string">&#x27;mail&#x27;</span> <span class="comment"># 需要先安装 mail gem</span></span><br><span class="line"><span class="comment"># 构建 RFC822 标准的邮件字符串</span></span><br><span class="line">mail = <span class="title class_">Mail</span>.new <span class="keyword">do</span></span><br><span class="line">  from    <span class="string">&#x27;mikel@test.lindsaar.net&#x27;</span></span><br><span class="line">  to      <span class="string">&#x27;you@test.lindsaar.net&#x27;</span></span><br><span class="line">  subject <span class="string">&#x27;This is a test email&#x27;</span></span><br><span class="line">  content_type <span class="string">&#x27;text/plain; charset=GBK&#x27;</span></span><br><span class="line">  body    <span class="string">&quot;中&quot;</span>.encode(<span class="string">&#x27;GBK&#x27;</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">original_msg = mail.to_s</span><br><span class="line"><span class="comment">=begin</span></span><br><span class="line"><span class="comment">Date: Sat, 19 Aug 2017 13:39:10 +0800</span></span><br><span class="line"><span class="comment">From: mikel<span class="doctag">@test</span>.lindsaar.net</span></span><br><span class="line"><span class="comment">To: you<span class="doctag">@test</span>.lindsaar.net</span></span><br><span class="line"><span class="comment">Message-ID: &lt;5997cefe17bb6_5ded1e74693bc8972ac<span class="doctag">@hparch</span>.mail&gt;</span></span><br><span class="line"><span class="comment">Subject: This is a test email</span></span><br><span class="line"><span class="comment">Mime-Version: 1.0</span></span><br><span class="line"><span class="comment">Content-Type: text/plain;</span></span><br><span class="line"><span class="comment"> charset=GBK</span></span><br><span class="line"><span class="comment">Content-Transfer-Encoding: base64</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">1tA=</span></span><br><span class="line"><span class="comment">=end</span></span><br><span class="line">puts original_msg</span><br><span class="line"></span><br><span class="line"><span class="comment"># 开始解析</span></span><br><span class="line">puts <span class="string">&quot;*&quot;</span> * <span class="number">42</span></span><br><span class="line">mail = <span class="title class_">Mail</span>.new(original_msg)</span><br><span class="line">body = mail.body.decoded</span><br><span class="line">puts body <span class="comment"># 乱码</span></span><br><span class="line">puts body.encoding <span class="comment"># ASCII-8BIT</span></span><br><span class="line">puts body.force_encoding(mail.charset).encode(<span class="string">&#x27;UTF-8&#x27;</span>) <span class="comment"># 中</span></span><br><span class="line"><span class="comment"># 需要检查 charset 是否存在,通过 Encoding.find 方法</span></span><br></pre></td></tr></table></figure><p>理想的世界, content_type 是带了 charset .但现实与理想总是存在差距.有些没有带 charset ,有些甚至连 content_type 整行都没有.</p><p>这个时候编码就要靠猜了, <a href="https://github.com/brianmario/charlock_holmes">charlock_holmes</a> 就是干这事的.</p><p>但这个只有在正文很多的时候才会有可能猜的准.</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">puts <span class="title class_">CharlockHolmes</span><span class="symbol">:</span><span class="symbol">:EncodingDetector</span>.detect(<span class="string">&#x27;中文测试很长的文字&#x27;</span>.encode(<span class="string">&#x27;GBK&#x27;</span>).force_encoding(<span class="string">&#x27;ASCII-8BIT&#x27;</span>))</span><br><span class="line"><span class="comment"># &#123;:type=&gt;:text, :encoding=&gt;&quot;UTF-16BE&quot;, :ruby_encoding=&gt;&quot;UTF-16BE&quot;, :confidence=&gt;10&#125;</span></span><br><span class="line"></span><br><span class="line">puts <span class="title class_">CharlockHolmes</span><span class="symbol">:</span><span class="symbol">:EncodingDetector</span>.detect(<span class="string">&#x27;遍身罗绮者 不是养蚕人&#x27;</span>.encode(<span class="string">&#x27;GBK&#x27;</span>).force_encoding(<span class="string">&#x27;ASCII-8BIT&#x27;</span>))</span><br><span class="line"><span class="comment"># &#123;:type=&gt;:text, :encoding=&gt;&quot;ISO-8859-6&quot;, :ruby_encoding=&gt;&quot;ISO-8859-6&quot;, :confidence=&gt;16, :language=&gt;&quot;ar&quot;&#125;</span></span><br><span class="line"></span><br><span class="line">puts <span class="title class_">CharlockHolmes</span><span class="symbol">:</span><span class="symbol">:EncodingDetector</span>.detect(<span class="string">&#x27;中文测试,工要在地一上是中国；&#x27;</span>.encode(<span class="string">&#x27;GBK&#x27;</span>).force_encoding(<span class="string">&#x27;ASCII-8BIT&#x27;</span>))</span><br><span class="line"><span class="comment"># &#123;:type=&gt;:text, :encoding=&gt;&quot;GB18030&quot;, :ruby_encoding=&gt;&quot;GB18030&quot;, :confidence=&gt;100, :language=&gt;&quot;zh&quot;&#125;</span></span><br></pre></td></tr></table></figure><p><strong>所以,优先以 content_type 的 charset 去解码, charlock_holmes 只是最后方案.</strong></p><p>事实上 encode 方法有个坑,就是有可能 encode 碰到无效字节,会导致报错. 推荐加上 invalid 和 undef 参数. replace 默认是替换成问号.还可以直接删除无效字节,推荐使用替换.</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;abc&quot;</span>.encode(<span class="string">&#x27;UTF-8&#x27;</span>, <span class="symbol">invalid:</span> <span class="symbol">:replace</span>, <span class="symbol">undef:</span> <span class="symbol">:replace</span>)</span><br></pre></td></tr></table></figure><h3 id="邮件非常见格式解析"><a href="#邮件非常见格式解析" class="headerlink" title="邮件非常见格式解析"></a>邮件非常见格式解析</h3><h4 id="1-正文只有图片"><a href="#1-正文只有图片" class="headerlink" title="1. 正文只有图片"></a>1. 正文只有图片</h4><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">require</span> <span class="string">&#x27;mail&#x27;</span> <span class="comment"># 需要先安装 mail gem</span></span><br><span class="line"><span class="keyword">require</span> <span class="string">&#x27;open-uri&#x27;</span></span><br><span class="line"><span class="comment"># 构建 RFC822 标准的邮件字符串</span></span><br><span class="line">mail = <span class="title class_">Mail</span>.new <span class="keyword">do</span></span><br><span class="line">  from    <span class="string">&#x27;mikel@test.lindsaar.net&#x27;</span></span><br><span class="line">  to      <span class="string">&#x27;you@test.lindsaar.net&#x27;</span></span><br><span class="line">  subject <span class="string">&#x27;This is a test email&#x27;</span></span><br><span class="line">  content_type <span class="string">&#x27;image/png; filename=One_black_Pixel.png&#x27;</span></span><br><span class="line">  body    open(<span class="string">&#x27;https://upload.wikimedia.org/wikipedia/en/4/45/One_black_Pixel.png&#x27;</span>).read</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">original_msg = mail.to_s</span><br><span class="line"><span class="comment">=begin</span></span><br><span class="line"><span class="comment">Date: Sat, 19 Aug 2017 14:25:48 +0800</span></span><br><span class="line"><span class="comment">From: mikel<span class="doctag">@test</span>.lindsaar.net</span></span><br><span class="line"><span class="comment">To: you<span class="doctag">@test</span>.lindsaar.net</span></span><br><span class="line"><span class="comment">Message-ID: &lt;5997d9ec98315_695f2a4c229bd097632<span class="doctag">@hparch</span>.mail&gt;</span></span><br><span class="line"><span class="comment">Subject: This is a test email</span></span><br><span class="line"><span class="comment">Mime-Version: 1.0</span></span><br><span class="line"><span class="comment">Content-Type: image/png;</span></span><br><span class="line"><span class="comment"> filename=One_black_Pixel.png</span></span><br><span class="line"><span class="comment">Content-Transfer-Encoding: base64</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">iVBORwoaCgAAAApJSERSAAAAAQAAAAEIAgAAAJB3U94AAAABc1JHQgCuzhzp</span></span><br><span class="line"><span class="comment">AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAAxJREFU</span></span><br><span class="line"><span class="comment">GFdjYGBgAAAABAABXM3/aQAAAABJRU5ErkJggg==</span></span><br><span class="line"><span class="comment">=end</span></span><br><span class="line">puts original_msg</span><br><span class="line"></span><br><span class="line"><span class="comment"># 开始解析</span></span><br><span class="line">puts <span class="string">&quot;*&quot;</span> * <span class="number">42</span></span><br><span class="line">mail = <span class="title class_">Mail</span>.new(original_msg)</span><br><span class="line">puts mail.body <span class="comment"># 正文是乱码</span></span><br><span class="line">puts mail.attachment? <span class="comment"># true</span></span><br></pre></td></tr></table></figure><p>对于这种只有图片的邮件,我们先用 attachment? 方法判断是不是附件,是附件的话,按附件的逻辑处理,比如保存到本地.</p><h4 id="2-正文有多个文本段"><a href="#2-正文有多个文本段" class="headerlink" title="2. 正文有多个文本段"></a>2. 正文有多个文本段</h4><p>此非常见格式的邮件一般是苹果设备自带的邮件客户端发出来的.</p><p>苹果邮件客户端这么做是为了实现在纯文本格式邮件插入图片的上下环绕效果.</p><p>主流的做法纯文本不插入图片,图片只作为普通附件存在.要插入图片,使用 html 格式,通过 html img 标签来实现,img src 填图片附件的 cid .</p><p>这样的格式,在 gmail 显示不出环绕效果,只作为普通附件显示. 在苹果的客户端可以显示.</p><p>像 <a href="https://mailcatcher.me/">MailCatcher</a> 这个接收测试工具,和我开始一样,以为一个邮件只有一个 text part,所以导致这种邮件只会显示部分文本.</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">require</span> <span class="string">&#x27;mail&#x27;</span> <span class="comment"># 需要先安装 mail gem</span></span><br><span class="line"><span class="keyword">require</span> <span class="string">&#x27;open-uri&#x27;</span></span><br><span class="line"><span class="comment"># 构建 RFC822 标准的邮件字符串</span></span><br><span class="line">mail = <span class="title class_">Mail</span>.new <span class="keyword">do</span></span><br><span class="line">  from    <span class="string">&#x27;mikel@test.lindsaar.net&#x27;</span></span><br><span class="line">  to      <span class="string">&#x27;you@test.lindsaar.net&#x27;</span></span><br><span class="line">  subject <span class="string">&#x27;This is a test email&#x27;</span></span><br><span class="line">  part <span class="symbol">:content_type</span> =&gt; <span class="string">&quot;multipart/alternative&quot;</span>, <span class="symbol">:content_disposition</span> =&gt; <span class="string">&quot;inline&quot;</span> <span class="keyword">do</span> |<span class="params">p</span>|</span><br><span class="line">    p.part <span class="symbol">body:</span> <span class="string">&quot;abc&quot;</span></span><br><span class="line">    p.part <span class="symbol">content_type:</span> <span class="string">&#x27;image/png; filename=One_black_Pixel.png&#x27;</span>, <span class="symbol">body:</span> open(<span class="string">&#x27;https://upload.wikimedia.org/wikipedia/en/4/45/One_black_Pixel.png&#x27;</span>).read</span><br><span class="line">    p.part <span class="symbol">body:</span> <span class="string">&quot;def&quot;</span></span><br><span class="line">  <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">original_msg = mail.to_s</span><br><span class="line"><span class="comment">=begin</span></span><br><span class="line"><span class="comment">Date: Sat, 19 Aug 2017 14:58:30 +0800</span></span><br><span class="line"><span class="comment">From: mikel<span class="doctag">@test</span>.lindsaar.net</span></span><br><span class="line"><span class="comment">To: you<span class="doctag">@test</span>.lindsaar.net</span></span><br><span class="line"><span class="comment">Message-ID: &lt;5997e19678f0f_70774d198d1bc8306db<span class="doctag">@hparch</span>.mail&gt;</span></span><br><span class="line"><span class="comment">Subject: This is a test email</span></span><br><span class="line"><span class="comment">Mime-Version: 1.0</span></span><br><span class="line"><span class="comment">Content-Type: multipart/mixed;</span></span><br><span class="line"><span class="comment"> boundary=&quot;--==_mimepart_5997e19675b41_70774d198d1bc8305cd&quot;;</span></span><br><span class="line"><span class="comment"> charset=UTF-8</span></span><br><span class="line"><span class="comment">Content-Transfer-Encoding: 7bit</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">----==_mimepart_5997e19675b41_70774d198d1bc8305cd</span></span><br><span class="line"><span class="comment">Content-Type: multipart/alternative;</span></span><br><span class="line"><span class="comment"> boundary=&quot;--==_mimepart_5997e1959f3c7_70774d198d1bc830458&quot;;</span></span><br><span class="line"><span class="comment"> charset=UTF-8</span></span><br><span class="line"><span class="comment">Content-Transfer-Encoding: 7bit</span></span><br><span class="line"><span class="comment">Content-Disposition: inline</span></span><br><span class="line"><span class="comment">Content-ID: &lt;5997e1967a6a3_70774d198d1bc830787<span class="doctag">@hparch</span>.mail&gt;</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">----==_mimepart_5997e1959f3c7_70774d198d1bc830458</span></span><br><span class="line"><span class="comment">Content-Type: text/plain;</span></span><br><span class="line"><span class="comment"> charset=UTF-8</span></span><br><span class="line"><span class="comment">Content-Transfer-Encoding: 7bit</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">abc</span></span><br><span class="line"><span class="comment">----==_mimepart_5997e1959f3c7_70774d198d1bc830458</span></span><br><span class="line"><span class="comment">Content-Type: text/plain;</span></span><br><span class="line"><span class="comment"> charset=UTF-8</span></span><br><span class="line"><span class="comment">Content-Transfer-Encoding: 7bit</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">def</span></span><br><span class="line"><span class="comment">----==_mimepart_5997e1959f3c7_70774d198d1bc830458</span></span><br><span class="line"><span class="comment">Content-Type: image/png;</span></span><br><span class="line"><span class="comment"> filename=One_black_Pixel.png</span></span><br><span class="line"><span class="comment">Content-Transfer-Encoding: base64</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">iVBORwoaCgAAAApJSERSAAAAAQAAAAEIAgAAAJB3U94AAAABc1JHQgCuzhzp</span></span><br><span class="line"><span class="comment">AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAAxJREFU</span></span><br><span class="line"><span class="comment">GFdjYGBgAAAABAABXM3/aQAAAABJRU5ErkJggg==</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">----==_mimepart_5997e1959f3c7_70774d198d1bc830458--</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">----==_mimepart_5997e19675b41_70774d198d1bc8305cd--</span></span><br><span class="line"><span class="comment">=end</span></span><br><span class="line">puts original_msg</span><br><span class="line"></span><br><span class="line"><span class="comment"># 开始解析</span></span><br><span class="line">puts <span class="string">&quot;*&quot;</span> * <span class="number">42</span></span><br><span class="line">mail = <span class="title class_">Mail</span>.new(original_msg)</span><br><span class="line">puts mail.multipart?</span><br><span class="line">puts mail.text_part.body <span class="comment"># 默认只取了第一个 text part</span></span><br><span class="line"></span><br><span class="line">puts <span class="string">&quot;*&quot;</span> * <span class="number">42</span></span><br><span class="line"><span class="comment"># 通过 all_parts 拿到所有 part, 包含本身.</span></span><br><span class="line">mail.all_parts.each <span class="keyword">do</span> |<span class="params">part</span>|</span><br><span class="line">  <span class="comment"># 需要排除 multipart , attachment, 生产代码还需要区分 text 还是 html. text 和 text 加在一起, html 和 html 加在一起.</span></span><br><span class="line">  <span class="comment"># 这里还有一个大坑,就是多 text part 字符拼接时,一定要先把编码转成 utf-8 .因为苹果设备如果刚好那部分只有英文,那么编码为 ASCII, 如果有中文,编码为 GBK .</span></span><br><span class="line">  <span class="comment"># 有兴趣的朋友可以用苹果邮件客户端自己测试一下</span></span><br><span class="line">  puts part.body <span class="keyword">if</span> !part.multipart? &amp;&amp; !part.attachment?</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h4 id="multipart-再包含-multipart"><a href="#multipart-再包含-multipart" class="headerlink" title="multipart 再包含 multipart"></a>multipart 再包含 multipart</h4><p>这种情景主要出现在苹果邮件客户端同时发送 text 和 html 格式的. html 是一个 sub multipart.</p><p>处理方法同上, all_parts 会自动遍历 sub multipart . 我们只要排除 multipart? 和 attachment? 即可.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2017-08-19-1/</id>
    <link href="https://blog.mangege.com/tech/2017-08-19-1/"/>
    <published>2017-08-18T19:22:21.000Z</published>
    <summary>
      <![CDATA[<p>本文主要讲解使用 <a href="https://github.com/mikel/mail">mail</a> 库解析邮件所碰到的坑.</p>
<p>邮件格式本身的解析由 mail 库.由于邮件格式标准<a]]>
    </summary>
    <title>解析邮件碰到的那些坑</title>
    <updated>2026-04-14T15:05:49.626Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="ruby" scheme="https://blog.mangege.com/tags/ruby/"/>
    <category term="rails" scheme="https://blog.mangege.com/tags/rails/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <content>
      <![CDATA[<p>按个人理解, Admin Interfaces 的主要作用是减少后台管理界面的 CRUD 开发的重复工作量,并提供登录注销等常见功能的实现.</p><p>Admin Interfaces 实现主要分两大块.</p><ol><li>基于继承和配置.</li></ol><ul><li>代表: Django Admin(最著名的), ActiveAdmin.</li><li>优点: 代码量少.</li><li>缺点: 定制难度高.</li></ul><ol start="2"><li>基于代码生成.</li></ol><ul><li>代表: Rails Scaffold(自带的太简单了).</li><li>优点: 代码生成在项目里,定制只要直接修改代码即可,非常灵活.</li><li>缺点: 写自定义代码生成器有点难度.</li></ul><ol start="3"><li>同时使用代码生成和继承方式.</li></ol><ul><li>代表: ActiveScaffold</li><li>优点: 在减少代码的同时也保证了定制的灵活性.</li><li>缺点: 同上</li></ul><p>作为有一个有追求的人,虽然已经 ActiveAdmin 和 ActiveScaffold 这样不错 Rails Admin Interfaces.但为了追求定制的灵活性的最大化,必须得自己造个轮子出来,哪怕是方的轮子.</p><h3 id="什么是-Meta-Generator"><a href="#什么是-Meta-Generator" class="headerlink" title="什么是 Meta Generator?"></a>什么是 Meta Generator?</h3><p>就是写个 Generator A, Generator A 生成一个 Generator B 到你的项目里, 平常你主要运行 Generator B 生成 CRUD 相关代码. 这里的 Generator A 就是 Meta Generator.</p><h3 id="Meta-Generator-示例"><a href="#Meta-Generator-示例" class="headerlink" title="Meta Generator 示例"></a>Meta Generator 示例</h3><p>首先,请认真读完 <a href="http://guides.rubyonrails.org/generators.html">Creating and Customizing Rails Generators &amp; Templates</a> .</p><p>文中的 <code>bin/rails generate generator initializer</code> 的 generator 即是一个 Meta Generator,利用 generator 产生的代码存放在你的 Rails 项目的 lib&#x2F;generators ,这样我们就可以很方便的修改. 我们只要参考 generator ,写一个类似的 Gem ,把代码生成到 lib&#x2F;generators 目录即可.</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> your_workspace <span class="comment"># 修改成你自己的目录</span></span><br><span class="line">bundle <span class="built_in">exec</span> rails plugin new meta_generator_demo <span class="comment"># 创建一个 Rails Engine 的 Gem 项目</span></span><br><span class="line"><span class="built_in">cd</span> meta_generator_demo</span><br><span class="line">vi meta_generator_demo.gemspec <span class="comment"># 把里面的带 TODO 的都改成你准备填写的信息</span></span><br><span class="line"><span class="built_in">mkdir</span> -p lib/generators</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> your_rails_project</span><br><span class="line">bundle <span class="built_in">exec</span> rails g generator meta_generator_demo <span class="comment"># 创建一个生成器</span></span><br><span class="line"><span class="built_in">mv</span> lib/generators/meta_generator_demo your_workspace/meta_generator_demo/lib/generators/meta_generator_demo <span class="comment"># 修改成你自己的目录</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> your_workspace/meta_generator_demo <span class="comment"># 修改成你自己的目录</span></span><br><span class="line">vi lib/generators/meta_generator_demo/meta_generator_demo_generator.rb <span class="comment"># 文件内容如下</span></span><br></pre></td></tr></table></figure><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># lib/generators/meta_generator_demo/meta_generator_demo_generator.rb</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MetaGeneratorDemoGenerator</span> &lt; <span class="title class_ inherited__">Rails::Generators::NamedBase</span></span><br><span class="line">  source_root <span class="title class_">File</span>.expand_path(<span class="string">&#x27;../templates&#x27;</span>, <span class="variable constant_">__FILE__</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">def</span> <span class="title function_">create_demo_file</span></span><br><span class="line">    create_file <span class="string">&#x27;lib/generators/demo_generator.rb&#x27;</span>, <span class="string">&lt;&lt;-EOS</span></span><br><span class="line"><span class="string">class DemoGenerator &lt; Rails::Generators::Base</span></span><br><span class="line"><span class="string">  desc &quot;This generator creates an model file at app/models&quot;</span></span><br><span class="line"><span class="string">  def create_demo_file</span></span><br><span class="line"><span class="string">    create_file &quot;app/models/demo.rb&quot;, &quot;class Demo; end&quot;</span></span><br><span class="line"><span class="string">  end</span></span><br><span class="line"><span class="string">end</span></span><br><span class="line"><span class="string">    EOS</span></span><br><span class="line">  <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>Meta Generator  已经编写完成,可以测试了.</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> your_rails_project</span><br><span class="line">vi Gemfile <span class="comment"># 添加 &quot;gem &#x27;meta_generator_demo&#x27;, path: &#x27;your_workspace/meta_generator_demo&#x27;&quot; 到 Gemfile 里面,不包含 &quot; 符号.</span></span><br><span class="line">bundle install</span><br><span class="line">bundle <span class="built_in">exec</span> rails g <span class="comment"># 是不是可以看到 meta_generator_demo 这个选项了?</span></span><br><span class="line">bundle <span class="built_in">exec</span> rails g meta_generator_demo Demo</span><br><span class="line">bundle <span class="built_in">exec</span> rails g <span class="comment"># 是不是可以看到 demo 这个选项了?</span></span><br><span class="line">bundle <span class="built_in">exec</span> rails g demo <span class="comment"># 是不是可以看到 demo 这个选项了?</span></span><br><span class="line"><span class="built_in">cat</span> app/models/demo.rb <span class="comment"># 最终生成的文件, 如果需要改 demo.rb 生成的内容,只需要改 Rails 项目里的 lib/generators/demo_generator.rb 文件即可.</span></span><br></pre></td></tr></table></figure><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><ol><li>Gem 的 lib&#x2F;generators&#x2F;meta_generator_demo&#x2F;meta_generator_demo_generator.rb 文件有个 create_demo_file 方法, 此方法在你的 Rails 项目生成 lib&#x2F;generators&#x2F;demo_generator.rb 文件.</li><li>生成的 lib&#x2F;generators&#x2F;demo_generator.rb 文件有个 create_demo_file 方法,此方法创建了 app&#x2F;models.demo.rb 文件.</li></ol><h3 id="Rails-Admin-Generator-示例"><a href="#Rails-Admin-Generator-示例" class="headerlink" title="Rails Admin Generator 示例"></a>Rails Admin Generator 示例</h3><p>示例项目: <a href="https://github.com/adminonrails/aor">https://github.com/adminonrails/aor</a></p><p><strong>此项目已经弃坑,作为示例提供参考. 抛砖引玉,希望高手们能定制出更加适合自己的方案</strong></p><p>挖了这个坑,后面因为做的项目都是前后端分离,就没有再填了. 另外觉得这个项目也比较鸡肋,对新手不友好,上手和定制有点难度.对高手来说,有自己的一套方式,一般是自己挖个坑.</p><p>大部分功能都有写单元测试,项目有在生产环境使用过.</p><p>项目介绍:</p><ul><li>bootstrap: bootstrap 的静态资源,无 sass 依赖.</li><li>authentication: 提供登录验证的一些辅助方法,源码就一个文件. 主要提供 logged_in?, current_user 等方法. 参考老版 publify .</li><li>authorization: 基于 cancancan 提供后台权限验证的一些辅助方法. 主要基于 controller_name 和 action_name 来限制. 参考 spree 的后台验证逻辑.</li><li>theme: 代码生成器</li></ul><p>使用请参考 test 和 dummy 目录测试代码.</p><h4 id="theme-详解"><a href="#theme-详解" class="headerlink" title="theme 详解:"></a>theme 详解:</h4><p><a href="https://github.com/adminonrails/aor/blob/master/theme/Rakefile">https://github.com/adminonrails/aor/blob/master/theme/Rakefile</a></p><p>上文件 DummyGenerator 部分,是一个新 rails 项目使用 aor 的主要流程, 这里是用来每次运行 dummy rails 测试项目,先生成最新的 aor 代码.</p><ol><li>生成 kaminari 的 bootstrap3 模板.</li><li>添加 cancan 的 AdmAbility 文件.</li><li>运行 aor:theme</li><li>生成 admin user model.</li></ol><p><a href="https://github.com/adminonrails/aor/blob/master/theme/lib/generators/aor/theme/theme_generator.rb">https://github.com/adminonrails/aor/blob/master/theme/lib/generators/aor/theme/theme_generator.rb</a></p><p>上文件主要在安装了 aor-theme gem,运行 aor:theme 命令的代码.</p><ol><li>复制 admin js 和 css 文件.</li><li>添加公共的头部,侧边,表单验证错误提示文件.</li><li>添加 base admin controller 和 helper 文件.</li><li>把 admin.js 和 admin.css 添加到 assets 里,这样编译 js 和 css 会单独生成 admin 文件.</li><li>生成表单验证错误提示的 bootstrap 样式.<br>6  复制项目的 admin generator 到当前 rails 的 lib&#x2F;generators 目录.</li></ol><p><a href="https://github.com/adminonrails/aor/tree/master/theme/lib/generators/aor/theme/templates/generator">https://github.com/adminonrails/aor/tree/master/theme/lib/generators/aor/theme/templates/generator</a></p><p>此目录的文件,主要增强 rails 自带的 scaffold, scaffold_controller . 我们不覆盖 rails scaffold,只是添加一个自己的 admin:scaffold . 使用时运行 rails g admin:scaffold .</p><p>里面的 rb 文件逻辑,主要是修改 scaffold 的 source_paths 路径,优先使用我们的 controller 和 views 模板.</p><p>子目录 erb 和 rails 即是模板.</p><h5 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h5><p>此项目混合两种方式,一种是通过代码继承,子类通过重写父类方法来实现自定义.一种是生成代码,再修改生成的生成器代码,来实现自定义.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2017-03-19-1/</id>
    <link href="https://blog.mangege.com/tech/2017-03-19-1/"/>
    <published>2017-03-18T23:51:35.000Z</published>
    <summary>
      <![CDATA[<p>按个人理解, Admin Interfaces 的主要作用是减少后台管理界面的 CRUD 开发的重复工作量,并提供登录注销等常见功能的实现.</p>
<p>Admin Interfaces]]>
    </summary>
    <title>使用 Meta Generator 打造你的 Rails Admin</title>
    <updated>2026-04-14T15:05:49.626Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <content>
      <![CDATA[<p>虽然 WDS 是平价的无线漫游实现最优先考虑的方案.但 WDS 几个路由之间的通信还是无线,无线没有有线稳定这是不争的事实.</p><p>使用有线桥接来实现无线漫游,各个路由之间的通信通过有线,相对来说理论上要稳定些.</p><h3 id="示例"><a href="#示例" class="headerlink" title="示例:"></a>示例:</h3><p>三个路由器,一主二从.</p><p>两个也行,我这只是演示,表示接两个三个从路由都是没问题的.</p><h4 id="1-主路由设置"><a href="#1-主路由设置" class="headerlink" title="1. 主路由设置"></a>1. 主路由设置</h4><p>主路由和普通路由设置没有差别, Wan 口接光猫, Lan 接电脑和从路由器.</p><p>主路由为 openwrt 系统,其 <strong>Lan IP 为 192.168.2.1</strong> .</p><p>我的主路由设置截图:</p><p><img src="/images/wireless-roaming/20161115211142.png" alt="master router"></p><h4 id="2-从路由-1-设置"><a href="#2-从路由-1-设置" class="headerlink" title="2. 从路由 1 设置"></a>2. 从路由 1 设置</h4><p>所有 <strong>从路由要关闭 DHCP 服务器</strong> ,并 <strong>Lan IP 要设置为与主路由的 Lan IP 同网段</strong>.</p><p>从路由的 Wan 无需要接网线, <strong>主路由接根网线直接插到从路由的 Lan 上</strong> .</p><p>从路由 1 为 openwrt 系统, 其 <strong>Lan IP 为 192.168.2.5</strong> .</p><p>从路由器 Lan 口设置截图:</p><p><img src="/images/wireless-roaming/20161115213034.png" alt="slave router 1"></p><ul><li>圈1 为 Lan 的 IP ,通过此 IP 可以访问到你的从路由器.</li><li>圈2 和 圈3 设置从路由器的网关和DNS,不设置从路由器不能上网,但接入从路由器的设备能上网.</li><li>圈4 为关闭 Lan 口的 DHCP 服务器.</li></ul><h4 id="3-从路由-2-设置"><a href="#3-从路由-2-设置" class="headerlink" title="3. 从路由 2 设置"></a>3. 从路由 2 设置</h4><p>从路由 2 设置和 从路由 1 设置没什么差别,我这边只是给了一个 TP-LINK 的路由来演示,表示路由器不一定要用 openwrt 系统.</p><p>Wan 不接网线, 主路由过来的网线接在 Lan 口上. 设置 Lan 口 IP 和关闭 Lan DHCP 服务器.</p><p><img src="/images/wireless-roaming/20161115213821.png" alt="slave router 2"></p><h4 id="4-结尾"><a href="#4-结尾" class="headerlink" title="4. 结尾"></a>4. 结尾</h4><p>主要设置已经完成,之后配置 Wifi 为相同的 SSID 和 密码就行了.注意信道不要一样.推荐使用 1 6 11信道.比如我的主路由的信道是1 ,从路由 1 的信道是 6 ,从路由 2 的信道是 11 .</p><p>这样就低成本的实现高质量的无线漫游了.</p><h3 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h3><p>主要把从路由的 Lan 区当交换机用,只是比普通的交换机多了支持无线的接入,而无线接入进来还是在 Lan 的网段.</p><p>为什么要关闭 DHCP ? 因为从路由器都是当交换机,这样所有的 IP 都是从主路由的 DHCP 获取,这样获取的 IP 都是一样的,并且网关还是主路由器的 LAN IP .</p><p>网上有说有些路由器不支持把 LAN 口当交换机使用,感觉现在的路由应该都支持这么基础的功能吧.</p><h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><p><a href="https://wiki.openwrt.org/doc/recipes/bridgedclient">https://wiki.openwrt.org/doc/recipes/bridgedclient</a></p><p>上文提到要关闭防火墙,个人测试不关也没关系.</p><p><a href="https://wiki.openwrt.org/doc/howto/wide.area.wifi">https://wiki.openwrt.org/doc/howto/wide.area.wifi</a></p><p>上文提到多个路由要用不同信道.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2016-11-15-1/</id>
    <link href="https://blog.mangege.com/tech/2016-11-15-1/"/>
    <published>2016-11-15T05:02:00.000Z</published>
    <summary>
      <![CDATA[<p>虽然 WDS 是平价的无线漫游实现最优先考虑的方案.但 WDS 几个路由之间的通信还是无线,无线没有有线稳定这是不争的事实.</p>
<p>使用有线桥接来实现无线漫游,各个路由之间的通信通过有线,相对来说理论上要稳定些.</p>
<h3 id="示例"><a]]>
    </summary>
    <title>使用多个路由器有线桥接实现无线漫游</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="linux" scheme="https://blog.mangege.com/tags/linux/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <content>
      <![CDATA[<p>本文主要测试 linux 软件阵列卡与 LSI SAS 9211-8i 硬件阵列卡做raid 0 的性能.</p><p>测试不太严谨,结果仅供参考.</p><h3 id="测试结果"><a href="#测试结果" class="headerlink" title="测试结果:"></a>测试结果:</h3><p><img src="/images/hwraidvssoftraid/result.png" alt="result"></p><p>从结果可以看出,低端的硬件阵列卡性能还不如软件阵列.</p><p>软件阵列顺序写 Block 的时候,大概比硬件阵列要快 4M&#x2F;s 的样子. Rewrite 都要快 38M&#x2F;s . 顺序读 Block 要快 168M&#x2F;s .</p><p>但是软件阵列确实比硬件阵列消耗 CPU 些. 很多测试的结果软件阵列大概比硬件阵列多用了 5%~10% 的 CPU.</p><h3 id="测试环境"><a href="#测试环境" class="headerlink" title="测试环境:"></a>测试环境:</h3><p>CPU: Pentium E5400<br>内存: 4G (可用 3782 M)<br>系统: Centos 7.2.1511<br>硬盘: 4个同型号的 2TB 的硬盘,两个用来组硬件阵列,两个用来组软阵列.</p><p>LSI SAS 9211-8i 之所以称为低端阵列卡,因为阵列卡本身无缓存.</p><h3 id="测试工具-bonnie"><a href="#测试工具-bonnie" class="headerlink" title="测试工具: bonnie++"></a>测试工具: bonnie++</h3><h3 id="相关信息"><a href="#相关信息" class="headerlink" title="相关信息:"></a>相关信息:</h3><h4 id="列出所有硬盘"><a href="#列出所有硬盘" class="headerlink" title="列出所有硬盘:"></a>列出所有硬盘:</h4><pre><code>[root@host220 ~]# fdisk -lDisk /dev/sdb: 3998.0 GB, 3997997989888 bytes, 7808589824 sectorsUnits = sectors of 1 * 512 = 512 bytesSector size (logical/physical): 512 bytes / 4096 bytesI/O size (minimum/optimal): 4096 bytes / 4096 bytesDisk label type: dosDisk identifier: 0x00000000   Device Boot      Start         End      Blocks   Id  System/dev/sdb1               1  4294967295  2147483647+  ee  GPTPartition 1 does not start on physical sector boundary.Disk /dev/sdc: 2000.4 GB, 2000398934016 bytes, 3907029168 sectorsUnits = sectors of 1 * 512 = 512 bytesSector size (logical/physical): 512 bytes / 512 bytesI/O size (minimum/optimal): 512 bytes / 512 bytesDisk /dev/sdd: 2000.4 GB, 2000398934016 bytes, 3907029168 sectorsUnits = sectors of 1 * 512 = 512 bytesSector size (logical/physical): 512 bytes / 512 bytesI/O size (minimum/optimal): 512 bytes / 512 bytes</code></pre><p>sdb 为由硬件阵列卡组的 raid0 ,两个 2TB,所以 sdb 大小 将近有 4TB.<br>sdc sdd 也是插在阵列卡上,但是直通的,所以直接显示 2TB 一个.</p><h4 id="查看软阵列信息"><a href="#查看软阵列信息" class="headerlink" title="查看软阵列信息:"></a>查看软阵列信息:</h4><pre><code>[root@host220 ~]# cat /proc/mdstat Personalities : [raid0] md0 : active raid0 sdd[1] sdc[0]      3906766848 blocks super 1.2 512k chunks      unused devices: &lt;none&gt;</code></pre><p>可以看到由 sdc sdd 两个硬盘组建成 md0 这个软 raid0 阵列.</p><h4 id="查看挂载信息"><a href="#查看挂载信息" class="headerlink" title="查看挂载信息:"></a>查看挂载信息:</h4><pre><code>[root@host220 ~]# df -h | grep -E &#39;mnt|Mount&#39;Filesystem               Size  Used Avail Use% Mounted on/dev/md0p1               3.6T   89M  3.4T   1% /mnt/raidtest/soft/dev/sdb1                3.6T   89M  3.4T   1% /mnt/raidtest/hw</code></pre><p>soft 文件夹挂载的是软阵列.hw 文件夹挂载的是硬阵列.</p><h4 id="硬件阵列卡测试结果"><a href="#硬件阵列卡测试结果" class="headerlink" title="硬件阵列卡测试结果:"></a>硬件阵列卡测试结果:</h4><pre><code>[root@host220 ~]# bonnie++ -u root -d /mnt/raidtest/hw/Using uid:0, gid:0.Writing a byte at a time...doneWriting intelligently...doneRewriting...doneReading a byte at a time...doneReading intelligently...donestart &#39;em...done...done...done...done...done...Create files in sequential order...done.Stat files in sequential order...done.Delete files in sequential order...done.Create files in random order...done.Stat files in random order...done.Delete files in random order...done.Version  1.96       ------Sequential Output------ --Sequential Input- --Random-Concurrency   1     -Per Chr- --Block-- -Rewrite- -Per Chr- --Block-- --Seeks--Machine        Size K/sec %CP K/sec %CP K/sec %CP K/sec %CP K/sec %CP  /sec %CPhost220.cs.lo 7560M   283  99 298715  53 102106  21  2699  98 190370  16 388.6   8Latency             30693us     253ms     758ms   17693us     246ms     372msVersion  1.96       ------Sequential Create------ --------Random Create--------host220.cs.local    -Create-- --Read--- -Delete-- -Create-- --Read--- -Delete--              files  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP                 16 28154  91 +++++ +++ +++++ +++ 31745  90 +++++ +++ +++++ +++Latency               169us     534us     554us      99us      29us      61us1.96,1.96,host220.cs.local,1,1476076832,7560M,,283,99,298715,53,102106,21,2699,98,190370,16,388.6,8,16,,,,,28154,91,+++++,+++,+++++,+++,31745,90,+++++,+++,+++++,+++,30693us,253ms,758ms,17693us,246ms,372ms,169us,534us,554us,99us,29us,61us</code></pre><p><a href="/images/hwraidvssoftraid/hw.png">防作弊截图</a></p><h4 id="软件阵列卡测试结果"><a href="#软件阵列卡测试结果" class="headerlink" title="软件阵列卡测试结果:"></a>软件阵列卡测试结果:</h4><pre><code>[root@host220 ~]# bonnie++ -u root -d /mnt/raidtest/soft/Using uid:0, gid:0.Writing a byte at a time...doneWriting intelligently...doneRewriting...doneReading a byte at a time...doneReading intelligently...donestart &#39;em...done...done...done...done...done...Create files in sequential order...done.Stat files in sequential order...done.Delete files in sequential order...done.Create files in random order...done.Stat files in random order...done.Delete files in random order...done.Version  1.96       ------Sequential Output------ --Sequential Input- --Random-Concurrency   1     -Per Chr- --Block-- -Rewrite- -Per Chr- --Block-- --Seeks--Machine        Size K/sec %CP K/sec %CP K/sec %CP K/sec %CP K/sec %CP  /sec %CPhost220.cs.lo 7560M   590  99 302015  50 140381  26  2738  96 359120  26 631.2  12Latency             29796us     444ms     302ms   21224us     225ms     572msVersion  1.96       ------Sequential Create------ --------Random Create--------host220.cs.local    -Create-- --Read--- -Delete-- -Create-- --Read--- -Delete--              files  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP  /sec %CP                 16 27982  89 +++++ +++ +++++ +++ 31718  88 +++++ +++ +++++ +++Latency               164us     432us     468us      94us      30us      59us1.96,1.96,host220.cs.local,1,1476076562,7560M,,590,99,302015,50,140381,26,2738,96,359120,26,631.2,12,16,,,,,27982,89,+++++,+++,+++++,+++,31718,88,+++++,+++,+++++,+++,29796us,444ms,302ms,21224us,225ms,572ms,164us,432us,468us,94us,30us,59us</code></pre><p><a href="/images/hwraidvssoftraid/soft.png">防作弊截图</a></p><h3 id="结果整理"><a href="#结果整理" class="headerlink" title="结果整理"></a>结果整理</h3><p>bonnie++ 的命令输出的最后一行是 csv 的行,我们把它们都写到 &#x2F;tmp&#x2F;a.csv 文件里,再调用 <code>cat a.csv | bon_csv2html</code> .这样就把结果通过 html 表格来展示了.</p><p>bonnie++ 的结果解析请看参考此两篇文章.</p><ul><li><a href="http://blog.csdn.net/choice_jj/article/details/8026130">http://blog.csdn.net/choice_jj/article/details/8026130</a></li><li><a href="http://blog.chinaunix.net/uid-24774106-id-3728780.html">http://blog.chinaunix.net/uid-24774106-id-3728780.html</a></li></ul><blockquote><p>Sequential Output部分表示写文件的相关信息<br>Sequential Input部分表示读文件的相关信息<br>Per Chr表示以字符为单位读写文件<br>Block表示以block为单位读写文件<br>Rewrite表示修改并重写已经存在的文件的每一个block<br>K&#x2F;sec表示每秒读或写文件的速率，以K为单位<br>%CP表示在某阶段执行操作时平均消耗的CPU</p></blockquote><blockquote><p>Sequential Create和Radom Create 这两大类测试均是用创建，读取，删除大量的小文件来测试磁盘效率。</p></blockquote>]]>
    </content>
    <id>https://blog.mangege.com/tech/2016-10-10-1/</id>
    <link href="https://blog.mangege.com/tech/2016-10-10-1/"/>
    <published>2016-10-09T19:49:05.000Z</published>
    <summary>
      <![CDATA[<p>本文主要测试 linux 软件阵列卡与 LSI SAS 9211-8i 硬件阵列卡做raid 0 的性能.</p>
<p>测试不太严谨,结果仅供参考.</p>
<h3 id="测试结果"><a href="#测试结果" class="headerlink"]]>
    </summary>
    <title>Linux 软件阵列与低端硬件阵列卡性能对比</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="ruby" scheme="https://blog.mangege.com/tags/ruby/"/>
    <category term="rails" scheme="https://blog.mangege.com/tags/rails/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="frontend" scheme="https://blog.mangege.com/tags/frontend/"/>
    <content>
      <![CDATA[<p>尝试过用 Wiki 和 Swagger 等工具写基于 HTTP 协议的 API 的 文档,虽然有提供 curl 示例,但接口调用者使用起来还是觉得不方便,毕竟不是所有人都习惯命令行.</p><p>直到了解到 <a href="http://swagger.io/">Swagger</a>, 简直发现了写 HTTP API 文档的神器啊. 现已捐赠给 <a href="https://openapis.org/">Open API Initiative (OAI)</a> , 和 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md">OpenAPI 2.0 Specification</a> 合并了.</p><h3 id="Swagger-简介"><a href="#Swagger-简介" class="headerlink" title="Swagger 简介"></a>Swagger 简介</h3><p>详情介绍可以直接看官网.</p><p>按个人的理解. Swagger 提供一种简单的方式为 HTTP API 写文档,同时又方便 API 调用者测试.</p><p>Swagger 本身是由标准与工具组成的.</p><h4 id="Swagger-标准"><a href="#Swagger-标准" class="headerlink" title="Swagger 标准"></a>Swagger 标准</h4><p>现已和 OpenAPI 2.0 Specification 是同一个标准了.</p><p>这个标准,有点像是 XML 和 XML Schema 的关系.</p><p>XML 是非结构化数据,根本就不清楚 A 接点下面包含的是 B 还是 C 接口. 而 XML Schema 就是用来告诉我们, A 接点下面包含什么接点,同时还支持数据验证等功能.</p><p>我们写出来的 HTTP API 接口,如果没有文档,调用者根本就知道要传什么参数,返回什么数据. 而 OpenAPI Specification 就是这样一种标准,告诉我们应该怎样描述我们的接口,描述接口要传什么参数,返回什么数据.</p><p>OpenAPI Specification 最终是 JSON 或 YAML 数据格式表示, Specification 本身是告诉我们应该生成怎样的 JSON 或 YAML 数据.</p><p>主要描述请求的主机是什么,路径是什么,请求是 GET 还是 POST 等; 传参是 QUERY STRING 还是 BODY 等, 需要传什么头,返回什么头. 返回的数据是什么格式.</p><h4 id="Swagger-工具"><a href="#Swagger-工具" class="headerlink" title="Swagger 工具"></a>Swagger 工具</h4><ul><li><a href="https://github.com/swagger-api/swagger-ui">Swagger UI</a>: 把 Swagger 标准的 JSON 数据,显示成友好可操作的 HTML 文档,方便调用者查看与调试接口.</li><li><a href="http://editor.swagger.io/">Swagger Editor</a>: 一个在线 YAML 编辑器,方便编写 Swagger 标准的接口描述数据,并能生成JSON格式的数据,同时能生成本地客户端,方便文档分发.</li><li><a href="https://github.com/swagger-api/swagger-codegen">Sdk Generators</a>: 根据 Swagger 标准的数据生成接口代码.</li></ul><h4 id="在-Rails-里使用-Swagger"><a href="#在-Rails-里使用-Swagger" class="headerlink" title="在 Rails 里使用 Swagger"></a>在 Rails 里使用 Swagger</h4><p>上面三个工具,只用到 Swagger UI, 用它把写的接口描述JSON数据显示成友好的 HTML 界面.</p><p>ruby 中,如果用 <a href="https://github.com/ruby-grape/grape">grape</a> 写 HTTP API,那配合 <a href="https://github.com/ruby-grape/grape-swagger">grape-swagger</a> ,可以同步生成好文档,非常方便.</p><p>但个人习惯 Rails 了,觉得用 grape 要自己管理数据迁移脚本之类的,太麻烦了.</p><p>Ruby 里面的 Swagger 库我选 <a href="https://github.com/fotinakis/swagger-blocks">Swagger::Blocks</a> ,纯 Ruby 实现,代码只有一个文件,700多行,简单. 其本身只是一个生成 Swagger 标准的 JSON 数据的 DSL . 调用 <code>Swagger::Blocks.build_root_json</code> 方法,最终生成的只是 json 字符串而已.与 Web 框架无关,只要把此 json 数据做为 response 数据返回即可.</p><p>下面创建一个 Rails 项目,对 Swagger 主要点进行演示,算是个人踩过坑后的一点心得.</p><h3 id="1-创建-Rails-演示项目"><a href="#1-创建-Rails-演示项目" class="headerlink" title="1. 创建 Rails 演示项目"></a>1. 创建 Rails 演示项目</h3><p>参考 <a href="http://guides.rubyonrails.org/getting_started.html">Getting Started with Rails</a> 创建一个 Rails 项目,并带有简单的 CURD 接口.</p><pre><code>gem install rails rails new -B swagger_democd swagger_demovi Gemfile # 编辑 Gemfile 文件,把第一行的 https://rubygems.org 替换成 https://gems.ruby-china.orgbundle installbundle exec rails g scaffold Article title:string text:text # 生成 Article 的 model 和 controllerbundle exec rake db:create db:migrate # 创建数据库，运行迁移</code></pre><p>代码: <a href="https://github.com/mangege/swagger_demo/tree/step1">https://github.com/mangege/swagger_demo&#x2F;tree&#x2F;step1</a></p><h3 id="2-建立-swagger-初始文件"><a href="#2-建立-swagger-初始文件" class="headerlink" title="2. 建立 swagger 初始文件"></a>2. 建立 swagger 初始文件</h3><pre><code>vi Gemfile # 添加 gem &#39;swagger-blocks&#39; 到最后一行bundle installbundle exec rails g controller Apidocs indexvi app/controllers/apidocs_controller.rb # 复制此段内容 https://github.com/fotinakis/swagger-blocks#docs-controller ,还需要再编辑此文件内容,最终请看仓库代码vi config/routes.rb # 删除掉 get &#39;apidocs/index&#39; ，添加 resources :apidocs, only: [:index]cd /tmp; git clone https://github.com/swagger-api/swagger-ui.gitcp -R /tmp/swagger-ui/dist ~/workspace/swagger_demo/public/ # 复制 swagger 的静态文件到 rails 项目的 public 目录下.cd ~/workspace/swagger_demo/public/; mv dist swagger-ui # 重命名 dist 文件夹为 swagger-uivi public/swagger-ui/index.html # 替换 http://petstore.swagger.io/v2/swagger.json 为 /apidocs.json</code></pre><p>打开浏览器,访问 <a href="http://localhost:3000/swagger-ui/">http://localhost:3000/swagger-ui/</a> 即可.</p><p>代码: <a href="https://github.com/mangege/swagger_demo/tree/step2">https://github.com/mangege/swagger_demo&#x2F;tree&#x2F;step2</a></p><h3 id="3-为-Article-接口添加文档"><a href="#3-为-Article-接口添加文档" class="headerlink" title="3. 为 Article 接口添加文档"></a>3. 为 Article 接口添加文档</h3><p>按照 Swagger::Blocks 的示例,一般是在 model 或 controller 文件里写文档.但这样有可能导致 model 或 controller 文件行数过长.</p><p>通过分析源码了解,我们随便建一个类也可以.所以我们在 app 目录下建立专门的 swagger 目录.</p><pre><code>mkdir app/swaggervi config/application.rb # 添加 config.autoload_paths &lt;&lt; Rails.root.join(&#39;app/swagger&#39;) ,改了此文件记得重启 rails servervi app/swagger/app/swagger/article_swagger.rb</code></pre><p>添加以下内容</p><pre><code>class ArticleSwagger  include Swagger::Blocks  swagger_schema :Article do    key :required, [:id]    property :id do      key :type, :integer    end    property :title do      key :type, :string      key :description, &#39;标题&#39;    end    property :text do      key :type, :string      key :description, &#39;正文&#39;    end  endend</code></pre><hr><pre><code>vi app/controllers/apidocs_controller.rb # 在 SWAGGERED_CLASSES 添加 ArticleSwaggervi app/swagger/articles_controller_swagger.rb</code></pre><p>添加以下内容</p><pre><code>class ArticlesControllerSwagger  include Swagger::Blocks  swagger_path &#39;/articles&#39; dooperation :get do  key :description, &#39;article list&#39;  key :operationId, &#39;articleIndex&#39;  key :tags, [&#39;article&#39;  ]  response 200 dokey :description, &#39;article response&#39;schema do  key :type, :array  items dokey :&#39;$ref&#39;, :Article  endend  end  response :default dokey :description, &#39;unexpected error&#39;schema do  key :&#39;$ref&#39;, :ErrorModelend  endendoperation :post do  key :description, &#39;create article&#39;  key :operationId, &#39;articleCreate&#39;  key :tags, [&#39;article&#39;  ]  parameter dokey :name, :articlekey :in, :bodykey :required, trueschema do  key :&#39;$ref&#39;, :Articleend  end  response 200 dokey :description, &#39;article response&#39;schema do  key :&#39;$ref&#39;, :Articleend  end  response :default dokey :description, &#39;unexpected error&#39;schema do  key :&#39;$ref&#39;, :ErrorModelend  endend  end  swagger_path &#39;/articles/{id}&#39; dooperation :get do  key :description, &#39;article show&#39;  key :operationId, &#39;articleShow&#39;  key :tags, [&#39;article&#39;  ]      parameter do        key :name, :id        key :in, :path        key :required, true        key :type, :integer      end  response 200 dokey :description, &#39;article response&#39;schema do          key :&#39;$ref&#39;, :Articleend  end  response :default dokey :description, &#39;unexpected error&#39;schema do  key :&#39;$ref&#39;, :ErrorModelend  endendoperation :patch do  key :description, &#39;article update&#39;  key :operationId, &#39;articleUpdate&#39;  key :tags, [&#39;article&#39;  ]      parameter do        key :name, :id        key :in, :path        key :required, true        key :type, :integer      end  parameter dokey :name, :articlekey :in, :bodykey :required, trueschema do  key :&#39;$ref&#39;, :Articleend  end  response 200 dokey :description, &#39;article response&#39;schema do          key :&#39;$ref&#39;, :Articleend  end  response :default dokey :description, &#39;unexpected error&#39;schema do  key :&#39;$ref&#39;, :ErrorModelend  endendoperation :delete do  key :description, &#39;article destroy&#39;  key :operationId, &#39;articleDestroy&#39;  key :tags, [&#39;article&#39;  ]      parameter do        key :name, :id        key :in, :path        key :required, true        key :type, :integer      end  response 204 doschema do  key :type, :stringend  end  response :default dokey :description, &#39;unexpected error&#39;schema do  key :&#39;$ref&#39;, :ErrorModelend  endend  endend</code></pre><hr><pre><code>vi app/swagger/error_model_swagger.rb</code></pre><p>添加以下内容</p><pre><code>module ErrorModelSwagger  include Swagger::Blocks  swagger_schema :ErrorModel do    key :description, &#39;错误定义&#39;    key :required, [:code, :message]    property :code do      key :type, :integer      key :description, &#39;错误代码. 401 没有登录, 403 没有权限, 422 表单数据有误&#39;    end    property :message do      key :type, :string      key :description, &#39;错误消息&#39;    end    property :errors do      key :type, :object      key :description, &#39;错误详情. 键为出错的属性名.值为出错信息,值是字符串数组.&#39;    end  endend</code></pre><hr><pre><code>vi app/controllers/apidocs_controller.rb # 在 SWAGGERED_CLASSES 添加 ErrorModelSwagger 和 ArticlesControllerSwaggervi app/controllers/application_controller.rb # 替换 exception 为 null_session ,注意,如果项目还有普通的web页面,不要把此改成 null_session ,而是新建一个 api_controller.rb 文件,在新建的文件里设置为 null_session ,然后所有的 api controller 都继承与它.</code></pre><p>好了, Article 的 CRUD 操作的接口文档都已经编写完成,现在我们打开浏览器,访问 <a href="http://localhost:3000/swagger-ui/">http://localhost:3000/swagger-ui/</a> ,即可通过 swagger ui 来阅读文档,并测试接口了.</p><p>代码: <a href="https://github.com/mangege/swagger_demo/tree/step3">https://github.com/mangege/swagger_demo&#x2F;tree&#x2F;step3</a></p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2016-05-23-1/</id>
    <link href="https://blog.mangege.com/tech/2016-05-23-1/"/>
    <published>2016-05-23T05:04:41.000Z</published>
    <summary>
      <![CDATA[<p>尝试过用 Wiki 和 Swagger 等工具写基于 HTTP 协议的 API 的 文档,虽然有提供 curl 示例,但接口调用者使用起来还是觉得不方便,毕竟不是所有人都习惯命令行.</p>
<p>直到了解到 <a]]>
    </summary>
    <title>使用 Swagger 为你的 HTTP API 写文档</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="ruby" scheme="https://blog.mangege.com/tags/ruby/"/>
    <category term="rails" scheme="https://blog.mangege.com/tags/rails/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="frontend" scheme="https://blog.mangege.com/tags/frontend/"/>
    <category term="backend" scheme="https://blog.mangege.com/tags/backend/"/>
    <content>
      <![CDATA[<p>现在前后端分离比较流行,如果项目集成第三方登录,一般集成也是由前端完成.</p><p>前端把 user_id 与 token 相关信息传给后端,后端需要验证 user_id 与 token 的有效性,否则伪造新用户的成本非常低.</p><p>本文主要讲 google 与 facebook 的登录的 token 验证. 如果需要看国内第三方登录的,可以直接略过此文.</p><h3 id="Google-篇"><a href="#Google-篇" class="headerlink" title="Google 篇"></a>Google 篇</h3><p>前端集成可以看此文档 <a href="https://developers.google.com/identity/sign-in/web/sign-in">https://developers.google.com/identity/sign-in/web/sign-in</a> .</p><p>本文已经相关的 html 复制保存到 gist <a href="https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-g-html">https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-g-html</a> ,把页面上的 google-signin-client_id 值改成你的即可.</p><p>之后用 <code>python2 -m SimpleHTTPServer</code> 运行一个 web server , 访问 <code>http://localhost:8000/g.html</code> .</p><p>登录后可以在浏览器的控制台看到 id_token 的值, 复制出来备用.</p><p>后端验证 Google 的文档,找的好辛苦. <a href="https://developers.google.com/identity/sign-in/web/backend-auth">https://developers.google.com/identity/sign-in/web/backend-auth</a></p><p>验证很简单, 替换此连接的 XYZ123 为之前复制的 id_token <a href="https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123">https://www.googleapis.com/oauth2/v3/tokeninfo?id_token&#x3D;XYZ123</a> ,直接到浏览器访问即可. 或使用 curl 命令.</p><p><a href="https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4OWJmYjRhNTUxNGQ3NDVkYTZiN2ZhOGJkOTE2YTZlOTM5NDI0MmQifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXRfaGFzaCI6IjBrcjhXaUdPNmdIak5yOEtLM1l0enciLCJhdWQiOiIyMzg3NzM5OTgwMi1pcHVhbWJoNWlwdm12ZHJmbjJtZWpkYTFzcWowNHVnai5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNzI5NTMzNjA0ODc3NTc0NDk5NCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhenAiOiIyMzg3NzM5OTgwMi1pcHVhbWJoNWlwdm12ZHJmbjJtZWpkYTFzcWowNHVnai5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImhkIjoib2NlYW53aW5nLmNvbSIsImVtYWlsIjoiamVzc2UuY2FvQG9jZWFud2luZy5jb20iLCJpYXQiOjE0NjA3MDQ5MzIsImV4cCI6MTQ2MDcwODUzMiwibmFtZSI6Ikplc3NlIENhbyIsImdpdmVuX25hbWUiOiJKZXNzZSIsImZhbWlseV9uYW1lIjoiQ2FvIiwibG9jYWxlIjoiZW4ifQ.H3OGXsLXmT4QyJNhl2POdJYu5lcMjTlLDWFXZG0IsmySO-_2BkLVpJbJkCKMXmPQg61X0NpO_LEFMRLPxi-Iuf1gElHTJHPJgcd0Jdyr1LEK5ICG_mJN2ebbZxef1TkpombdJxApybMKe-pjh0Vmzmv6JykwsC6ulTrSmYqPAiRIgIRa9B5Dg3dWCpIEuwoPYjl-LpEWUer-Ko1nuRXN75-jOiD6XH1xLBNEDf0AwEVU7CK1U93JGOUf3l_kFSB0Vx7kRHpJ9VVEUvlxAAn0ER6L-LaZ4njzpeaRrtuFvoktW90xt54o34cY9uEIETumrTKnaTiEzCzUL0L8wkeyvA">Example</a></p><p>我们主要要验证结果的 aud 与 sub , aud 保证此 token 为你的站点生成, sub 保证 user_id 的正确性.</p><h3 id="Facebook-篇"><a href="#Facebook-篇" class="headerlink" title="Facebook 篇"></a>Facebook 篇</h3><p>前端集成可以看此文档 <a href="https://developers.facebook.com/docs/facebook-login/web">https://developers.facebook.com/docs/facebook-login/web</a> .</p><p>gist <a href="https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-fb-html">https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-fb-html</a></p><p>后端验证 Facebook 的文档没找到直接的,是翻到 debug_token 这个接口觉得符合需求,就用它了.</p><p>Facebook PHP SDK 有提到与 JS 结合 <a href="https://developers.facebook.com/docs/php/howto/example_access_token_from_javascript">https://developers.facebook.com/docs/php/howto/example_access_token_from_javascript</a> .</p><p>看了一下 PHP SDK 的实现, 主要通过读取 cookies 里面 fbsr_ 的值来进行验证 <a href="https://github.com/facebook/facebook-php-sdk-v4/blob/master/src/Facebook/Helpers/FacebookSignedRequestFromInputHelper.php#L158">https://github.com/facebook/facebook-php-sdk-v4/blob/master/src/Facebook/Helpers/FacebookSignedRequestFromInputHelper.php#L158</a> . 太复杂,想找简单的接口实现.</p><p>最初的想法是直接拿到前端的 access token, 再后端去调用 &#x2F;me 接口,成功则有效. 后来仔细考虑过.这也会导致有安全漏洞.比如某站长拿自己网站的用户 access_token 来登录,这样是有效的,因为 &#x2F;me 接口无法验证 access_token 的来源.</p><p>后来在这里看到 debug_token <a href="https://developers.facebook.com/docs/facebook-login/access-tokens/debugging-and-error-handling">https://developers.facebook.com/docs/facebook-login/access-tokens/debugging-and-error-handling</a> ,觉得返回很适合用来做验证.</p><p>对应的 graph api 文档 <a href="https://developers.facebook.com/docs/graph-api/reference/v2.6/debug_token">https://developers.facebook.com/docs/graph-api/reference/v2.6/debug_token</a></p><p>因为我们把 用户的 access_token 作为 input_token 传给了 &#x2F;debug_token 接口,所以调用 &#x2F;debug_token 接口我们得传应用的 access_token .<br>应用的 access_token 有两种方式生成,一种是通过接口生成,另外一种是把 app_id 与 app_secret 拼接在一起作为 access_token 传给接口.<br>拼接的这种方式简单,我们就是拼接. <a href="https://developers.facebook.com/docs/facebook-login/access-tokens#apptokens">https://developers.facebook.com/docs/facebook-login/access-tokens#apptokens</a></p><p>最终我们只要把页面拿到的 access_token 和拼接出来的 access_token 传给 &#x2F;debug_token 接口即可.</p><p><a href="https://graph.facebook.com/v2.6/debug_token?input_token=EAAVUeqi4jXwBAFgQrjEc1rfHe2pwqaNmCjJd2gLFOsati3IgVeiFFt1udjU9JrkP04hot0YbMm6VZCHLXdA90LBTrlIyniNXsXIW46EuEFfEFXVFOoBZCSIFNHp3SQF22CJHbiW6bjbCzUuoPhFeNhBnDU6L8cG60wKZAWByQZDZD&access_token=1500260676963708%7C018f0e4ee844e4a06f2854dc31242a10">Example</a></p><h3 id="最终-Ruby-代码"><a href="#最终-Ruby-代码" class="headerlink" title="最终 Ruby 代码"></a>最终 Ruby 代码</h3><p>gist : <a href="https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-auth_token_util-rb">https://gist.github.com/mangege/ff9a41ff2898cf19f88070e2945519c7#file-auth_token_util-rb</a></p><p>这样就不用安装 google 或 facebook 的 sdk.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2016-04-18-1/</id>
    <link href="https://blog.mangege.com/tech/2016-04-18-1/"/>
    <published>2016-04-18T03:48:41.000Z</published>
    <summary>
      <![CDATA[<p>现在前后端分离比较流行,如果项目集成第三方登录,一般集成也是由前端完成.</p>
<p>前端把 user_id 与 token 相关信息传给后端,后端需要验证 user_id 与 token 的有效性,否则伪造新用户的成本非常低.</p>
<p>本文主要讲 google]]>
    </summary>
    <title>前端第三方登录,后端Token验证备忘(Google 与 Facebook)</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="ruby" scheme="https://blog.mangege.com/tags/ruby/"/>
    <category term="rails" scheme="https://blog.mangege.com/tags/rails/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <content>
      <![CDATA[<p>本文简单的介绍 Ruby 包管理的相关原理,写的比较粗浅,欢迎补充.</p><h4 id="大纲"><a href="#大纲" class="headerlink" title="大纲"></a>大纲</h4><ul><li>Ruby 本身的包管理</li><li>Rubygems</li><li>Bundler</li><li>RVM 与 rbenv</li></ul><h4 id="Ruby-本身的包管理"><a href="#Ruby-本身的包管理" class="headerlink" title="Ruby 本身的包管理"></a>Ruby 本身的包管理</h4><p><strong>require method</strong></p><p>Ruby 主要通过 require 函数来引入外部的库文件. 函数原型如下:</p><pre><code># http://ruby-doc.org/core-1.8.7/Kernel.html#method-i-require# http://ruby-doc.org/core-2.2.3/Kernel.html#method-i-requirerequire(string) =&gt; true or false</code></pre><p>参数需要传一个 string , 文件名或文件路径.<br>返回值为 boolean 值, true 为 require 成功.</p><p>演示代码:</p><pre><code># shellecho &#39;puts &quot;a&quot;&#39; &gt; /tmp/a.rbcd /tmpirbrequire &#39;csv&#39; # 文件名方式,在 $LOAD_PATH 全局变量定义的路径里搜索require &#39;./a&#39; # 相对路径方式,基于进程的工作目录, Dir.pwd 可以查看当前进程的工作路径require &#39;/tmp/a&#39; # 绝对路径. 1.8.7 返回 true, 1.9 以后返回false. 1.9 以后同一文件,用不同的路径方式加载,也算同一文件,不会重复加载.</code></pre><p><strong>$LOAD_PATH</strong></p><p>本部分基于 ruby 1.8.7 的原因是因为 ruby 1.8.7 默认还是用 ruby 自身的 require 函数, 1.8 以后,默认用的是 Rubygems 实现的 require 函数.</p><p>大部分时候,我们使用 require 使用的是文件名,而不是相对路径或绝对路径的方式,所以 $LOAD_PATH 变量是个关键点.</p><pre><code>ruby -e &quot;puts $:&quot; # shell, 用 ruby 命令的 -e 参数运行单行 ruby 代码. 以下为命令执行后的输出/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/site_ruby/1.8/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/site_ruby/1.8/x86_64-linux/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/site_ruby/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/vendor_ruby/1.8/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/vendor_ruby/1.8/x86_64-linux/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/vendor_ruby/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/x86_64-linux</code></pre><p>$LOAD_PATH 变量为一个数组,里面存放的路径字符串.</p><p>打印出来的有三个重要的目录分类.</p><ul><li>site_ruby 默认优先级最高,安装本机相关库. 摘自&lt;&lt;Ruby 编程语言&gt;&gt; 254页.</li><li>vendor_ruby 操作系统供应商进行定制用的,一般为空.</li><li>1.8 ruby 标准库目录. 比如 date, csv 库.</li></ul><p>可以进入对应的目录查看一下,目录下有什么文件.</p><p>演示代码:</p><pre><code>echo &#39;puts &quot;priority2&quot;&#39; &gt; /usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/vendor_ruby/1.8/prioritydemo.rb # vendor_rubyruby -e &quot;require &#39;prioritydemo&#39;&quot; # puts priority2echo &#39;puts&quot;priority1&quot;&#39;&gt; /usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/site_ruby/1.8/prioritydemo.rb # site_rubyruby -e &quot;require &#39;prioritydemo&#39;&quot; # puts priority1</code></pre><p>通过代码演示可以看见, require 查找的顺序是基于 $LOAD_PATH 数组里面的路径的顺序来找的,找到了就不继续往下找.</p><p>上测试代码如果要强制加载 vendor_ruby 目录下的 prioritydemo 文件,可以使用绝对路径.</p><h4 id="Rubygems"><a href="#Rubygems" class="headerlink" title="Rubygems"></a>Rubygems</h4><p>Rubygems 主要通过 ruby 的 monkey patch 特性,重写了 require 函数的实现.</p><p>gem 一般安装到和 site_ruby 平级的 gems 目录下面,我们主要关心 gems(代码) 目录和 specifications(gemspec) 目录.</p><p><strong>rubygems require 解析</strong></p><p>此部分基于 2.3.4 的 ruby 源码分析.</p><p>文件跳转有点晕,觉得麻烦的朋友,可以略过.结论是把 对应的 gem 的 gems 目录添加到 $LOAD_PATH 变量里面.</p><ul><li>当加载 lib&#x2F;rubygems.rb 时,会调用 Gem::Specification.load_defaults 代码 # 1.9自动加载</li><li>lib&#x2F;rubygems&#x2F;specification.rb#load_defaults 会把 specifications 目录下的所有 gemspec 文件的 files 描述的文件通过 lib&#x2F;rubygems.rb#register_default_spec 方法注册到 @path_to_default_spec_map 变量. key 文件名,value为 spec 对象</li><li>require 方法会调用 lib&#x2F;rubygems.rb#find_unresolved_default_spec , find_unresolved_default_spec 拼上 .rb .so 在 @path_to_default_spec_map 变量里查找,如果找到,则返回对应的 spec , 再调用 lib&#x2F;rubygems.rb#remove_unresolved_default_spec 方法,从 @path_to_default_spec_map 变量删除这个 spec 的相关值,防止重复加载.</li><li>最后再调用  lib&#x2F;rubygems&#x2F;core_ext&#x2F;kernel_gem.rb#gem, lib&#x2F;rubygems&#x2F;specification.rb#activate ,  lib&#x2F;rubygems&#x2F;specification.rb#add_self_to_load_path 再把这个 gems 添加到 $LOAD_PATH 变量.</li></ul><p>演示代码:</p><pre><code>puts Gem.instance_eval(&quot;@path_to_default_spec_map.keys.any?{|k| k =~ /minitest/}&quot;) # trueputs $LOAD_PATH # 没有 minitest gemsputs require &#39;minitest&#39; # trueputs Gem.instance_eval(&quot;@path_to_default_spec_map.keys.any?{|k| k =~ /minitest/}&quot;) # falseputs $LOAD_PATH # 有 minitest gems</code></pre><p>可以看到在 require 之前与之后的差别,多了 minitest gem 的 lib 路径( &#x2F;home&#x2F;outman&#x2F;.rbenv&#x2F;versions&#x2F;2.2.4&#x2F;lib&#x2F;ruby&#x2F;gems&#x2F;2.2.0&#x2F;gems&#x2F;minitest-5.4.3&#x2F;lib ) .</p><p>最终结论是 rubygems 所做的一切,只是为了把 gem 的 lib 目录添加到 $LOAD_PATH 变量里,再用原生的 require 方法加载.</p><h4 id="Bundler"><a href="#Bundler" class="headerlink" title="Bundler"></a>Bundler</h4><p>个人现在的使用习惯是 rbenv + bundler .而不是使用 rvm 的 gemset . 项目第一次执行 bundle install 加 –path&#x3D;vendor&#x2F;bundle 参数,把 gem 安装到项目的 vendor&#x2F;bundle 目录下.再在 git 忽略此目录.</p><p>这样做就不会因为多个项目安装 gem 到系统目录,而导致系统里的 gem 冲突.</p><p>Bundler 和 Rubygems 一样,最终还是为了把项目的 gem 的 lib 目录添加到 $LOAD_PATH 变量里.</p><p>演示代码:</p><pre><code>ruby -e &#39;puts $LOAD_PATH&#39;bundle exec ruby -e &#39;puts $LOAD_PATH&#39; #可以看到 bundle 把项目 Gemfile 里定义的所有 gem 的 lib 目录都已经加到 $LOAD_PATH 变量里.</code></pre><p>源码简单解析:</p><p>bundle exec 主要修改 PATH  RUBYOPT RUBYLIB 变量,再用 exec 函数替换当前进程,从而继承修改后的 PATH RUBYOPT RUBYLIB 环境变量. </p><p>exec后的新进程读取 RUBYOPT 环境变量的 -rbundler&#x2F;setup 值,从而会先加载运行 bundler&#x2F;setup 这个文件的代码.</p><ul><li>lib&#x2F;bundler&#x2F;cli&#x2F;exec.rb#run 方法</li><li>SharedHelpers.set_bundle_environment <ul><li>把 bundle 的 bin 目录加到了 PATH 环境变量 <ul><li>bundle exec ruby -e ‘puts ENV[“PATH”]’</li></ul></li><li>再把 -rbundler&#x2F;setup 添加到 RUBYOPT 变量.<ul><li>ruby -h, -rlibrary       require the library before executing your script</li><li>echo “puts 123” &gt; &#x2F;tmp&#x2F;s.rb; ruby -r ‘&#x2F;tmp&#x2F;s.rb’ -e ‘puts 456’</li></ul></li><li>把 bundle 的 lib 目录添加到RUBYLIB 变量<ul><li>RUBYLIB&#x3D;&#x2F;tmp ruby -e ‘puts $LOAD_PATH’ # 把 &#x2F;tmp 添加到 $LOAD_PATH 的第一位了</li></ul></li></ul></li><li>再执行 Kernel.exec ,用 exec 参数后面的命令替换当前进程.新进程会在修改的 ENV 执行.</li><li>lib&#x2F;bundler&#x2F;setup.rb -&gt; Bundler.setup  -&gt; lib&#x2F;bundler.rb -&gt; load.setup -&gt; lib&#x2F;bundler&#x2F;runtime.rb </li><li>Runtime 从 Bundler.definition 里拿到所有 specs ,再遍历 specs,调用 Bundler.rubygems.loaded_specs 方法把所有 gem 都加载到 $LOAD_PATH .<ul><li>bundle exec ruby -e ‘puts Bundler::Runtime.new(Bundler.root, Bundler.definition).requested_specs.first.inspect</li></ul></li></ul><h4 id="rbenv"><a href="#rbenv" class="headerlink" title="rbenv"></a>rbenv</h4><p>rbenv 的原理和 bundler 差不多,主要是先修改环境变量,再调用 exec 替换当前进程.</p><p>在 rbenv 环境我们调用 which ruby 命令可以看到, ruby 执行文件总是在 ~&#x2F;.rbenv&#x2F;shims 目录下面. shims 目录下的 ruby 脚本会根据 .ruby-version 文件,找到对应 ruby 的执行文件路径,修改好环境变量后,再执行 exec 命令.</p><h4 id="rvm"><a href="#rvm" class="headerlink" title="rvm"></a>rvm</h4><p>rvm 与 rbenv 不同, rbenv 实现类似于设计模式里的委托模式,所有的 ruby 执行都交给 ~&#x2F;.rbenv&#x2F;shims 目录下的执行文件.</p><p>而 rvm 简单粗暴,直接把对应版本的 ruby 的 bin 目录添加到 PATH 环境变量里. </p><p><strong>rvm gemset解析</strong></p><p>rvm 的 gemset 主要是通过修改环境变量 GEM_HOME 和 GEM_PATH 变量来实现的. 此两变量 rubygems 根据其值在值定义的目录查找 gem .</p><p>演示代码:</p><pre><code>rvm gemset use 1.8.7@testset --createenv | grep GEMGEM_HOME=/usr/local/rvm/gems/ruby-1.8.7-head@testsetGEM_PATH=/usr/local/rvm/gems/ruby-1.8.7-head@testset:/usr/local/rvm/gems/ruby-1.8.7-head@globalgem install rackcd /usr/local/rvm/gems/ruby-1.8.7-head@testset; ls#bin  build_info  cachedoc  environment  gemsspecificationswrappersruby -e &#39;require &quot;rubygems&quot;; puts Gem.paths.path.inspect&#39;#[&quot;/usr/local/rvm/gems/ruby-1.8.7-head@testset&quot;, &quot;/usr/local/rvm/gems/ruby-1.8.7-head@global&quot;]</code></pre><p>可以看到把 gemset 的目录添加到 Gem.paths 变量里面去了. 而且固定有 global 目录,这样当我们把 gem 安装到 global 的 gemset 里,当在我们自己的 gemset 里找不到时,会去 global 的 gemset 目录里面找.</p><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p>$LOAD_PATH 很强大,利用它好,可以实现不错的 hack 技巧,但注意别让自己掉到坑里去了.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2016-03-27-1/</id>
    <link href="https://blog.mangege.com/tech/2016-03-27-1/"/>
    <published>2016-03-26T22:45:00.000Z</published>
    <summary>
      <![CDATA[<p>本文简单的介绍 Ruby 包管理的相关原理,写的比较粗浅,欢迎补充.</p>
<h4 id="大纲"><a href="#大纲" class="headerlink" title="大纲"></a>大纲</h4><ul>
<li>Ruby]]>
    </summary>
    <title>Ruby 包管理分析</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <content>
      <![CDATA[<p>此文献给同样爱穷折腾的同志们. 坑已经挖好,等着你们…</p><p>先讲一个悲伤的故事.<br>本来 双CPU + 32G内存 + DL180 G6 只要 1000 元的样子就可以.<br>由于本人没有服务器安装经验, 主板的CPU2的插座的针脚安装没放好,弯了,再手残挑断了针脚. 主板只可以用 CPU1 的插座,双路用不了.没办法,加320元换了一块主板(主板+机箱的准 系统也只要420块钱,原主板还要寄回去). 泪奔…</p><h4 id="重要的事情说三遍-慎入-慎入-慎入"><a href="#重要的事情说三遍-慎入-慎入-慎入" class="headerlink" title="重要的事情说三遍. 慎入,慎入,慎入."></a>重要的事情说三遍. 慎入,慎入,慎入.</h4><ol><li>这价钱,不实用. 主机加内存加CPU差不多要1000,还没有买显卡和硬盘的.推荐买占美这种小主机,或自己组装台式主机.</li><li>机箱太大, 长 72 厘米, 宽 45 厘米. 差 30 厘米就 1 米了,真心没地方摆.不过可以改装安装到普通的台式机机箱里面.</li><li>默认的风扇太吵,刚开机和发廊的热吹风对着耳朵吹没区别.风扇虽然可以改装,但改装后电源的风扇还是有点吵,除非你决定换成普通的台式机电源.改装后带上耳机听歌就听不到.隔一面墙也听不到.如果放在睡房,睡房有其它人,请不要购买.</li><li>CPU 耐热能力较差,像 L5630 最高只允许 63 度. 不过如果买 E 或 X 系列的 CPU 要好点.</li><li>南桥(IOH)温度太高,用自带风扇,室温20度 IOH TEMP 显示60多度. 自己在南桥加个风扇,就变 50 多度,不晓得夏天怎么样. ( X58 主板貌似这个问题貌似比较多 )</li><li>淘宝上面有全套改装好的,有兴趣的朋友可以入. 算上自己买机箱的钱,价格其它差不多了. <a href="https://item.taobao.com/item.htm?id=37302088648">https://item.taobao.com/item.htm?id=37302088648</a> 淘宝标题: HP DIY服务器 24核 X5650 3D建模 渲染主机 图形工作站 游戏多开</li></ol><h4 id="购买清单总览"><a href="#购买清单总览" class="headerlink" title="购买清单总览"></a>购买清单总览</h4><ol><li>CPU L5630 X2 - 84 元</li><li>HP DL180 G6 X1 - 440 元 (准系统,只要主板,电源和机箱.无CPU,内存,硬盘)</li><li>硬盘架 X4 - 40 元</li><li>内存 8根 - 320 元</li><li>风扇 改装线+PWM风扇+普通3000转风扇 - 大概要60元</li><li>usb 声卡 - 20 元</li></ol><p>双CPU 8 核 16 线程 32G 内存 总价只要 934 元. 硬盘与显卡钱另计.<br>如果内存改成 24G ,只要 854 元.会砍价的话,砍到 800 应该可以的.</p><h4 id="清单详情"><a href="#清单详情" class="headerlink" title="清单详情"></a>清单详情</h4><p>淘宝标题方便商品过期后通过关键词还能找到相关商品.</p><h5 id="CPU-L5630"><a href="#CPU-L5630" class="headerlink" title="CPU: L5630"></a>CPU: L5630</h5><p>淘宝标题: XEON志强L5630 2.13G 12M 40W CPU 正式版 取代E5520 L5520</p><p>单价 37 元, 数量 2 个.<br>此 CPU 为 4 核 8 线程,主频只有 2.13 GHz, 功耗 40W .<br>主频相对来说比较低,不适合用来玩游戏. 功耗只有 40W , 两个加起来 80W , 8 核 16 线程只有 80W 看起来还是不错.</p><p>另外可以上 E 或 X 系列的 CPU ,主频与耐热能力都有提升, 比如 <a href="http://ark.intel.com/products/47925/Intel-Xeon-Processor-E5620-12M-Cache-2_40-GHz-5_86-GTs-Intel-QPI">E5620</a>  <a href="http://ark.intel.com/zh-cn/products/47922/Intel-Xeon-Processor-X5650-12M-Cache-2_66-GHz-6_40-GTs-Intel-QPI">X5650</a> . 当然 <strong>价格与功耗</strong> 都要高一倍以上.</p><p>所有的 1366 针 CPU 列表: <a href="https://en.wikipedia.org/wiki/Xeon#3600.2F5600-series_.22Gulftown.22_.26_.22Westmere-EP.22">https://en.wikipedia.org/wiki/Xeon#3600.2F5600-series_.22Gulftown.22_.26_.22Westmere-EP.22</a></p><p>淘宝上面还有 30 元的购买链接,貌似我之前搜索没有搜索到.估计被淘宝搜索排序给隐藏了.</p><h5 id="服务器-HP-DL180-G6"><a href="#服务器-HP-DL180-G6" class="headerlink" title="服务器: HP DL180 G6"></a>服务器: HP DL180 G6</h5><p>淘宝标题: 特价惠普HP DL180G6 2U服务器3.5寸8盘位HP 380G6 G7秒DELL R710</p><p>买的是准系统,没有 CPU 内存 硬盘. 有 主板 电源 一个散热器.<br>单价为 420 元,多去淘宝找找,应该还有便宜的.默认只有一个散热器,所以加 20 元再买了一个散热器.<br>硬盘托架 10 元一个,买了 4 个备用.<br>内存单根 4G, 40元一根,共买了8根. 本来只买了四根,后来发现每个 CPU 需要 3 根内存才能组成 3 通道. 共 32G 内存.<br>每边上4根内存,发现也可以三通道,用 AIDA32 的内存测试,一样可以把 CPU 的内存带宽跑满,跑到 25Gb&#x2F;s .</p><p>网上关于 HP DL160 G6 的文章相对来说比较多,两个型号差不多</p><ul><li>DL160 是 1U 的, DL180 是 2U 的. 2U 相对来说更厚些,更好放独立显卡.  </li><li>DL180 比 DL160 支持更大的内存,好多 DL160 最大只支持 16G 内存. 而大部分 DL180 都支持 192G 内存, 可以在中关村网页看参数.</li></ul><h4 id="风扇改造"><a href="#风扇改造" class="headerlink" title="风扇改造"></a>风扇改造</h4><p>自带的风扇虽然有带温控,但一样很吵,特别是在装了独立显卡后,转速稳定在 4000 ,比一般台式机吵多了.开机那就更不要说.</p><p>基于 HP DL160 G6 的文章分析,风扇改造主要有三种方案.</p><h5 id="1-把6针的插座改成4针的-然后再买4针的PWM风扇-注意风扇大小与功率"><a href="#1-把6针的插座改成4针的-然后再买4针的PWM风扇-注意风扇大小与功率" class="headerlink" title="1. 把6针的插座改成4针的,然后再买4针的PWM风扇,注意风扇大小与功率."></a>1. 把6针的插座改成4针的,然后再买4针的PWM风扇,注意风扇大小与功率.</h5><p>推荐此方案,换风扇就可以了.</p><p>技术文章: <a href="http://www.360doc.com/content/14/0318/13/73007_361564350.shtml">http://www.360doc.com/content/14/0318/13/73007_361564350.shtml</a></p><p>淘宝标题: HP DL160G6&#x2F;DL180G6&#x2F;SE316M1 静音改装 四线温控线 改装线<br>淘宝链接: <a href="https://item.taobao.com/item.htm?id=522213532817">https://item.taobao.com/item.htm?id=522213532817</a></p><h5 id="2-利用可调电阻来降速"><a href="#2-利用可调电阻来降速" class="headerlink" title="2. 利用可调电阻来降速."></a>2. 利用可调电阻来降速.</h5><p>技术文章: <a href="http://tieba.baidu.com/p/2968626728">http://tieba.baidu.com/p/2968626728</a></p><p>淘宝购买链接: <a href="https://item.taobao.com/item.htm?id=45394666862">https://item.taobao.com/item.htm?id=45394666862</a></p><p>卖家说了,最大只能降速50%,没试过,怕还是很吵.</p><h5 id="3-刷-BIOS-关闭风扇检测"><a href="#3-刷-BIOS-关闭风扇检测" class="headerlink" title="3. 刷 BIOS 关闭风扇检测."></a>3. 刷 BIOS 关闭风扇检测.</h5><p>技术文章: <a href="http://tieba.baidu.com/p/3530406494">http://tieba.baidu.com/p/3530406494</a></p><p>下载地址在15楼. 没试过,怕刷坏. 刷坏就是320.</p><p>这个只是关闭风扇检测,不是调速.所以也没有刷的必要.</p><h5 id="我的风扇折腾之路"><a href="#我的风扇折腾之路" class="headerlink" title="我的风扇折腾之路"></a>我的风扇折腾之路</h5><p>瞎折腾在风扇上面花了 100 多块.</p><p>第一次在这家买了个 50 的套餐,加邮费 60. <a href="https://item.taobao.com/item.htm?id=522213532817">https://item.taobao.com/item.htm?id=522213532817</a><br>到手后看老板只送了一个 4 线的风扇,三个 3 线的风扇.三线的风扇不是 6 厘米的,还放不进原来的风扇位置里面.装上压测 CPU 温度会超过40.<br>以为是老板小气,才配三个 3 线风扇.后来才知道是我太菜了.</p><p>参考老板送的 4 线风扇型号 台达AFB0612EH <a href="https://item.taobao.com/item.htm?id=39664158650">https://item.taobao.com/item.htm?id=39664158650</a>,再买了4个,加邮费花了 31 块钱.<br>到手后才发现,所以的风扇接上转速都一样,都只要 1500 的转速左右,跟没装风扇一样, CPU 温度上 60 有望. 折腾了几个小时,发现只接风扇只接电源正负极速度就是全速转.全速转有 6800 转,参数写着噪音 46.5 dB,比原装的风扇还要吵.<br>终于知道卖线老板为什么送 3 个三线低转速的风扇了.因为那3个三线风扇只接了正负,所以全速转,但全速转的速度估计也只是 2000~3000 转,所以不吵.</p><p>为了完美,再买了 3 个 6cm 3000转 风扇, 包邮 25元. <a href="https://item.taobao.com/item.htm?id=43741478446">https://item.taobao.com/item.htm?id=43741478446</a><br>装上后效果可以,但是 IOH 温度还是有 60 度左右,估计到夏天上 80 度没问题.到时得换成 5000 转或 4000 转的风扇.</p><p>最终给你们的推荐方案,直接买第一家老板的套餐,然后再把风扇直接绑到 CPU 散热上面和 IOH 散热上面.</p><p>淘宝上面有两种改装线,有一种只能接两个风扇的,这种不要买,到时没办法为 IOH 接风扇.</p><h4 id="USB-声卡"><a href="#USB-声卡" class="headerlink" title="USB 声卡"></a>USB 声卡</h4><p>淘宝标题: 全新7.1USB声卡 高品质苹果外置独立声卡&#x2F;电脑声卡&#x2F;免驱WIN7 <a href="https://item.taobao.com/item.htm?id=21875420495">https://item.taobao.com/item.htm?id=21875420495</a></p><p>顺便在这家买了根显卡和sata供电线</p><p>淘宝标题: 做工很好 HP DL180se G6 显卡6P 8P SATA硬盘 电源供电线 转接线 <a href="https://item.taobao.com/item.htm?id=527137606595">https://item.taobao.com/item.htm?id=527137606595</a></p><p>声卡质量一般,用着用着,有时忽然变成噪音.然后要重插一下才可以.</p><p>linux 显卡如果带 hdmi,需要调整一个顺序. <a href="https://bbs.archlinux.org/viewtopic.php?id=171097">https://bbs.archlinux.org/viewtopic.php?id=171097</a></p><h4 id="显卡"><a href="#显卡" class="headerlink" title="显卡"></a>显卡</h4><p>此主板是带两个 PCI-E X16 插口,不过通过PCI-E转换卡,转成了一个PCI-E X16, 两个PCI-E X8 插口.<br>PCI-E X16 插口肯定用来插显卡,自带显卡太差,连 1920x1080 的分辨率真都跑不起.<br>注意显卡最好不要还需要独立供电的,不然得买转接线.<br>显卡不要太大,我的小显卡都是拆了显卡固定条才安装上去的.</p><h4 id="功耗"><a href="#功耗" class="headerlink" title="功耗"></a>功耗</h4><p>为了此文,给个准确的功耗数据,淘宝花了50元买了个功耗测试工具.</p><p>我接的显卡的 ati hd5450 入门显卡,功耗低.</p><p>接电源不开机 10W 左右.<br>平常使用 120w 左右波动. (加了阵列卡130w)<br>用鲁大师压测 145W 的样子.  </p><h4 id="机箱"><a href="#机箱" class="headerlink" title="机箱"></a>机箱</h4><p>网上说此主板可以放到普通机箱,本人测试过,拆掉硬盘架之类的,确实放的下.<br>参考文章 <a href="http://tieba.baidu.com/p/3790196594?pn=1"> http://tieba.baidu.com/p/3790196594?pn=1</a></p><p>淘宝有机箱现货.<br>淘宝标题: 静音机箱 原装 DL160 G6 DIY 180G6 316M1 C1100主板支持长显卡 <a href="https://item.taobao.com/item.htm?id=521305177590">https://item.taobao.com/item.htm?id=521305177590</a><br>不过这个机箱需要另外买电源,用不了原装电源.不差钱的买.原装电源声音还是有点大.</p><p>本人没钱折腾了,所以没有折腾机箱了.</p><h4 id="阵列卡"><a href="#阵列卡" class="headerlink" title="阵列卡"></a>阵列卡</h4><p>如果你需要接 SAS 硬盘,或组 RAID5, 你需要买 p410 阵列卡. 记得买一根 mini sas x4 的线. 跟老板说是 dl180 g6用的,他就知道了.</p><p>淘宝 阵列卡+电池+512M缓存+线 100包邮.</p><p>原装HP P410 6GB 阵列卡462919-001 013233-001 P410 阵列卡 <a href="https://item.taobao.com/item.htm?id=525780632164">https://item.taobao.com/item.htm?id=525780632164</a></p><p>看了下,包装上面有写着 RMA ,应该是 HP 的退换货件流出来的.</p><h4 id="IOH-温度"><a href="#IOH-温度" class="headerlink" title="IOH 温度"></a>IOH 温度</h4><p>x58 貌似比较多的主板 IOH 温度都有过高问题,所以需要接个风扇到 IOH 散热片上面.<br>IOH 散热片的位置位于CPU2与PCI接口的中间. 具体看图. 图在文尾.<br>有可能是这块主板的问题.到时有入手的朋友,可以告诉一下 IOH 的温度.  </p><p><a href="https://www.chiphell.com/thread-918499-1-1.html">https://www.chiphell.com/thread-918499-1-1.html</a><br><a href="http://game.ali213.net/thread-2970986-1-1.html">http://game.ali213.net/thread-2970986-1-1.html</a>  </p><p>中间以为自己的也是散热片没接好的问题,然后把 IOH 的那个散热拆了,看到芯片旁边写着 IOH ,确认是 IOH 无误.<br>不小心把散热膏搞到芯片上面后,开不了机,又折腾了几个小时,最后用医生酒精清洗,才开得了机. 散热膏本身是绝缘的,不知道为什么会开不了机.</p><p>还碰到另外一个问题,就是用风扇的改装线,本来只要接4个,我接了6个,再换回4个.开机后BIOS检测到变动,貌似在改 BMC .然后再重启,出现 “bmc not responding” 错误,然后找资料,重刷了 BMC ROM 和 SYSTEM ROM 才解决. 怕了.</p><h4 id="重置-IPMI-密码"><a href="#重置-IPMI-密码" class="headerlink" title="重置 IPMI 密码"></a>重置 IPMI 密码</h4><p>通过 IPMI ,可以通过 <strong>http界面远程开关机</strong> .</p><p>默认用户与密码是 admin admin  </p><p>重置教程: <a href="http://pan.baidu.com/wap/link?uk=1396009279&shareid=4159928792&third=0">http://pan.baidu.com/wap/link?uk=1396009279&amp;shareid=4159928792&amp;third=0</a></p><h4 id="安装提示"><a href="#安装提示" class="headerlink" title="安装提示"></a>安装提示</h4><ol><li>盖子的背面有图,那里有标记 CPU1 和 CPU2 的位置. 前期只安装 CPU1 和插一根内存条.方便测试能否开机.</li><li>离风扇的近的那边的 CPU 与 内存是 1 . 内存不是按数字的顺序插,而是按字母的顺序插. A B C D E F 这样的顺序. 背面的图有说.</li><li>CPU 安装请小心,不怕弄坏 CPU ,因为CPU没有针脚,怕搞坏CPU底座,要换主板. 可以参考此文 <a href="http://support.huawei.com/enterprise/KnowledgebaseReadAction.action?contentId=KB1000054278&idAbsPath=7919749">http://support.huawei.com/enterprise/KnowledgebaseReadAction.action?contentId=KB1000054278&amp;idAbsPath=7919749</a></li><li><strong>开机自检很久,差不多要1分钟,屏幕没显示时请耐心等待</strong>.</li><li>按f10进入bios设置.</li><li>默认优先从 USB 启动,所以做个 USB PE 启动盘,或刷个 Ubuntu 到 U 盘,可以先玩玩.</li></ol><h4 id="求打赏"><a href="#求打赏" class="headerlink" title="求打赏"></a>求打赏</h4><p>在2月22日这个犯2的日子下了第一笔单,前前后后折腾了一个多月.累.</p><p>如果你觉得此坑挖的不错,准备入坑,可以打赏10元或20元小费到我的支付宝,谢谢. <strong>ciro9527ATgmail.com (AT换成@)</strong>  </p><h4 id="卖主板-CPU-内存"><a href="#卖主板-CPU-内存" class="headerlink" title="卖主板,CPU,内存"></a>卖主板,CPU,内存</h4><p>主板: 梅捷SY-N7HM3-GR<br>CPU: AMD X2 240<br>内存: AMD专用内存条两根4G, 共8G. (稳定,没有蓝屏死机过).  </p><p>180 包邮. 长沙地址自提送机箱加电源,接上显卡和硬盘就能用.</p><p><a href="https://2.taobao.com/item.htm?id=528249630350&grade=4.0">https://2.taobao.com/item.htm?id=528249630350&amp;grade=4.0</a></p><h4 id="图"><a href="#图" class="headerlink" title="图"></a>图</h4><h5 id="装2B图"><a href="#装2B图" class="headerlink" title="装2B图"></a>装2B图</h5><p><img src="/images/dl180g6/IMG_2016-03-14_22-24-21.png" alt="装B图"></p><h5 id="IOH-风扇安装位置"><a href="#IOH-风扇安装位置" class="headerlink" title="IOH 风扇安装位置"></a>IOH 风扇安装位置</h5><p><img src="/images/dl180g6/IMG_20160314_221127.jpg" alt="IOH 风扇安装位置"></p><h5 id="开机进入系统后不操作时功耗"><a href="#开机进入系统后不操作时功耗" class="headerlink" title="开机进入系统后不操作时功耗"></a>开机进入系统后不操作时功耗</h5><p><img src="/images/dl180g6/IMG_20160314_221855.jpg" alt="开机进入系统后不操作时功耗"></p><h5 id="打开-CRHOME-功耗"><a href="#打开-CRHOME-功耗" class="headerlink" title="打开 CRHOME 功耗"></a>打开 CRHOME 功耗</h5><p><img src="/images/dl180g6/IMG_20160314_221943.jpg" alt="打开 CRHOME 功耗"></p><p>持续更新地址: <a href="https://blog.mangege.com/tech/2016/03/14/1.html">https://blog.mangege.com/tech/2016/03/14/1.html</a></p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2016-03-14-1/</id>
    <link href="https://blog.mangege.com/tech/2016-03-14-1/"/>
    <published>2016-03-14T04:08:01.000Z</published>
    <summary>
      <![CDATA[<p>此文献给同样爱穷折腾的同志们. 坑已经挖好,等着你们…</p>
<p>先讲一个悲伤的故事.<br>本来 双CPU + 32G内存 + DL180 G6 只要 1000 元的样子就可以.<br>由于本人没有服务器安装经验,]]>
    </summary>
    <title>穷折腾,淘了台服务器当台式机用</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <content>
      <![CDATA[<p>试用了十几个win store的pdf应用,都没办法点击按屏翻页.<br>只能先把pdf软件设置成按页查看,然后才能点击翻页.</p><p>但如果屏幕少于10寸,加上pdf的页边空白,那样看字会好小.</p><p>大部分 pdf 软件都支持按 page down 键按屏分页. 包括win自带的阅读器.<br>但是我们必须得用桌面应用,而不是用store里面的应用.因为 autohotkey 无法热键绑定到 store 里面的应用.</p><p>安装 adobe reader, 再运行此脚本.<br>双击开启按屏分页,再双击关闭按屏分页.<br>支持放大pdf再按屏分页,这样就可以隐藏页边空白.</p><p>脚本: <a href="https://gist.github.com/mangege/d28f03634ba6d2e1abe5">https://gist.github.com/mangege/d28f03634ba6d2e1abe5</a></p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2015-12-05-1/</id>
    <link href="https://blog.mangege.com/tech/2015-12-05-1/"/>
    <published>2015-12-05T01:26:27.000Z</published>
    <summary>
      <![CDATA[<p>试用了十几个win store的pdf应用,都没办法点击按屏翻页.<br>只能先把pdf软件设置成按页查看,然后才能点击翻页.</p>
<p>但如果屏幕少于10寸,加上pdf的页边空白,那样看字会好小.</p>
<p>大部分 pdf 软件都支持按 page down]]>
    </summary>
    <title>撸了个Win平板点击按屏翻页的小工具</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="ruby" scheme="https://blog.mangege.com/tags/ruby/"/>
    <category term="rails" scheme="https://blog.mangege.com/tags/rails/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="frontend" scheme="https://blog.mangege.com/tags/frontend/"/>
    <content>
      <![CDATA[<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul><li><a href="https://guides.spreecommerce.com/developer/extensions_tutorial.html">https://guides.spreecommerce.com/developer/extensions_tutorial.html</a></li><li><a href="https://guides.spreecommerce.com/developer/deface_overrides_tutorial.html">https://guides.spreecommerce.com/developer/deface_overrides_tutorial.html</a></li></ul><h3 id="扩展的分类"><a href="#扩展的分类" class="headerlink" title="扩展的分类"></a>扩展的分类</h3><ul><li>类的扩展,主要是对 Model 与 Controller 进行修改. 其它像 Concern 与 Helper 都从属于 Model 与 Controller,一般直接改 Model 与 Controller 即可.</li><li>视图的扩展,主要是对 Html 视图进行修改. JS 与 CSS 因为可以通过代码加载顺序来重写现有功能.</li></ul><h3 id="类的扩展"><a href="#类的扩展" class="headerlink" title="类的扩展"></a>类的扩展</h3><p>类的扩展的实现主要是基于 Ruby 的 Open classes 特性实现.</p><p>创建一个测试项目,请先参考 <a href="https://github.com/spree/spree">https://github.com/spree/spree</a> 建立一个 Rails Project .<br>图省事,就不用 <code>spree extension</code> 命令建立一个 Rails engine ,而直接在 Rails Project 写代码测试.</p><h4 id="示例一-访问首页时在控制台打印文字"><a href="#示例一-访问首页时在控制台打印文字" class="headerlink" title="示例一: 访问首页时在控制台打印文字"></a>示例一: 访问首页时在控制台打印文字</h4><p>添加 app&#x2F;controllers&#x2F;spree&#x2F;home_controller_decorator.rb 文件,文件内容如下:</p><pre><code>module Spree  HomeController.class_eval do    alias_method :old_index, :index    def index      puts &quot;#{&#39;#&#39;*100} index test&quot;      old_index    end  endend</code></pre><p>alias_method 是 Rails 的方法,用于重命名现有的方法并删除,方便重写方法时再调用老的方法.</p><p>Open classes 除了可以用 class_eval 这样来实现,还可以直接用 <code>class A; end</code> 这样的类定义语法来实现同样的功能.<br>之所以用 class_eval ,有两个个人能想到的优点:  </p><ol><li>用 class_eval 这种形式,肯定会先把原来的 class 给加载, 而用类定义语法就不一定了.</li><li>类定义语法,再次打开类,还需要记得原来的 class 的父类,如果不同的话,到时会报 <code>superclass mismatch for class</code> 错误.</li></ol><p>文件名结尾一定要以 decorator 结尾,这样才能保证在开发模式时,每次自动请求会自动重新加载此文件.</p><h5 id="decorator-分析"><a href="#decorator-分析" class="headerlink" title="decorator 分析"></a>decorator 分析</h5><p>查看 Spree 源码的 core&#x2F;lib&#x2F;spree&#x2F;core&#x2F;engine.rb 文件,可以看到这样一段代码:</p><pre><code>  config.to_prepare do    # Load application&#39;s model / class decorators    Dir.glob(File.join(File.dirname(__FILE__), &#39;../../../app/**/*_decorator*.rb&#39;)) do |c|      Rails.configuration.cache_classes ? require(c) : load(c)    end  end</code></pre><p>to_prepare 为 Rails 的方法,此处用来加载 decorator 文件.<br>glob 用来查找所有包含 <code>_decorator</code> 的文件.<br>Rails.configuration.cache_classes 判断是否开启类缓存, 开启的话,用require加载文件,可以防止重复加载.否则用load方法,这样能保证每次请求,decorator的代码都是最新的.  </p><h5 id="to-prepare-分析"><a href="#to-prepare-分析" class="headerlink" title="to_prepare 分析"></a>to_prepare 分析</h5><p>在项目里的 config&#x2F;application.rb 文件增加以下内容:</p><pre><code>config.to_prepare do  puts &quot;#{&#39;$&#39;*100} to_prepare test&quot;end</code></pre><p>重启 rails server, 可以看到在启动后,就执行了添加的回调. 但再次访问不会执行回调. 随便修改一个 controller 文件,可以看到回调再次执行了.<br>基于 to_prepare 方法,这样就可以保证被修改的类不会被漏加载.</p><h3 id="视图的扩展"><a href="#视图的扩展" class="headerlink" title="视图的扩展"></a>视图的扩展</h3><p>视图的扩展有两种实现方法</p><h4 id="1-基于-Rails-view-path的加载顺序实现"><a href="#1-基于-Rails-view-path的加载顺序实现" class="headerlink" title="1. 基于 Rails view path的加载顺序实现"></a>1. 基于 Rails view path的加载顺序实现</h4><p>添加 app&#x2F;views&#x2F;spree&#x2F;home&#x2F;index.html 文件,内容随便写点,比如 <code>hello</code></p><p>再次访问首页,可以看到首页的内容变成 hello 去了.</p><p><a href="http://guides.rubyonrails.org/action_view_overview.html#view-paths">View Paths</a> 这一章的文档刚好没有,所以个人简单的介绍一下.</p><p>在rails console运行 <code>ActionController::Base.view_paths.each{|a| puts a.to_path}; nil</code> , 可以看到所有视图目录, Rails 是在这些目录下一个一个找,找到了就停止查找. 可以看到, Rails Proejct 的目录是在最前面的.</p><p>这种方式会替换此视图,没办法像 Deface 可以根据 DOM 查找添加内容到指定位置,或删除指定节点.</p><p>删除 app&#x2F;views&#x2F;spree&#x2F;home&#x2F;index.html 文件,方便再测试.</p><h4 id="2-基于-Deface-实现"><a href="#2-基于-Deface-实现" class="headerlink" title="2. 基于 Deface 实现"></a>2. 基于 Deface 实现</h4><p>示例在首页的侧边添加一行 Hello world</p><p>在 Rails 项目里新建 app&#x2F;overrides&#x2F;add_hello_to_home.rb 文件,文件内容如下:</p><pre><code>Deface::Override.new(  :virtual_path =&gt; &#39;spree/home/index&#39;,  :name =&gt; &#39;add_hello_to_home&#39;,  :insert_after =&gt; &quot;erb[silent]:contains(&#39;sidebar&#39;)&quot;,  :text =&gt; &quot;&lt;p&gt;&lt;%= &#39;hello world&#39; * 10 %&gt;&lt;/p&gt;&quot;)</code></pre><p>之后访问首页,可以看到侧边顶部增加一行hello world.</p><p>执行 <code>rake deface:precompile</code> 命令,可以看到生成了 app&#x2F;compiled_views&#x2F;spree&#x2F;home&#x2F;index.html.erb 文件内容,内容如下:</p><pre><code>&lt;% content_for :sidebar do %&gt;&lt;p&gt;&lt;%= &#39;hello world&#39; * 10 %&gt;&lt;/p&gt;  &lt;div data-hook=&quot;homepage_sidebar_navigation&quot;&gt;    &lt;%= render :partial =&gt; &#39;spree/shared/taxonomies&#39; %&gt;  &lt;/div&gt;&lt;% end %&gt;&lt;div data-hook=&quot;homepage_products&quot;&gt;  &lt;% cache(cache_key_for_products) do %&gt;    &lt;%= render :partial =&gt; &#39;spree/shared/products&#39;, :locals =&gt; { :products =&gt; @products } %&gt;  &lt;% end %&gt;&lt;/div&gt;</code></pre><p>而原始文件 frontend&#x2F;app&#x2F;views&#x2F;spree&#x2F;home&#x2F;index.html.erb 内容如下:</p><pre><code>&lt;% content_for :sidebar do %&gt;  &lt;div data-hook=&quot;homepage_sidebar_navigation&quot;&gt;    &lt;%= render :partial =&gt; &#39;spree/shared/taxonomies&#39; %&gt;  &lt;/div&gt;&lt;% end %&gt;&lt;div data-hook=&quot;homepage_products&quot;&gt;  &lt;% cache(cache_key_for_products) do %&gt;    &lt;%= render :partial =&gt; &#39;spree/shared/products&#39;, :locals =&gt; { :products =&gt; @products } %&gt;  &lt;% end %&gt;&lt;/div&gt;</code></pre><p>重启 rails console,再运行 <code>ActionController::Base.view_paths.each{|a| puts a.to_path}; nil</code> 语句,可以看到, app&#x2F;compiled_views 这个目录的顺序是在 app&#x2F;views 前面,排在第一位,所以最终还是靠 view paths来实现的.</p><p>deface 的作用是用来修改 erb 文件,但它解决了 erb 不能通过 dom 树来查找的问题.</p><p>分析 deface 的源码发现, 在 lib&#x2F;deface&#x2F;parser.rb 此文件,可以知道 deface 只是简单的把 <code>&lt;%= %&gt; &lt;% %&gt;</code> 替换成 <code>&lt;erb loud&gt; &lt;erb silent&gt; &lt;/erb&gt;</code> 这样的非标准的html标签,再通过 Nokogiri 解析,执行 deface override代码里的替换,替换完后再把erb标签替换回来.</p><h3 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h3><p>示例项目源码: <a href="https://github.com/mangege/spree_hack_example">https://github.com/mangege/spree_hack_example</a></p><p>为类增加代码很简单,但删除就很麻烦.比如从 Model 移除一个属性的 validate ,这个时候需要分析Rails的validate的实现,再写hack代码.</p><p>单元测试非常重要,因为没有单元测试,你没有办法保证你的 hack 代码在下个版本的 spree 和 rails 还是能正常运行.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2015-09-20-1/</id>
    <link href="https://blog.mangege.com/tech/2015-09-20-1/"/>
    <published>2015-09-19T17:13:25.000Z</published>
    <summary>
      <![CDATA[<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a]]>
    </summary>
    <title>Spree 扩展机制分析</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="linux" scheme="https://blog.mangege.com/tags/linux/"/>
    <content>
      <![CDATA[<p>喜欢 ThinkPad 的指点杆键盘,移动光标时手不需要离开键盘区.</p><p>在淘宝上面淘一个 0B47190 的国行指点杆键盘. 接上电脑,在 archlinux 上面,上下滚动能正常使用.<br>但有点小问题,按中键的时候,会先执行中键的功能,比如在终端上会粘贴剪切版的内容.</p><p>参考 <a href="https://wiki.archlinux.org/index.php/TrackPoint">https://wiki.archlinux.org/index.php/TrackPoint</a> 的配置,执行 ~&#x2F;.xinitrc 文件的内容,然后中键滚动都不能用了.</p><p>折腾模式开启了,主要参考了以下资料.</p><ol><li><a href="http://www.x.org/archive/X11R7.7/doc/man/man5/xorg.conf.5.xhtml#heading9">http://www.x.org/archive/X11R7.7/doc/man/man5/xorg.conf.5.xhtml#heading9</a></li><li><a href="http://www.x.org/archive/X11R7.5/doc/man/man4/evdev.4.html">http://www.x.org/archive/X11R7.5/doc/man/man4/evdev.4.html</a></li><li><a href="https://wiki.ubuntu.com/X/Config/Input">https://wiki.ubuntu.com/X/Config/Input</a></li></ol><p>首先用 <code>xinput list</code> 列出输入设备,查找设备的ID.指点杆设备是在 “Virtual core pointer” 节点下,另外个同名的设备为键盘.</p><pre><code>$ xinput list⎡ Virtual core pointer                      id=2015[master pointer  (3)]⎜   ↳ Virtual core XTEST pointer                id=4[slave  pointer  (2)]⎜   ↳ Lenovo ThinkPad Compact USB Keyboard with TrackPointid=9[slave  pointer  (2)]⎣ Virtual core keyboard                     id=3[master keyboard (2)]    ↳ Virtual core XTEST keyboard               id=5[slave  keyboard (3)]    ↳ Power Button                              id=6[slave  keyboard (3)]    ↳ Power Button                              id=7[slave  keyboard (3)]    ↳ Lenovo ThinkPad Compact USB Keyboard with TrackPointidid=8[slave  keyboard (3)]</code></pre><p>再用 <code>xinput test 9</code> 测试键值,命令中的9为你的测试的输入设备的ID. 测试发现,按下中键会打印出 “button press 2”,在不释放中键的同时触摸指点杆,提示 “button press 4” 之类的.<br>接上一个普通的usb鼠标,再测试,发现滚动的键值都一样. 猜测键值: 1&#x3D;左键, 2&#x3D;中键, 3&#x3D;右键, 4 5 6 7对应着上下左右滚动.<br>因手边没有ThinkPad电脑,没办法测试上文提到的 .xinitrc 配置的开启 Evdev Wheel Emulation 选项后 xinput-test 打印值的差别.<br>个人估计,新版的thinkpad键盘直接是硬件上就直接兼容标准的鼠标键值了,而不再需要配置 Wheel Emulation .</p><p>所有,解决方案根本不需要管滚动这一块,只需要处理中键粘贴的问题.简单粗爆的方法,直接禁用中键.</p><p>方式一是在 .xinitrc 里添加以下行命令.</p><pre><code>xinput set-button-map &quot;Lenovo ThinkPad Compact USB Keyboard with TrackPoint&quot; 1 0 3 4 5 6 7 8 9 10 11 12 13</code></pre><p>“Lenovo ThinkPad Compact USB Keyboard with TrackPoint” 这个值是通过xinput获取到的, 后面的数值是通过 xinput get-button-map 9获取来的.<br>设置时,把2的值置为0,这样就禁用掉中键的.  </p><p>4 5为上下,如果我们把4 5的键值位置对换一下,应该可以实现 Mac 的滚动习惯的.</p><p>方式二是通过 xorg configuration 实现,创建一个 <code>/etc/X11/xorg.conf.d/20-thinkpad.conf</code> 文件,内容如下:</p><pre><code>Section &quot;InputClass&quot;    Identifier  &quot;Trackpoint&quot;    MatchUSBID          &quot;17ef:6047&quot;    MatchIsPointer      &quot;true&quot;    Option              &quot;ButtonMapping&quot;         &quot;1 0 3 4 5 6 7 8 9 10 11 12 13&quot;EndSection</code></pre><p>查看 xorg conf 文档,觉得 MatchUSBID 更适合,而不是示例给的 MatchProduct 指令,通过 <code>lsusb</code> 命令查找设备的id.<br>查看 man evdev 文档,配置 InvertX 选项,应该也可以实现 Mac 的滚动习惯.</p><p>xorg conf 还是很强大,可惜不了解 Linux 系统编程.得加油!!!</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2015-09-19-1/</id>
    <link href="https://blog.mangege.com/tech/2015-09-19-1/"/>
    <published>2015-09-18T20:08:03.000Z</published>
    <summary>
      <![CDATA[<p>喜欢 ThinkPad 的指点杆键盘,移动光标时手不需要离开键盘区.</p>
<p>在淘宝上面淘一个 0B47190 的国行指点杆键盘. 接上电脑,在 archlinux]]>
    </summary>
    <title>新版ThinkPad USB指点杆键盘linux配置</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="backend" scheme="https://blog.mangege.com/tags/backend/"/>
    <content>
      <![CDATA[<p>saltstack本身是用python写的,用python调用saltstack相对来说比较简单.</p><p>个人想到的调用方式有下:</p><h4 id="1-Python-client-API"><a href="#1-Python-client-API" class="headerlink" title="1. Python client API"></a>1. Python client API</h4><p><a href="http://docs.saltstack.com/en/latest/ref/clients/#salt.wheel.WheelClient.cmd">http://docs.saltstack.com/en/latest/ref/clients/#salt.wheel.WheelClient.cmd</a></p><p>优点:</p><ol><li>最简单,只要机器安装了saltstack,直接在python代码里 <code>import salt</code> 即可.</li></ol><p>缺点:</p><ol><li>只有python语言才能这样使用.</li><li>必须得与salt master部署在同一台机器.</li><li>saltstack任务执行时,某些处理是在调用方进程里执行,这样意味调用方的必须与salt master同用户,否则到时会没有权限往 <code>/var/cache/salt/master</code> 目录写缓存文件. 比如salt master是root用户运行,那么django网站调用salt时也必须得也root用户运行.</li></ol><h4 id="2-salt-api"><a href="#2-salt-api" class="headerlink" title="2. salt api"></a>2. salt api</h4><p><a href="https://salt-api.readthedocs.org/en/latest/">https://salt-api.readthedocs.org/en/latest/</a></p><p>优点:</p><ol><li>以 http rest api形式提供访问,这样什么语言都可以调用.</li><li>调用方可以不与salt master在同一机器.</li><li>调用方完全可用普通用户运行.</li></ol><p>缺点:</p><ol><li>需要自己对rest api做一次封装.</li><li>调用 <code>salt.modules.cp.push</code> 时,文件存放在 salt master上面.这个时候需要拿到文件必须得走NFS或自定义接口.</li></ol><h4 id="3-salt-command"><a href="#3-salt-command" class="headerlink" title="3. salt command"></a>3. salt command</h4><p>直接在程序里调用 salt 命令来执行任务</p><p>优点:</p><ol><li>简单粗暴</li></ol><p>缺点:</p><ol><li>需要自己解决命令执行结果,出错处理太难.</li></ol>]]>
    </content>
    <id>https://blog.mangege.com/tech/2015-05-27-1/</id>
    <link href="https://blog.mangege.com/tech/2015-05-27-1/"/>
    <published>2015-05-27T04:40:34.000Z</published>
    <summary>
      <![CDATA[<p>saltstack本身是用python写的,用python调用saltstack相对来说比较简单.</p>
<p>个人想到的调用方式有下:</p>
<h4 id="1-Python-client-API"><a href="#1-Python-client-API"]]>
    </summary>
    <title>python调用saltstack的几种方式</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="linux" scheme="https://blog.mangege.com/tags/linux/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <content>
      <![CDATA[<p>各直播网站直播教程都有提到OBS(Open Broadcaster Software),但在archlinux编译成功,运行却报错.<br>后来了解到ffmpeg可以推流,花了一两天简单的了解ffmpeg的命令参数.并成功在斗鱼上面直播.</p><p>主要参考 ffmpeg 的 wiki 的 <a href="http://trac.ffmpeg.org/wiki/EncodingForStreamingSites">Encoding for streaming sites</a></p><p>斗鱼的rtmp推流地址是 <em>rtmp地址 加 &#x2F; 加 直播码</em>.<br>示例: <code>rtmp://send3.douyutv.com/live/209282rr9tq83W9I?wsSecret=6d7cb31146e722d561e55eb75f9b6e6f&amp;wsTime=54e051be</code>.<br>注意此直播码每次关闭直播后会变动.</p><h4 id="关于码率"><a href="#关于码率" class="headerlink" title="关于码率"></a>关于码率</h4><p>请参考此文章 <a href="http://www.douyutv.com/cms/zhibo/201311/13/250.shtml">斗鱼TV游戏直播教程-OBS直播软件篇[推荐]</a></p><blockquote><p>1Mbps左右的请设置最大码率为500kbps，压缩分辨率为720x480或与480接近。<br>2Mbps左右的请设置最大码率为1500kbps，压缩分辨率为1280x720或与720接近。<br>低于0.5Mbps的，还是去看直播吧。</p></blockquote><p>对应的参数为 maxrate 与 bufsize, bufsize 斗鱼是推荐是 maxrate 一样.</p><h4 id="ffmpeg-示例"><a href="#ffmpeg-示例" class="headerlink" title="ffmpeg 示例"></a>ffmpeg 示例</h4><p>捕获桌面,麦克风,摄像头命令参数示例:</p><pre><code>$ ffmpeg -f x11grab -video_size 1680x1050 -framerate 30 -i :0.0 \-f v4l2 -video_size 320x240 -framerate 30 -i /dev/video0 \-f alsa -ac 2 -i hw:0,0 -filter_complex \&quot;[0:v]scale=1024:-1,setpts=PTS-STARTPTS[bg]; \ [1:v]scale=120:-1,setpts=PTS-STARTPTS[fg]; \ [bg][fg]overlay=W-w-10:10,format=yuv420p[out]&quot;-map &quot;[out]&quot; -map 2:a -vcodec libx264 -preset veryfast \-maxrate 3000k -bufsize 4000k -acodec libmp3lame -ar 44100 -b:a 128k \-f flv rtmp://send3.douyutv.com/live/209282rr9tq83W9I?wsSecret=6d7cb31146e722d561e55eb75f9b6e6f&amp;wsTime=54e051be</code></pre><p>循环播放文件示例,使用的是 <a href="https://trac.ffmpeg.org/wiki/Concatenate">concat</a>  特性.</p><pre><code>ffmpeg -re -f concat -i playlist.txt \-vcodec libx264 -preset veryfast -maxrate 500k -bufsize 500k \-vf &quot;format=yuv420p:scale=720:-1&quot; -g 48 -acodec libmp3lame -b:a 96k -ar 44100 \-f flv &quot;rtmp://send3.douyutv.com/live/209282rezHd16aNm?wsSecret=f759db6daa4137c184e2f16125d8af5d&amp;wsTime=54dd5819&quot;</code></pre><p>视频加文字示例,使用的是 <a href="https://ffmpeg.org/ffmpeg-filters.html#drawtext-1">drawtext</a> 特性.</p><pre><code>ffmpeg -re -f concat -i playlist.txt \-vcodec libx264 -preset veryfast -maxrate 500k -bufsize 500k \-vf &quot;format=yuv420p:scale=720:-1&quot; -g 48 -acodec libmp3lame -b:a 96k -ar 44100 \-filter:v &quot;drawtext=fontsize=12:fontcolor=red:fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:text=&#39;CodeMonkeys %{localtime\:%T}&#39;:y=17:x=900&quot; \-f flv &quot;rtmp://send3.douyutv.com/live/209282rezHd16aNm?wsSecret=f759db6daa4137c184e2f16125d8af5d&amp;wsTime=54dd5819&quot;</code></pre><p>大部分功能ffmpeg wiki都有示例,比如配置摄像头,配置捕获桌面的,请记住 wiki 地址 <a href="https://trac.ffmpeg.org/wiki">https://trac.ffmpeg.org/wiki</a></p><p>另外 ffmpeg 的参数排列你要看懂.</p><pre><code>usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...</code></pre><p>ffmpeg 通用选项 输入文件选项 输入文件 很多的输入文件 输出文件选项 输出文件 很多的输出文件<br>也就是说,可以有多个输入,也可以有多个输出.这样可以实现推流到网站时同时本地保存一份.</p><h4 id="关于在linux服务器播放视频"><a href="#关于在linux服务器播放视频" class="headerlink" title="关于在linux服务器播放视频."></a>关于在linux服务器播放视频.</h4><p>安装可以直接安装静态编译包,解压后就可以运行了, 而且一般比较新. <a href="http://johnvansickle.com/ffmpeg/">http://johnvansickle.com/ffmpeg/</a></p><p>测试过在阿里云的最低配置,码率设置为500k,差不多刚好够播放.<br>通过ifstat命令测试,2M带宽播放码率1500k也够了,就实时转码CPU不够用.<br>请提前把文件转换成flv格式,这样应该就不卡了.</p><h4 id="关于在斗鱼上面播放视频"><a href="#关于在斗鱼上面播放视频" class="headerlink" title="关于在斗鱼上面播放视频"></a>关于在斗鱼上面播放视频</h4><p>貌似大部分有版权问题的视频都不能放,但不清楚别人为什么能放.</p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2015-02-15-1/</id>
    <link href="https://blog.mangege.com/tech/2015-02-15-1/"/>
    <published>2015-02-14T23:44:21.000Z</published>
    <summary>
      <![CDATA[<p>各直播网站直播教程都有提到OBS(Open Broadcaster Software),但在archlinux编译成功,运行却报错.<br>后来了解到ffmpeg可以推流,花了一两天简单的了解ffmpeg的命令参数.并成功在斗鱼上面直播.</p>
<p>主要参考]]>
    </summary>
    <title>Linux使用ffmpeg直播推流</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <content>
      <![CDATA[<p>本文是基于vs920 v9系统写的,理论上vs840 v9也可以用.</p><p>首先你得安装adb,通过adb shell连接上手机后,运行下面命令</p><pre><code>pm list packages -f -s</code></pre><p>列表系统所有的程序. 运行结果请看 <a href="#appendix1">附录一</a> .</p><p>把附录一的结果保存为一个普通文件,保存到linux电脑的&#x2F;tmp&#x2F;目录,之后再运行下面命令过滤出可以删除的应用.</p><pre><code>cat v9_list.txt | grep -v com.android | grep -v com.lge | grep -v ipsec | grep -v com.google | grep -v com.qualcomm | grep -v framework | grep -v com.adobe.flashplayer | grep -v com.broadcom.bt.app.system | grep -v com.innopath</code></pre><p>运行结果请看<a href="#appendix2">附录二</a>,此命令主要保留Google与LG厂商开发的应用.</p><p>重新挂载 system 目录为可写</p><pre><code>mount -o remount,rw /system</code></pre><p>之后就是执行rm 与 pm uninstall 命令了</p><pre><code>rm /system/app/Aetherpal.apkrm /system/app/Kindle.apkrm /system/app/NFSHP.apkrm /system/app/ScoreCenter.apkrm /system/app/BaSyncService.apkrm /system/app/LGolf2.apkrm /vendor/app/PolarisOffice.apkrm /system/app/NFL.apkrm /system/app/SSO_P3.apkrm /system/app/SmartMovieHD.apkrm /system/app/Netflix.apkrm /system/app/Slacker.apkrm /system/app/Swype.apkrm /system/app/TuneWiki.apkrm /system/app/BUAPLUS.apkrm /system/app/VideoCallingPortal.apkrm /system/app/appdirectedsmspermission.apkrm /system/app/qospermission.apkrm /system/app/securesettingspermission.apkrm /system/app/ssopermission.apkrm /system/app/vzwapnpermission.apkrm /system/app/SecureSettingsService.apkrm /system/app/VZNavigator.apkrm /system/app/VZWAPNService.apkrm /system/app/VCastTones.apkrm /system/app/MyVerizon.apkrm /system/app/VzwSMS.apkpm uninstall com.Aetherpal.Devicepm uninstall com.amazon.kindlepm uninstall com.ea.nfshppm uninstall com.espn.score_centerpm uninstall com.fusionone.android.sync.servicepm uninstall com.gameloft.android.Verizon.GloftLG2Ppm uninstall com.infraware.polarisofficepm uninstall com.mobitv.client.nfl2010pm uninstall com.motricity.verizon.ssodownloadablepm uninstall com.muvee.studiopm uninstall com.netflix.mediaclientpm uninstall com.slacker.radiopm uninstall com.swype.android.inputmethodpm uninstall com.tunewiki.lyricplayer.androidpm uninstall com.vcast.mediamanagerpm uninstall com.verizon.android.videocallingportalpm uninstall com.verizon.permissions.appdirectedsmspm uninstall com.verizon.permissions.qospm uninstall com.verizon.permissions.securesettingspm uninstall com.verizon.permissions.ssopm uninstall com.verizon.permissions.vzwappapnpm uninstall com.verizon.settingsservicepm uninstall com.vznavigator.VS9204Gpm uninstall com.vzw.apnservicepm uninstall com.vzw.hs.android.modlitepm uninstall com.vzw.hss.myverizonpm uninstall com.vzw.sms</code></pre><p>不喜欢Google的应用,可以直接用Android的应用程序管理,把Google的应用都停用掉,本人亲测,都可以停用.<br>之所以选择停用,是网上说删除掉Google Play电影会开不了机.</p><h4 id="附录一"><a href="#附录一" class="headerlink" title=" 附录一"></a><a name="appendix1"></a> 附录一</h4><pre><code>package:/system/framework/framework-res.apk=androidpackage:/system/app/Aetherpal.apk=com.Aetherpal.Devicepackage:/system/app/oem_install_flash_player_ics.apk=com.adobe.flashplayerpackage:/system/app/Kindle.apk=com.amazon.kindlepackage:/system/app/LGVZWSetupWizard.apk=com.android.LGSetupWizardpackage:/system/app/BackupRestoreConfirmation.apk=com.android.backupconfirmpackage:/system/app/Bluetooth.apk=com.android.bluetoothpackage:/system/app/LGBrowser.apk=com.android.browserpackage:/system/app/LGCalculator.apk=com.android.calculator2package:/system/app/LGCalendar.apk=com.android.calendarpackage:/system/app/CertInstaller.apk=com.android.certinstallerpackage:/system/app/LGContacts3.apk=com.android.contactspackage:/system/app/DefaultContainerService.apk=com.android.defcontainerpackage:/system/app/FaceLock.apk=com.android.facelockpackage:/system/app/GalleryLG2.apk=com.android.gallery3dpackage:/system/app/HTMLViewer.apk=com.android.htmlviewerpackage:/system/app/KeyChain.apk=com.android.keychainpackage:/system/app/NativeMagicSmokeWallpapers.apk=com.android.magicsmokepackage:/system/app/LGUSMms.apk=com.android.mmspackage:/system/app/MusicFX.apk=com.android.musicfxpackage:/system/app/PackageInstaller.apk=com.android.packageinstallerpackage:/system/app/LGPhone.apk=com.android.phonepackage:/system/app/ApplicationsProvider.apk=com.android.providers.applicationspackage:/system/app/LGCalendarProvider.apk=com.android.providers.calendarpackage:/system/app/LGContactsProvider3.apk=com.android.providers.contactspackage:/system/app/LGDownloadProvider.apk=com.android.providers.downloadspackage:/system/app/LGDownloadProviderUi.apk=com.android.providers.downloads.uipackage:/system/app/DrmProvider.apk=com.android.providers.drmpackage:/system/app/MediaProvider.apk=com.android.providers.mediapackage:/system/app/LGSettingsProvider.apk=com.android.providers.settingspackage:/system/app/LGUSTelephonyProvider.apk=com.android.providers.telephonypackage:/system/app/UserDictionaryProvider.apk=com.android.providers.userdictionarypackage:/system/app/LGSettings.apk=com.android.settingspackage:/system/app/SharedStorageBackup.apk=com.android.sharedstoragebackuppackage:/system/app/WAPPushManager.apk=com.android.smspushpackage:/system/app/Stk.apk=com.android.stkpackage:/system/app/SystemUI.apk=com.android.systemuipackage:/system/app/Phonesky.apk=com.android.vendingpackage:/system/app/VoiceDialer_ics.apk=com.android.voicedialerpackage:/system/app/VpnDialogs.apk=com.android.vpndialogspackage:/vendor/app/NativeLiveWallpapers2.apk=com.android.wallpaperpackage:/system/app/NativeLiveWallpapersPicker.apk=com.android.wallpaper.livepickerpackage:/system/app/BluetoothServices.apk=com.broadcom.bt.app.systempackage:/system/app/NFSHP.apk=com.ea.nfshppackage:/system/app/ScoreCenter.apk=com.espn.score_centerpackage:/system/app/BaSyncService.apk=com.fusionone.android.sync.servicepackage:/system/app/LGolf2.apk=com.gameloft.android.Verizon.GloftLG2Ppackage:/system/app/Books.apk=com.google.android.apps.bookspackage:/system/app/Magazines.apk=com.google.android.apps.magazinespackage:/system/app/GMS_Maps.apk=com.google.android.apps.mapspackage:/system/app/PlusOne.apk=com.google.android.apps.pluspackage:/system/app/MediaUploader.apk=com.google.android.apps.uploaderpackage:/system/app/GoogleBackupTransport.apk=com.google.android.backuppackage:/system/app/GoogleFeedback.apk=com.google.android.feedbackpackage:/system/app/Gmail2.apk=com.google.android.gmpackage:/system/app/GmsCore.apk=com.google.android.gmspackage:/system/app/GoogleQuickSearchBox.apk=com.google.android.googlequicksearchboxpackage:/system/app/GoogleServicesFramework.apk=com.google.android.gsfpackage:/system/app/GoogleLoginService.apk=com.google.android.gsf.loginpackage:/system/app/NetworkLocation.apk=com.google.android.locationpackage:/system/app/talkback.apk=com.google.android.marvin.talkbackpackage:/system/app/Music2.apk=com.google.android.musicpackage:/system/app/GooglePartnerSetup.apk=com.google.android.partnersetuppackage:/system/app/SetupWizard.apk=com.google.android.setupwizardpackage:/system/app/Street.apk=com.google.android.streetpackage:/system/app/GoogleCalendarSyncAdapter.apk=com.google.android.syncadapters.calendarpackage:/system/app/GoogleContactsSyncAdapter.apk=com.google.android.syncadapters.contactspackage:/system/app/Hangouts.apk=com.google.android.talkpackage:/system/app/GoogleTTS.apk=com.google.android.ttspackage:/system/app/Videos.apk=com.google.android.videospackage:/system/app/VoiceSearch.apk=com.google.android.voicesearchpackage:/system/app/YouTube.apk=com.google.android.youtubepackage:/vendor/app/PolarisOffice.apk=com.infraware.polarisofficepackage:/system/app/MobileUpdateClient.apk=com.innopath.activecarepackage:/system/app/SDMPlugin.apk=com.innopath.activecare.dev.oempackage:/system/app/VpnClient.apk=com.ipsec.vpnclientpackage:/system/app/LGVZWAppSetupWizard.apk=com.lge.AppSetupWizardpackage:/system/app/LgTestMenu.apk=com.lge.LgHiddenMenupackage:/system/app/VerizonHiddenMenu.apk=com.lge.VerizonHiddenMenupackage:/system/app/RichNote_VZW.apk=com.lge.app.richnotepackage:/system/app/BluetoothControllerWidget.apk=com.lge.appwidget.settings.bluetoothcontrollerpackage:/system/app/CameraApp.apk=com.lge.camerapackage:/system/app/CameraTestApp.apk=com.lge.cameratestpackage:/system/app/CarHome.apk=com.lge.carhomepackage:/system/app/NightScapeCW.apk=com.lge.chargingwallpaper.nightscapepackage:/system/app/ChargingWallpaperPicker.apk=com.lge.chargingwallpaper.pickerpackage:/system/app/RisingTideCW_RS.apk=com.lge.chargingwallpaper.risingtidecwpackage:/system/app/LGAlarmClock.apk=com.lge.clockpackage:/system/app/LGDefaultAccount.apk=com.lge.defaultaccountpackage:/system/app/DeskHome.apk=com.lge.deskhomepackage:/system/app/LGEmail_vzw.apk=com.lge.emailpackage:/system/app/HelpCenter.apk=com.lge.helpcenterpackage:/system/app/HotspotProvision.apk=com.lge.hotspotprovisionpackage:/system/app/LGEIME.apk=com.lge.imepackage:/system/app/ImsProvider.apk=com.lge.ims.providerpackage:/system/framework/lge-res.apk=com.lge.internalpackage:/system/app/InTouchWidget.apk=com.lge.intouchpackage:/system/app/IOTTestMenu.apk=com.lge.iot_hidden_menupackage:/system/app/LGHome3.apk=com.lge.launcher2package:/system/app/LGDrm.apk=com.lge.lgdrm.permissionpackage:/system/app/LockScreen.apk=com.lge.lockscreenpackage:/system/app/LockScreenSettings.apk=com.lge.lockscreensettingspackage:/system/app/MLT.apk=com.lge.mltpackage:/system/app/MobileHotspot.apk=com.lge.mobilehotspot.uipackage:/system/app/MusicPlayer.apk=com.lge.musicpackage:/system/app/PhoneTestMode.apk=com.lge.phonetestmodepackage:/system/app/PouchLockScreen.apk=com.lge.pouchlockscreenpackage:/system/app/CompatibilityMode.apk=com.lge.settings.compatmodepackage:/system/app/com.lge.shutdownmonitor.apk=com.lge.shutdownmonitorpackage:/system/app/CalendarWidget.apk=com.lge.sizechangable.calendarpackage:/system/app/EmailWidget.apk=com.lge.sizechangable.emailpackage:/system/app/FavoriteContactsWidget.apk=com.lge.sizechangable.favoritecontactspackage:/system/app/MemoWidget.apk=com.lge.sizechangable.memopackage:/system/app/MessageWidget.apk=com.lge.sizechangable.messagepackage:/system/app/MusicWidget.apk=com.lge.sizechangable.musicwidget.widgetpackage:/system/app/PhotoAlbumWidget.apk=com.lge.sizechangable.photoalbumpackage:/system/app/PhotoFrameWidget.apk=com.lge.sizechangable.photoframepackage:/system/app/WeatherMultiCPbin.apk=com.lge.sizechangable.weatherpackage:/system/app/WeatherThemebin.apk=com.lge.sizechangable.weather.theme.optimuspackage:/system/app/LGWorldClockWidget.apk=com.lge.sizechangable.worldclockpackage:/system/app/SmartShare.apk=com.lge.smartsharepackage:/system/app/SNC.apk=com.lge.sncpackage:/system/app/StreamingPlayer.apk=com.lge.streamingplayerpackage:/system/app/LGSystemServer.apk=com.lge.systemservicepackage:/system/app/androidUTSManager.apk=com.lge.utspackage:/system/app/AnalogClockWidget.apk=com.lge.variousstyle.anlaogclockpackage:/system/app/DigitalClockWidget.apk=com.lge.variousstyle.digitalclockpackage:/system/app/VideoPlayer.apk=com.lge.videoplayerpackage:/system/app/VoiceRecorder.apk=com.lge.voicerecorderpackage:/system/app/VVM.apk=com.lge.vvmpackage:/system/app/BaSyncClient.apk=com.lge.vzw.buapackage:/system/app/NFL.apk=com.mobitv.client.nfl2010package:/system/app/SSO_P3.apk=com.motricity.verizon.ssodownloadablepackage:/system/app/SmartMovieHD.apk=com.muvee.studiopackage:/system/app/Netflix.apk=com.netflix.mediaclientpackage:/system/app/PrivInit.apk=com.qualcomm.privinitpackage:/system/app/Wiper.apk=com.qualcomm.wiperpackage:/system/app/Slacker.apk=com.slacker.radiopackage:/system/app/Swype.apk=com.swype.android.inputmethodpackage:/system/app/TuneWiki.apk=com.tunewiki.lyricplayer.androidpackage:/system/app/BUAPLUS.apk=com.vcast.mediamanagerpackage:/system/app/VideoCallingPortal.apk=com.verizon.android.videocallingportalpackage:/system/app/appdirectedsmspermission.apk=com.verizon.permissions.appdirectedsmspackage:/system/app/qospermission.apk=com.verizon.permissions.qospackage:/system/app/securesettingspermission.apk=com.verizon.permissions.securesettingspackage:/system/app/ssopermission.apk=com.verizon.permissions.ssopackage:/system/app/vzwapnpermission.apk=com.verizon.permissions.vzwappapnpackage:/system/app/SecureSettingsService.apk=com.verizon.settingsservicepackage:/system/app/VZNavigator.apk=com.vznavigator.VS9204Gpackage:/system/app/VZWAPNService.apk=com.vzw.apnservicepackage:/system/app/VCastTones.apk=com.vzw.hs.android.modlitepackage:/system/app/MyVerizon.apk=com.vzw.hss.myverizonpackage:/system/app/VzwSMS.apk=com.vzw.sms</code></pre><h4 id="附录二"><a href="#附录二" class="headerlink" title=" 附录二"></a><a name="appendix2"></a> 附录二</h4><pre><code>package:/system/app/Aetherpal.apk=com.Aetherpal.Devicepackage:/system/app/Kindle.apk=com.amazon.kindlepackage:/system/app/NFSHP.apk=com.ea.nfshppackage:/system/app/ScoreCenter.apk=com.espn.score_centerpackage:/system/app/BaSyncService.apk=com.fusionone.android.sync.servicepackage:/system/app/LGolf2.apk=com.gameloft.android.Verizon.GloftLG2Ppackage:/vendor/app/PolarisOffice.apk=com.infraware.polarisofficepackage:/system/app/NFL.apk=com.mobitv.client.nfl2010package:/system/app/SSO_P3.apk=com.motricity.verizon.ssodownloadablepackage:/system/app/SmartMovieHD.apk=com.muvee.studiopackage:/system/app/Netflix.apk=com.netflix.mediaclientpackage:/system/app/Slacker.apk=com.slacker.radiopackage:/system/app/Swype.apk=com.swype.android.inputmethodpackage:/system/app/TuneWiki.apk=com.tunewiki.lyricplayer.androidpackage:/system/app/BUAPLUS.apk=com.vcast.mediamanagerpackage:/system/app/VideoCallingPortal.apk=com.verizon.android.videocallingportalpackage:/system/app/appdirectedsmspermission.apk=com.verizon.permissions.appdirectedsmspackage:/system/app/qospermission.apk=com.verizon.permissions.qospackage:/system/app/securesettingspermission.apk=com.verizon.permissions.securesettingspackage:/system/app/ssopermission.apk=com.verizon.permissions.ssopackage:/system/app/vzwapnpermission.apk=com.verizon.permissions.vzwappapnpackage:/system/app/SecureSettingsService.apk=com.verizon.settingsservicepackage:/system/app/VZNavigator.apk=com.vznavigator.VS9204Gpackage:/system/app/VZWAPNService.apk=com.vzw.apnservicepackage:/system/app/VCastTones.apk=com.vzw.hs.android.modlitepackage:/system/app/MyVerizon.apk=com.vzw.hss.myverizonpackage:/system/app/VzwSMS.apk=com.vzw.sms</code></pre>]]>
    </content>
    <id>https://blog.mangege.com/tech/2015-02-05-1/</id>
    <link href="https://blog.mangege.com/tech/2015-02-05-1/"/>
    <published>2015-02-05T00:10:16.000Z</published>
    <summary>
      <![CDATA[<p>本文是基于vs920 v9系统写的,理论上vs840 v9也可以用.</p>
<p>首先你得安装adb,通过adb shell连接上手机后,运行下面命令</p>
<pre><code>pm list packages -f]]>
    </summary>
    <title>LG VS840 VS920可精简应用列表</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
  <entry>
    <author>
      <name>小华</name>
    </author>
    <category term="tech" scheme="https://blog.mangege.com/categories/tech/"/>
    <category term="tech" scheme="https://blog.mangege.com/tags/tech/"/>
    <category term="programming" scheme="https://blog.mangege.com/tags/programming/"/>
    <category term="hardware" scheme="https://blog.mangege.com/tags/hardware/"/>
    <content>
      <![CDATA[<h2 id="废话"><a href="#废话" class="headerlink" title="废话"></a>废话</h2><p>买两百从淘宝买了个LG VS840电信手机来玩,这手机性价比不错,双核,1G内存,8G存储,IPS硬屏.<br>有点郁闷的是原生系统默认是连接不上国内的电信网络打电话,需要修改设置里的选项才行.而此选项却隐藏起来,需要修改LGSettings.apk文件才能使用.</p><p>网上有修改好的LGSettings.apk文件，不过都是rom v8以前的版本,v9无法使用.<br>个人不喜欢替换原生系统的文件,用过一个不错的权限管理软件<em>xprivacy</em>,从而了解到xposed这个强大的rom修改框架,此框架修改rom无需要修改apk文件,大爱.</p><h2 id="XposedLucidMod功能列表"><a href="#XposedLucidMod功能列表" class="headerlink" title="XposedLucidMod功能列表"></a>XposedLucidMod功能列表</h2><ul><li>添加系统选择到设置</li><li>从设置里删除Backup Assistant Plus</li><li>从帐号与同步里删除VERIZON帐户选项</li><li>删除状态栏的三角形漫游图标</li><li>删除定位的十字架图标</li></ul><p>效果图请看 <a href="http://pan.baidu.com/s/1sjzyceL">http://pan.baidu.com/s/1sjzyceL</a></p><h2 id="安装教程"><a href="#安装教程" class="headerlink" title="安装教程"></a>安装教程</h2><ul><li>升级手机为原生最新的v9系统<ol><li>rom 下载地址 <a href="https://app.box.com/s/hpapn930pad7afw85bth">https://app.box.com/s/hpapn930pad7afw85bth</a></li><li>教程链接 <a href="http://androidforums.com/lucid-all-things-root/654009-guide-unbrick-lg-lucid.html">http://androidforums.com/lucid-all-things-root/654009-guide-unbrick-lg-lucid.html</a></li></ol></li><li>使用 LG_Spectrum_ICS.zip root你的手机<ol><li>工具下载地址 <a href="https://app.box.com/s/hpapn930pad7afw85bth">https://app.box.com/s/hpapn930pad7afw85bth</a></li><li>解压后运行window-install.bat</li></ol></li><li>安装Xposed<ol><li>官方教程地址 <a href="http://forum.xda-developers.com/showthread.php?t=1574401">http://forum.xda-developers.com/showthread.php?t=1574401</a></li><li>下载apk并安装 <a href="http://dl.xposed.info/latest.apk">http://dl.xposed.info/latest.apk</a></li><li>运行Xposed安装器,点击框架,安装,重启手机</li></ol></li><li>安装XposedLucidMod模块<ol><li>下载XposedLucidMod.apk并安装 <a href="http://pan.baidu.com/s/1sjzyceL">http://pan.baidu.com/s/1sjzyceL</a></li><li>打开Xposed安装器,点击模块,启用XposedLucidMod即可</li></ol></li></ul><h2 id="XposedLucidMod源码"><a href="#XposedLucidMod源码" class="headerlink" title="XposedLucidMod源码"></a>XposedLucidMod源码</h2><p><a href="https://github.com/mangege/XposedLucidMod">https://github.com/mangege/XposedLucidMod</a></p>]]>
    </content>
    <id>https://blog.mangege.com/tech/2014-02-23-1/</id>
    <link href="https://blog.mangege.com/tech/2014-02-23-1/"/>
    <published>2014-02-22T23:02:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="废话"><a href="#废话" class="headerlink" title="废话"></a>废话</h2><p>买两百从淘宝买了个LG]]>
    </summary>
    <title>LG VS840系统修改小工具 XposedLucidMod</title>
    <updated>2026-04-14T15:05:49.625Z</updated>
  </entry>
</feed>
