<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="https://bleatingsheep.org/atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN">
  <id>https://bleatingsheep.org/</id>
  <title>bleatingsheep 的博客</title>
  <author>
    <name>bleatingsheep</name>
  </author>
  <rights>Copyright by bleatingsheep</rights>
  <updated>2026-05-17T12:31:43.555Z</updated>
  <generator>@vuepress/plugin-feed</generator>
  <link rel="self" href="https://bleatingsheep.org/atom.xml"/>
  <link rel="alternate" href="https://bleatingsheep.org/"/>
  <contributor>
    <name>bleatingsheep</name>
  </contributor>
  <entry>
    <title type="text">关于我</title>
    <id>https://bleatingsheep.org/about/</id>
    <link href="https://bleatingsheep.org/about/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>这里是 bleatingsheep 的~~ 无 ~~技术博客。</p>
<p>主要内容是，当自己折腾什么东西，遇到了困难或出了问题，而中文网络没有相关资料时，我就把它写下来，方便后人搜索。</p>
<p>不求高深，不求先进，但求准确。</p>
]]></summary>
    <content type="html"><![CDATA[<p>这里是 bleatingsheep 的~~ 无 ~~技术博客。</p>
<p>主要内容是，当自己折腾什么东西，遇到了困难或出了问题，而中文网络没有相关资料时，我就把它写下来，方便后人搜索。</p>
<p>不求高深，不求先进，但求准确。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2023-04-23T12:53:10.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">Wiki</title>
    <id>https://bleatingsheep.org/wiki/</id>
    <link href="https://bleatingsheep.org/wiki/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>由于某些知识若要专门写一篇博客来分享略显啰嗦（见{% post_link 我想建立个人-wiki %}），我决定建立个人 wiki 来分享这些零碎的内容。</p>
<h2>编程相关</h2>
<h3>作为云服务（IaaS）/VPS 用户</h3>
<p><a href="https://notes.bleatingsheep.org/s/iaas-user" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/iaas-user</a></p>
<h2>Homelab</h2>
<p><a href="https://notes.bleatingsheep.org/s/homelab" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/homelab</a></p>]]></summary>
    <content type="html"><![CDATA[<p>由于某些知识若要专门写一篇博客来分享略显啰嗦（见{% post_link 我想建立个人-wiki %}），我决定建立个人 wiki 来分享这些零碎的内容。</p>
<h2>编程相关</h2>
<h3>作为云服务（IaaS）/VPS 用户</h3>
<p><a href="https://notes.bleatingsheep.org/s/iaas-user" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/iaas-user</a></p>
<h2>Homelab</h2>
<p><a href="https://notes.bleatingsheep.org/s/homelab" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/homelab</a></p>
<h3>Proxmox VE (PVE)</h3>
<p><a href="https://notes.bleatingsheep.org/s/proxmox-ve" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/proxmox-ve</a></p>
<h2>各类服务</h2>
<h3>直播</h3>
<p><a href="https://notes.bleatingsheep.org/s/livestreaming" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/livestreaming</a></p>
<h2>计算机日常使用</h2>
<h3>Windows 相关</h3>
<p><a href="https://notes.bleatingsheep.org/s/daily" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/daily</a></p>
<h3>铭凡迷你主机 UM773 SE 相关</h3>
<p>包括通过 BIOS 设置把 TDP 提升至 54W 并加强散热、BIOS 更新等内容。</p>
<p><a href="https://notes.bleatingsheep.org/s/um773" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/um773</a></p>
<h2>生活及其他</h2>
<h3>基金交易</h3>
<p><a href="https://notes.bleatingsheep.org/s/trading" target="_blank" rel="noopener noreferrer">https://notes.bleatingsheep.org/s/trading</a></p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2024-03-19T15:52:13.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">C++ 的神奇运算</title>
    <id>https://bleatingsheep.org/2024/02/19/CPP%E7%9A%84%E7%A5%9E%E5%A5%87%E8%BF%90%E7%AE%97/</id>
    <link href="https://bleatingsheep.org/2024/02/19/CPP%E7%9A%84%E7%A5%9E%E5%A5%87%E8%BF%90%E7%AE%97/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>最近同学问我，为什么他的程序跑的结果不符合预期，甚至跑出了非常离谱的结果，于是我们进行了一番调查。</p>
<p>简化后的程序如下所示：</p>
<div class="language-cpp line-numbers-mode" data-highlighter="shiki" data-ext="cpp" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-cpp"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a </span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 2</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> func</span><span style="--shiki-light:#999999;--shiki-dark:#666666">()</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    for</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 0</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">&#x3C;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">++</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">        printf</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">i = </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">, a = </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">, i&#x3C;a: </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d\n</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">&#x3C;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>当我们调用这个函数，并使用 <code>g++ -O3</code> 编译运行时发现，这个函数无限循环了下去，并且输出了 <code>i = 231904, a = 2, i&lt;a: 1</code> 这样的内容，宣称 231904 &lt; 2。</p>
]]></summary>
    <content type="html"><![CDATA[<p>最近同学问我，为什么他的程序跑的结果不符合预期，甚至跑出了非常离谱的结果，于是我们进行了一番调查。</p>
<p>简化后的程序如下所示：</p>
<div class="language-cpp line-numbers-mode" data-highlighter="shiki" data-ext="cpp" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-cpp"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a </span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 2</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> func</span><span style="--shiki-light:#999999;--shiki-dark:#666666">()</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    for</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 0</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">&#x3C;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">++</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">        printf</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">i = </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">, a = </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">, i&#x3C;a: </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d\n</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">&#x3C;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>当我们调用这个函数，并使用 <code>g++ -O3</code> 编译运行时发现，这个函数无限循环了下去，并且输出了 <code>i = 231904, a = 2, i&lt;a: 1</code> 这样的内容，宣称 231904 &lt; 2。</p>
<!-- more -->
<h2>问题描述</h2>
<p>为了描述问题，我们先放上完整的复现代码，保存为“test.cpp”文件：</p>
<div class="language-cpp line-numbers-mode" data-highlighter="shiki" data-ext="cpp" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-cpp"><span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">#</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">include</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> &#x3C;</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">iostream</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a </span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 2</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> func</span><span style="--shiki-light:#999999;--shiki-dark:#666666">()</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    for</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 0</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">&#x3C;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">++</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">        printf</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">i = </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">, a = </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">, i&#x3C;a: </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d\n</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">&#x3C;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD">        // 其他程序逻辑</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> main</span><span style="--shiki-light:#999999;--shiki-dark:#666666">()</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    func</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    return</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 0</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>可以看到，本程序的意图应该是运行两次后，就结束循环。</p>
<p>然后使用 g++ 编译并运行：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">g++</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -O3</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -o</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> test.o</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> test.cpp</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">./test.o</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>看到如下输出</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>i = 0, a = 2, i&#x3C;a: 1</span></span>
<span class="line"><span>i = 1, a = 2, i&#x3C;a: 1</span></span>
<span class="line"><span>i = 2, a = 2, i&#x3C;a: 1</span></span>
<span class="line"><span>i = 3, a = 2, i&#x3C;a: 1</span></span>
<span class="line"><span>i = 4, a = 2, i&#x3C;a: 1</span></span>
<span class="line"><span>...</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>循环没有结束，而是无限进行了下去，甚至在 <code>printf</code> 中，明明 <code>i</code> 的值一直在增长并超过了 <code>a</code>，程序却算出了 <code>i &lt; a</code>，非常奇怪。</p>
<p>然而，当我在循环后面再加上一些语句时，却又不会无限循环了</p>
<div class="language-cpp line-numbers-mode" data-highlighter="shiki" data-ext="cpp" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-cpp"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> func</span><span style="--shiki-light:#999999;--shiki-dark:#666666">()</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    for</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">int</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 0</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">&#x3C;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">++</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">        printf</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">i = </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">, a = </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">, i&#x3C;a: </span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">%d\n</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> i </span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">&#x3C;</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    printf</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">End of func!</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">\n</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>输出</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>i = 0, a = 2, i&#x3C;a: 1</span></span>
<span class="line"><span>i = 1, a = 2, i&#x3C;a: 1</span></span>
<span class="line"><span>End of func!</span></span>
<span class="line"><span>Segmentation fault</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这时程序不会再死循环，但是也报了段错误。因此我开始判断它和带返回值的函数没有 <code>return</code> 一个值有关。</p>
<h2>汇编代码</h2>
<p>从个人经验判断，出现这种奇怪的问题，往往是因为编译器在优化时做了某些奇怪的假设，当我关闭 <code>-O3</code> 优化后，问题也确实消失了。于是我尝试查看编译出来的汇编代码来确定编译器究竟做了什么样的优化。</p>
<p>从 https://godbolt.org/ 上选择最新的“x86-64 gcc 13.2”，设置 <code>-O3</code> 选项，生成的汇编代码如下：</p>
<p></p>
<p>左右两边相同颜色的行为 C++ 代码所生成的对应汇编代码。从汇编的第 7 - 15 行可以看到，此循环被编译成了无条件跳转（第 15 行跳转到 L2），也就是死循环。</p>
<h2>原因分析</h2>
<p>究其原因，我认为是，在一个返回值类型不是 <code>void</code> 的函数中不 <code>return</code> 是未定义的行为（UB），GCC 在优化时，是在假定你的程序不会出现 UB 的基础上进行的。如此一来，如果一个函数的最后一个语句是循环，那么编译器就可以推定你一定会在这个循环中 <code>return</code>（实际上，他原来的代码有比较复杂的判断逻辑，在有些分支是会返回的），也就是循环条件应该是一直满足的，于是就可以大胆地把循环优化成死循环。而 <code>printf</code> 比较大小的结果错误，我想，是因为输出内容的计算公式和循环的判断条件相同（<code>i &lt; a</code>），而既然循环条件推定满足了，输出内容就不需要再浪费 CPU 周期进行计算了。实际上，在汇编第 9 行可以看到，编译出的代码直接把 <code>1</code> 传给了 <code>printf</code>。</p>
<h2>C++ 规范</h2>
<p>为了确认在有返回值的函数中不 <code>return</code> 是 UB，我查阅了相关的规范，但遗憾的是，并没有找到完全解释现象的描述。C++ 规范 <em>ISO/IEC 9899:201x</em> 中是这样描述的（6.9.1 节，12）</p>
<blockquote>
<p>If the <code>}</code> that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.</p>
</blockquote>
<p>根据 C++ 规范，只有在该返回值被使用时，才是 UB。这样的话，由于测试代码中并没有使用 <code>func</code> 的返回值，死循环似乎不符合规范？</p>
<h2>结论</h2>
<p>在编写 C++ 程序时，应该格外注意避免出现 UB。本身编译器把 UB 编译成任何东西都是合法的，经过编译器的优化，就更加可能出现预料之外的行为。而当以 DEBUG 模式编译，或者增加调试用的输出时，有时也会破坏优化前提，导致无法再复现。如果编译本例的代码，编译器会给出警告，告诉你缺失 <code>return</code> 语句。所以编写 C++ 代码时应该更加重视编译器的警告。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>test.cpp: In function ‘int func()’:</span></span>
<span class="line"><span>test.cpp:12:1: warning: no return statement in function returning non-void [-Wreturn-type]</span></span>
<span class="line"><span>   12 | }</span></span>
<span class="line"><span>      | ^</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>当然，原本的程序是更复杂的，而且涉及多线程，在排查时也都考虑过多种可能的情况，比如尝试在某些地方添加 <code>mfence</code> 等。最终才确认是 UB 与编译器优化组合出了意料之外的结果，与多线程无关。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">C#中编译期可以检测到的类型转换失败</title>
    <id>https://bleatingsheep.org/2021/05/21/CSharp%E4%B8%AD%E7%BC%96%E8%AF%91%E6%9C%9F%E5%8F%AF%E4%BB%A5%E6%A3%80%E6%B5%8B%E5%88%B0%E7%9A%84%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2%E5%A4%B1%E8%B4%A5/</id>
    <link href="https://bleatingsheep.org/2021/05/21/CSharp%E4%B8%AD%E7%BC%96%E8%AF%91%E6%9C%9F%E5%8F%AF%E4%BB%A5%E6%A3%80%E6%B5%8B%E5%88%B0%E7%9A%84%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2%E5%A4%B1%E8%B4%A5/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>严格来说，这算是 C# 基础知识的一部分。不知是太久远我记不清了，还是这部分本来就没注意，最近发现了，不算坑，在这里写一下。</p>
]]></summary>
    <content type="html"><![CDATA[<p>严格来说，这算是 C# 基础知识的一部分。不知是太久远我记不清了，还是这部分本来就没注意，最近发现了，不算坑，在这里写一下。</p>
<!-- more -->
<h2>描述</h2>
<p>请看下面这段代码：</p>
<div class="language-cs line-numbers-mode" data-highlighter="shiki" data-ext="cs" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-cs"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> s</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">s</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> d</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> new</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> Dictionary</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">string</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#999999;--shiki-dark:#666666">>();</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> disposable1</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">IDisposable</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">s</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> disposable2</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">IDisposable</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">d</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>我们知道，不管是 <code>String</code>，还是 <code>Dictionary&lt;TKey, TValue&gt;</code>，都没实现 <code>IDisposable</code>，所以这两个地方都是没法转换的。可是奇怪的是，编译时，<code>disposable1</code> 那一行会报 Error，而第二个转换则是运行时才会抛出异常。</p>
<p>我一开始以为是编译器特殊处理了一些内置类型，准备过会儿查查文档。</p>
<h2>原因</h2>
<p>然后我又去翻了 EF Core 和 System.Linq.Async 一起使用出现冲突的问题，找到了在 EF Core 6.0 中修复这个问题的 <a href="https://github.com/dotnet/efcore/pull/24145/files#diff-88b4b541856db15aa272d41dfa05e1226323f89154cdc4089bb357e4b3e6c037R57" target="_blank" rel="noopener noreferrer">Pull Request</a>。其中的讨论提醒了我。</p>
<p>上面说，虽然 <code>DbSet&lt;TEntity&gt;</code> 不再实现 <code>IAsyncEnumerable&lt;TEntity&gt;</code>，但是它的具体类一定会实现，这就是可以转换的原因。</p>
<p>我猛地想起刚才那个现象，像 <code>String</code>、<code>Int32</code> 这样的类型，要么是 <code>sealed class</code>，要么是 <code>struct</code>，都不可能有派生类了。这样，编译器完全可以在编译期就发现，这个转换没法进行，因为没有派生类可以再去实现接口了。</p>
<p>要验证也非常简单，手写下面代码。</p>
<div class="language-cs line-numbers-mode" data-highlighter="shiki" data-ext="cs" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-cs"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> class</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> MyClass</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> {</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> myClass</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> new</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> MyClass</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> disposable</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">IDisposable</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">myClass</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这段代码可以通过编译。然后把类加上 <code>sealed</code>。</p>
<div class="language-cs line-numbers-mode" data-highlighter="shiki" data-ext="cs" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-cs"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> sealed</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> class</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> MyClass</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> {</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> }</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>此时就无法通过编译了。</p>
<h2>文档</h2>
<p>在我查找的范围内，<a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/type-testing-and-cast#cast-expression" target="_blank" rel="noopener noreferrer">文档</a>中对此现象的描述比较模糊，仅说显示转换可能失败，并在运行时抛出异常。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">Cloudflare Tunnel 的使用方法</title>
    <id>https://bleatingsheep.org/2023/07/01/Cloudflare-Tunnel-%E7%9A%84%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/</id>
    <link href="https://bleatingsheep.org/2023/07/01/Cloudflare-Tunnel-%E7%9A%84%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p><a href="https://github.com/he0119" target="_blank" rel="noopener noreferrer">日日</a>说不会用 CF Tunnel，就写一篇简单说明一下用法。</p>
]]></summary>
    <content type="html"><![CDATA[<p><a href="https://github.com/he0119" target="_blank" rel="noopener noreferrer">日日</a>说不会用 CF Tunnel，就写一篇简单说明一下用法。</p>
<!-- more -->
<h2>Cloudflare Tunnel 是什么、可以用来干什么</h2>
<p>当用户访问 CDN 需要回源时，CDN 节点会去访问服务器。尽管可以通过配置让服务器只接受 CDN IP 访问，但配置可能较为繁琐且容易出错，导致服务器仍然受到各种威胁。</p>
<p><a href="https://www.cloudflare.com/products/tunnel/" target="_blank" rel="noopener noreferrer">Cloudflare Tunnel</a> 可以让服务器主动访问 Cloudflare (CF) 的 CDN 节点。这样服务器完全不需要接受任何外来连接，增强安全性。此外，Cloudflare Tunnel 也可以让服务器处于 NAT 后面，无需公网 IP 也可以提供 Web 等服务。</p>
<h2>如何使用 Cloudflare Tunnel</h2>
<h3>前提条件</h3>
<p>要使用 Cloudflare Tunnel，相应域名必须在 CF 解析，且服务器必须能够访问最近的 CF 节点。</p>
<p>本文以 Docker 为例讲解 CF Tunnel 的使用方法，但 Docker 不是必须的。</p>
<h3>启动服务</h3>
<p>本文以在 <code>backend</code> 网络中启动 <a href="https://docs.rsshub.app/" target="_blank" rel="noopener noreferrer">RSSHub</a> 容器为例，使用的命令如下：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">docker</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> pull</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> diygod/rsshub</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">docker</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> run</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -d</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --name</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> rsshub</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --restart=always</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> \</span></span>
<span class="line"><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE">--net </span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">backend</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --net-alias</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> rsshub</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> \</span></span>
<span class="line"><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE">diygod/rsshub</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这里设置 RSSHub 连接到 Docker 的 <code>backend</code> 网络（需要自行创建），设置容器的网络别名为 <code>rsshub</code>，以便访问。注意 RSSHub 默认监听 1200 端口，这里无需对外开放此端口。</p>
<h3>创建并运行 Tunnel</h3>
<p></p>
<p>登录 CF，在 dash 左侧找到“Zero Trust”并进入。</p>
<p></p>
<p>进入后在左侧点击“Access、Tunnels”，然后再点“Create a tunnel”。</p>
<p></p>
<p>给 Tunnel 设置一个名字，然后继续，进入到上图的界面，选择相应的运行环境（这里选择 Docker），然后执行下面的代码。</p>
<p>注意，可以根据自己的需要，在命令中加入 <code>-d --net backend --restart always</code> 等参数。</p>
<p></p>
<p>运行后，如果一切正常，可以在网页上看到已经连接的 Connector。继续前往下一个页面。</p>
<p></p>
<p>在这个页面中，选择要使用的域名和子域，“Service”设置为 http://rsshub:1200。</p>
<p>之后点击保存，访问相应域名即可。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">Fedora 系统部署 LXD</title>
    <id>https://bleatingsheep.org/2023/04/23/Fedora-%E7%B3%BB%E7%BB%9F%E9%83%A8%E7%BD%B2-LXD/</id>
    <link href="https://bleatingsheep.org/2023/04/23/Fedora-%E7%B3%BB%E7%BB%9F%E9%83%A8%E7%BD%B2-LXD/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>近来一直想着买台迷你主机，把家里的树莓派换掉，毕竟树莓派性能太低了，连测速都跑不满。要换的话，我可能会选准系统的迷你主机，然后开启一些虚拟机，这样能满足我折腾的需求。我看到比较合适的是 <a href="https://www.proxmox.com/en/proxmox-ve" target="_blank" rel="noopener noreferrer">PVE</a>，但我暂时没有可以安装的机器。另一个看起来比较有趣的技术是 <a href="https://linuxcontainers.org/lxd/introduction/" target="_blank" rel="noopener noreferrer">LXD</a>，看起来可以更充分地利用资源。虽然不知道什么时候能回国（毕竟装系统什么的都很麻烦，恐怕没法远程进行），但是就是想现在看看怎么折腾，就开了一台 Fedora 37 的云服务器，安装了一下试试。</p>
]]></summary>
    <content type="html"><![CDATA[<p>近来一直想着买台迷你主机，把家里的树莓派换掉，毕竟树莓派性能太低了，连测速都跑不满。要换的话，我可能会选准系统的迷你主机，然后开启一些虚拟机，这样能满足我折腾的需求。我看到比较合适的是 <a href="https://www.proxmox.com/en/proxmox-ve" target="_blank" rel="noopener noreferrer">PVE</a>，但我暂时没有可以安装的机器。另一个看起来比较有趣的技术是 <a href="https://linuxcontainers.org/lxd/introduction/" target="_blank" rel="noopener noreferrer">LXD</a>，看起来可以更充分地利用资源。虽然不知道什么时候能回国（毕竟装系统什么的都很麻烦，恐怕没法远程进行），但是就是想现在看看怎么折腾，就开了一台 Fedora 37 的云服务器，安装了一下试试。</p>
<!-- more -->
<p>除此之外，最近由于要在实验室的服务器上跑一些东西，就让人给我开了个用户。我发现，原来这个服务器是所有人一起跑在一台机器上，各自都没有 root 权限。这对我使用造成了不便，因为我是用 Tailscale 把我各台机器连接起来的，没有 root 权限就没法安装 Tailscale，把运行结果储存到另一台服务器上就很困难。如果是用的虚拟机，那我就可以安装 Tailscale。但是，虚拟机比较耗资源，那台机器内存不是很大，同时每台虚拟机需要一直占用它峰值所需的内存，利用率低。我想到了 LXD 这样的系统容器技术。不过，我并不是那台服务器的管理员，所以只能所以玩儿玩儿。</p>
<p>我也是头一次实践这个 LXD，如果有什么写得不对的地方，恳请各位斧正。</p>
<h2>什么是 LXD</h2>
<p>LXD 是一个系统容器管理器，关于它的详细介绍可以在 <a href="https://linuxcontainers.org/lxd/introduction/" target="_blank" rel="noopener noreferrer">What is LXD?</a> 中查看。</p>
<h2>安装</h2>
<p>如果你用的是 Ubuntu，安装过程可以参考 <a href="https://linuxcontainers.org/lxd/getting-started-cli/" target="_blank" rel="noopener noreferrer">https://linuxcontainers.org/lxd/getting-started-cli/</a>。尽管该页面也有 Fedora 的安装步骤，但是并不完整。关于 Fedora 完整的安装步骤请前往 <a href="https://github.com/ganto/copr-lxc4/wiki" target="_blank" rel="noopener noreferrer">Installation Guide</a> 查看。</p>
<p>本文就以 Fedora 37 为例。要安装 LXD，请执行下面的命令：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> dnf</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> copr</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> enable</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ganto/lxc4</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> dnf</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> install</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> lxd</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>当询问是否继续时，输入 <code>y</code>，回车继续。</p>
<p>安装完成后，需要添加必要的信息并启动服务，执行以下命令：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> usermod</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -a</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -G</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> lxd</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ${</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">USER</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">echo</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">root:1000000:65536</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> |</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> tee</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -a</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /etc/subuid</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ></span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/null</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">echo</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">root:1000000:65536</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> |</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> tee</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -a</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /etc/subgid</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ></span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/null</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> systemctl</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> enable</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> lxd</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> &#x26;&#x26;</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> systemctl</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> start</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> lxd</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">source</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /etc/profile</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>执行完毕后，可能需要退出登陆并重新登录。</p>
<h3>初始化</h3>
<p>接下来我们初始化配置。执行以下命令：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">lxd</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> init</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>接下来会交互式地询问你一些问题，大部分问题回车使用默认选项即可，少部分可根据需要自行配置。在此过程中，我修改了存储池的大小、开启 API 端口，并要求在初始化完成后打印配置。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>[fedora@vultr ~]$ lxd init</span></span>
<span class="line"><span>Would you like to use LXD clustering? (yes/no) [default=no]: </span></span>
<span class="line"><span>Do you want to configure a new storage pool? (yes/no) [default=yes]: </span></span>
<span class="line"><span>Name of the new storage pool [default=default]: </span></span>
<span class="line"><span>Name of the storage backend to use (btrfs, dir, lvm) [default=btrfs]:    </span></span>
<span class="line"><span>Create a new BTRFS pool? (yes/no) [default=yes]: </span></span>
<span class="line"><span>Would you like to use an existing empty block device (e.g. a disk or partition)? (yes/no) [default=no]: </span></span>
<span class="line"><span>Size in GiB of the new loop device (1GiB minimum) [default=11GiB]: 25GiB</span></span>
<span class="line"><span>Would you like to connect to a MAAS server? (yes/no) [default=no]: </span></span>
<span class="line"><span>Would you like to create a new local network bridge? (yes/no) [default=yes]: </span></span>
<span class="line"><span>What should the new bridge be called? [default=lxdbr0]: </span></span>
<span class="line"><span>What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]: </span></span>
<span class="line"><span>What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]: </span></span>
<span class="line"><span>Would you like the LXD server to be available over the network? (yes/no) [default=no]: yes</span></span>
<span class="line"><span>Address to bind LXD to (not including port) [default=all]: </span></span>
<span class="line"><span>Port to bind LXD to [default=8443]: </span></span>
<span class="line"><span>Would you like stale cached images to be updated automatically? (yes/no) [default=yes]: </span></span>
<span class="line"><span>Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: yes</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>初始化只是自动帮你创建一些配置、存储、网络等，这个过程完全可以手动完成。熟悉本文提到的内容后，你可以自行查阅文档，在需要的时候手动更改相关配置。</p>
<!-- **注意：**`lxc` 命令中每个用户都有独立的配置、存储、实例等，只要该用户在 `lxd` 用户组中，即可使用，无需 root 权限。

x: 不确定，似乎只能单用户？
 -->
<h2>创建容器</h2>
<p>可以使用以下命令创建一个容器实例：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">lxc</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> launch</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ubuntu:22.04</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ubuntu-2204</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>然后使用以下命令查看所有实例：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">lxc</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> list</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>你会看到如下输出：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>+</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div>]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">.NET一处奇怪的测试未运行</title>
    <id>https://bleatingsheep.org/2021/03/22/NET%E4%B8%80%E5%A4%84%E5%A5%87%E6%80%AA%E7%9A%84%E6%B5%8B%E8%AF%95%E6%9C%AA%E8%BF%90%E8%A1%8C/</id>
    <link href="https://bleatingsheep.org/2021/03/22/NET%E4%B8%80%E5%A4%84%E5%A5%87%E6%80%AA%E7%9A%84%E6%B5%8B%E8%AF%95%E6%9C%AA%E8%BF%90%E8%A1%8C/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<h2>问题描述</h2>
<p>最近写 <a href="https://github.com/rcnbapp/RCNB.csharp" target="_blank" rel="noopener noreferrer">RCNB</a> 的时候遇到了奇怪的测试未运行的问题。Visual Studio 可以列出测试，但是所有测试都显示未运行。</p>
]]></summary>
    <content type="html"><![CDATA[<h2>问题描述</h2>
<p>最近写 <a href="https://github.com/rcnbapp/RCNB.csharp" target="_blank" rel="noopener noreferrer">RCNB</a> 的时候遇到了奇怪的测试未运行的问题。Visual Studio 可以列出测试，但是所有测试都显示未运行。</p>
<!-- more -->
<p></p>
<h2>解决过程</h2>
<p>我一开始以为是被测项目 TargetFrameworks 的问题，就把别的删了，只留 netcoreapp2.1，结果还是不行。</p>
<p>于是我想试试命令行能不能运行测试，执行 <code>dotnet test</code> 以后产生如下输出：</p>
<p></p>
<p>这里显示所有的测试都 pass 了，遗憾的是，一开始我眼花了，看成“skipped: 2”，走了很多弯路。</p>
<p>我又给 GitHub 仓库加上 CI，结果却运行失败了，原因是没有安装对应版本的框架及 SDK。</p>
<p></p>
<p>起初我想把 CI 的 dotnet 版本改为 2.1，但 .NET Core 2.1 无法编译 .NET Standard 2.1 的程序，于是就把测试项目和 CI 都设置到 3.1。通过 CI 以后，我发现 VS 也可以运行测试了。</p>
<p></p>
<h2>进一步调查</h2>
<h3>本机环境</h3>
<p>很明显，如果没有安装对应环境，那么测试是无法运行的。在我的机器上运行 <code>dotnet --list-sdks</code> 会输出以下内容。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>3.1.407 [C:\Program Files\dotnet\sdk]</span></span>
<span class="line"><span>5.0.104 [C:\Program Files\dotnet\sdk]</span></span>
<span class="line"><span>5.0.201 [C:\Program Files\dotnet\sdk]</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>没有安装 2.1 可能是 VS 里测试没有运行的原因。那么为什么使用命令行却可以测试呢？运行 <code>dotnet --list-runtimes</code> 看一下。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>... 省略 ASP .NET Core runtimes</span></span>
<span class="line"><span>Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]</span></span>
<span class="line"><span>Microsoft.NETCore.App 2.1.26 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]</span></span>
<span class="line"><span>Microsoft.NETCore.App 3.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]</span></span>
<span class="line"><span>Microsoft.NETCore.App 5.0.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]</span></span>
<span class="line"><span>... 省略 Windows Desktop runtimes</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>可以看到我的机器上装了 2.1 的 runtime，这可能是通过命令行可以顺利运行测试的原因。</p>
<h3>.NET Core 的向后兼容性</h3>
<p>3.1 的 SDK/Runtime 跑不了 2.1 的程序，这是由 .NET 的 roll forward 策略决定的。微软在 <a href="https://docs.microsoft.com/en-us/dotnet/core/versions/selection#framework-dependent-apps-roll-forward" target="_blank" rel="noopener noreferrer">Select the .NET version to use</a> 中介绍了这个策略。</p>
<p>实际上，我们也可以通过配置使用较高 Major 版本的 runtime 运行较低版本的程序，例如通过创建 <code>global.json</code> 文件。<a href="https://docs.microsoft.com/en-us/dotnet/core/tools/global-json?tabs=netcore3x#rollforward" target="_blank" rel="noopener noreferrer">global.json overview</a> 中介绍了所有的可选项。我们还可以通过 <a href="https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet#runtime-options" target="_blank" rel="noopener noreferrer">Runtime options</a> 或者 <a href="https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet#environment-variables" target="_blank" rel="noopener noreferrer">环境变量</a> 来修改此行为。</p>
<h2>结论</h2>
<p>我们可以得到这么几点结论：</p>
<ul>
<li>.NET SDK 可以为低版本编译</li>
<li>但主版本号不同的情况下，默认无法运行低版本测试</li>
<li>安装了正确版本的 .NET Runtime 就可以用命令行测试，但要在 Visual Studio 里测试，可能还是需要正确版本的 SDK 才行</li>
</ul>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">.NET程序生成OpenAPI客户端</title>
    <id>https://bleatingsheep.org/2021/04/26/NET%E7%A8%8B%E5%BA%8F%E7%94%9F%E6%88%90OpenAPI%E5%AE%A2%E6%88%B7%E7%AB%AF/</id>
    <link href="https://bleatingsheep.org/2021/04/26/NET%E7%A8%8B%E5%BA%8F%E7%94%9F%E6%88%90OpenAPI%E5%AE%A2%E6%88%B7%E7%AB%AF/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>当使用 ASP.NET Core 实现 Web API 服务时，可以用 Swagger 生成 <code>openapi.json</code> 文件及对应文档。</p>
]]></summary>
    <content type="html"><![CDATA[<p>当使用 ASP.NET Core 实现 Web API 服务时，可以用 Swagger 生成 <code>openapi.json</code> 文件及对应文档。</p>
<!-- more -->
<h2>在 Visual Studio 2019 中操作</h2>
<h3>添加服务</h3>
<p>首先新建一个 C# 项目，右键点击项目，点“添加”、“已连接的服务”，然后按步骤添加 OpenAPI 服务即可。</p>
<h3>使用</h3>
<p>传入新建的 <code>HttpClient</code> 及 <code>baseUrl</code> 即可使用。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>var httpClient = new HttpClient();</span></span>
<span class="line"><span>var api = new MyApi("https://example.com/", httpClient);</span></span>
<span class="line"><span>var result = await api.GetBindingAsync(parameters);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2>不使用 IDE</h2>
<p>Visual Studio 实际调用了 <code>Microsoft.dotnet-openapi</code> 来生成客户端代码。<a href="https://docs.microsoft.com/en-us/aspnet/core/web-api/microsoft.dotnet-openapi?view=aspnetcore-5.0" target="_blank" rel="noopener noreferrer">文档</a>中也提了一部分使用方法，但是十分含糊。</p>
<h3>准备工作</h3>
<p>首先确保已安装 .NET SDK。然后执行命令安装工具。</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">dotnet</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> tool</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> install</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --global</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> Microsoft.dotnet-openapi</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>若要升级工具，执行下列代码。</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">dotnet</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> tool</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> update</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --global</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> Microsoft.dotnet-openapi</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h3>查看帮助</h3>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>dotnet openapi --help</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>执行此命令会显示帮助</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>OpenApi reference management tool 5.0.5+b7a2ec8c7ed6b48857af0a69688a73e8c14fe6cb</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Usage: openapi [options] [command]</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Options:</span></span>
<span class="line"><span>  -?|-h|--help  Show help information</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Commands:</span></span>
<span class="line"><span>  add</span></span>
<span class="line"><span>  refresh</span></span>
<span class="line"><span>  remove</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Use "openapi [command] --help" for more information about a command.</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>可以看到总共有三条命令，先看看 <code>add</code>。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>dotnet openapi add --help</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>Usage: openapi add [options] [command]</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Options:</span></span>
<span class="line"><span>  -p|--updateProject  The project file update.</span></span>
<span class="line"><span>  -?|-h|--help        Show help information</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Commands:</span></span>
<span class="line"><span>  file</span></span>
<span class="line"><span>  url</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>显示出了用法，注意这时 <code>file</code> 和 <code>url</code> 都是命令，不是参数。继续想看帮助</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>dotnet openapi add url --help</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>Usage: openapi add url [arguments] [options]</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Arguments:</span></span>
<span class="line"><span>  source-URL  The OpenAPI file to add. This must be a URL to a remote OpenAPI file.</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Options:</span></span>
<span class="line"><span>  -p|--updateProject   The project file update.</span></span>
<span class="line"><span>  -c|--code-generator  The code generator to use. Defaults to 'NSwagCSharp'.</span></span>
<span class="line"><span>  --output-file        The destination to download the remote OpenAPI file to.</span></span>
<span class="line"><span>  -?|-h|--help         Show help information</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这就是完整用法了。</p>
<h3>添加 API 引用并调用 API</h3>
<p>按照上面的命令，在项目目录执行以下代码。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>dotnet openapi add url "https://api.bleatingsheep.org/swagger/v1/swagger.json" --output-file "HydrantApi.json"</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>然后项目目录中就会出现 <code>HydrantApi.json</code> 文件。运行 <code>dotnet build</code>，然后就可以使用 API 了，默认的类名是文件名+<code>Client</code>，在这个例子里，也就是 <code>HydrantApiClient</code>，使用方法同上。</p>
<h3>更新</h3>
<p>可以用 <code>dotnet openapi refresh</code> 命令更新 <code>json</code>，然后用 <code>dotnet build</code> 更新生成的代码。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">PVE 非特权容器 (CT) 无法使用 Docker 的临时解决方案</title>
    <id>https://bleatingsheep.org/2026/01/03/PVE-%E9%9D%9E%E7%89%B9%E6%9D%83%E5%AE%B9%E5%99%A8-CT-%E6%97%A0%E6%B3%95%E4%BD%BF%E7%94%A8-Docker-%E7%9A%84%E4%B8%B4%E6%97%B6%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</id>
    <link href="https://bleatingsheep.org/2026/01/03/PVE-%E9%9D%9E%E7%89%B9%E6%9D%83%E5%AE%B9%E5%99%A8-CT-%E6%97%A0%E6%B3%95%E4%BD%BF%E7%94%A8-Docker-%E7%9A%84%E4%B8%B4%E6%97%B6%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>我之前在自己的几台机器上装了 Proxmox VE (PVE)，并给日日开了个容器 (CT)，让他跑一些自己的东西。日日在容器中装了 Docker，之前一直跑得好好的。2024 年 11 月，日日告诉我，他的几个服务 down 了，并且 Docker 容器打不开。</p>
]]></summary>
    <content type="html"><![CDATA[<p>我之前在自己的几台机器上装了 Proxmox VE (PVE)，并给日日开了个容器 (CT)，让他跑一些自己的东西。日日在容器中装了 Docker，之前一直跑得好好的。2024 年 11 月，日日告诉我，他的几个服务 down 了，并且 Docker 容器打不开。</p>
<!-- more -->
<p>日日提供的错误信息如下：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: open sysctl net.ipv4.ip_unprivileged_port_start file: reopen fd 8: permission denied: unknown</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>经过检索，发现是 AppArmor profiles 的问题，可以通过 Workaround 缓解。</p>
<h2>Workaround: 绕过 AppArmor</h2>
<p>首先在 PVE host 编辑容器配置：打开 <code>/etc/pve/lxc/&lt;容器ID&gt;.conf</code>，并添加以下内容：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file</span></span>
<span class="line"><span>lxc.apparmor.profile: unconfined</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>完成后（可能需要重启 CT 容器来应用配置），在容器中执行以下命令：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">mount</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --bind</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/null</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /sys/module/apparmor/parameters/enabled</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">systemctl</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> restart</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> docker</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>注意，CT 容器重启后，需要重新执行此命令，应该可以通过编写 systemd unit，并将 docker 服务设置为“WantedBy”来自动挂载，但我未测试。</p>
<h2>解决：更新 Proxmox VE</h2>
<p>有报告称 Proxmox VE 9.1 已解决此问题，我当前使用的是 Proxmox VE 8，无法验证。</p>
<h2>参考：</h2>
<p><a href="https://forum.proxmox.com/threads/docker-inside-lxc-net-ipv4-ip_unprivileged_port_start-error.175437/" target="_blank" rel="noopener noreferrer">https://forum.proxmox.com/threads/docker-inside-lxc-net-ipv4-ip_unprivileged_port_start-error.175437/</a></p>
<p><a href="https://github.com/opencontainers/runc/issues/4968#issuecomment-3500775431" target="_blank" rel="noopener noreferrer">https://github.com/opencontainers/runc/issues/4968#issuecomment-3500775431</a></p>
<p><a href="https://github.com/lxc/lxc/issues/4606" target="_blank" rel="noopener noreferrer">https://github.com/lxc/lxc/issues/4606</a></p>
<p><a href="https://bugzilla.proxmox.com/show_bug.cgi?id=7006" target="_blank" rel="noopener noreferrer">https://bugzilla.proxmox.com/show_bug.cgi?id=7006</a></p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">String.Create的局限性</title>
    <id>https://bleatingsheep.org/2021/03/22/String-Create%E7%9A%84%E5%B1%80%E9%99%90%E6%80%A7/</id>
    <link href="https://bleatingsheep.org/2021/03/22/String-Create%E7%9A%84%E5%B1%80%E9%99%90%E6%80%A7/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<h2><code>String.Create</code> 介绍</h2>
<p>.NET Core 2.1 和 .NET Standard 2.1 新增了 <code>Span&lt;T&gt;</code>、<code>Memory&lt;T&gt;</code> 等非常方便的操作内存的方式，同时增加了如 <code>String.Create()</code> 这种高效的创建字符串的方法。最近在写 <a href="https://github.com/rcnbapp/RCNB.csharp" target="_blank" rel="noopener noreferrer">RCNB</a> 终于得以一试，结果发现了这个方法的许多局限之处。</p>
]]></summary>
    <content type="html"><![CDATA[<h2><code>String.Create</code> 介绍</h2>
<p>.NET Core 2.1 和 .NET Standard 2.1 新增了 <code>Span&lt;T&gt;</code>、<code>Memory&lt;T&gt;</code> 等非常方便的操作内存的方式，同时增加了如 <code>String.Create()</code> 这种高效的创建字符串的方法。最近在写 <a href="https://github.com/rcnbapp/RCNB.csharp" target="_blank" rel="noopener noreferrer">RCNB</a> 终于得以一试，结果发现了这个方法的许多局限之处。</p>
<!-- more -->
<p>我们先看一个这个方法的定义。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> static</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> String</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Create</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">TState</span><span style="--shiki-light:#999999;--shiki-dark:#666666">>(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> TState</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> state</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> SpanAction</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">char</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> TState</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> action</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>首先这是一个泛型方法，接受一个 <code>TState</code> 实例并直接传给 <code>action</code>。我们看一下 <code>SpanAction&lt;char, TState&gt;</code> 的定义。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> delegate</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> void</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> SpanAction</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">T</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> in</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> TArg</span><span style="--shiki-light:#999999;--shiki-dark:#666666">>(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">Span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">T</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> TArg</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> arg</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>为什么不直接用 <code>Action&lt;in T1, in T2&gt;</code> 呢？因为 <code>Span&lt;T&gt;</code> 是 <code>readonly ref struct</code>，不能用于泛型参数。问题就出在这一点，由于它不能用在泛型参数上，给 <code>String.Create</code> 也带来了一些局限，只能用很麻烦的方式绕过。</p>
<h2><code>String.Create</code> 的局限性</h2>
<p>我给 RCNB 编码设计了这么一个 API 方法：</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> static</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> ToRcnbString</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>这个方法接收一个 <code>ReadOnlySpan&lt;byte&gt;</code>，将基用 RCNB 编码，并返回结果字符串。在底层，我已经实现了编码方法：</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">private</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> static</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> void</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> EncodeRcnb</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">Span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">char</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> resultArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD">    // implementation</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>我底层的方法从 <code>ReadOnlySpan&lt;byte&gt; inArray</code> 中读取数据，并把编码好的字符串存入 <code>Span&lt;char&gt; resultArray</code> 中。我本以为，.NET Core 2.1 新增的 <code>Span&lt;T&gt;</code> 等结构足够强大，可以让我把任何传入参数转换成这两个东西，并进行 RCNB 编码。于是我就先这么写：</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> static</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> ToRcnbString</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> length</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> CalculateLength</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    return</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Create</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> =></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> EncodeRcnb</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">));</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>思路是，先计算出结果字符串的长度，再把 <code>inArray</code> 传给 <code>String.Create</code> 方法，调用 <code>EncodeRcnb</code> 方法进行编码。</p>
<p>但我得到一个错误。</p>
<p></p>
<p>意思是 <code>ReadOnlySpan&lt;byte&gt;</code> 不能被用作类型参数。还记得 <code>String.Create</code> 的定义吗？传入参数的类型是用泛型参数 <code>TState</code> 确定的，自然会受到这个限制。</p>
<h2>.NET 内部的解决方式</h2>
<p>如果你用 .NET 编码或解码过 base64，你应该会发现 <code>System.Convert</code> 类有这样一个方法：</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> static</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> ToBase64String</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> bytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> Base64FormattingOptions</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> options</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Base64FormattingOptions</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">None</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>这个方法也接收 <code>ReadOnlySpan&lt;byte&gt;</code>，返回 <code>String</code>，它是怎么实现的呢？我们来看一看 <a href="https://github.com/dotnet/runtime/blob/11868166a541b2746eb86b8812302bd65ecfd2f7/src/libraries/System.Private.CoreLib/src/System/Convert.cs#L2349" target="_blank" rel="noopener noreferrer">.NET 实现源码</a>。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> static</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> ToBase64String</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> bytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> Base64FormattingOptions</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> options</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Base64FormattingOptions</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">None</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    if</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">options</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> &#x3C;</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Base64FormattingOptions</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">None</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ||</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> options</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ></span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Base64FormattingOptions</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">InsertLineBreaks</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        throw</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> new</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> ArgumentException</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">SR</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Format</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">SR</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Arg_EnumIllegalVal</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">options</span><span style="--shiki-light:#999999;--shiki-dark:#666666">),</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> nameof</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">options</span><span style="--shiki-light:#999999;--shiki-dark:#666666">));</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    if</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">bytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ==</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 0</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        return</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Empty</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    bool</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> insertLineBreaks</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">options</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ==</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Base64FormattingOptions</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">InsertLineBreaks</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    string</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> result</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">FastAllocateString</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">ToBase64_CalculateAndValidateOutputLength</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">bytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> insertLineBreaks</span><span style="--shiki-light:#999999;--shiki-dark:#666666">));</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    unsafe</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        fixed</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">*</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> bytesPtr</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> &#x26;</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">MemoryMarshal</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">GetReference</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">bytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666">))</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        fixed</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">char</span><span style="--shiki-light:#999999;--shiki-dark:#666666">*</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> charsPtr</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> result</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">        {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">            int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> charsWritten</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> ConvertToBase64Array</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">charsPtr</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> bytesPtr</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 0</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> bytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> insertLineBreaks</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">            Debug</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Assert</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">result</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ==</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> charsWritten</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> $"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">Expected </span><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">result</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> == </span><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">charsWritten</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">        }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    return</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> result</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>我们看到，.NET 实现上首先用了 <code>string.FastAllocateString</code> 快速分配字符串所需的内存，然后用 <code>fixed (byte* bytesPtr = &amp;MemoryMarshal.GetReference(bytes))</code> 获取 <code>bytes</code> 的指针。最早实现 <code>ConvertToBase64Array</code> 时还没有 <code>Span&lt;T&gt;</code>，我认为转换成指针是为了复用以前的代码。问题是 <code>string.FastAllocateString</code> 是个内部方法，我无法调用。尽管有人问了 <a href="https://stackoverflow.com/questions/19992144/how-can-i-implement-stringbuilder-and-or-call-string-fastallocatestring" target="_blank" rel="noopener noreferrer">如何调用这个方法</a>，但是是用反射强行调用的，无法保证所有版本 .NET 都是这么实现的，不太好。</p>
<h2>尝试解决问题</h2>
<h3>转换成 <code>ReadOnlyMemory&lt;T&gt;</code></h3>
<p>我首先想到的是转换成 <code>ReadOnlyMemory&lt;T&gt;</code>。我知道 <code>Memory&lt;T&gt;</code> 及 <code>ReadOnlyMemory&lt;T&gt;</code> 可以转换成 <code>Span&lt;T&gt;</code> 和 <code>ReadOnlySpan&lt;T&gt;</code>，也知道反过来会出现问题。</p>
<h4><code>ReadOnlyMemory&lt;T&gt;</code>、<code>Memory&lt;T&gt;</code>、<code>ReadOnlySpan&lt;T&gt;</code>、<code>Span&lt;T&gt;</code> 的关系</h4>
<p>首先，<code>ReadOnlySpan&lt;T&gt;</code>、<code>Span&lt;T&gt;</code> 是用 <code>readonly</code> 修饰的 <a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#ref-struct" target="_blank" rel="noopener noreferrer"><code>ref struct</code></a>。<code>ref struct</code> 有诸多限制，而这些限制可以提升性能和代码安全性。例如，我们可以安全地编写下面的代码：</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">Span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> bytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> stackalloc</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">[</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91">2000</span><span style="--shiki-light:#999999;--shiki-dark:#666666">];</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>这段代码从栈上分配 2000 字节，存储在 <code>bytes</code> 中。在 C 或 C++ 中，你可能不小心把栈上指针赋值到别处，或者当成返回值返回，导致稍后可能非法访问。而在 C# 中，如果你用了 <code>Span&lt;T&gt;</code>，由于本身的限制，你无法轻易把它传到别处，这样就可以在不牺牲安全性的前提下提升性能。</p>
<p>再看 <code>ReadOnlyMemory&lt;T&gt;</code> 和 <code>Memory&lt;T&gt;</code>。它们只是普通的结构体，只是用了 <code>readonly</code> 修饰。你完全可以把它随便赋值或者返回到别处。</p>
<p>如果 <code>Span&lt;T&gt;</code> 可以随随便便转换成 <code>Memory&lt;T&gt;</code>，你再把它传到别处，万一那个 <code>Span&lt;T&gt;</code> 是栈上分配的，不就带来不安全访问了吗？所以 <code>Span&lt;T&gt;</code> 是不能随随便便转换成 <code>Memory&lt;T&gt;</code> 的，只读版本亦然。</p>
<h4>.NET 中其他操作内存的方法</h4>
<p>与上面四个 <code>struct</code> 一起到来的，还有操作这些结构体的方法，比如 <a href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.memorymarshal?view=net-5.0" target="_blank" rel="noopener noreferrer"><code>System.Runtime.InteropServices.MemoryMarshal</code></a> 类。<code>MemoryMarshal</code> 类包含一些操作内存的方法，而这些方法看上去并不是那么安全。</p>
<p>例如 <a href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.memorymarshal.asbytes?view=net-5.0" target="_blank" rel="noopener noreferrer"><code>AsBytes</code></a> 方法，可以把任意类型 <code>T</code> 的 <code>Span&lt;T&gt;</code> 转换成 <code>Span&lt;byte&gt;</code>。例如要生成随机 <code>uint</code> 时，就<a href="https://github.com/dotnet/runtime/blob/90010d055393074123c7e3198a6eda150624f30c/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/RandomNumberGenerator.cs#L135" target="_blank" rel="noopener noreferrer">可以把 <code>Span&lt;uint&gt;</code> 转换成 <code>Span&lt;bytes&gt;</code> 放置生成的随机字节</a>。</p>
<p>此外还有 <a href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.memorymarshal.asmemory?view=net-5.0" target="_blank" rel="noopener noreferrer"><code>AsMemory</code></a> 可以把只读的 <code>ReadOnlyMemory&lt;T&gt;</code> 变成可写的 <code>Memory&lt;T&gt;</code>，同样可能带来潜在的安全问题。</p>
<p>除了 <code>MemoryMarshal</code>，<a href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafe?view=net-5.0" target="_blank" rel="noopener noreferrer"><code>System.Runtime.CompilerServices.Unsafe</code></a> 类也包含一些操作内存的方法。</p>
<p>遗憾的是，这两个类都没有把 <code>ReadOnlySpan&lt;T&gt;</code> 转换成 <code>ReadOnlyMemory&lt;T&gt;</code> 的方法。</p>
<h3>借助指针</h3>
<p>但问题还是要解决的，我不希望无意义地复制内存。</p>
<p>我首先想到的是把指针传入 <code>String.Create</code> 方法。我先开启了 unsafe 编译，再把方法定义前面加上 <code>unsafe</code>。</p>
<p>上面的 base64 编码的源码解释了如何从 <code>ReadOnlySpan&lt;T&gt;</code> 中提取 <code>T</code> 的指针 <code>T*</code>。<a href="https://stackoverflow.com/a/54257716" target="_blank" rel="noopener noreferrer">此外还有别的方法提取</a>，只要保证正确地 <code>fixed</code> 了就行。由于我们需要指针和长度两个参数，我们把它形成一个元组，传入匿名方法，并在匿名方法里构造出 <code>ReadOnlySpan&lt;byte&gt;</code>。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> static</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> unsafe</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> ToRcnbString</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> length</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> CalculateLength</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    fixed</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">*</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> data</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        return</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Create</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">            (</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">data</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">),</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">            (</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> =></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> EncodeRcnb</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> new</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">>(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">data</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)));</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>结果我们在 <code>(data, inArray.Length),</code> 这一行的 <code>data</code> 处遇到一个错误，说：</p>
<p></p>
<p>也就是说，除了 <code>ref struct</code>，指针类型也不能用作泛型参数。</p>
<p>然后我想到，指针也不行，那 <code>ReadOnlyMemory&lt;T&gt;</code> 可以传入吗，它就是普通的 <code>struct</code>。遗憾的是，尽管可以用指定的指针和长度创建 <code>Span&lt;T&gt;</code>，<code>Memory&lt;T&gt;</code> 及只读版本却没有用指针创建的构造方法。</p>
<p>然后我想，<code>ReadOnlyMemory&lt;T&gt;</code> 没有合适的构造方法，那我自己定义一个 <code>struct</code> 总可以了吧。于是就<a href="https://github.com/rcnbapp/RCNB.csharp/blob/a5190d31f220e02df0aa76b0c22cde6024b8441d/RCNB/RcnbConvert.cs#L120" target="_blank" rel="noopener noreferrer">写成了这样</a>：</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> static</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> unsafe</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> ToRcnbString</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> length</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> CalculateLength</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    fixed</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">*</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> data</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        return</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Create</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">            new</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> ByteMemoryMedium</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">data</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> inArray</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">),</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">            (</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> =></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> EncodeRcnb</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> new</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> ReadOnlySpan</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">>(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Pointer</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)));</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">private</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> unsafe</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> readonly</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> struct</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> ByteMemoryMedium</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> ByteMemoryMedium</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE">byte* pointer</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">        Pointer</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> pointer</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">        Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE"> byte* Pointer { get</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> }</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> {</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> get</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> }</span></span>
<span class="line"><span style="--shiki-light:#393A34;--shiki-dark:#DBD7CAEE">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>我自己定义了一个 <code>unsafe struct</code>，接受指针和长度，并把这个结构的实例传入 <code>String.Create</code>，终于达成目标。</p>
<h2>总结</h2>
<p>.NET Core 2.1 引入的 <code>ref struct</code> 和 <code>Span&lt;T&gt;</code> 等类型为操作内存提供了相当程度的便利。上面没细说的 <code>EncodeRcnb</code> 方法就充分利用了这些类型。同时引入的 <code>String.Create</code> 方法可以减少创建字符串时的内存分配，尽管它用起来有很多局限。我希望 C# 未来的版本可以放宽 <code>ref struct</code> 的条件，以便 <code>Span&lt;T&gt;</code> 可以传入 <code>String.Create</code> 方法。<s>希望 .NET 基金会不要不识抬举。</s></p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">产生任意范围的整数随机数</title>
    <id>https://bleatingsheep.org/2021/03/19/%E4%BA%A7%E7%94%9F%E4%BB%BB%E6%84%8F%E8%8C%83%E5%9B%B4%E7%9A%84%E6%95%B4%E6%95%B0%E9%9A%8F%E6%9C%BA%E6%95%B0/</id>
    <link href="https://bleatingsheep.org/2021/03/19/%E4%BA%A7%E7%94%9F%E4%BB%BB%E6%84%8F%E8%8C%83%E5%9B%B4%E7%9A%84%E6%95%B4%E6%95%B0%E9%9A%8F%E6%9C%BA%E6%95%B0/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>有一天，我突然想验证一下 .NET 产生任意范围的整数随机数是不是保证完全等概率的。</p>
]]></summary>
    <content type="html"><![CDATA[<p>有一天，我突然想验证一下 .NET 产生任意范围的整数随机数是不是保证完全等概率的。</p>
<!-- more -->
<h2>零</h2>
<p>我们先讨论一下，如何用 0~(n-1) 的随机数生成器，生成 0~(m-1) 范围的随机数。</p>
<h3>最朴素的想法</h3>
<p>先考虑 n&gt;m 的情形，最简单的做法是，先产生一个 0~(n-1) 的随机数，如果小于 m，就使用这个结果；如果大于等于 m，就重来。如果 n&gt;2m，可以目标范围的一个数对应原生成器的两个或以上的数。</p>
<p>至于 n&lt;m 的情况，可以多次生成来获得一个更大范围的生成器，比如生成两次就是 0~(n^2-1) 的随机数生成器。</p>
<h3>另外一种想法</h3>
<p>由于上面的那种想法没有完全利用产生的随机数，我希望能够产生尽量少的随机数达到目的。我首先考虑的是，把 [0,1) 分成更小的区间，比如 [0,1/m), [1/m, 2/m), ...。这些区间作为目标，同时划分出 [0,1/n), [1/n, 2/n), ... 这些区间。每生成一个随机数，就从后者当中选取一个区间。如果选中的区间是前面区间的子集，就取该区间对应的数字；如果不是，就继续划分为 n 个区间并且随机取一个。</p>
<p>这种想法在产生无限个随机数时，效率是最高的，因为所有信息都被用到了。但是只生成一个随机数时，却不如上面的。或者说，需要使用 0~(n-1) 的生成器的次数的期望更高。</p>
<p>举个例子，用 n=8 的生成器，生成 m=7 的随机数。按朴素的想法，每次有 1/8 的概率需要再生成一次。但是按第二种想法，只有第一次产生 0 或 7 时，才只需要生成一次。从第二次开始，依旧每次有 1/8 的概率需要再次生成。</p>
<h2>一</h2>
<h3>.NET 的 <code>System.Random</code> 类</h3>
<p>现在我们来看 .NET 中的随机数。</p>
<p>.NET 内置了两个与产生随机数有关的类。一个是 <code>System.Random</code>，通过算法产生伪随机数；另一个实际上有至少两个相关类，用来产生无法预测的，可以用作加密用途的生成器。</p>
<p>先看 <code>System.Random</code>。这个类是这么定义的：</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> class</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> Random</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Random</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Random</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Seed</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> virtual</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Next</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> virtual</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Next</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> virtual</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Next</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> virtual</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> void</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> NextBytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">[]</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> buffer</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> virtual</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> void</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> NextBytes</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">Span</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">byte</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> buffer</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> virtual</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> double</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> NextDouble</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    protected</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> virtual</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> double</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Sample</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>其中 <code>Next(int maxValue)</code> 和 <code>Next(int minValue, int maxValue)</code> 包含 <code>minValue</code>，但均不包含 <code>maxValue</code>，前者默认 <code>minValue</code> 为 0。而 <code>Next()</code> 默认 <code>minValue</code> 为 0，<code>maxValue</code> 为 <code>int.MaxValue</code>（是的，范围比 <code>Int32</code> 可表示的范围的一半少 1）。</p>
<p>先看有趣的 <code>NextDouble()</code> 方法。这个方法产生一个大于等于 0 但小于 1 的随机浮点数。有些地方用这种方式产生一定范围内的整数</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Next</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    return</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Math</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Floor</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">maxValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> *</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> NextDouble</span><span style="--shiki-light:#999999;--shiki-dark:#666666">());</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>很明显，这种方法只能近似等概率。这是因为，由于精度限制，能产生的不同的随机浮点数的个数是有限的，而每一个浮点数都会固定对应一个结果，这样就不能保证每个结果都对应同样个数的浮点数，因而概率不等。</p>
<h3>生成指定范围的随机数</h3>
<p>如果现在我告诉你，随机数生成器的底层是产生一堆随机比特，或者说，是第一位都等概率随机的 <code>System.UInt32</code>，让你去写产生指定范围随机数，即 <code>int Next(int minValue, int maxValue)</code>，你会怎么写？</p>
<p>最简单的办法，如果还是不考虑概率的微小差别，可以简单这么写</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Next</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    Debug</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Assert</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> &#x3C;</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    uint</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> range</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> maxValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> -</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    if</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">range</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ==</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 1</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> return</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    uint</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> rand</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> NextUInt32</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    return</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> +</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">rand</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> %</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> range</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>为了保证完全等概率，可以这么改写</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Next</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    Debug</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Assert</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> &#x3C;</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    uint</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> range</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">uint</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">maxValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> -</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    uint</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> max</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">uint</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)(((</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">ulong</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">uint</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">MaxValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> +</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 1</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> /</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> range</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> *</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> range</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> -</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 1</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    while</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">true</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        uint</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> rand</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> NextUInt32</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        if</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">rand</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> &#x3C;=</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> max</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">            return</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> +</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">rand</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> %</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> range</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>好了，我们看一下<a href="https://github.com/dotnet/runtime/blob/eceb5c8951ca3e5902a6eecdd594f82916ff4762/src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro256StarStarImpl.cs#L113" target="_blank" rel="noopener noreferrer">.NET 中是如何实现的</a></p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> override</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Next</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    ulong</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> exclusiveRange</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">ulong</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">maxValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> -</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    if</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">exclusiveRange</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ></span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 1</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD">        // Narrow down to the smallest range [0, 2^bits] that contains maxValue.</span></span>
<span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD">        // Then repeatedly generate a value in that outer range until we get one within the inner range.</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        int</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> bits</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> BitOperations</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Log2Ceiling</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">exclusiveRange</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">        while</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">true</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">        {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">            ulong</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> result</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> NextUInt64</span><span style="--shiki-light:#999999;--shiki-dark:#666666">()</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> >></span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">sizeof</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">ulong</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> *</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 8</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> -</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> bits</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">            if</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">result</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> &#x3C;</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> exclusiveRange</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">            {</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">                return</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> (</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">int</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">result</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> +</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">            }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">        }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    Debug</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Assert</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">minValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ==</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> maxValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ||</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> +</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 1</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> ==</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> maxValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    return</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> minValue</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>.NET 并没有使用乘、除算出范围，再用模确定数字，而是计算出需要的比特，并直接抛弃掉多余的。这么做可能是考虑到性能。</p>
<h2>二</h2>
<p>在 .NET 中，还有一个类是与生成随机数有关的（确切地说，有多个类）。最主要的就是 <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/RandomNumberGenerator.cs#L11" target="_blank" rel="noopener noreferrer"><code>System.Security.Cryptography.RandomNumberGenerator</code></a>。从命名空间就可以看出来，这是用来产生可用于加密的随机数的。有趣的是，它用来<a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/RandomNumberGenerator.cs#L11" target="_blank" rel="noopener noreferrer">生成给定范围随机数</a>的方法是通过位运算得到一个 <code>mask</code>，然后用按位与去取需要的比特，而 <code>System.Random</code> 是计算出需要的比特数，用位移取得需要的比特。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">使用 DisplayCAL 和 Spyder 红蓝蜘蛛校色仪进行屏幕校色的注意事项</title>
    <id>https://bleatingsheep.org/2023/02/11/%E4%BD%BF%E7%94%A8-DisplayCAL-%E5%92%8C-Spyder-%E7%BA%A2%E8%93%9D%E8%9C%98%E8%9B%9B%E6%A0%A1%E8%89%B2%E4%BB%AA%E8%BF%9B%E8%A1%8C%E5%B1%8F%E5%B9%95%E6%A0%A1%E8%89%B2%E7%9A%84%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/</id>
    <link href="https://bleatingsheep.org/2023/02/11/%E4%BD%BF%E7%94%A8-DisplayCAL-%E5%92%8C-Spyder-%E7%BA%A2%E8%93%9D%E8%9C%98%E8%9B%9B%E6%A0%A1%E8%89%B2%E4%BB%AA%E8%BF%9B%E8%A1%8C%E5%B1%8F%E5%B9%95%E6%A0%A1%E8%89%B2%E7%9A%84%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>最近<a href="https://github.com/AkiraXie" target="_blank" rel="noopener noreferrer">药药</a>买了蓝蜘蛛 Spyder X，我告诉他不要用配套软件，直接用 DisplayCAL。DisplayCAL 已经三年多没有更新了，软件不更新也会变质，有些东西直接按当年的步骤，就会出现奇奇怪怪的问题。因此我在此总结一下当前使用 DisplayCAL 和 Spyder X 的完整流程及注意事项。</p>
]]></summary>
    <content type="html"><![CDATA[<p>最近<a href="https://github.com/AkiraXie" target="_blank" rel="noopener noreferrer">药药</a>买了蓝蜘蛛 Spyder X，我告诉他不要用配套软件，直接用 DisplayCAL。DisplayCAL 已经三年多没有更新了，软件不更新也会变质，有些东西直接按当年的步骤，就会出现奇奇怪怪的问题。因此我在此总结一下当前使用 DisplayCAL 和 Spyder X 的完整流程及注意事项。</p>
<!-- more -->
<h2>软硬件准备</h2>
<p><strong>校色仪。</strong>可以说是最重要的部分。本文重点使用 Spyder5 和 Spyder X。其他校色仪按说是大同小异，只是驱动安装方式可能会不同。</p>
<p><strong>显示器。</strong>把显示器的设置恢复为出厂默认，并且需要保证在校色正式开始前亮屏 30 分钟。</p>
<p><strong>操作系统。</strong>本文使用 Windows。MacOS 由于系统安全规则，会难以执行外挂的校色程序。博主没有在 Linux 上校色的经验。</p>
<p><strong>软件。</strong>需要准备下列软件：</p>
<ul>
<li>DisplayCAL。建议在官网 <a href="https://displaycal.net/" target="_blank" rel="noopener noreferrer">https://displaycal.net/</a> 下载。点击“Get DisplayCAL”按钮后选择自己的系统下载安装。</li>
<li>Argyll 2.1.1。<a href="https://ytt3q-my.sharepoint.com/:u:/g/personal/bleatingsheep_fireant_dev/EXTFdCgzHIBDtrXzUzF-KPYBGpUW8ghDI4V5j2lP1w0MvA?e=hgeYxe" target="_blank" rel="noopener noreferrer">点此链接下载</a>，必须使用特定版本。下载后解压到某个目录。</li>
<li>（推荐，但不必需）Argyll 最新版。Windows 版的下载地址为 <a href="https://www.argyllcms.com/downloadwin.html" target="_blank" rel="noopener noreferrer">https://www.argyllcms.com/downloadwin.html</a>，其他系统在 <a href="https://www.argyllcms.com/" target="_blank" rel="noopener noreferrer">https://www.argyllcms.com/</a> 向下拉找到“Downloads”。下载后解压到某个目录永久保存。</li>
</ul>
<p>解压后内容应如图所示。</p>
<p></p>
<h2>安装配置 DisplayCAL</h2>
<p>首先下载安装 DisplayCAL。</p>
<h3>准备驱动</h3>
<p>由于本文以 Spyder 系列校色仪为例，所以需要安装驱动。打开 DisplayCAL，在弹出的窗口点击“浏览”，然后选择“Argyll_V2.1.1”文件夹。</p>
<p></p>
<p>选择 Tools -&gt; Instrument -&gt; Install ArgyllCMS instrument drivers，然后在弹出的窗口中选择“Download and install”，按提示操作。</p>
<p></p>
<!-- "%APPDATA%\DisplayCAL\dl\Argyll_V2.1.1_USB_driver_installer.exe" --noprompt --vid 0x85c --pid 0x500 --create "Spyder5 (Argyll)" -->
<h3>配置 Argyll（推荐）</h3>
<p>如果你还下载了最新版本的 Argyll，那么选择 File -&gt; Locate ArgyllCMS executables 来配置 DisplayCAL 使用最新版本。</p>
<p></p>
<p>选择“Argyll_V2.3.1”或者最新版本所在的文件夹。</p>
<h3>开启高级选项</h3>
<p>Options -&gt; Show advanced options</p>
<p></p>
<h2>开始校色</h2>
<p>在进行校色前，需要打开显示器预热三十分钟以上。或者当前已连续使用显示器超过三十分钟即可。</p>
<h3>选择要校正的显示器</h3>
<p>在主界面选择要校正的显示器。请注意如果你的多台显示器缩放比例不同，副显示器的分辨率可能显示不正确。选择显示器对应型号即可。</p>
<p>![](./5-2.select monitor.png)</p>
<h3>把校色仪贴到屏幕上</h3>
<p>连接校色仪并打开 DisplayCAL。把 Settings 选为“Office &amp; Web”，在“Profiling”标签下把“Profile type”选为“Single Curve + matrix”。然后点击“Calibrate &amp; Profile”。</p>
<p></p>
<p>然后把弹出的窗口拖到要校色的显示器，用“+、-”调整大小，尽量把窗口放在屏幕中心，并把校色仪贴紧到窗口中心。调整好后点击“Start measurement”。</p>
<p><strong>注意：</strong>校色仪必须贴紧屏幕。放置校色仪前可以把屏幕向后倾斜，把盖子卡在屏幕顶部后方使其不要下滑，依靠重力使校色仪贴紧。</p>
<h3>调整显示器白点色温</h3>
<p>稍后会出现一个新窗口，点击“Start measurement”后，仪器就会开始测量色温。使用显示器的设置调整红、绿、蓝的颜色数值，直到三色平衡。同时，把亮度调整到合适的水平（不用管亮度条是否在中间）。</p>
<p></p>
<p><strong>注意：</strong>如果你的显示器红、绿、蓝的数值默认都是 100，那调低偏高的数值即可。但是，如果默认是 50 或者其他的值，则需要尽量先调低偏高的那项数值，并且尽量让三色不要大于默认值。</p>
<p>调整完毕后，点击“Stop measurement”，然后点“Continue on to calibration”。</p>
<p><em>提示：</em>如果 Spyder X 出现和“self-calibration offsets”有关的错误，如下图所示，可能是因为室温偏低。解决方法是用手把校色仪捂热，捂热后再校准即可。</p>
<p></p>
<p>如果你校准的是笔记本显示器等不能调节三色平衡的设备，可以跳过这一步，直接点击“Continue on to calibration”。</p>
<h3>安装配置文件</h3>
<p>然后等待校色完成。Spyder5 这步需要大约一小时。完成后会出现如下窗口，显示了显示器的色域等信息，点击“Install profile”即可。</p>
<p></p>
<p>如果要查看校色的效果，可以点击“Show profile information”，弹出的窗口包含彩色配置文件的详细信息。</p>
<h2>参考资料</h2>
<p>本文主要参考<a href="https://bbs.saraba1st.com/2b/thread-1157782-1-1.html" target="_blank" rel="noopener noreferrer">简明屏幕校色及色彩管理</a>，根据当前实际情况，及指导<a href="https://github.com/AkiraXie" target="_blank" rel="noopener noreferrer">药药</a>校色时出现的状况有所补充。</p>
<p>关于捂热校色仪的资料来自视频 <a href="https://youtu.be/0KYiU1apa8M" target="_blank" rel="noopener noreferrer">https://youtu.be/0KYiU1apa8M</a>。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">关于升级 MIUI 13 后无法打电话的问题</title>
    <id>https://bleatingsheep.org/2022/02/26/%E5%85%B3%E4%BA%8E%E5%8D%87%E7%BA%A7-MIUI-13-%E5%90%8E%E6%97%A0%E6%B3%95%E6%89%93%E7%94%B5%E8%AF%9D%E7%9A%84%E9%97%AE%E9%A2%98/</id>
    <link href="https://bleatingsheep.org/2022/02/26/%E5%85%B3%E4%BA%8E%E5%8D%87%E7%BA%A7-MIUI-13-%E5%90%8E%E6%97%A0%E6%B3%95%E6%89%93%E7%94%B5%E8%AF%9D%E7%9A%84%E9%97%AE%E9%A2%98/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>去年回国，买了小米 K40。年底回日本时，换了 cmlink 卡。在东京待机（隔离）时，用着没什么问题，到福冈后的某个时间开始不能打电话了。由于我打电话少，我是一段时间以后才意识到这个问题的。</p>
]]></summary>
    <content type="html"><![CDATA[<p>去年回国，买了小米 K40。年底回日本时，换了 cmlink 卡。在东京待机（隔离）时，用着没什么问题，到福冈后的某个时间开始不能打电话了。由于我打电话少，我是一段时间以后才意识到这个问题的。</p>
<!-- more -->
<h2>解决方法</h2>
<p>在拨号盘输入“*#*#86583#*#*”，系统设置中出现 VoLTE 开关，开启即可通话。</p>
<h2>分析</h2>
<p>在东京通话时开启了 VoLTE，所以我判断是我住的地方没有 K40 VoLTE 所需的频段，导致无法使用 VoLTE。同时非 VoLTE 的通话可能本来也不能用，结果就完全不能打电话了。</p>
<p>一段时间后我开始意识到不对，因为无论我去福冈什么地方，我都没法打电话。只有东京有那个频段，福冈所有地方都没有，这也太奇怪了点。而且，我不但没法使用 VoLTE，系统里也完全没有 VoLTE 开关。今天突然意识到，我最近升级了 MIUI 13，会不会是 MIUI 13 的问题呢？</p>
<p>遗憾的是，我没找到无痛回退 MIUI 版本的资料。我想，之前没用过非 VoLTE 通话，但是 VoLTE 的确是可以用的。同时考虑到数据没问题，从 VoLTE 入手，想办法开启 VoLTE 应该就是解决的办法。</p>
<p>于是我搜索“MIUI13 无法 volte”，很快找到一篇<a href="https://tieba.baidu.com/p/7721271362" target="_blank" rel="noopener noreferrer">帖子</a>，提到了上面的解决方法。</p>
<p>当我输入后，浮窗提示“VoLTE carrier check was disabled.”，更加重了我对 MIUI 13 的怀疑。</p>
<p>我感觉，在国外还是别用国行手机了。我的 K40 除了没法打电话，还有杀 Google 服务造成通知延迟等问题。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">使用 iPXE 和 netboot.xyz 重装 VPS 系统</title>
    <id>https://bleatingsheep.org/2021/09/24/%E4%BD%BF%E7%94%A8iPXE%E5%92%8Cnetboot-xyz%E9%87%8D%E8%A3%85VPS%E7%B3%BB%E7%BB%9F/</id>
    <link href="https://bleatingsheep.org/2021/09/24/%E4%BD%BF%E7%94%A8iPXE%E5%92%8Cnetboot-xyz%E9%87%8D%E8%A3%85VPS%E7%B3%BB%E7%BB%9F/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>如果你购买一些小厂商的 VPS，你可能会觉得这些厂商自带系统不好用。哪怕是国内大厂，也经常有瞎 JB 改系统，影响功能的情况。有些厂商允许你挂载 ISO 自己安装系统，而有些厂商则不行。下面我将介绍使用 <a href="https://ipxe.org/" target="_blank" rel="noopener noreferrer">iPXE</a> 和 <a href="https://netboot.xyz/" target="_blank" rel="noopener noreferrer">netboot.xyz</a> 引导 Linux 安装程序（本文以 Ubuntu Server 为例），自行安装系统。</p>
]]></summary>
    <content type="html"><![CDATA[<p>如果你购买一些小厂商的 VPS，你可能会觉得这些厂商自带系统不好用。哪怕是国内大厂，也经常有瞎 JB 改系统，影响功能的情况。有些厂商允许你挂载 ISO 自己安装系统，而有些厂商则不行。下面我将介绍使用 <a href="https://ipxe.org/" target="_blank" rel="noopener noreferrer">iPXE</a> 和 <a href="https://netboot.xyz/" target="_blank" rel="noopener noreferrer">netboot.xyz</a> 引导 Linux 安装程序（本文以 Ubuntu Server 为例），自行安装系统。</p>
<!-- more -->
<h2>前提条件</h2>
<p>要使用此方法重装系统，VPS 需要满足以下几个条件：</p>
<ul>
<li>不能是 OpenVZ 等虚拟化技术</li>
<li>有 VNC</li>
<li>使用 GRUB2 引导系统</li>
<li>包管理有 iPXE（如果没有，则需要手动安装）</li>
</ul>
<p>如果 VPS 使用的是 OpenVZ 技术，或者不提供 VNC 功能，则无法进行安装；不满足其他两项的，理论上也可以实现，但不在本文讨论范围之内。</p>
<p>此外，由于各种 VPS 小厂商千奇百怪，不保证你安装过程中不会遇到奇奇怪怪的问题。安装前请备份数据。</p>
<h2>重装步骤</h2>
<h3>1. 修改 grub 配置文件，延长默认等待时间</h3>
<p>默认情况下引导菜单可能太快消失，或者根本不出现。这一步的目的是使稍后选择 iPXE 引导选项更容易。</p>
<p>编辑 grub 配置文件 <code>/etc/default/grub</code>。</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> nano</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /etc/default/grub</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>修改前的配置文件：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>GRUB_DEFAULT=0</span></span>
<span class="line"><span>GRUB_TIMEOUT_STYLE=hidden</span></span>
<span class="line"><span>GRUB_TIMEOUT=0</span></span>
<span class="line"><span>GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`</span></span>
<span class="line"><span>GRUB_CMDLINE_LINUX_DEFAULT="maybe-ubiquity"</span></span>
<span class="line"><span>GRUB_CMDLINE_LINUX=""</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>修改后的配置文件：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>GRUB_DEFAULT=0</span></span>
<span class="line"><span>#GRUB_TIMEOUT_STYLE=hidden</span></span>
<span class="line"><span>GRUB_TIMEOUT=300</span></span>
<span class="line"><span>GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`</span></span>
<span class="line"><span>#GRUB_CMDLINE_LINUX_DEFAULT="maybe-ubiquity"</span></span>
<span class="line"><span>GRUB_CMDLINE_LINUX=""</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这一步的要点是，让引导菜单显示足够长的时间以便选择引导选项。</p>
<p>修改完成后，运行更新命令：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> update-grub</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h3>2. 记录机器 IP 地址（如有必要）</h3>
<p>如果你的 VPS 是用 DHCP 获取地址的，那就不需要进行这一步操作；否则需要记录下接口名、IP 地址、掩码、网关等信息。</p>
<p>运行命令:</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>> ifconfig</span></span>
<span class="line"><span>ens3: flags=4163&#x3C;UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500</span></span>
<span class="line"><span>        inet 104.*.*.*  netmask 255.255.255.0  broadcast 104.*.*.255</span></span>
<span class="line"><span>        inet6 fe80::*  prefixlen 64  scopeid 0x20&#x3C;link></span></span>
<span class="line"><span>        ether 00:*  txqueuelen 1000  (Ethernet)</span></span>
<span class="line"><span>        RX packets 9779  bytes 707184 (707.1 KB)</span></span>
<span class="line"><span>        RX errors 0  dropped 0  overruns 0  frame 0</span></span>
<span class="line"><span>        TX packets 109  bytes 14479 (14.4 KB)</span></span>
<span class="line"><span>        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>此处接口名是 <code>ens3</code>，IP 地址是 <code>104.*.*.*</code>，掩码是 <code>255.255.255.0</code>。</p>
<p>网关：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>> ip route</span></span>
<span class="line"><span>default via 104.*.*.1 dev ens3 proto static</span></span>
<span class="line"><span>104.*.*.0/24 dev ens3 proto kernel scope link src 104.*.*.*</span></span>
<span class="line"><span>172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这里显示网关是 <code>104.*.*.1</code>。</p>
<h3>3. 安装 iPXE 并重启</h3>
<p>Ubuntu 或 Debian 可以用以下命令安装 iPXE：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> apt</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> install</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ipxe</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> reboot</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>如果你用的是 CentOS，可以参考<a href="https://lala.im/4524.html" target="_blank" rel="noopener noreferrer">《CentOS7配置GRUB2+iPXE进行网络重装》</a>。</p>
<p>完成之后重启并进入 VNC。你将看到以下画面：</p>
<p></p>
<p>选择“Network boot (iPXE)”选项，进入 iPXE。</p>
<h3>4. 在 iPXE 中引导 netboot.xyz</h3>
<h4>进入 iPXE 命令行</h4>
<p>进入 iPXE 时，你将看到以下画面：</p>
<p></p>
<p>当你看到“Press Ctrl-B for the iPXE command line...”时，请立即按下 Ctrl + B 快捷键，进入 iPXE 命令行。<strong>此消息转瞬即逝，可以在进入之前就保持狂按，以免错过。</strong></p>
<h4>配置网络</h4>
<p>接下来，我们需要配置网络。根据 VPS 是否有 DHCP，配置方式有所不同。</p>
<ul>
<li>有 DHCP 时，只需执行一条命令<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>dhcp</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div></li>
<li>无 DHCP 时，需要执行下列命令，并把其中的“&lt;ip&gt;、&lt;netmask&gt;、&lt;gateway&gt;、&lt;nameserver&gt;”换成之前记下的 IP 地址、掩码、网关及 <code>1.1.1.1</code> 或其他可用的 DNS 服务器<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>set net0/ip &#x3C;ip></span></span>
<span class="line"><span>set net0/netmask &#x3C;netmask></span></span>
<span class="line"><span>set net0/gateway &#x3C;gateway></span></span>
<span class="line"><span>set dns &#x3C;nameserver></span></span>
<span class="line"><span>ifopen net0</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div></li>
</ul>
<h4>引导 netboot.xyz</h4>
<p>配置好网络后，就可以用以下命令引导进入 netboot.xyz 了：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>chain --autofree https://boot.netboot.xyz</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p></p>
<h4>无 DHCP 时的额外步骤</h4>
<p>如果你的 VPS 没有 DHCP，那么 netboot.xyz 还会再问你一遍网络信息。此处默认已填好，直接回车即可。</p>
<p></p>
<h3>5. 引导 Linux 安装程度</h3>
<p>进入 netboot.xyz 后，你将看到以下画面：</p>
<p></p>
<p>选择“Linux Network Installs (64-bit)”，进入以下界面，我们选择“Ubuntu”：</p>
<p></p>
<p>之后我们选择 Ubuntu 20.04，一般选“Subiquity”即可，因为这个引导的是 Ubuntu Server 的镜像，而“Legacy”引导的安装程序不是专门供服务器用的。</p>
<p></p>
<p>当然，你也可以在此选择你想用的其他系统。</p>
<h4>无 DHCP 时的额外步骤</h4>
<p>如果你的 VPS 没有 DHCP，你将看到以下画面。</p>
<p></p>
<p>不过，不必担心，稍后将提示你配置网络。出现提示时，请按提示填写网络信息，<strong>注意请手动输入 <code>static</code>，而不要回车使用默认的“dhcp”</strong>：</p>
<p></p>
<p>需要注意的是，<strong>当你填写 url 时，请勿直接回车默认</strong>，因为默认映像是原始版本，已被删除。<strong>请前往<a href="https://releases.ubuntu.com/20.04/" target="_blank" rel="noopener noreferrer">发布页面</a>查找最新的映像地址。</strong>当然，你也可以自己托管安装镜像，或者使用其他的镜像源。</p>
<h3>6. 进入安装程序</h3>
<p>以上操作完成后，你将看到镜像下载进度，下载好之后将会自动引导安装镜像。</p>
<p></p>
<p></p>
<p>当你看到此界面时，就成功引导了安装程序。下面就按照提示一步步安装吧！</p>
<blockquote>
<ol>
<li><a href="https://www.cnblogs.com/re1n/p/6009946.html" target="_blank" rel="noopener noreferrer">ubuntu16.10 设置grub菜单的默认等待时间，开机时显示详细的引导信息</a></li>
<li><a href="https://lala.im/4524.html" target="_blank" rel="noopener noreferrer">CentOS7配置GRUB2+iPXE进行网络重装</a></li>
<li><a href="https://netboot.xyz/docs/booting/ipxe/" target="_blank" rel="noopener noreferrer">Boot using iPXE</a></li>
</ol>
</blockquote>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">关于树莓派上 Ubuntu Server 20.04 安装 HWE 的问题</title>
    <id>https://bleatingsheep.org/2021/12/04/%E5%85%B3%E4%BA%8E%E6%A0%91%E8%8E%93%E6%B4%BE%E4%B8%8A-Ubuntu-Server-20-04-%E5%AE%89%E8%A3%85-HWE-%E7%9A%84%E9%97%AE%E9%A2%98/</id>
    <link href="https://bleatingsheep.org/2021/12/04/%E5%85%B3%E4%BA%8E%E6%A0%91%E8%8E%93%E6%B4%BE%E4%B8%8A-Ubuntu-Server-20-04-%E5%AE%89%E8%A3%85-HWE-%E7%9A%84%E9%97%AE%E9%A2%98/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p><strong>结论：</strong>目前无法给树莓派上的 Ubuntu Server 20.04 系统安装 Hardware Enablement (HWE)。</p>
]]></summary>
    <content type="html"><![CDATA[<p><strong>结论：</strong>目前无法给树莓派上的 Ubuntu Server 20.04 系统安装 Hardware Enablement (HWE)。</p>
<!-- more -->
<h2>什么是 Hardware Enablement (HWE)</h2>
<p>Hardware Enablement (HWE) 帮助在 Ubuntu LTS 上安装更新的内核。下面两个链接介绍了 HWE。</p>
<blockquote>
<p><a href="https://askubuntu.com/a/248936" target="_blank" rel="noopener noreferrer">What is hardware enablement (HWE)?</a></p>
<p><a href="https://wiki.ubuntu.com/Kernel/LTSEnablementStack" target="_blank" rel="noopener noreferrer">LTS Enablement Stacks</a></p>
</blockquote>
<h2>症状</h2>
<p>我按照上面第二个链接的说法，执行 <code>sudo apt install --install-recommends linux-generic-hwe-20.04</code>，却以错误告终。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>dpkg: error processing package linux-image-5.11.0-41-generic (--configure):</span></span>
<span class="line"><span> installed linux-image-5.11.0-41-generic package post-installation script subprocess returned error exit status 1</span></span>
<span class="line"><span>Errors were encountered while processing:</span></span>
<span class="line"><span> linux-image-5.11.0-41-generic</span></span>
<span class="line"><span>E: Sub-process /usr/bin/dpkg returned an error code (1)</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2>讨论</h2>
<p>我在网上搜索，发现有人讨论过这个问题。要在树莓派上的 Ubuntu 20.04 系统安装 HWE 需要一点手段。然而，这个手段现在会导致无法启动，只能作罢。</p>
<p><a href="https://askubuntu.com/questions/1320381/how-to-install-the-hwe-kernel-on-ubuntu-server-20-04-on-a-raspberry-pi" target="_blank" rel="noopener noreferrer">How to install the HWE kernel on Ubuntu Server 20.04 on a Raspberry Pi</a></p>
<h2>总结：如何检查是否可以在树莓派上安装 HWE</h2>
<p>执行以下命令来检查：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">apt</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> list</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> linux-raspi-hwe-</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">*</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>你会看到如下输出：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>linux-raspi-hwe-18.04-edge/focal-updates,focal-security 5.4.0.1047.82 arm64</span></span>
<span class="line"><span>linux-raspi-hwe-18.04/focal-updates,focal-security 5.4.0.1047.82 arm64</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>可以看到，目前只有 18.04 可以安装 HWE，版本为 5.4，和 Ubuntu 20.04 的内核版本一致。根据此处的输出，20.04 目前不能安装 HWE。</p>
<h2>清理</h2>
<p>直接放结论好了，执行以下命令清理安装失败的残留。</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> apt</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remove</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> linux-</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">*</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">generic-hwe-20.04</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> linux-</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">*</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">-5.11.0-41</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">*</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>其中 <code>5.11.0-41</code> 是安装失败的内核版本，需要根据安装日志替换成实际尝试安装的版本。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">在 Fedora 41+ 上使用 nmcli 创建 IPv6 隧道的注意事项</title>
    <id>https://bleatingsheep.org/2026/01/31/%E5%9C%A8-Fedora-41-%E4%B8%8A%E4%BD%BF%E7%94%A8-nmcli-%E5%88%9B%E5%BB%BA-IPv6-%E9%9A%A7%E9%81%93%E7%9A%84%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/</id>
    <link href="https://bleatingsheep.org/2026/01/31/%E5%9C%A8-Fedora-41-%E4%B8%8A%E4%BD%BF%E7%94%A8-nmcli-%E5%88%9B%E5%BB%BA-IPv6-%E9%9A%A7%E9%81%93%E7%9A%84%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>我之前一直使用 <a href="https://tunnelbroker.net/" target="_blank" rel="noopener noreferrer">Hurricane Electric</a> 的 IPv6 隧道服务为我的 VPS 添加 IPv6 支持。<a href="https://fedoraproject.org/wiki/IPv6_tunnel_via_Hurricane_Electric" target="_blank" rel="noopener noreferrer">Fedora Wiki</a> 有具体命令。但是，自从 Fedora 41（也许是 40，记不清具体时间了）升级以来，此方法失效了，创建的隧道一直无法启用。</p>
]]></summary>
    <content type="html"><![CDATA[<p>我之前一直使用 <a href="https://tunnelbroker.net/" target="_blank" rel="noopener noreferrer">Hurricane Electric</a> 的 IPv6 隧道服务为我的 VPS 添加 IPv6 支持。<a href="https://fedoraproject.org/wiki/IPv6_tunnel_via_Hurricane_Electric" target="_blank" rel="noopener noreferrer">Fedora Wiki</a> 有具体命令。但是，自从 Fedora 41（也许是 40，记不清具体时间了）升级以来，此方法失效了，创建的隧道一直无法启用。</p>
<!-- more -->
<p>经过研究，发现是自从 Fedora 41 以来，需要把隧道的 ipv6.addr-gen-mode 设置为 stable-privacy。参考命令如下：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> nmcli</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> connection</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> add</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> type</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ip-tunnel</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> con-name</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> sit1</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ifname</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> sit1</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> mode</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> sit</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">   remote</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ${</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">REMOTE</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ipv4.method</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> disabled</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ipv6.method</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> manual</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">   ipv6.address</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ${</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">ADDR</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ipv6.gateway</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ${</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">GATEWAY</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">   ipv6.addr-gen-mode</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> stable-privacy</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">   ip-tunnel.ttl</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 64</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>这样设置后，就可以正常地通过此隧道访问 IPv6 网络。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">安装 Ubuntu Server 时配置磁盘 RAID</title>
    <id>https://bleatingsheep.org/2021/10/02/%E5%AE%89%E8%A3%85-Ubuntu-Server-%E6%97%B6%E9%85%8D%E7%BD%AE%E7%A3%81%E7%9B%98-RAID/</id>
    <link href="https://bleatingsheep.org/2021/10/02/%E5%AE%89%E8%A3%85-Ubuntu-Server-%E6%97%B6%E9%85%8D%E7%BD%AE%E7%A3%81%E7%9B%98-RAID/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>有很多 IDC 提供的 VPS 分系统盘和数据盘。这种方式可以使重装系统时不影响一部分数据。但是，这么做也会造成可利用的空间减少。如果你不需要经常在保留数据的前提下重装系统，可以采用 RAID 方式将硬盘空间组合起来，还能提高速度。</p>
<p>需要注意的是，操作时要在系统盘和数据盘上创建 RAID，然后安装 Ubuntu Server。此操作会毁掉所有已有数据。如有重要数据，请提前备份。</p>
]]></summary>
    <content type="html"><![CDATA[<p>有很多 IDC 提供的 VPS 分系统盘和数据盘。这种方式可以使重装系统时不影响一部分数据。但是，这么做也会造成可利用的空间减少。如果你不需要经常在保留数据的前提下重装系统，可以采用 RAID 方式将硬盘空间组合起来，还能提高速度。</p>
<p>需要注意的是，操作时要在系统盘和数据盘上创建 RAID，然后安装 Ubuntu Server。此操作会毁掉所有已有数据。如有重要数据，请提前备份。</p>
<!-- more -->
<h2>引导安装映像并进入存储配置</h2>
<p>首先需要引导 Ubuntu Server 20.04 的安装映像。如果你的 VPS 不支持挂载此 ISO，请参见{% post_link 使用iPXE和netboot-xyz重装VPS系统 &quot;《使用 iPXE 和 netboot.xyz 重装 VPS 系统》&quot; %}。</p>
<p>前几步就略过了，在存储配置的页面，请选择自定义。</p>
<p></p>
<p>然后进入到此界面。</p>
<p></p>
<h2>清除已有分区</h2>
<p>可以看到，此 VPS 上有两块虚拟硬盘，大小均为 20GB，其中“/dev/vda”已经设置了几个分区，而“/dev/vdb”还未使用。</p>
<p>选中“/dev/vda”并按回车，然后进入“Reformat”选项。此时会提示你，此操作会清除所有数据，确认即可。如果你的数据盘也有数据，也按此方式清除掉所有分区。</p>
<p>清除完成后，你会看到此界面。</p>
<p></p>
<h2>在硬盘上创建分区</h2>
<p>此时不要急着创建 RAID。再次选中“/dev/vda”，选择“Use As Boot Device”并确认。此操作是为了在系统盘上创建引导分区。</p>
<p>创建完成后，剩余空间是没法直接使用的，需要创建分区。选择可用空间，然后进入“Add GPT Partition”创建分区，空间留空（默认使用全部剩余空间），“Format”选择“Leave unformatted”。</p>
<p>然后，在数据盘上也创建一个分区，空间留空，“Format”同样选择“Leave unformatted”。</p>
<p></p>
<h2>创建 RAID 并分区</h2>
<p>在两个硬盘上创建好分区以后，回到分区界面，选择“Create software RAID (md)”，然后在弹出的界面中选中刚才在两块硬盘上创建的分区，类型选择 RAID 0，名称默认是“md0”，并确认。</p>
<p>此时你会在“AVAILABLE DEVICES”中看到刚才创建的 RAID 磁盘。选中并进入“Add GPT Partition”创建分区，“Format”选择“ext4”，“Mount”设置为“/”，即根目录。</p>
<p></p>
<p>完成后，可以看到如下界面。</p>
<p></p>
<p>选择“Done”，按回车就完成啦！此后系统会安装在由两块硬盘组成的 RAID 磁盘上。</p>
<h2>总结</h2>
<p>Linux 中的 RAID 磁盘可以创建在硬盘或者分区上。当硬盘上已有分区，但未占用全部空间时，必须先用剩余空间创建分区，然后使用该分区组成 RAID，而不能直接使用该分区的一部分。</p>
<p>Linux 的软 RAID 和 LVM 可以套娃，也就是说拿 RAID 或者 LVM 创建出来的磁盘（或者在上面创建的分区）继续创建 RAID 或者 LVM。</p>
<p>对于本文中的“/dev/vdb”，不分区直接用整块硬盘和“/dev/vda”中的分区组成 RAID 也是可行的。考虑到避免扩展硬盘大小时可能出现的错误，本文选择在该硬盘上也创建分区。</p>
<p>Linux 并不强制要求给硬盘（无论是否是 RAID 创建的磁盘）分区。直接把 RAID 出来的磁盘格式化成“ext4”并挂载也是可行的。也是为了避免扩展时出问题，本文在 RAID 磁盘上也创建了分区。</p>
<p>一般来讲，RAID0 比较危险，因为任何一块硬盘挂掉都会导致所有数据丢失。但是，由于我们用的是 IDC 的虚拟硬盘，安全性比直接用物理硬盘高，所以，因为 RAID0 而丢失数据的概率会小一些。当然，按时备份是非常重要的。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">将 Btrfs root 分区转换为 subvolume</title>
    <id>https://bleatingsheep.org/2022/04/29/%E5%B0%86-Btrfs-root-%E5%88%86%E5%8C%BA%E8%BD%AC%E6%8D%A2%E4%B8%BA-subvolume/</id>
    <link href="https://bleatingsheep.org/2022/04/29/%E5%B0%86-Btrfs-root-%E5%88%86%E5%8C%BA%E8%BD%AC%E6%8D%A2%E4%B8%BA-subvolume/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>Btrfs 是具有 subvolume、快照等高级功能的写入时复制（copy-on-write）文件系统。Fedora、Ubuntu 桌面版系统在安装到 Btrfs 分区时会为根目录 / 和 home 目录 /home 分别创建不同的 subvolume。但是，Ubuntu Server 默认不会创建任何 subvolume。本文介绍如何将 Btrfs 分区中的根目录转换为 subvolume。</p>
]]></summary>
    <content type="html"><![CDATA[<p>Btrfs 是具有 subvolume、快照等高级功能的写入时复制（copy-on-write）文件系统。Fedora、Ubuntu 桌面版系统在安装到 Btrfs 分区时会为根目录 / 和 home 目录 /home 分别创建不同的 subvolume。但是，Ubuntu Server 默认不会创建任何 subvolume。本文介绍如何将 Btrfs 分区中的根目录转换为 subvolume。</p>
<!-- more -->
<p>本文中使用的环境是 Ubuntu Server 22.04 LTS，已用 ISO 镜像安装到 Btrfs 分区，未安装其他软件。</p>
<h2>进入恢复模式</h2>
<p>启动系统并按住左 Shift 键，直到出现 GRUB 选择菜单，选择“Advanced options for Ubuntu”然后选择“Recovery mode”。</p>
<p></p>
<p>进入恢复模式后，如果界面被日志打碎，请用键盘上下键移动光标，直到可以看清选项，然后选择“root”，按两次回车进入。</p>
<p></p>
<p>Ubuntu 的恢复模式会默认挂载根目录。如果你用的是其他系统的恢复模式，可能需要手动挂载。手动挂载时推荐挂载到 /mnt 目录，同时修改下面相关命令的路径。此后不再赘述。</p>
<h2>修改 /etc/fstab</h2>
<p>进入恢复模式后，执行 <code>vi /etc/fstab</code> 编辑文件。打开后，将光标移动到根目录那一行，按两次“y”键复制，并按“p”键粘贴。</p>
<p>然后将第二行的挂载点改为“/home”，并在两行的挂载选项中分别添加“,subvol=@”和“,subvol=@home”。“@”和“@home”是 Ubuntu 风格的 subvolume 名。</p>
<p>同时可以添加“compress=zstd:1”选项来启用压缩。</p>
<p></p>
<h2>对 swap 文件的额外操作</h2>
<p>Btrfs 分区上的 swap 文件具有 nodatacow 属性，与 <code>snapshot</code> 等功能冲突。如果你的根目录中有 swap 文件，需要在 fstab 中注释掉，然后删掉文件。</p>
<p></p>
<h2>创建 subvolumes</h2>
<h3>为 / 创建 subvolume</h3>
<p>修改完 /etc/fstab 后，执行以下命令：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">cd</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">btrfs</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> subvolume</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> snapshot</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> .</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> @</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><h3>为 /home 创建 subvolume</h3>
<p>执行以下命令为 /home 创建 subvolume：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">btrfs</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> subvolume</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> create</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> @home</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">cp</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -a</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --reflink</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> home/.</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> @home/.</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p></p>
<h2>重启进入系统，执行最后的操作</h2>
<p>执行 <code>reboot</code> 重启，启动时按住左 Shift 键，出现 GRUB 选择菜单后，按“e”进入编辑模式，在“linux”开头的一行末尾添加“rootflags=subvol=@”，然后按 Ctrl+X 启动系统。</p>
<p></p>
<h3>检查是否挂载了正确的 subvolume</h3>
<p>系统启动后执行 <code>cat /proc/mounts</code>，检查是否挂载了正确的 subvolume。</p>
<p></p>
<h3>更新 GRUB 配置</h3>
<p>执行以下命令更新 GRUB：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> update-grub</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> grub-install</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --no-nvram</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/sda</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>其中 <code>/dev/sda</code> 是 GRUB 所在磁盘，一般就是根分区所在的磁盘。若有不同，请用实际的磁盘替代。</p>
<p>完成后，用 <code>sudo reboot</code> 重启，<strong>不要</strong>按住 Shift，直接进入系统，并再次检查是否挂载了正确的 subvolume。</p>
<p></p>
<h2>清理</h2>
<p>原来的文件还残留在根目录，要想清理，可以先用 <code>sudo mount /dev/sda2 /mnt</code> 挂载——其中 <code>/dev/sda2</code> 为根分区——然后转到 <code>/mnt</code> 目录并删除除了“@”和“@home”以外的所有文件。</p>
<p></p>
<p>要清理 /home 中的残留，执行：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">cd</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /mnt/@</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">rm</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -r</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> home/</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076">*</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><h3>重新创建 swap 文件</h3>
<p>请参考 <a href="https://askubuntu.com/questions/1206157/can-i-have-a-swapfile-on-btrfs/1206161#1206161" target="_blank" rel="noopener noreferrer">https://askubuntu.com/questions/1206157/can-i-have-a-swapfile-on-btrfs/1206161#1206161</a>。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://www.reddit.com/r/archlinux/comments/dyh95n/comment/f81j3k4/?utm_source=share&amp;utm_medium=web2x&amp;context=3" target="_blank" rel="noopener noreferrer">https://www.reddit.com/r/archlinux/comments/dyh95n/comment/f81j3k4/?utm_source=share&amp;utm_medium=web2x&amp;context=3</a></li>
<li><a href="https://www.reddit.com/r/Gentoo/comments/qlskjp/comment/hj52hc2/?utm_source=share&amp;utm_medium=web2x&amp;context=3" target="_blank" rel="noopener noreferrer">https://www.reddit.com/r/Gentoo/comments/qlskjp/comment/hj52hc2/?utm_source=share&amp;utm_medium=web2x&amp;context=3</a></li>
</ul>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">我想建立个人 wiki</title>
    <id>https://bleatingsheep.org/2024/03/05/%E6%88%91%E6%83%B3%E5%BB%BA%E7%AB%8B%E4%B8%AA%E4%BA%BA-wiki/</id>
    <link href="https://bleatingsheep.org/2024/03/05/%E6%88%91%E6%83%B3%E5%BB%BA%E7%AB%8B%E4%B8%AA%E4%BA%BA-wiki/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>我建立这个博客的初衷是记录一些网络上可能没有直接记载的信息，而不知道这些信息在某些时候可能会导致踩坑，或者要绕很多弯路才能找到最佳实践。所以，我的博客内容很少是单纯的内容整合，而是针对具体的问题提出网络上暂时不存在的解决方法或者讨论。但是，近来我发现，博客似乎无法很好地实现这个目标……</p>
]]></summary>
    <content type="html"><![CDATA[<p>我建立这个博客的初衷是记录一些网络上可能没有直接记载的信息，而不知道这些信息在某些时候可能会导致踩坑，或者要绕很多弯路才能找到最佳实践。所以，我的博客内容很少是单纯的内容整合，而是针对具体的问题提出网络上暂时不存在的解决方法或者讨论。但是，近来我发现，博客似乎无法很好地实现这个目标……</p>
<!-- more -->
<h2>博客存在的问题</h2>
<p>首先我来说一下为什么我认为博客无法很好地实现这个目标。一个很明显的表现是，近来我的博客完全断更了，虽然有我博士科研比较繁忙的原因，但是，在这段时间我也并不是完全没有在学习新的东西，最终却没把这些东西分享出来。我认为原因有几点，最关键的是，每篇博客都要至少把一件事情本身讲明白，而不是简单地贴一些配置上去。同时，我不想重复地写网上很容易搜到的信息，也希望保证信息的完整性和正确性。这就导致了写博客成为了一种相对较重的负担，因为每篇都需要进行相当的整理和验证，比如在全新的环境中验证是否可行等。结果就是，本来有些有分享价值的东西，我也有分享的意愿，但最终就是没能分享出来。</p>
<ul>
<li>当我每次尝试新东西时，我可以根据我的已有知识迅速理解它的原理和思想，同时提炼出一些实践做法。而要写成博客时，除了我所刚学到的东西，还需要把关联的背景知识进行一定的介绍，这样才能让读者更好地理解内容，才更有可能帮助到人。但是这样也会使得文章更加难产，比如我做了很多关于 SRS 直播的笔记，但是没有发出来。</li>
<li>有时我遇到了一些情况，进行一番探究已经得出了很有价值的结果，但是，针对这个问题，还值得更深层地探索。要写成一篇博客，我往往倾向于把问题研究清楚，再把整个来龙去脉展示给读者，同时确保细节也是正确的，避免传讹。这种想法可能会阻止我最终写出博客。例如，我曾经研究过 Proxmox VE 提供的各种存储方式是否正确地向底层设备下发了 trim (discard) 指令。结论之一就是我最想使用的 LVM-thin 在删除卷时未能正确下发。</li>
<li>关于以上问题，我也想过对自己不要要求那么高，只要能让遇到某种特定情况的人读到后可以解决手头的问题，就足够了。不过，很多情况下，其实只需要几句话就可以解决一个问题，比如只需要修改一项配置。有时用英语可以很容易地搜到相关内容。而我最终发现自己还是不希望自己的博客充满这样几句话就写完的文章，于是就不写了。</li>
</ul>
<!-- ### 博客需要详细介绍
当我每次尝试新东西时， -->
<!-- ### 我还没有完全理解某些东西 -->
<!-- ### 有时我的新内容相对网络上已有的内容是 minor improvement


### 不希望博客全是比较 trivial 的东西，比如配置文件 -->
<!-- ### 博客不方便关联相关知识
由于相关知识本身可能是网上已有的，读者可能需要背景或者自己上网找。
wiki 更灵活，可以放一些相关知识。

### 有些有英文资料的我就不愿意贴了 -->
<!-- ## 发现
### 我以为简单的东西不一定简单 -->
<!-- （别人实际不知道）
（实际没那么容易搜到，比如英语内容）
（例子：pve说明） -->
<h2>潜在的损失</h2>
<p>一直以来，我都低估了我没分享出来的这些内容的价值。我认为这些知识很大比例都是已经普遍知道的，但是最近在一些 QQ 群里交流，发现很多我认为比较基础，或者比较容易搜到的知识，依然有很多人不了解。</p>
<p>比如，最近我在家里搭建了 PVE 环境，并给几个人免费分享了虚拟机，于是我给他们写了一篇 <a href="https://notes.bleatingsheep.org/s/pve" target="_blank" rel="noopener noreferrer">PVE 客户机使用手册</a>供参考。当我发出去后，立刻有其他 PVE 用户表示自己并不了解我这篇文章中提到的一些 PVE 的细节。实际上，相关内容是可以很容易地在官方文档中找到的。</p>
<p>还有一次，一位群友问我自己托管 PostgreSQL 时应考虑哪些事情，以及有没有数据备份的脚本。而我确实有一系列管理 PostgreSQL 的脚本。这也让我萌生了把我积攒的各种脚本分享出来的想法。</p>
<p>虽然这类内容大多都可以在网上找到类似的，也不是很深，但是回想我初次接触时，理解它们并转化成适合自己场景的脚本也是费了一番工夫的。比如，PostgreSQL 中，使用 <code>pgdump</code> 导出备份是备份到文件还是走 stdout，这类取舍都是经过我的性能测试的。这些内容虽然不太好写成博客（因为几句话就能讲清楚），但是依然有分享的价值。</p>
<h2>解决方法：建立个人 wiki</h2>
<p>我本来想的是把积累的脚本、配置文件等分享出来，但是又一想，干脆建立一个个人 wiki，来分享我所学的各种方面的知识。相比通过博客分享，wiki 有以下几个好处：</p>
<ul>
<li>可以直接放脚本和配置等等，不需要每次都详细解释：Wiki 定位于持续地更新，而非一次发一篇相对完整的文章，因此，无论我的结论、进展如何，是否有精力把背景补充齐全，都可以有所分享。</li>
<li>可以关联相关的知识：Wiki 可以为知识建立一定的结构，方便查阅相关的内容，例如可以把 PostgreSQL 的放一起，关于 SRS 的放一起等等。尽管 wiki 可以有结构，但我并不以建立非常系统、完善的知识体系为目标，以免因此造成负担。</li>
<li>可以包含更多方面的内容：当前博客内容偏向技术，除了技术，我还有其他可以分享的东西，例如生活方面、基金交易等。新建的 wiki 定位于分享我所有方面的知识。</li>
</ul>
<h2>Wiki 的内容</h2>
<p>作为建立 wiki 的预告，我计划在 wiki 中包含以下几方面的内容。</p>
<ul>
<li>基金交易心得：介绍我对场内、场外基金的交易心得，如选基、何时买卖、简单的策略等。同时也可以介绍交易规则等。</li>
<li>配置、脚本分享：把我零散的各种脚本和配置文件分享出来，同时简要说明一些关键配置项。</li>
<li>最佳实践分享：分享一些我摸索出来的最佳实践，而不仅仅是单独的配置、脚本。</li>
</ul>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">用 C# 与 .NET 中的内插字符串组装消息</title>
    <id>https://bleatingsheep.org/2021/04/03/%E7%94%A8-C-%E4%B8%8E-NET-%E4%B8%AD%E7%9A%84%E5%86%85%E6%8F%92%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%BB%84%E8%A3%85%E6%B6%88%E6%81%AF/</id>
    <link href="https://bleatingsheep.org/2021/04/03/%E7%94%A8-C-%E4%B8%8E-NET-%E4%B8%AD%E7%9A%84%E5%86%85%E6%8F%92%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%BB%84%E8%A3%85%E6%B6%88%E6%81%AF/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<h2>背景</h2>
<h3>OneBot 及其消息格式</h3>
<p><a href="https://github.com/howmanybots/onebot" target="_blank" rel="noopener noreferrer">OneBot</a> 标准是从原酷 Q 平台的 CQHTTP 插件接口修改而来的通用聊天机器人应用接口标准。</p>
]]></summary>
    <content type="html"><![CDATA[<h2>背景</h2>
<h3>OneBot 及其消息格式</h3>
<p><a href="https://github.com/howmanybots/onebot" target="_blank" rel="noopener noreferrer">OneBot</a> 标准是从原酷 Q 平台的 CQHTTP 插件接口修改而来的通用聊天机器人应用接口标准。</p>
<!-- more -->
<p>OneBot 规定了<a href="https://github.com/howmanybots/onebot/blob/master/v11/specs/message/README.md" target="_blank" rel="noopener noreferrer">消息类型</a>的格式。</p>
<p>简而言之，例如我想发送一个图文混合消息，有<a href="https://github.com/howmanybots/onebot/blob/master/v11/specs/message/string.md" target="_blank" rel="noopener noreferrer">字符串</a>和<a href="https://github.com/howmanybots/onebot/blob/master/v11/specs/message/array.md" target="_blank" rel="noopener noreferrer">数组</a>两种方式。</p>
<h4>字符串方式</h4>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>看看我刚拍的照片[CQ:image,file=123.jpg]</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h4>数组方式</h4>
<div class="language-json line-numbers-mode" data-highlighter="shiki" data-ext="json" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-json"><span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">[</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">        "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">type</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">text</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">        "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">data</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> {</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">            "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">text</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">[第一部分]</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">        }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    },</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">        "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">type</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">image</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">        "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">data</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> {</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">            "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">file</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">123.jpg</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">        }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    },</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">        "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">type</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">text</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">        "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">data</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> {</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">            "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">text</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">图片之后的部分，表情：</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">        }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    },</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    {</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">        "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">type</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">face</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">        "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">data</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> {</span></span>
<span class="line"><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">            "</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965">id</span><span style="--shiki-light:#99841877;--shiki-dark:#B8A96577">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">123</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">        }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    }</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">]</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3>如何在 SDK 中设计发送消息的接口</h3>
<p>毫无疑问，如果直接接收字符串并按原样传送，开发者（SDK 的用户）就必须自己处理转义等复杂问题。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">public</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> async</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> Task</span><span style="--shiki-light:#999999;--shiki-dark:#666666">&#x3C;</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">MessageResponse</span><span style="--shiki-light:#999999;--shiki-dark:#666666">></span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> SendMessage</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">Endpoint</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> sendTo</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> string</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>可是，如果直接接收一个数组，会给开发者构建消息带来麻烦。</p>
<p>一个好方法是把消息封装成一个类，这就牵扯出如何拼接或组装消息的问题。</p>
<h2>组装消息的方式</h2>
<h3>重载 <code>+</code> 运算符</h3>
<p>最直接的想法就是，消息应该像字符串那样可以直接用 <code>+</code> 连接。C# 中的运算符重载可以做到这一点，效果类似下面这样。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> message</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">FromText</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">看看我刚拍的照片</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> +</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">FromImage</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">123.jpg</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>效果还不错，但是屏幕上充斥着大量类名和方法名，容易眼花。</p>
<h3>Fluent API</h3>
<p>还有一种设计方式是，类似 <code>StringBuilder</code>，用连续的方法调用拼接消息。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> message</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> new</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> MessageBuilder</span><span style="--shiki-light:#999999;--shiki-dark:#666666">().</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">AppendText</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">看看我刚拍的照片</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">).</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">AppendImage</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">123.jpg</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">).</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">ToMessage</span><span style="--shiki-light:#999999;--shiki-dark:#666666">();</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>这种方式依旧有同样的缺点。此外，直接构造消息的方法依然是必要的，这样会加重 SDK 开发者的负担。</p>
<h3>内插字符串</h3>
<p>可以利用 C# 的内插字符串实现消息组装。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">using</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> static</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994"> MessageSegmentBuilder</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> message</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">FromInterpolated</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">$"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">看看我刚拍的照片</span><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">Image</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">123.jpg</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)}</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>这种方式把一条消息中文字和图片的部分紧密地结合在一起，没有 SDK 的方法隔开，视觉上更紧密。对排版较复杂的消息效果更明显。</p>
<p>有人可能会问，这样不还是要用户自己处理转义吗。利用 C# 中内插字符串的特性，可以实现自动转义。</p>
<h2>C# 的内插字符串</h2>
<p>内插字符串最常见的用法是直接当字符串用，例如当你用 <code>var</code> 接收时，默认的类型就是 <code>System.String</code>。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> s</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> $"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">hello</span><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91">123</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"> // s is System.String</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>但是，当它作为参数传入时，却可以被接收者特殊处理。例如 EF Core 查询就有 <code>FromSqlInterpolated</code> 方法，<a href="https://docs.microsoft.com/en-us/ef/core/querying/raw-sql#passing-parameters" target="_blank" rel="noopener noreferrer">利用插值字符串传入参数</a>，由 EF Core 进行处理，可以避免 SQL 注入的问题。</p>
<p>查看文档，<a href="https://docs.microsoft.com/en-us/ef/core/querying/raw-sql#passing-parameters" target="_blank" rel="noopener noreferrer"><code>FromSqlInterpolated</code></a> 接收的是 <code>FormattableString</code> 类型的参数。那么这个 <code>FormattableString</code> 怎么用呢？</p>
<p>我们先看一下 <code>FormattableString</code> 的定义。</p>
<p></p>
<p>可以看到，这个方法有两个属性，还有一些方法，看起来是与参数有关的。我们写个代码测试一下。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">static</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> void</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> FormatInterpolated</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2E8F82;--shiki-dark:#5DA994">FormattableString</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    Console</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">WriteLine</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">ArgumentCount</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    Console</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">WriteLine</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Format</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    Console</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">WriteLine</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">GetArguments</span><span style="--shiki-light:#999999;--shiki-dark:#666666">().</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Length</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    Console</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">WriteLine</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">GetArgument</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91">0</span><span style="--shiki-light:#999999;--shiki-dark:#666666">).</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">GetType</span><span style="--shiki-light:#999999;--shiki-dark:#666666">());</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    Console</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">WriteLine</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">static</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> void</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> Main</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">string</span><span style="--shiki-light:#999999;--shiki-dark:#666666">[]</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> args</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span></span>
<span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">    var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> a</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 123.45</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    FormatInterpolated</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">$"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">hell{{o{a:#.#}w}}</span><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">a</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">ow</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>输出结果</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>2</span></span>
<span class="line"><span>hell{{o{0:#.#}w}}{1}ow</span></span>
<span class="line"><span>2</span></span>
<span class="line"><span>System.Double</span></span>
<span class="line"><span>hell{o123.5w}123.45ow</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>从这个结果可以得出这样的结论：</p>
<ul>
<li><code>Format</code> 属性返回的字符串是类似于在 <code>String.Format</code> 方法中传入的格式字符串。</li>
<li><code>{</code> 和 <code>}</code> 本身在插值字符串中需要双写转义，这种双写也会出现在 <code>Format</code> 属性中。</li>
<li><code>ArgumentCount</code> 属性指示此插值字符串包含几个参数。</li>
<li><code>GetArguments</code> 方法可以获取包含参数的数组，其长度与 <code>ArgumentCount</code> 属性的值相同。</li>
<li>即使同一个变量多次出现在插值字符串中，也会作为多个参数传入，而非一个参数出现多次。</li>
</ul>
<p>这样，在 <code>Message.FromInterpolated</code> 方法中，就可以通过接收 <code>FormattableString</code> 实例，并利用相关的属性和方法获取哪里是文本，哪里是 CQ 码。此外，还可以对其切割，以获取数组格式的消息。</p>
<h2>实现内插字符串组装消息需要注意的地方</h2>
<h3>转义问题</h3>
<p>内插字符串的 <code>Format</code> 属性会转义 <code>{</code> 和 <code>}</code>，当读到这两个字符时，并不一定就是属性所在的位置。例如 <code>{{0}}</code> 实际就没有用到任何属性，而是 <code>&quot;{0}&quot;</code> 这个字符串。</p>
<p>如果内部用数组格式实现消息，那只需注意上面的转义问题。如果用字符串格式实现消息，则还需要注意消息和 CQ 码中的转义。文本部分应该转义 <code>[</code>、<code>]</code> 和 <code>&amp;</code>，CQ 码部分应该转义 <code>[</code>、<code>]</code>、<code>,</code> 和 <code>&amp;</code>。</p>
<h3>参数的类型</h3>
<p>传入的参数类型不一定是 CQ 码，例如下面的例子。</p>
<div class="language-c# line-numbers-mode" data-highlighter="shiki" data-ext="c#" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-c#"><span class="line"><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">var</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> message</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> =</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> Message</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">FromIntropolated</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">$"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">At</span><span style="--shiki-light:#999999;--shiki-dark:#666666">(</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91">10000</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)}</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> Hello, 现在时间是 </span><span style="--shiki-light:#999999;--shiki-dark:#666666">{</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">DateTime</span><span style="--shiki-light:#999999;--shiki-dark:#666666">.</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">Now</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">HH</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">mm</span><span style="--shiki-light:#999999;--shiki-dark:#666666">}</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">);</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>其中第一个参数是 <code>At</code> 方法返回的值。这个 <code>At</code> 方法是 SDK 开发者定义的，应返回表示 CQ 码的一个实例。第二个参数是 .NET 自带的，用户只希望按照字符串处理。这样，在 <code>Message.FromIntropolated</code> 方法内部就应该判断每个参数是否是 CQ 码，然后分别进行不同的处理。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">用 GitHub Actions 和 SSH 部署静态网站</title>
    <id>https://bleatingsheep.org/2021/06/20/%E7%94%A8-GitHub-Actions-%E5%92%8C-SSH-%E9%83%A8%E7%BD%B2%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99/</id>
    <link href="https://bleatingsheep.org/2021/06/20/%E7%94%A8-GitHub-Actions-%E5%92%8C-SSH-%E9%83%A8%E7%BD%B2%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<h2>前言</h2>
<p>一直以来，我都用 Caddy 1 的 git 插件，配合 GitHub 上的 Webhook 部署静态网站。简单说就是，把 Hexo 或者 VuePress 生成的网页推到另一个分支上，用 Webhook 通知插件，然后插件拉取变更。由于 Caddy 1 已经停止维护，我想升级到 Caddy 2，但是 Caddy 2 没有官方 git 插件，第三方那个还要自己编译，我还想用 Docker 跑，也挺麻烦的。</p>
]]></summary>
    <content type="html"><![CDATA[<h2>前言</h2>
<p>一直以来，我都用 Caddy 1 的 git 插件，配合 GitHub 上的 Webhook 部署静态网站。简单说就是，把 Hexo 或者 VuePress 生成的网页推到另一个分支上，用 Webhook 通知插件，然后插件拉取变更。由于 Caddy 1 已经停止维护，我想升级到 Caddy 2，但是 Caddy 2 没有官方 git 插件，第三方那个还要自己编译，我还想用 Docker 跑，也挺麻烦的。</p>
<!-- more -->
<p>本文的重点是，在你已经用 GitHub Actions 生成静态网站的情况下，如何用 SSH 部署到你的服务器上，并避免不必要的数据传输。</p>
<h2>为什么不用现有的免费静态网站托管服务</h2>
<p>现在有很多免费托管静态网站的服务，但它们都或多或少都存在一些我不满意的地方。当然也有我就是想 self-host 的因素在。</p>
<h3>GitHub Pages</h3>
<p>不支持 IPv6，网络经常受到干扰，而且只能用于公开仓库，私有的要钱。</p>
<h3>Azure Static Web App</h3>
<p>东亚地区的机房在香港，速度很快。除了不支持 IPv6 都挺好的。如果你能接受不支持 IPv6 访问，那推荐使用。</p>
<h3>Cloudflare + GH Pages/Azure</h3>
<p>Cloudflare 外号 Slowflare，曾经非常 slow，现在稍好了点，也支持 IPv6。我不用的原因是，挂上 CDN 可能会影响后面的托管的服务申请 SSL 证书。另一个原因是，中国大陆会连到 Cloudflare 美国节点（至少我家的电信是），如果用 Azure 香港托管网站，相当于绕路了。</p>
<h3>Netlify</h3>
<p>Netlify 支持 IPv6，而且自己有全球 CDN，各方面都非常好，除了两点。</p>
<p>第一，Netlify 在中国大陆会解析到新加坡节点。据我观察，解析结果包含 Amazon 和 DigitalOcean 双栈共 4 个 IP 地址。Amazon 的机房延迟不错，但 DigitalOcean 会绕美国。也就是说，速度很大程度上取决于浏览器有没有“刚好”选对地址。</p>
<p>第二，Netlify 的 TTFB (Time to first byte) 很长，不管什么时候都高达三百毫秒，我怀疑是故意加上去的。</p>
<h2>产生想法</h2>
<p>一开始我是用 Hexo 内置的 <a href="https://hexo.io/docs/one-command-deployment#Git" target="_blank" rel="noopener noreferrer">Deploy</a> 指令把生成的网站推送到 git 上，后来就用 GitHub Actions 自动生成了。推送操作也从 Hexo 命令换成了 <a href="https://github.com/peaceiris/actions-gh-pages" target="_blank" rel="noopener noreferrer">peaceiris/actions-gh-pages</a>。</p>
<p>为了解决 Caddy 2 没有支持度较好的 git 插件的问题，我一开始想自己写一个服务器处理 Webhook，但是这样太麻烦了，配置、维护都麻烦。</p>
<p>我也考虑过用像 <a href="https://github.com/easingthemes/ssh-deploy" target="_blank" rel="noopener noreferrer">easingthemes/ssh-deploy</a> 这样的 Action 部署，但是由于要部署的内容都是实时生成的，哪怕没有变更，修改日期也会变，这类 Action 常用的 SFTP 和 rsync 很难很好地处理这种情况。虽然每次都完整地重传一遍问题也不大，但我还是希望每次只传输要更新的部分。</p>
<p>有一天我突然想到，为什么不直接让 CI 用 SSH 连到我服务器上运行 git 呢？这样不就什么都解决了吗？于是我找到了 <a href="https://github.com/appleboy/ssh-action" target="_blank" rel="noopener noreferrer">appleboy/ssh-action</a>，简单浏览后意识到，这就是我心目中的完美方案。不如说，以前一直没想到这一点，真的很奇怪。</p>
<h2>准备 git 命令</h2>
<p>在真正开始编写 CI 流程之前，先写一个能在服务器上跑的脚本，用来拉取更改。我希望这个脚本不会传输不必要的文件，能正确处理远程分支被强制覆盖的情况，并且尽可能地节约本地空间。</p>
<p>假定要把网站放在服务器的 <code>DEPLOY_DIR</code> 变量所代表的目录，远程仓库是 <code>REPO_URL</code>，分支是 <code>BRANCH</code>。</p>
<h3>初始化仓库</h3>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">mkdir</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -p</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> $DEPLOY_DIR</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">cd</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> $DEPLOY_DIR</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> init</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>之所以不用 <code>git clone</code>，是因为在仓库已经建立的情况下不能再次 <code>git clone</code>，但是可以再次 <code>git init</code>。</p>
<h3>添加远程仓库</h3>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">if</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remote</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> |</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> grep</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -q</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -P</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> '</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">^b11p-git-pages$</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">'</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> then</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    echo</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">Cleaning old remote</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remote</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remove</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> b11p-git-pages</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">fi</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remote</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> add</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -t</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> $BRANCH</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> b11p-git-pages</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> $REPO_URL</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>其中 <code>b11p-git-pages</code> 是要设置的 remote 名，可以任意设置。由于这个值不必随仓库而变化，就不用环境变量了。</p>
<p>这一步是为了 remote 能有正确的分支和地址。由于 <code>git remote add</code> 没法覆盖之前的，我只能先检测是否已经存在这个 remote，如果存在就删掉。之后就可以保证顺利添加了。</p>
<h3>拉取网站内容</h3>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> fetch</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -v</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --set-upstream</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> b11p-git-pages</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --depth</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 1</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --auto-gc</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> reset</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --hard</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remotes/b11p-git-pages/</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">$BRANCH</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>这一步算是我调试了最长时间的，因为它涉及了我三个目标中的两个：“不会传输不必要的文件，能正确处理远程分支被强制覆盖的情况。”</p>
<p>我一开始用的是 <code>git pull --depth 1</code>，但是我特意做了个测试，本地往远程 push 很多提交然后服务器拉取，本地 reset 到很早的一个版本，再交很多提交然后本地 push，服务器 pull，结果就出错了：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>> git pull --depth 1</span></span>
<span class="line"><span>remote: Enumerating objects: 7, done.</span></span>
<span class="line"><span>remote: Counting objects: 100% (7/7), done.</span></span>
<span class="line"><span>remote: Compressing objects: 100% (3/3), done.</span></span>
<span class="line"><span>remote: Total 4 (delta 2), reused 0 (delta 0), pack-reused 0</span></span>
<span class="line"><span>Unpacking objects: 100% (4/4), 742 bytes | 371.00 KiB/s, done.</span></span>
<span class="line"><span>From https://github.com/b11p/*Hidden Repository Name*</span></span>
<span class="line"><span> + *hash1*...*hash2* *Branch*        -> origin/*Branch*  (forced update)</span></span>
<span class="line"><span>fatal: refusing to merge unrelated histories</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3>清理</h3>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> reflog</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> expire</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --expire-unreachable=all</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --all</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --verbose</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> gc</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --prune=all</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remote</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remove</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> b11p-git-pages</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>拉取完成后，进行清理。清理主要有两部分：第一是，fetch 以后原来的提交还在本地保留着，而这些都不必要了。第二是，如果仓库是私有的，那 URL 里可能会包含 Token，清理掉更安全。</p>
<p>本来清理旧内容的语句只有 <code>git gc --prune=all</code>，但我发现这个命令并不能真正地把已经不需要的文件从服务器上删除，加上前一句才可以：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>> git gc --prune=all</span></span>
<span class="line"><span>Enumerating objects: 10, done.</span></span>
<span class="line"><span>Counting objects: 100% (10/10), done.</span></span>
<span class="line"><span>Compressing objects: 100% (10/10), done.</span></span>
<span class="line"><span>Writing objects: 100% (10/10), done.</span></span>
<span class="line"><span>Total 10 (delta 2), reused 0 (delta 0)</span></span>
<span class="line"><span>> git count-objects -H -v</span></span>
<span class="line"><span>count: 0</span></span>
<span class="line"><span>size: 0 bytes</span></span>
<span class="line"><span>in-pack: 10</span></span>
<span class="line"><span>packs: 1</span></span>
<span class="line"><span>size-pack: 26.81 MiB</span></span>
<span class="line"><span>prune-packable: 0</span></span>
<span class="line"><span>garbage: 0</span></span>
<span class="line"><span>size-garbage: 0 bytes</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这个例子说明，尽管几十 MB 的大文件在最新的提交上已经不存在了，却依然占了服务器的空间。</p>
<h2>准备服务器</h2>
<p>由于 SSH 连上之后可以任意执行命令，为了安全，需要创建一个低权限的账户，为其设置不同的 SSH 密钥。可使用 <code>adduser</code> 命令创建用户，此处不再赘述。</p>
<h2>配置 GitHub Actions</h2>
<h3>Secrets</h3>
<p>首先在 repo 设置里添加相关的 Secrets。我添加了“SSH_HOST、SSH_PORT、SSH_USERNAME、SSH_PRIVATE_KEY”四个。</p>
<h3>Yaml 流程</h3>
<div class="language-yaml line-numbers-mode" data-highlighter="shiki" data-ext="yaml" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-yaml"><span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> name</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> Deploy using SSH</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">      uses</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> appleboy/ssh-action@master</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">      env</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        DEPLOY_DIR</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /home/${{ secrets.SSH_USERNAME }}/www/*Folder*</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        BRANCH</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> *</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">Branch*</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        TOKEN</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ${{ secrets.GITHUB_TOKEN }}</span></span>
<span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD">        # REPO_URL: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">      with</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        host</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ${{ secrets.SSH_HOST }}</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        username</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ${{ secrets.SSH_USERNAME }}</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        port</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ${{ secrets.SSH_PORT }}</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        key</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ${{ secrets.SSH_PRIVATE_KEY }}</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        envs</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> DEPLOY_DIR,BRANCH,GITHUB_REPOSITORY,TOKEN</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        script_stop</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> true</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        script</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> |</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          REPO_URL=https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          mkdir -p $DEPLOY_DIR</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          cd $DEPLOY_DIR</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          git init</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          if git remote | grep -q -P '^b11p-git-pages$'; then</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">              echo "Cleaning old remote"</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">              git remote remove b11p-git-pages</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          fi</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          git remote add -t $BRANCH b11p-git-pages $REPO_URL</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          git fetch -v --set-upstream b11p-git-pages --depth 1 --auto-gc</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          git reset --hard remotes/b11p-git-pages/$BRANCH --</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          git reflog expire --expire-unreachable=all --all --verbose</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          git gc --prune=all</span></span>
<span class="line"><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">          git remote remove b11p-git-pages</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>上面是有关部署的部分，假定生成的网站已经被推送到了另一个分支。</p>
<p>被注释掉的一句是我踩的一个坑。在这种情况下，<code>${GITHUB_REPOSITORY}</code> 会被<strong>原样</strong>传入，也就是说会克隆 <code>https://github.com/${GITHUB_REPOSITORY}.git</code>，这样当然会失败。关于这个坑，可以参考 GitHub 文档中的<a href="https://docs.github.com/cn/actions/reference/environment-variables" target="_blank" rel="noopener noreferrer">环境变量</a>和<a href="https://docs.github.com/cn/actions/reference/context-and-expression-syntax-for-github-actions#contexts" target="_blank" rel="noopener noreferrer">上下文</a>。</p>
<h2>总结</h2>
<p>相比用 SFTP 或者 rsync 传输，SSH 执行 git 命令的主要好处就是没有改变的文件不会被传输。同时，git 仓库中可能会出现覆盖掉提交历史的情况，这时 Caddy 1 的 git 插件会工作不正常，因为它只是简单地 pull。为了这两点，我进行了反复测试。同时我也反复测试了文件更新或删除的情况，确保服务器不会保留旧文件。</p>
<p>由于 SSH 可以执行任意命令，这就使得此方式可以不局限于静态网站，更好地发挥 CI/CD 的威力。<s>对我来说主要是省钱，毕竟我想要的效果，Azure 或者 AWS 都有相应的服务能做到，就是太贵了，用不起罢了。</s></p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">用 GitHub Actions 和 Webhook 自动构建并部署 ASP.NET Core 应用</title>
    <id>https://bleatingsheep.org/2022/03/19/%E7%94%A8-GitHub-Actions-%E5%92%8C-Webhook-%E8%87%AA%E5%8A%A8%E6%9E%84%E5%BB%BA%E5%B9%B6%E9%83%A8%E7%BD%B2-ASP-NET-Core-%E5%BA%94%E7%94%A8/</id>
    <link href="https://bleatingsheep.org/2022/03/19/%E7%94%A8-GitHub-Actions-%E5%92%8C-Webhook-%E8%87%AA%E5%8A%A8%E6%9E%84%E5%BB%BA%E5%B9%B6%E9%83%A8%E7%BD%B2-ASP-NET-Core-%E5%BA%94%E7%94%A8/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>最近给自己的直播写了个弹幕后端，基于 ASP.NET Core 和 SignalR，实现非常简单。部署反而比预期麻烦很多，因为我之前自动部署的应用都是静态网站，这次要部署动态应用，就遇到了预料之外的问题。</p>
<p>最终，我选择了用 GitHub Actions 构建并推送到另一分支，然后触发 Webhook，由自己的服务器拉取构建好的新版本并重启应用更新。</p>
]]></summary>
    <content type="html"><![CDATA[<p>最近给自己的直播写了个弹幕后端，基于 ASP.NET Core 和 SignalR，实现非常简单。部署反而比预期麻烦很多，因为我之前自动部署的应用都是静态网站，这次要部署动态应用，就遇到了预料之外的问题。</p>
<p>最终，我选择了用 GitHub Actions 构建并推送到另一分支，然后触发 Webhook，由自己的服务器拉取构建好的新版本并重启应用更新。</p>
<!-- more -->
<h2>配置 Webhook 服务器</h2>
<p>这种东西当然不要自己写，我选了 <a href="https://github.com/adnanh/webhook" target="_blank" rel="noopener noreferrer">adnanh/webhook</a>。在服务器上下载好，编写一个简单的配置文件并运行，用浏览器或 curl 访问一下，观察日志及脚本副作用确定 Webhook 是否正常工作。</p>
<div class="language-yaml line-numbers-mode" data-highlighter="shiki" data-ext="yaml" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-yaml"><span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">-</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> id</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> redeploy-webhook</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">  execute-command</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">/var/scripts/redeploy.sh</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">  command-working-directory</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">/var/webhook</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">/path/to/webhook</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -hooks</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> hooks.yaml</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -verbose</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h2>配置 GitHub Actions</h2>
<p>只需要用 Actions 构建并推送到其他分支即可，可以参考<a href="https://github.com/b11p/bleatingsheep.Danmaku/blob/75b3ec4519ebf7789ec1533d896e14e21f8b0fa6/.github/workflows/dotnet.yml" target="_blank" rel="noopener noreferrer">此文件</a>，本文不再赘述。</p>
<h2>配置 GitHub Webhook</h2>
<p>在仓库设置中添加 Webhook，“Payload URL”填写 http://your-server:9000/hooks/redeploy-webhook，其中把“your-server”替换成你服务器的 IP 或域名，最后一部分改为你设置的 Webhook ID。“Content type”选择“application/json”，“Secret”填写一个秘密字符串，在下面选择“Just the push event.”。</p>

<p>添加后，GitHub 会自动触发一次，配置正确的情况下会出现绿勾，如果出现错误，请排查配置问题。</p>

<p>确认配置无误后，随便往存储库 push 点什么东西，触发 CI。回到管理 Webhook 页面，应该会看到又触发了两次 Webhook，一次是 push 代码，一次是 push 构建结果。Webhook 推送数据会用作 Webhook Server 配置的参考。</p>
<h2>更新 Webhook Server 配置</h2>
<p>参考推送内容和 adnanh/webhook 的文档，更新 Webhook Server 配置，确保只在构建结果分支 push 时触发脚本，同时对传入数据进行验证，防止脚本被恶意触发。</p>
<p>下面是我的配置：</p>
<div class="language-yaml line-numbers-mode" data-highlighter="shiki" data-ext="yaml" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-yaml"><span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">-</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> id</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> deploy-danmaku</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">  execute-command</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">/home/user/danmaku.sh</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">  command-working-directory</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">/home/user</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">  pass-arguments-to-command</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> source</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> payload</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">      name</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> repository.clone_url</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> source</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> payload</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">      name</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> head_commit.id</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">  trigger-rule</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    and</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">      -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> match</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          type</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> payload-hmac-sha1</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          secret</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> &#x3C; here is my secret ></span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          parameter</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">            source</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> header</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">            name</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> X-Hub-Signature</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">      -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> match</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          type</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> value</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          value</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> push</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          parameter</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">            source</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> header</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">            name</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> X-GitHub-Event</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">      -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> match</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          type</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> value</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          value</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> refs/heads/build</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">          parameter</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">            source</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> payload</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">            name</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ref</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>此配置的的意图是，当“build”分支有“push”事件，并且通过 HMAC SHA1 验证时，才触发脚本，同时从 Webhook 推送中提取存储库链接和 commit ID 信息传入脚本。</p>
<h2>编写更新脚本</h2>
<p>下面是我的脚本：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD">#!/bin/bash</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"># omitting stopping old server</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">DEPLOY_DIR</span><span style="--shiki-light:#999999;--shiki-dark:#666666">=$(</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665">realpath</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ~/danmaku</span><span style="--shiki-light:#999999;--shiki-dark:#666666">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">if</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> [</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">$1</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> !=</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> ""</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ]</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> &#x26;&#x26;</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> [</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">$2</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> !=</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> ""</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ];</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> then</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    echo</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">Webhook mode.</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    REPO_URL</span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">$1</span></span>
<span class="line"><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">    COMMIT_ID</span><span style="--shiki-light:#999999;--shiki-dark:#666666">=</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A">$2</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    mkdir</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -p</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> $DEPLOY_DIR</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    cd</span><span style="--shiki-light:#B07D48;--shiki-dark:#BD976A"> $DEPLOY_DIR</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> init</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    if</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remote</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> |</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> grep</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -q</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -P</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> '</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">^b11p-git-pages$</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">'</span><span style="--shiki-light:#999999;--shiki-dark:#666666">;</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> then</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">        echo</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">Cleaning old remote</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">        git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remote</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remove</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> b11p-git-pages</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">    fi</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remote</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> add</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> b11p-git-pages</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">$REPO_URL</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> fetch</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -v</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --set-upstream</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> b11p-git-pages</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --depth</span><span style="--shiki-light:#2F798A;--shiki-dark:#4C9A91"> 1</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">$COMMIT_ID</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> reset</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --hard</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">$COMMIT_ID</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> reflog</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> expire</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --expire-unreachable=all</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --all</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --verbose</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> gc</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --prune=all</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">    git</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remote</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> remove</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> b11p-git-pages</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">else</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    echo</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> "</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">Not webhook mode.</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span></span>
<span class="line"><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375">fi</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"># omitting start new server</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>此脚本会自动拉取对应 commit，并重新启动服务，应用新版本。</p>
<h2>用 systemd 自动启动 Webhook Server</h2>
<p>下面，用你喜欢的编辑器创建并编辑“~/.config/systemd/user/webhook.service”，编写 systemd 服务，以便开机自动启动 Webhook Server。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>[Unit]</span></span>
<span class="line"><span>Description=Webhooks</span></span>
<span class="line"><span></span></span>
<span class="line"><span>[Service]</span></span>
<span class="line"><span>Restart=on-failure</span></span>
<span class="line"><span>RestartSec=5s</span></span>
<span class="line"><span></span></span>
<span class="line"><span>WorkingDirectory=/home/user/webhooks</span></span>
<span class="line"><span>ExecStart=/home/user/webhooks/webhook -hooks /home/user/webhooks/hooks.yaml -hotreload</span></span>
<span class="line"><span></span></span>
<span class="line"><span>[Install]</span></span>
<span class="line"><span>WantedBy=default.target</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>运行下面的命令，启动服务，并设置开机自动启动：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">systemctl</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --user</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> enable</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> webhook.service</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">systemctl</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> --user</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> start</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> webhook.service</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">loginctl</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> enable-linger</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2>使用配置 HTTPS（可选）</h2>
<p>如果你不想明文 HTTP 通信，可以开启 HTTPS。既可以在 Webhook Server 配置中配置 HTTPS，也可以在反向代理中配置。此处不再赘述。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://davidauthier.com/blog/2017/09/07/deploy-using-github-webhooks/" target="_blank" rel="noopener noreferrer">Deploy using GitHub webhooks</a></li>
<li></li>
<li><a href="https://www.unixsysadmin.com/systemd-user-services/" target="_blank" rel="noopener noreferrer">systemd user services</a></li>
<li><a href="https://wiki.archlinux.org/title/systemd/User#Automatic_start-up_of_systemd_user_instances" target="_blank" rel="noopener noreferrer">Automatic start-up of systemd user instances</a></li>
</ul>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">用 Ubuntu Cloud Images 制作自己的云镜像（配置 cloud-init 的 NoCloud 数据源）</title>
    <id>https://bleatingsheep.org/2022/03/14/%E7%94%A8-Ubuntu-Cloud-Images-%E5%88%B6%E4%BD%9C%E8%87%AA%E5%B7%B1%E7%9A%84%E4%BA%91%E9%95%9C%E5%83%8F%EF%BC%88%E9%85%8D%E7%BD%AE-cloud-init-%E7%9A%84-NoCloud-%E6%95%B0%E6%8D%AE%E6%BA%90%EF%BC%89/</id>
    <link href="https://bleatingsheep.org/2022/03/14/%E7%94%A8-Ubuntu-Cloud-Images-%E5%88%B6%E4%BD%9C%E8%87%AA%E5%B7%B1%E7%9A%84%E4%BA%91%E9%95%9C%E5%83%8F%EF%BC%88%E9%85%8D%E7%BD%AE-cloud-init-%E7%9A%84-NoCloud-%E6%95%B0%E6%8D%AE%E6%BA%90%EF%BC%89/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>还是之前写过的，云厂商提供的系统镜像不好使的问题。之前写过，但是，由于各种原因（如系统内存不足、国内网络不稳定等），其适用性受到了限制。这次改用了 Ubuntu Cloud Images，可以直接用 dd 命令覆盖系统盘，然后重装系统。</p>
]]></summary>
    <content type="html"><![CDATA[<p>还是之前写过的，云厂商提供的系统镜像不好使的问题。之前写过，但是，由于各种原因（如系统内存不足、国内网络不稳定等），其适用性受到了限制。这次改用了 Ubuntu Cloud Images，可以直接用 dd 命令覆盖系统盘，然后重装系统。</p>
<!-- more -->
<h2>介绍 cloud-init 和 Ubuntu Cloud Images</h2>
<p><a href="https://cloud-init.io/" target="_blank" rel="noopener noreferrer">cloud-init</a> 是一个初始化云镜像的工具，同时也是一个自动化的配置工具。</p>
<p><a href="https://cloud-images.ubuntu.com/" target="_blank" rel="noopener noreferrer">Ubuntu Cloud Images</a> 是由 Ubuntu 官方提供的用于在公有云上运行的系统镜像。它在首次启动时会运行 <a href="https://cloud-init.io/" target="_blank" rel="noopener noreferrer">cloud-init</a> 初始化系统。</p>
<!-- 此博客选用 Ubuntu Cloud Images 是因为：1. 博主最熟悉 Ubuntu 2. Ubuntu Cloud Images 与 cloud-init 集成较好。我也试过用 [Fedora Cloud](https://alt.fedoraproject.org/cloud/) 镜像，尽管该镜像好像有 cloud-init 相关的配置文件，但是 cloud-init 似乎没有被执行，我仍在调查。 -->
<h2>如何制作自己的云镜像</h2>
<h3>系统准备</h3>
<p>要制作自己的云镜像，需要准备相应的环境。本文假设你使用最新的 Ubuntu 20.04 LTS 系统，已包含“kpartx”和“losetup”工具。</p>
<p>此外，你还需要“qemu-img”，可以通过 <code>sudo apt install qemu-utils</code> 安装需要的软件包。</p>
<p>为了将镜像传输到要安装的云服务器，你还需要配置 web 服务器。配置方法不再赘述。</p>
<p>要安装的机器需要可以启动救援模式，并且救援模式需要可以联网（至少需要可以连接到镜像所在机器）。</p>
<h3>下载 Ubuntu Cloud Images</h3>
<p>打开 <a href="https://cloud-images.ubuntu.com/" target="_blank" rel="noopener noreferrer">https://cloud-images.ubuntu.com/</a>，选择要安装的 Ubuntu 版本（如“focal”），点击“current”选择最新构建，然后根据架构选择要下载的文件。例如，最常见的 amd64 架构请下载“focal-server-cloudimg-amd64.img”。</p>
<h3>转换为 raw 文件</h3>
<p>下载到的镜像是 qcow2 格式的，需要转换为 raw 格式，才能被挂载、修改和使用。运行下面的命令转换为 raw 格式。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>qemu-img convert -f qcow2 -O raw focal-server-cloudimg-amd64.img focal-server-cloudimg-amd64.raw</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h3>挂载 raw 文件</h3>
<p>需要把 raw 文件映射为 loop 设备，然后挂载到系统上。</p>
<p>请参阅<a href="https://www.jianshu.com/p/0c8a471622df" target="_blank" rel="noopener noreferrer">《如何挂载.img格式的镜像》</a>，完成挂载。</p>
<p>需要注意的是，文中的“.img格式的镜像”，是指我们转换出来的 raw 文件，而<strong>不是</strong>下载到的后缀为“.img”的 qcow2 文件。</p>
<p><em>另外，Ubuntu Cloud Images 的系统分区是分区 1，其他系统可能略有不同。</em></p>
<p>举例来说，如果 <code>sudo losetup -f</code> 查到的空闲 loop 设备为 /dev/loop9，那么就执行以下命令：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> losetup</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/loop9</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> focal-server-cloudimg-amd64.raw</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> kpartx</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -av</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/loop9</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> mount</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/mapper/loop9p1</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /mnt</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p><em>如果你想安装其他系统镜像，可以用 <code>sudo fdisk -l /dev/loop9</code> 查看分区信息。</em></p>
<h3>修改 cloud-init 配置</h3>
<p>用 <code>cd /mnt/etc/cloud/cloud.cfg.d</code> 切换到 cloud.cfg.d 目录，<code>sudo nano 99-fake_cloud.cfg</code> 创建文件 99-fake_cloud.cfg，内容如下：</p>
<div class="language-yml line-numbers-mode" data-highlighter="shiki" data-ext="yml" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-yml"><span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"># CLOUD_IMG: This file was created/modified by the Cloud Image build process</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">system_info</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">   package_mirrors</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">     -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> arches</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> [</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">i386</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> amd64</span><span style="--shiki-light:#999999;--shiki-dark:#666666">]</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">       failsafe</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">         primary</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> http://archive.ubuntu.com/ubuntu</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">         security</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> http://security.ubuntu.com/ubuntu</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">       search</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">         primary</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">           -</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> http://azure.archive.ubuntu.com/ubuntu</span><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"> # 修改为你想使用的软件包源</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">         security</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> []</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">     -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> arches</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> [</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">armhf</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> armel</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> default</span><span style="--shiki-light:#999999;--shiki-dark:#666666">]</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">       failsafe</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">         primary</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> http://ports.ubuntu.com/ubuntu-ports</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">         security</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> http://ports.ubuntu.com/ubuntu-ports</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"># configure cloud-init for NoCloud</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">hostname</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> myhost</span><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"> # 修改为你的主机名</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">users</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">  -</span><span style="--shiki-light:#998418;--shiki-dark:#B8A965"> name</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> foobar</span><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"> # 修改为你的用户名</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    gecos</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> Foo B. Bar</span><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"> # 修改为你的全名</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    groups</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> [</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">adm</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> audio</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> cdrom</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> dialout</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> dip</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> floppy</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> lxd</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> netdev</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> plugdev</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> sudo</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> video</span><span style="--shiki-light:#999999;--shiki-dark:#666666">]</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    ssh_authorized_keys</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">      -</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77"> '</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">&#x3C;paste your public key here></span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">'</span><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"> # 修改为你的 SSH 公钥</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    sudo</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> [</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D">ALL=(ALL) NOPASSWD:ALL</span><span style="--shiki-light:#B5695977;--shiki-dark:#C98A7D77">"</span><span style="--shiki-light:#999999;--shiki-dark:#666666">]</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">    shell</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /bin/bash</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">chpasswd</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">  expire</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#1E754F;--shiki-dark:#4D9375"> true</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">  list</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span></span>
<span class="line"><span style="--shiki-light:#999999;--shiki-dark:#666666">    -</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> foobar:foobar</span><span style="--shiki-light:#A0ADA0;--shiki-dark:#758575DD"> # 修改为你的用户名和密码</span></span>
<span class="line"><span style="--shiki-light:#998418;--shiki-dark:#B8A965">datasource_list</span><span style="--shiki-light:#999999;--shiki-dark:#666666">:</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> [</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> NoCloud</span><span style="--shiki-light:#999999;--shiki-dark:#666666">,</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> None</span><span style="--shiki-light:#999999;--shiki-dark:#666666"> ]</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>修改完成后保存，用以下命令取消挂载：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> umount</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /mnt</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> kpartx</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -dv</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/loop9</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> losetup</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -d</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> /dev/loop9</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>其中把“/mnt”修改为你的实际挂载目录，把“/dev/loop9”修改为你的实际 loop 设备。</p>
<h3>安装 Ubuntu</h3>
<p>首先假定你已经把修改过的系统镜像放到 web 服务器上，镜像 url 为“http://web/image.raw”。在要安装的机器上进入救援模式，找到系统启动盘（如 /dev/sda，也可能为 /dev/vda、/dev/nvme0n1 等），然后执行以下命令：</p>
<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">wget</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> http://web/image.raw</span><span style="--shiki-light:#AB5959;--shiki-dark:#CB7676"> |</span><span style="--shiki-light:#59873A;--shiki-dark:#80A665"> sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> dd</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> of=/dev/sda</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>请注意硬盘需要有足够空间。Ubuntu Cloud Images 所需空间大约是 2.2G。完成后重启机器，等待初始化完成即可。</p>
<p>示例配置默认禁用密码登录，需要使用 ssh 密钥。首次登录会要求修改密码。</p>
<p>完成后，你就可以享受未经云厂商加料的原汁原味的系统了。</p>
<h3>定制镜像</h3>
<p>如果你想定制镜像（如安装某些软件、配置网络等），可以参考 cloud-init 的<a href="https://cloudinit.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer">文档</a>，包括<a href="https://cloudinit.readthedocs.io/en/latest/topics/examples.html" target="_blank" rel="noopener noreferrer">配置示例</a>、<a href="https://cloudinit.readthedocs.io/en/latest/topics/modules.html" target="_blank" rel="noopener noreferrer">模块</a>等，修改配置文件。这样，系统初始化时会进行对应配置，减少重复配置环境的工作量。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">解决 OpenWrt 无法在内网中开启 NAT-PMP 的问题</title>
    <id>https://bleatingsheep.org/2021/10/13/%E8%A7%A3%E5%86%B3-OpenWrt-%E6%97%A0%E6%B3%95%E5%9C%A8%E5%86%85%E7%BD%91%E4%B8%AD%E5%BC%80%E5%90%AF-NAT-PMP-%E7%9A%84%E9%97%AE%E9%A2%98/</id>
    <link href="https://bleatingsheep.org/2021/10/13/%E8%A7%A3%E5%86%B3-OpenWrt-%E6%97%A0%E6%B3%95%E5%9C%A8%E5%86%85%E7%BD%91%E4%B8%AD%E5%BC%80%E5%90%AF-NAT-PMP-%E7%9A%84%E9%97%AE%E9%A2%98/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>最近在路由器上刷了 OpenWrt，但是 OpenWrt 默认没有集成 UPnP 和 NAT-PMP 功能。我手动开启时遇到了一些问题，在此记录以帮助可能遇到同样问题的朋友。</p>
]]></summary>
    <content type="html"><![CDATA[<p>最近在路由器上刷了 OpenWrt，但是 OpenWrt 默认没有集成 UPnP 和 NAT-PMP 功能。我手动开启时遇到了一些问题，在此记录以帮助可能遇到同样问题的朋友。</p>
<!-- more -->
<h2>开启 UPnP 和 NAT-PMP</h2>
<p>首先说明一下我开启 UPnP 和 NAT-PMP 的步骤。</p>
<ol>
<li>执行以下命令安装对应的软件：<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">opkg</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> update</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">opkg</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> install</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> miniupnpd</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> luci-app-upnp</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div></li>
<li>编辑 <code>/etc/config/upnpd</code> 文件，修改下面几行<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>	option enabled          1</span></span>
<span class="line"><span>	option enable_natpmp    1</span></span>
<span class="line"><span>	option enable_upnp      1</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div></li>
<li>执行下列命令<div class="language-sh line-numbers-mode" data-highlighter="shiki" data-ext="sh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-sh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">/etc/init.d/miniupnpd</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> enable</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">/etc/init.d/miniupnpd</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> start</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">/etc/init.d/firewall</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> restart</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div></li>
</ol>
<h2>出现错误</h2>
<p>然而，完成配置，打开软件后，在 Web 控制台却找不到映射记录。查看日志却发现了错误信息。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>root@OpenWrt:/etc/init.d# logread | grep -i "\-pmp"</span></span>
<span class="line"><span>Wed Oct 13 08:49:37 2021 daemon.notice miniupnpd[1942]: Listening for NAT-PMP/PCP traffic on port 5351</span></span>
<span class="line"><span>Wed Oct 13 08:49:45 2021 daemon.notice miniupnpd[2717]: Listening for NAT-PMP/PCP traffic on port 5351</span></span>
<span class="line"><span>Wed Oct 13 08:58:23 2021 daemon.err miniupnpd[2717]: Failed to add NAT-PMP 58942 tcp->192.168.1.115:58942 'NAT-PMP 58942 tcp'</span></span>
<span class="line"><span>Wed Oct 13 08:58:23 2021 daemon.err miniupnpd[2717]: Failed to add NAT-PMP 58942 udp->192.168.1.115:58942 'NAT-PMP 58942 udp'</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2>部分解决</h2>
<p>经过查找，我发现了曾经有人针对此问题在 GitHub 上提了个 <a href="https://github.com/coolsnowwolf/lede/issues/5020" target="_blank" rel="noopener noreferrer">Issue</a>，尽管他是针对某个二次开发的固件提的。在 Issue 中提到了两条可能的原因：1. <a href="https://github.com/coolsnowwolf/lede/issues/5020#issuecomment-656967117" target="_blank" rel="noopener noreferrer">IPv6 的问题</a>；2. <a href="https://github.com/coolsnowwolf/lede/issues/5020#issuecomment-657367240" target="_blank" rel="noopener noreferrer">WAN 口 IP 是内网 IP 的问题</a>。很不幸，两条我都中了。</p>
<p>我验证发现，我的问题与 IPv6 没有关系。于是尝试了第二种方法，在 <code>/etc/config/upnpd</code> 配置文件中添加这么几行：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>	option use_stun '1'</span></span>
<span class="line"><span>	option stun_host 'stun.qq.com'</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>添加后，却继续遇到错误：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>Wed Oct 13 12:37:15 2021 daemon.err miniupnpd[1942]: resolve_stun_host: getaddrinfo(stun.qq.com, 3478, ...) failed : Try again</span></span>
<span class="line"><span>Wed Oct 13 12:37:15 2021 daemon.err miniupnpd[1942]: STUN: Performing STUN failed: Host is unreachable</span></span>
<span class="line"><span>Wed Oct 13 12:37:15 2021 daemon.err miniupnpd[1942]: Performing STUN failed. EXITING</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>看起来是 STUN 过程中出现了什么错误。我尝试了很久，都无法使 STUN 成功。好在，开启了此选项后，NAT-PMP 正常工作了。同时<a href="https://openwrt.org/docs/guide-user/firewall/upnp/upnp_setup" target="_blank" rel="noopener noreferrer">官方文档</a>也表明 UPnP 可能有安全风险，示例只开启了 NAT-PMP。我就先只启用 NAT-PMP，慢慢查找是哪里出了问题吧。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">给Chromium Edge设置任意缩放</title>
    <id>https://bleatingsheep.org/2021/03/24/%E7%BB%99Chromium-Edge%E8%AE%BE%E7%BD%AE%E4%BB%BB%E6%84%8F%E7%BC%A9%E6%94%BE/</id>
    <link href="https://bleatingsheep.org/2021/03/24/%E7%BB%99Chromium-Edge%E8%AE%BE%E7%BD%AE%E4%BB%BB%E6%84%8F%E7%BC%A9%E6%94%BE/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p><strong>警告：本文包含直接修改配置文件的危险操作，请谨慎模仿。</strong></p>
]]></summary>
    <content type="html"><![CDATA[<p><strong>警告：本文包含直接修改配置文件的危险操作，请谨慎模仿。</strong></p>
<!-- more -->]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">解决腾讯云轻量自带Ubuntu镜像不好使的问题</title>
    <id>https://bleatingsheep.org/2021/03/22/%E8%A7%A3%E5%86%B3%E8%85%BE%E8%AE%AF%E4%BA%91%E8%BD%BB%E9%87%8F%E8%87%AA%E5%B8%A6Ubuntu%E9%95%9C%E5%83%8F%E4%B8%8D%E5%A5%BD%E4%BD%BF%E7%9A%84%E9%97%AE%E9%A2%98/</id>
    <link href="https://bleatingsheep.org/2021/03/22/%E8%A7%A3%E5%86%B3%E8%85%BE%E8%AE%AF%E4%BA%91%E8%BD%BB%E9%87%8F%E8%87%AA%E5%B8%A6Ubuntu%E9%95%9C%E5%83%8F%E4%B8%8D%E5%A5%BD%E4%BD%BF%E7%9A%84%E9%97%AE%E9%A2%98/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>之前腾讯云搞了个轻量服务器免费升配活动，配合学生优惠，折合 432 元可以买到 4 年 2C4G6M 的国内机器，非常划算。</p>
<p>我装的是 Ubuntu 20.04 系统，ssh 连进去，随便敲敲就发现很多不好使的地方。</p>
]]></summary>
    <content type="html"><![CDATA[<p>之前腾讯云搞了个轻量服务器免费升配活动，配合学生优惠，折合 432 元可以买到 4 年 2C4G6M 的国内机器，非常划算。</p>
<p>我装的是 Ubuntu 20.04 系统，ssh 连进去，随便敲敲就发现很多不好使的地方。</p>
<!-- more -->
<h2>自带 Ubuntu 系统不好使的地方</h2>
<h3>bash 颜色和标题</h3>
<p>首先是颜色，bash 默认会使用彩色输出，这样可以很清晰地辨认出当前登录的用户名，以及是从哪里开始执行一个命令的，但是腾讯云的系统却只显示默认颜色，如图。</p>
<p></p>
<p></p>
<p>此外，正常安装的 Ubuntu 系统 ssh 上去，标题栏上会显示当前目录，或者正在执行的命令，而腾讯云的系统并不会显示。</p>
<p></p>
<h3>无法使用别名</h3>
<p>此外，当我尝试执行 <code>ll</code> 时，腾讯云的系统会提示 <code>ll: command not found</code>。这可能是缺少 <code>.bashrc</code> 导致的。</p>
<p>此后，我也安装了 acme.sh。用官方脚本安装完以后，按说应该自动创建 <code>acme.sh</code> 的别名，但却没有，不知道哪里出了问题。</p>
<h3>其他问题</h3>
<p>这个系统还有别的问题，比如第一次 <code>apt update</code> <code>apt upgrade</code> 时的巨量冲突、默认 motd 不包含可以升级的软件包数量等。这些问题我暂时还没有解决，但影响也不如上面两个严重。</p>
<h2>尝试自己安装系统</h2>
<p>诚然，我可以搜索我遇到的这些问题，然后一个个解决，但是这样太麻烦了，而且谁知道有没有别的暗坑，所以我决定重装系统。</p>
<p>由于腾讯云轻量不支持自己上传安装映像，我决定用以前看到的一个脚本重装。这个脚本本来只支持到 Ubuntu 18.04，我进行了改造，现在可以安装 Ubuntu 20.04 了。</p>
<p>结果我没有装成功。虽然腾讯云好像是基于 KVM 的，但如果服务商改了虚拟机的不知道什么地方，这个脚本还是很容易失败的。</p>
<h2>创建一个新用户绕开问题</h2>
<p>虽然没法完全解决，但我想，不用腾讯云已经建好的用户，而是自己建一个，会不会就没这问题了。</p>
<h3>创建用户并赋予 <code>sudo</code> 权限</h3>
<p>首先，添加用户，并按照提示设定密码，其余选项可以保持默认（留空）。以用户名为 <code>txuser</code> 为例，把下面命令中的 <code>txuser</code> 换成你想设置的用户名。</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> adduser</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> txuser</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>然后赋予 <code>sudo</code> 权限</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> usermod</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -aG</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> txuser</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>这时候，该用户已经被赋予了 <code>sudo</code> 权限，但是每次执行命令时还是需要输入密码。为了免掉这一步，编辑（或创建）<code>/etc/sudoers.d/90-cloud-init-users</code> 文件，在里面加上这么一行。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>txuser ALL=(ALL) NOPASSWD:ALL</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h3>切换用户并配置密钥（如有必要）</h3>
<p>现在尝试切换到刚刚创建的用户</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">su</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> -</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> txuser</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>如果出现下面的提示，则说明配置正确。</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>To run a command as administrator (user "root"), use "sudo &#x3C;command>".</span></span>
<span class="line"><span>See "man sudo_root" for details.</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>我们试一下能否正常使用 <code>sudo</code>。</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> apt</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> update</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>下面便可以尝试登录了，如果你想配置成密钥登陆，可以在这时配置。</p>
<h3>用 ssh 登录并禁止默认账户登录</h3>
<p>用你最喜欢的 ssh 客户端连上服务器，就可以用了。但是，为了安全起见，最好把之前的账户禁用或删除掉。</p>
<p>经过测试，如果自带账户你用的是密钥登录，那么无论用 <code>sudo usermod -L ubuntu</code>，还是 <code>sudo passwd -l ubuntu</code> 命令，都无法阻止登录。一个可行的办法是用 <code>sudo usermod ubuntu -s /sbin/nologin</code> 命令。</p>
<p>但是，你也可以用下面的命令直接删掉 ubuntu 用户。</p>
<div class="language-bash line-numbers-mode" data-highlighter="shiki" data-ext="bash" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-bash"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">sudo</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> userdel</span><span style="--shiki-light:#A65E2B;--shiki-dark:#C99076"> -r</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> ubuntu</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h2>总结</h2>
<p>众所周知，云服务商经常对系统镜像进行修改和定制，比如把软件源换成自己的镜像等等。如果你用的是某些不知名服务商的系统，那可能会遇到各种奇奇怪怪的问题，甚至可能有安全隐患。相比之下，Azure 等大厂的系统用着就舒服得多。</p>
<p>我本以为，腾讯云好歹也是正经商家，系统怎么也不会烂到哪去，结果没想到，它好的不改，怎么坏怎么来。</p>
<p>我有给服务配置双栈支持的习惯。如果服务器没有 IPv6 地址，我一般用 <a href="https://tunnelbroker.net/" target="_blank" rel="noopener noreferrer">HE 隧道</a> 或者 6to4 隧道支持 IPv6。记得以前用阿里云，配完了隧道却发现不通，原因是阿里云服务器默认关闭了 IPv6 协议栈。IPv6 协议栈本来是默认开启的，即使没有 IPv6 网络，也不应该关闭，因为某些应用可能需要这个协议。关闭本来不该关闭的功能，只会造成兼容性问题，给用户带来困扰。</p>
<p>希望某些云服务商把精力放在对的地方，不要把系统瞎改一通，没有任何好处，只能让用户难受。</p>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
  <entry>
    <title type="text">部署MongoDB的过程记录</title>
    <id>https://bleatingsheep.org/2021/06/01/%E9%83%A8%E7%BD%B2MongoDB%E7%9A%84%E8%BF%87%E7%A8%8B%E8%AE%B0%E5%BD%95/</id>
    <link href="https://bleatingsheep.org/2021/06/01/%E9%83%A8%E7%BD%B2MongoDB%E7%9A%84%E8%BF%87%E7%A8%8B%E8%AE%B0%E5%BD%95/"/>
    <updated>2026-05-17T12:31:16.000Z</updated>
    <summary type="html"><![CDATA[<p>此篇文章记录我部署 MongoDB 的过程，以及我遇到的一些问题，方便以后查阅，但是仅涉及最基础的内容，价值不高，我建议你不要浪费时间阅读。</p>
]]></summary>
    <content type="html"><![CDATA[<p>此篇文章记录我部署 MongoDB 的过程，以及我遇到的一些问题，方便以后查阅，但是仅涉及最基础的内容，价值不高，我建议你不要浪费时间阅读。</p>
<!-- more -->
<h2>背景</h2>
<p>最近开始和别的实验室合作研究，由对方提供数据。对方说传输数据最简单的方法就是，我建一个 MongoDB 实例，然后他用 <code>db.copyDatabase()</code> 把数据复制过来。</p>
<p>部署服务器的任务当然是交给我做了。</p>
<h2>建立服务器</h2>
<p>嗯，建立过程很简单，在 AWS 上建 EC2 实例。助理教授让我不要选东京，而是选美国东部，我刚想问是不是因为离对方近，结果他说，因为东京太贵了。</p>
<p>然后是选实例类型，我们商量了半天，最后选了个 ARM 类型的机器，配置很高。助教还想用 200G 系统盘，真是奢侈，我觉得 20G 都够了。最后改成了 50G。其实我觉得配置都不用那么高，刚省的钱就又扔进去了！</p>
<p>由于数据比较大，用 HDD 比较合适，就也挂载了一个 2TB 的 HDD，估计都挺贵的。助教说他比较熟悉 CentOS，但我比较熟悉 Ubuntu，最后选了 Ubuntu，他说如果出了问题，那他帮不了我。</p>
<p>装好系统以后先 <code>apt upgrade</code>，然后给 HDD 分区，格式化，挂载。本想一起配一下 swapfile，但是一看物理内存的量，得，拿来当内存盘还差不多，做锤子 swapfile。</p>
<h2>安装 MongoDB</h2>
<h3>在 Ubuntu 服务器安装</h3>
<p>安装过程也没什么难的，按照<a href="https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/" target="_blank" rel="noopener noreferrer">文档</a>一步一步装就是了。由于我们选的是 ARM 实例，我还特意确认了一下支不支持 ARM64，是支持的。</p>
<p>装完了应该是最新的 4.4 版。</p>
<h3>在 Mac 上安装</h3>
<p>Mac 也有<a href="https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/" target="_blank" rel="noopener noreferrer">文档</a>。由于我已经有开发者工具和 Homebrew 了，就主要是运行了这两个命令：</p>
<div class="language-zsh line-numbers-mode" data-highlighter="shiki" data-ext="zsh" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-zsh"><span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">brew</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> tap</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> mongodb/brew</span></span>
<span class="line"><span style="--shiki-light:#59873A;--shiki-dark:#80A665">brew</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> install</span><span style="--shiki-light:#B56959;--shiki-dark:#C98A7D"> mongodb-community@4.4</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>由于我不在 Mac 上跑这个服务，只用来连接，所以无需启动服务。安装完成后，运行 <code>mongo</code>，提示：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>zsh: command not found: mongo</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>嗯……只能自己找找安装在哪儿了。</p>
<p>问了群友，然后在 <code>/usr/local/Cellar/mongodb-community@4.4/4.4.5/bin</code> 找到了可执行文件，就在这里执行 <code>./mongo</code> 好了。</p>
<p>我也考虑过手动添加到 PATH，但是看到这个带着版本号的路径，想到一升级就得重新添加，就以后再说吧。</p>
<h4>更新</h4>
<p>后来我直接又执行了一遍 <code>brew install mongodb-community@4.4</code>，就可以直接用 <code>mongo</code> 了，Mac 真怪，还是 Windows 好用。</p>
<h2>测试连接</h2>
<p>在服务器上执行 <code>mongo</code>，然后就可以操作了。但是如何直接连到数据库上呢？毕竟直接给别人 SSH 密钥可能不太好，而且他要执行的那个命令也不一定可以通过 SSH。</p>
<p>然后我找到了<a href="https://medium.com/@Hardy2151/how-to-connect-to-your-remote-mongodb-server-68725a8e53f" target="_blank" rel="noopener noreferrer">这篇文章</a>。它是好用的，但是第一个指令两行粘在一起了，这导致我一开始进错了数据库，老是连接失败。都是他的错。</p>
<p>远程连接数据库似乎有两种命令，一种是用参数的，还有一种是用连接字符串。以前面那篇文章建立的数据库和用户为例，两种命令分别是</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>./mongo -u hardeep -p yourSecretPassword hostname/animals</span></span>
<span class="line"><span>./mongo "mongodb://hardeep@hostname:27017/animals"</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><h2>远程管理数据库</h2>
<p>现在可以连接数据库了，但是只能连接一个低权限的账号。为了方便我自己管理，似乎最好有办法远程连入管理账号。</p>
<p>按经验，在 Ubuntu 上执行 <code>mongo</code> 命令之后应该是用的类似 admin 用户之类的东西，但我不知道默认用户名是什么，于是搜索，找到 <a href="https://stackoverflow.com/questions/38921414/mongodb-what-are-the-default-user-and-password" target="_blank" rel="noopener noreferrer">MongoDB what are the default user and password?</a>。</p>
<p>按照最高票回答的说法，默认是没有访问控制的，也没有什么默认用户名和密码。但那篇回答给出了创建管理员的步骤。</p>
<p>由于在前面，为了远程访问，我开启了访问控制，所以创建管理员之前需要先临时关闭一下。</p>
<h2>添加用户并配置权限</h2>
<p>接下来应该就是给合作者提供一个可以访问数据库的账号了。</p>
<p>我想参考前面的<a href="https://medium.com/@Hardy2151/how-to-connect-to-your-remote-mongodb-server-68725a8e53f" target="_blank" rel="noopener noreferrer">这篇文章</a>，改一改用户名和数据库名就好，但是我不确定 <code>readWrite</code> 角色的权限是否足够。由于不了解 MongoDB 的具体实现，我不知道什么角色的权限高，就继续搜索。</p>
<p>我搜索了“mongodb grant all permissions to user”，然后在第四五个结果中找到了 <a href="https://docs.mongodb.com/manual/reference/built-in-roles/" target="_blank" rel="noopener noreferrer">Built-In Roles</a>，但是这一堆乱七八糟的谁看得懂啊！我灵机一动，干脆直接搜索“userAdminAnyDatabase”，又找到了同一个页面的一个警告。</p>
<p></p>
<p>大意是，如果给用户授予“admin”表的 <code>userAdmin</code> 角色，实际上是间接提供了超级用户的访问权限，因为用户可以给自己授予 <code>userAdminAnyDatabase</code>。</p>
<p>看到这，我就想到，诶？是不是可以在我想提供的数据库上创建一个有 <code>userAdmin</code> 角色的用户？点开看看 <code>userAdmin</code> 好了。</p>
<p>随便上下滚动两下，我又看到了 <code>dbOwner</code>，相当于 <code>readWrite</code>、<code>dbAdmin</code> 和 <code>userAdmin</code> 的结合。这不就是我要的权限吗！</p>
<p>然后我就创建了一个数据库，并且在那个数据库上创建了一个 <code>dbOwner</code> 的用户。</p>
<h2>移动数据存储位置</h2>
<p>刚想把创建好的用户和数据库发给对方，突然想起来数据还存在默认位置，在那个 50G 的 SSD 上，大概空间不够用。</p>
<p>于是我查了“mongodb where to store data”，发现配置文件写了，默认是 <code>/usr/local/var/mongodb</code>。我停止服务，把数据移到 HDD，修改配置文件，再启动。</p>
<p>启动之后，我突然想到，我应该只移数据，然后用个软链接链过去，免得改错了配置。不过既然已经改了，就那样吧，简单连了一下，似乎没有问题。</p>
<p>然后就把地址端口账号密码数据库发过去了。</p>
<h2>总结</h2>
<p>唔……感觉没什么好总结的。硬要说的话，就是 MongoDB 的用户和权限系统吧。默认是不需要，也没有什么用户名和密码的，连上去就是完全的权限。这时候可以建好需要的用户，授予对应权限，然后就可以用配置文件或者其他什么方式开启认证，这时候就可以给别人访问啦！比起 MySQL 等关系型数据库可以针对不同用户允许不同的 IP，MongoDB 的方式似乎更简单一点。</p>
<p>另外我在部署的时候也没遇到什么创建数据库的操作，不知道 <code>use</code> 的时候会不会自动创建，还是 MongoDB 是否有其他的机制。不过给他的用户应该授予过必要的权限，让他导入数据吧w。</p>
<p>然后就是加密，不知道 MongoDB 默认会不会给连接加密。简单搜了一下好像有<a href="https://docs.mongodb.com/manual/tutorial/configure-ssl/" target="_blank" rel="noopener noreferrer">相应的配置</a>，但还是以后再说吧，这种东西细究起来要做的太多啦，先让对方把数据传过来要紧。</p>
<p>再然后，对方提到他要用 <code>db.copyDatabase()</code> 这个命令，可是<a href="https://docs.mongodb.com/manual/reference/method/db.copyDatabase/" target="_blank" rel="noopener noreferrer">官方文档</a>说 4.0 开始已经不推荐使用这个命令了，而是要参考<a href="https://docs.mongodb.com/database-tools/mongodump/#std-label-mongodump-example-copy-clone-database" target="_blank" rel="noopener noreferrer">mongodump</a>。不知道他用的什么版本，算了，他复制不了再说吧。</p>
<p>这些只是我目前的理解，不一定对。</p>
<h2>补充：<code>resotre</code> 权限</h2>
<p>发给对方后，对方说没有 <code>restore</code> 权限，要求授予。emmmm，我觉得 <code>dbOwner</code> 应该就包含所有权限了啊，为什么会恢复不了呢？不过先在他的数据库上授予一下这个权限吧。用高权限的用户登录，执行了下面的命令：</p>
<div class="language- line-numbers-mode" data-highlighter="shiki" data-ext style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212"><pre class="shiki shiki-themes vitesse-light vitesse-dark vp-code"><code class="language-"><span class="line"><span>use database_name</span></span>
<span class="line"><span>db.grantRolesToUser("user_name", ["restore"])</span></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><p>结果提示“No role named restore@database_name”。</p>
<p>emmmm，难不成 <a href="https://docs.mongodb.com/manual/reference/built-in-roles/#mongodb-authrole-restore" target="_blank" rel="noopener noreferrer"><code>restore</code></a> 要授予在“admin”上？我在“admin”上执行相同命令，却又提示没有这个用户。</p>
<p>按经验判断，我在“admin”上创建同名用户的话，那应该会和之前的是两个用户，甚至密码也可以设置成不一样的。这样应该解决不了问题。那么怎么办呢？</p>
<p>我搜索“mongodb grant restore”，没有找到有用的结果，想了想，直接搜错误提示，“No role named restore@”，第一条 <a href="https://stackoverflow.com/questions/51452733/mongodb-no-role-named-restore" target="_blank" rel="noopener noreferrer">mongodb no role named restore</a> 就是有用的结果。</p>
<p>于是我执行了 <code>db.grantRolesToUser(&quot;user_name&quot;,[{role: &quot;restore&quot;, db: &quot;admin&quot;}])</code>，问题解决。</p>
<h3>关于当前我的理解的总结</h3>
<p>似乎用户必须是创建在某个数据库上，然后也可以授予关于别的数据库的权限。在“admin”上创建的用户似乎一般有一定的管理权限。某些角色，比如 <code>restore</code>，是只有“admin”才有的，其他数据库没有，也没法授予。</p>
<p>我怀疑这个授予在“admin”数据库的角色会有其他的权限，<a href="https://docs.mongodb.com/manual/reference/built-in-roles/#mongodb-authrole-restore" target="_blank" rel="noopener noreferrer">官方文档</a>我也没看懂（好吧，我没仔细看，现阶段又无所谓w）。</p>
<p>另外，即使开启了认证，也可以直接连，然后用 <a href="https://docs.mongodb.com/manual/reference/method/db.auth/" target="_blank" rel="noopener noreferrer"><code>db.auth()</code></a> 进行认证，而不是必须在连接时指定数据库、用户名和密码。</p>
<p>P.S: 传输数据的时候我 SSH 上去看了一下，内存竟然吃了一半，CPU 也吃了一小半。助教真是英明，选的配置刚刚好x。</p>
<p></p>
<p>想了想，其实是我自己的服务器太丐了，内存只有 1GB 到 2GB，毕竟我没钱嘛。</p>
<h2>参考资料</h2>
<p>上文没提到，但我查到的并且可能有用的参考资料。</p>
<blockquote>
<p><a href="https://www.digitalocean.com/community/tutorials/how-to-configure-remote-access-for-mongodb-on-ubuntu-20-04" target="_blank" rel="noopener noreferrer">How To Configure Remote Access for MongoDB on Ubuntu 20.04</a></p>
<p><a href="https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mongo-app?view=aspnetcore-5.0&amp;tabs=visual-studio" target="_blank" rel="noopener noreferrer">Create a web API with ASP.NET Core and MongoDB</a></p>
<p><a href="https://docs.mongodb.com/manual/tutorial/enable-authentication/" target="_blank" rel="noopener noreferrer">Enable Access Control</a></p>
<p><a href="https://docs.mongodb.com/manual/reference/method/db.grantRolesToUser/" target="_blank" rel="noopener noreferrer">db.grantRolesToUser()</a></p>
</blockquote>
]]></content>
    <author>
      <name>bleatingsheep</name>
    </author>
    <contributor>
      <name>bleatingsheep</name>
    </contributor>
    <published>2026-05-17T12:31:16.000Z</published>
    <rights>Copyright by bleatingsheep</rights>
  </entry>
</feed>