<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>kpmark</title>
  
  <subtitle>人與人總是在不聞不問的日子里慢慢告別</subtitle>
  <link href="https://kpmark.icu/atom.xml" rel="self"/>
  
  <link href="https://kpmark.icu/"/>
  <updated>2025-12-09T01:30:25.610Z</updated>
  <id>https://kpmark.icu/</id>
  
  <author>
    <name>kpmark</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>浅读 GS-meta</title>
    <link href="https://kpmark.icu/2025/12/10/GS-Meta/"/>
    <id>https://kpmark.icu/2025/12/10/GS-Meta/</id>
    <published>2025-12-10T03:43:47.000Z</published>
    <updated>2025-12-09T01:30:25.610Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="GS-meta"><a href="#GS-meta" class="headerlink" title="GS-meta"></a>GS-meta</h1><h2 id="前置"><a href="#前置" class="headerlink" title="前置"></a>前置</h2><p>MAML，GNN</p><h2 id="Abstract"><a href="#Abstract" class="headerlink" title="Abstract"></a>Abstract</h2><p>预测分子性质基于有限数据，普遍被视为小样本</p><p>但是分子性质与分子关联是多对多，所以需要给予图采样，本文包含三个核心步骤</p><ul><li>构建分子性质关系图（MPG）</li><li>基于 MPG 重新定义元学习中的任务</li><li>使用对比损失调度图采样过程</li></ul><h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><p><strong>有一篇 Meta-GNN 的论文</strong>，Meta-GNN [Guo et al., 2021]</p><p>总而言之，是一个在分子性质上应用元学习和图采样的方法工程</p><p><img src="/blog/../images/GS-Meta/image-20251125095620007.png" alt="image-20251125095620007"></p><h2 id="Related-Work"><a href="#Related-Work" class="headerlink" title="Related Work"></a>Related Work</h2><ul><li>Few-shot Learning for Molecules</li><li>Episode Scheduler in Meta-learning</li></ul><h2 id="Graph-Sampling-based-Meta-Learning"><a href="#Graph-Sampling-based-Meta-Learning" class="headerlink" title="Graph Sampling-based Meta-Learning"></a>Graph Sampling-based Meta-Learning</h2><p>给出了元学习结构如图</p><p><img src="/blog/../images/GS-Meta/image-20251125100017866.png" alt="image-20251125100017866"></p><p>手动构建 MPG，然后进行随机采样，构建任务片段池，用子图采样器计算选择概率，然后选择包含 B 个任务片段的 mini_batch</p><p>这里子图是一个数据，一组子图是一个任务，一个 mini_batch 中含有多个任务</p><h3 id="Problem-Definition"><a href="#Problem-Definition" class="headerlink" title="Problem Definition"></a>Problem Definition</h3><p>这里有$\mathcal{D}<em>{\text{train}} \ \mathcal{D}</em>{\text{test}}$，包含不同的<strong>任务</strong></p><p>训练性质和测试性质不相交，即测试中需要预测的完全是新的性质</p><blockquote><p>[!NOTE]</p><p>这里，任务和性质一一对应，模型每次输出的是分子在这个性质上的表现</p></blockquote><p>这里元学习的外层循环使用 mini_batch 进行训练，而内侧循环使用了 Episodic Training（情景训练）</p><blockquote><p>[!NOTE]</p><p>注意这里的 mini_batch 是对不同任务的划分，和传统的 mini_batch 区别</p></blockquote><p>内层循环，即对于一个单独的任务来说，采样一个训练集$\mathcal{S}_t$和一个测试集$\mathcal{Q}_t$</p><p>通常，训练集包含两个类别，分别是活跃与非活跃，每个类别包含 K 个分子，测试集包含 M 个分子</p><p>这里情境定义为$E &#x3D; (\mathcal{S}_t, \mathcal{Q}_t)$</p><h3 id="Molecule-Property-Relation-Graph"><a href="#Molecule-Property-Relation-Graph" class="headerlink" title="Molecule-Property Relation Graph"></a>Molecule-Property Relation Graph</h3><p><strong>构建分子-性质关系图</strong>，图表示为$\mathcal{G} &#x3D; (\mathcal{V}, \mathcal{E})$，表示节点集金和边集的二元组</p><p>有两种节点，分别是分子节点$\mathcal{V}_M &#x3D; \left{  x_m\right}$和性质节点$\mathcal{V}<em>T &#x3D; \left{  p_t\right}$，边集是二者的笛卡尔积$\mathcal{E} &#x3D; {e</em>{ij}}$，有两种标签<strong>活跃</strong>与<strong>非活跃</strong></p><p>对于一个分子$x_i$，使用一个基于图的编码器获得其嵌入表示</p><p>$$<br>x_i &#x3D; f_{mol}(x_i)<br>$$</p><p>性质节点的嵌入表示被随机初始化，长度与分子嵌入相同</p><blockquote><p>[!NOTE]</p><p>这里的嵌入表示为将图表示为向量</p></blockquote><p>节点特征表示为</p><p>$$<br>h_0^i &#x3D; \begin{cases}<br>p_t \ \ \text{if} \ i \in \mathcal{V}_T \<br>x_i \ \ \text{if} \ i \in \mathcal{V}_M<br>\end{cases}<br>$$</p><p>数据缺失的边类型使用 unk</p><p>这个图很大，所以肯定不可能直接进行预测，所以要采样子图，也就是手动构建任务</p><p>对于任务$\mathcal{T}_t$，构建如下情景</p><p>$$<br>\mathcal{S}<em>{t} \sim \mathcal{G}</em>{t}^{S}&#x3D;\left{\left(x_{i}, e_{i,t},p_{t}\right), y_{i,t}\mid x_{i}\in\mathcal{N}\left(p_{t}\right)\right}_{i&#x3D;1}^{2K}<br>$$</p><p>简单讲就是某个性质的相邻节点，这里的$\mathcal{N}(p_t)$表示邻居节点</p><p>同样，查询集如下</p><p>$$<br>\mathcal{Q}_t \sim \mathcal{G}<em>t^Q &#x3D; {(x_j, p_t), y</em>{i,t} | x_j \in \mathcal{N}(P_t)}<br>$$</p><p>有情景如下</p><p>$$<br>E_t \sim \mathcal{G}_t &#x3D; \mathcal{G}_t^s \cup  \mathcal{G}_t^Q<br>$$</p><p>这里把两个子图合并，就是整个情景</p><p>这里的问题在于，分子和性质的关系被局限于多对一，但是实际通常是多对多关系</p><p>在预测同一种分子的新性质时，可以利用其他可用性质，所以需要描述这种多对多关系</p><p>$$<br>\mathcal{G}<em>t^A &#x3D; { (x_i, e</em>{i,a}, p_a), y_{i,a} | p_a \in \mathcal{N}(x_i) \textbackslash p_t }<br>$$</p><p>$$<br>\mathcal{G}_t &#x3D; \mathcal{G}_t \cup \mathcal{G}_t^A<br>$$</p><blockquote><p>[!NOTE]</p><p>为什么要合并训练集和测试集的子图？</p><p>因为需要通过图结构（边的关系等）来进行预测，这也解释为什么测试集没有边：只能通过已知节点的关系来推断未知节点性质</p></blockquote><p>使用<strong>消息传递机制</strong>对采样子图进行迭代更新，这里不仅考虑分子和性质节点之间的关系，分子与分子间也有关系</p><p>$$<br>\alpha^l_{i,j} &#x3D; \sigma (\text{MLP}(\exp(- | h_i^{l-1} - h^{l-1}_j | )))<br>$$</p><p>这里最外层时 sigmoid 函数，最内部时上一次迭代时两个分子的嵌入表示，结果是两个分子估计出来的连接权重，这个边的类型为<em>mol2mol</em></p><p>首先得跑一遍这个把完整的子图嵌入表示弄好，现在一共有四种边的类型了</p><p>然后就可以开始跑 GNN 了，这是内层循环的流程，当前讨论的都是针对某一个任务$\mathcal{T}_t$的</p><p>根据以下公式更新节点嵌入</p><p>$$<br>h_i^l &#x3D; \text{GNN}^{l-1}(h_i^{l-1}, h_j^{l-1}, h_{i,j}^{l-1}, w_{i,j}^l | j \in \mathcal{N}(i))<br>$$</p><p>当更新节点的边不是分子与分子之间是，权重指定为 1</p><p>$$<br>w_{i,j}^l &#x3D; \begin{cases}<br>a_{i,j}^l \ \ \text{if} \ e_{i,j} \in \mathcal{E}_{M,M} \<br>1\ \ \text{otherwise}<br>\end{cases}<br>$$</p><p>经过 L 次迭代之后，拼接所有节点的最终嵌入，来预测标签</p><p>$$<br>\hat{y}<em>{i,t} &#x3D; \sigma\left(f</em>{cls}\left(\left[\boldsymbol{h}<em>{i}^{L} \oplus \boldsymbol{h}</em>{t}^{L}\right]\right)\right)<br>$$</p><p><a id="12">损失计算公式为</a></p><p>$$<br>\mathcal{L}<em>{t,s}^{cls}(f</em>{(\theta)}) &#x3D; - \sum_{S_t}(y\log\hat{y} + (1-y) \log (1-\hat{y}))<br>$$</p><h3 id="Subgrapha-Sampling-Scheduler"><a href="#Subgrapha-Sampling-Scheduler" class="headerlink" title="Subgrapha Sampling Scheduler"></a>Subgrapha Sampling Scheduler</h3><p>子图采样调度器，之前的相关研究中，mini_batch 用随机均匀概率采样 epsiode，这里设计一个更加合理的调度策略，因为不同任务之间是存在依赖关系的</p><p>上面的 MPG 是构建子图的方法，在采样时按需构造子图</p><ul><li>每个子图仅包含少量分子（设定了是 K 个（训练集，所以对于同一个性质能划分不同的子图，统一性质目标节点的子图应该相关</li><li>不同目标性质节点为中心的子图差异应该被放大</li></ul><p><img src="/blog/../images/GS-Meta/image-20251208111121204.png" alt="image-20251208111121204"></p><p>记子图采样调度器为$z(\phi)$，使用成对（pairwise）采样策略。对于相同的性质目标$p_t$，两张子图$\mathcal{G}_1 \ \mathcal{G}_2$会被同时采样</p><p>具体是，在一个 mini<em>batch 开始的时候，随机采样一个子图候选池$P &#x3D; \left{(\mathcal{G}_t^{(1)}, \mathcal{G}_t^{(2)})\right}</em>{t&#x3D;1}^{N{\text{pool}}}$</p><p>所有二元组中的子图都是围绕同一个性质节点的，调度器$\mathcal{z}_t$通过一下两个步骤选择出唯一的二元组</p><p>计算子图嵌入$\mathcal{g}_t$</p><p>$$<br>\mathcal{g}_t &#x3D; \mathbf{h}<em>t^{L} + \sigma\left(\sum</em>{i \in \mathcal{V} \setminus {p_t}} \mathbf{h}_i^{L}\right)<br>$$</p><p>这里$\mathbf{h}_t^L$是$p_t$的最终嵌入，求和那一坨是指对子图中除了目标性质节点以为其他所有节点最终嵌入的池化（这里就是求和），这里$\sigma$未作解释，应该是个非线性函数</p><p>然后，把$g_t$输入调度器$z_t$，得到选择概率$\eta_t$</p><p>$$<br>\eta_t &#x3D; z_1\left(g_t + z_2 \left( \sum_{t’ \in P \setminus \mathcal{G}<em>t} g</em>{t’}\right)\right)<br>$$</p><p>这里的$z$都是 MLP（全连接网络），一同构成最终的调度器$z(\phi)$，然后对$\eta_t$用 softmax 就能得到合理的概率了</p><blockquote><p>[!NOTE]</p><p>这里的表述没有问题，一个任务有多个子图，最终是多个数值应用 softmax</p></blockquote><p>对于候选池的每一个子图对，用走一套流程，然后最子图对的概率就是二者平均，最终 B 个子图对构成一个 mini_batch</p><p>分子类别之间的区别，用到 NT-Xen 损失，这个在对比学习中被广泛使用。统一性质的子图就是正样本对，不同性质的是负样本。一个 mini_batch 中对比损失定义如下</p><p>$$<br>\mathcal{L}^{\mathrm{ctr}} &#x3D;\frac{1}{B}\sum_{t&#x3D;1}^{B} -\log \frac{\exp\big(\mathrm{sim}({g}_t^{(1)},{g}<em>t^{(2)})&#x2F;\tau\big)} {\sum</em>{t’&#x3D;1,,t’\neq t}^{B}\exp\big(\mathrm{sim}({g}<em>t^{(1)},{g}</em>{t’}^{(2)})&#x2F;\tau\big)}<br>$$</p><p>这个主要是训练调度器的，但是最终也会影响 GNN 的迭代</p><p>sim 啥啥啥是余弦相似度，$\tau$为温度系数</p><p>总结一下，上面讲了元学习的任务定义，基于 GNN 的 MPG，然后有 MLP 模块的子图采样器</p><h2 id="Training-and-Testing"><a href="#Training-and-Testing" class="headerlink" title="Training and Testing"></a>Training and Testing</h2><p>终于要开始训练模型了</p><p>首先对于性质$t&#x3D;1$，从候选池拿出 B 个任务对(一个 mini<em>batch)$\left{\left(\mathcal{G}_1^{(1)}, \mathcal{G}_t^{(2)}\right)\right}</em>{t&#x3D;1}^B$</p><p>这里注意了，内层 GNN 使用的任务仍然是一个子图，这里的任务对是用来做对比损失的</p><p>关注内层循环(inner-loop)，根据<a href="#12">公式 12</a>计算支持集损失，然后做一次<a id="16">梯度下降</a></p><p>$$<br>\theta’ \leftarrow \theta - \beta_{inner}\nabla_\theta \mathcal{L}^{cls}_{t,S}\left(f(\theta)\right)<br>$$</p><p>这里$\beta$是内循环的学习率，更新完参数之后，再在查询集上计算损失，记为$\mathcal{L}_{t,\mathcal{Q}}^{cls}$，这个在外层循环有用</p><p>外层循环公式为</p><p>$$<br>\theta \leftarrow \theta \beta_{outer}\nabla_\theta \mathcal{L}\left( f(\theta’)\right)<br>$$</p><p>这里的损失函数定义为</p><p>$$<br>\mathcal{L}\left(f(\theta’)\right)&#x3D; \frac1{2B}\sum^B_{t&#x3D;1} \left({\mathcal{L}^{cls}<em>{t, \mathcal{Q}}}^{(1)} + {\mathcal{L}^{cls}</em>{t, \mathcal{Q}}}^{(2)}  \right) + \lambda \mathcal{L}^{ctr}<br>$$</p><p>$\lambda$是一个超参数，这里实际上就是将任务之间的关系量化为一个对比损失函数，然后添加到外循环里</p><p>看形式化伪代码</p><p><img src="/blog/../images/GS-Meta/image-20251208164333486.png" alt="image-20251208164333486"></p><p>这里还有一个基于 MLP 的采样器需要训练，但是这玩意不好直接求导，所以使用策略梯度方法来训练调度器</p><p>$$<br>\phi \leftarrow \phi + \gamma\nabla_\phi \log P(\eta)(R - b)<br>$$</p><blockquote><p>这里的 P 是什么东西，R 是什么</p></blockquote><p>测试的时候选一个新性质，然后用辅助性质+内层循环<a href="#16">公式 16</a>微调即可</p><h2 id="Expriments"><a href="#Expriments" class="headerlink" title="Expriments"></a>Expriments</h2><ul><li>GS-Meta 是否优于最先进的基线方法</li><li>辅助性质(auxiliary properties)效果如何</li><li>episode 和采样调度器有没有用</li><li>调度器怎么解释（$z(\phi)$）</li></ul><p>文章跑了下列模型</p><ul><li>从头学习分子编码器的方法 Siamese，ProtoNet，MAML，TPN，EGNN，InterReLSTM，PAR，本文模型 GS-Meta</li><li>预训练分子编码器的防范 Pre-GNN，Meta-MGNN，Pre-PAR，本文模型 Pre-GS-Meta</li></ul><p>不同种子下重复 10 次，看均值和标准差</p><h3 id="RQ1"><a href="#RQ1" class="headerlink" title="RQ1"></a>RQ1</h3><p>模型在所有基线上均有提升，其中给予关系图的方法普遍更好，表明在本文在跑 GNN 的时候考虑分子间关系是有效的</p><p>不同数据集效果有差异，可能取决于辅助性质的多少，标签完整度</p><h3 id="RQ2"><a href="#RQ2" class="headerlink" title="RQ2"></a>RQ2</h3><p>改变训练阶段和测试阶段采样的辅助性质的个数来进行测试，结果如图</p><p><img src="/blog/../images/GS-Meta/image-20251208172514337.png" alt="image-20251208172514337"></p><p>同时测试辅助性质缺失标签造成的影响，和增加标签 unk 的影响</p><p><img src="/blog/../images/GS-Meta/image-20251208172423111.png" alt="image-20251208172423111"></p><h2 id="RQ3"><a href="#RQ3" class="headerlink" title="RQ3"></a>RQ3</h2><p>也就是相关分子，这里测了移除 mol2mol 边的情况 w&#x2F;o m2m</p><p>采样调度器</p><ul><li>w&#x2F;o S，随机 mini_batch，不调度</li><li>w&#x2F;o CL，调度，会影响 GNN 迭代，但是外层循环不使用损失</li><li>w&#x2F;o S, w&#x2F;o CL，都不用</li></ul><p><img src="/blog/../images/GS-Meta/image-20251208173539080.png" alt="image-20251208173539080"></p><p>表现好，耗时短</p><h2 id="RQ4"><a href="#RQ4" class="headerlink" title="RQ4"></a>RQ4</h2><p>可视化性质关系</p><p><img src="/blog/../images/GS-Meta/image-20251208173729690.png" alt="image-20251208173729690"></p><p>性质越相似，越容易出现在同一个 mini_batch 中</p><h2 id="复现"><a href="#复现" class="headerlink" title="复现"></a>复现</h2><p>在 sider 数据集上跑了一个 10-shot，没有预训练编码器的 GS-Meta</p><p>params 如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;exp_name&quot;: &quot;run&quot;,</span><br><span class="line">    &quot;dump_path&quot;: &quot;dump/&quot;,</span><br><span class="line">    &quot;exp_id&quot;: &quot;19-04-52.941&quot;,</span><br><span class="line">    &quot;gpu&quot;: &quot;0&quot;,</span><br><span class="line">    &quot;random_seed&quot;: 0,</span><br><span class="line">    &quot;data_root&quot;: &quot;data&quot;,</span><br><span class="line">    &quot;dataset&quot;: &quot;sider&quot;,</span><br><span class="line">    &quot;mol_num_layer&quot;: 5,</span><br><span class="line">    &quot;emb_dim&quot;: 300,</span><br><span class="line">    &quot;JK&quot;: &quot;last&quot;,</span><br><span class="line">    &quot;mol_dropout&quot;: 0.1,</span><br><span class="line">    &quot;mol_graph_pooling&quot;: &quot;mean&quot;,</span><br><span class="line">    &quot;mol_gnn_type&quot;: &quot;gin&quot;,</span><br><span class="line">    &quot;mol_batch_norm&quot;: 1,</span><br><span class="line">    &quot;mol_pretrain_load_path&quot;: null,</span><br><span class="line">    &quot;rel_layer&quot;: 2,</span><br><span class="line">    &quot;rel_edge_n_layer&quot;: 2,</span><br><span class="line">    &quot;rel_top_k&quot;: 9,</span><br><span class="line">    &quot;rel_edge_hidden_dim&quot;: 100,</span><br><span class="line">    &quot;rel_dropout&quot;: 0.1,</span><br><span class="line">    &quot;rel_pre_dropout&quot;: 0.1,</span><br><span class="line">    &quot;rel_nan_w&quot;: 1.0,</span><br><span class="line">    &quot;rel_nan_type&quot;: &quot;nan&quot;,</span><br><span class="line">    &quot;rel_batch_norm&quot;: 1,</span><br><span class="line">    &quot;rel_edge_type&quot;: 1,</span><br><span class="line">    &quot;inner_lr&quot;: 0.5,</span><br><span class="line">    &quot;meta_lr&quot;: 0.001,</span><br><span class="line">    &quot;weight_decay&quot;: 5e-05,</span><br><span class="line">    &quot;second_order&quot;: 1,</span><br><span class="line">    &quot;inner_update_step&quot;: 1,</span><br><span class="line">    &quot;inner_tasks&quot;: 10,</span><br><span class="line">    &quot;episode&quot;: 2000,</span><br><span class="line">    &quot;n_support&quot;: 10,</span><br><span class="line">    &quot;n_query&quot;: 16,</span><br><span class="line">    &quot;n_test_tasks&quot;: 200,</span><br><span class="line">    &quot;eval_step&quot;: 100,</span><br><span class="line">    &quot;test_batch_size&quot;: 128,</span><br><span class="line">    &quot;train_auxi_task_num&quot;: null,</span><br><span class="line">    &quot;test_auxi_task_num&quot;: null,</span><br><span class="line">    &quot;nce_t&quot;: 0.08,</span><br><span class="line">    &quot;contr_w&quot;: 0.05,</span><br><span class="line">    &quot;pool_num&quot;: 10,</span><br><span class="line">    &quot;task_lr&quot;: 0.0005,</span><br><span class="line">    &quot;task_hid_dim&quot;: 10,</span><br><span class="line">    &quot;task_t&quot;: 1</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>模型描述如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br></pre></td><td class="code"><pre><span class="line">GSMeta(</span><br><span class="line">  (mol_encoder): GNN_Encoder(</span><br><span class="line">    (gnn): GNN(</span><br><span class="line">      (x_embedding1): Embedding(120, 300)</span><br><span class="line">      (x_embedding2): Embedding(3, 300)</span><br><span class="line">      (gnns): ModuleList(</span><br><span class="line">        (0): GINConv(</span><br><span class="line">          (mlp): Sequential(</span><br><span class="line">            (0): Linear(in_features=300, out_features=600, bias=True)</span><br><span class="line">            (1): ReLU()</span><br><span class="line">            (2): Linear(in_features=600, out_features=300, bias=True)</span><br><span class="line">          )</span><br><span class="line">          (edge_embedding1): Embedding(6, 300)</span><br><span class="line">          (edge_embedding2): Embedding(3, 300)</span><br><span class="line">        )</span><br><span class="line">        (1): GINConv(</span><br><span class="line">          (mlp): Sequential(</span><br><span class="line">            (0): Linear(in_features=300, out_features=600, bias=True)</span><br><span class="line">            (1): ReLU()</span><br><span class="line">            (2): Linear(in_features=600, out_features=300, bias=True)</span><br><span class="line">          )</span><br><span class="line">          (edge_embedding1): Embedding(6, 300)</span><br><span class="line">          (edge_embedding2): Embedding(3, 300)</span><br><span class="line">        )</span><br><span class="line">        (2): GINConv(</span><br><span class="line">          (mlp): Sequential(</span><br><span class="line">            (0): Linear(in_features=300, out_features=600, bias=True)</span><br><span class="line">            (1): ReLU()</span><br><span class="line">            (2): Linear(in_features=600, out_features=300, bias=True)</span><br><span class="line">          )</span><br><span class="line">          (edge_embedding1): Embedding(6, 300)</span><br><span class="line">          (edge_embedding2): Embedding(3, 300)</span><br><span class="line">        )</span><br><span class="line">        (3): GINConv(</span><br><span class="line">          (mlp): Sequential(</span><br><span class="line">            (0): Linear(in_features=300, out_features=600, bias=True)</span><br><span class="line">            (1): ReLU()</span><br><span class="line">            (2): Linear(in_features=600, out_features=300, bias=True)</span><br><span class="line">          )</span><br><span class="line">          (edge_embedding1): Embedding(6, 300)</span><br><span class="line">          (edge_embedding2): Embedding(3, 300)</span><br><span class="line">        )</span><br><span class="line">        (4): GINConv(</span><br><span class="line">          (mlp): Sequential(</span><br><span class="line">            (0): Linear(in_features=300, out_features=600, bias=True)</span><br><span class="line">            (1): ReLU()</span><br><span class="line">            (2): Linear(in_features=600, out_features=300, bias=True)</span><br><span class="line">          )</span><br><span class="line">          (edge_embedding1): Embedding(6, 300)</span><br><span class="line">          (edge_embedding2): Embedding(3, 300)</span><br><span class="line">        )</span><br><span class="line">      )</span><br><span class="line">      (batch_norms): ModuleList(</span><br><span class="line">        (0): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">        (1): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">        (2): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">        (3): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">        (4): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">      )</span><br><span class="line">    )</span><br><span class="line">  )</span><br><span class="line">  (relation_net): TaskRelationNet(</span><br><span class="line">    (pre_dropout): Dropout(p=0.1, inplace=False)</span><br><span class="line">    (task_emb): Embedding(27, 300)</span><br><span class="line">    (node_layer0): NodeUpdateNetwork(</span><br><span class="line">      (neigh_linear): Linear(in_features=300, out_features=300, bias=True)</span><br><span class="line">      (root_linear): Linear(in_features=300, out_features=300, bias=True)</span><br><span class="line">      (edge_emb): Embedding(4, 300)</span><br><span class="line">      (batch_norm): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">      (dropout): Dropout(p=0.1, inplace=False)</span><br><span class="line">      (act): LeakyReLU(negative_slope=0.01)</span><br><span class="line">    )</span><br><span class="line">    (edge_layer0): EdgeUpdateNetwork(</span><br><span class="line">      (sim_network): Sequential(</span><br><span class="line">        (conv0): Conv2d(300, 200, kernel_size=(1, 1), stride=(1, 1), bias=False)</span><br><span class="line">        (norm0): BatchNorm2d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">        (relu0): LeakyReLU(negative_slope=0.01)</span><br><span class="line">        (drop0): Dropout2d(p=0.1, inplace=False)</span><br><span class="line">        (conv1): Conv2d(200, 100, kernel_size=(1, 1), stride=(1, 1), bias=False)</span><br><span class="line">        (norm1): BatchNorm2d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">        (relu1): LeakyReLU(negative_slope=0.01)</span><br><span class="line">        (drop1): Dropout2d(p=0.1, inplace=False)</span><br><span class="line">        (conv_out): Conv2d(100, 1, kernel_size=(1, 1), stride=(1, 1))</span><br><span class="line">      )</span><br><span class="line">    )</span><br><span class="line">    (node_layer1): NodeUpdateNetwork(</span><br><span class="line">      (neigh_linear): Linear(in_features=300, out_features=300, bias=True)</span><br><span class="line">      (root_linear): Linear(in_features=300, out_features=300, bias=True)</span><br><span class="line">      (edge_emb): Embedding(4, 300)</span><br><span class="line">      (batch_norm): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">      (dropout): Dropout(p=0.1, inplace=False)</span><br><span class="line">      (act): LeakyReLU(negative_slope=0.01)</span><br><span class="line">    )</span><br><span class="line">    (edge_layer1): EdgeUpdateNetwork(</span><br><span class="line">      (sim_network): Sequential(</span><br><span class="line">        (conv0): Conv2d(300, 200, kernel_size=(1, 1), stride=(1, 1), bias=False)</span><br><span class="line">        (norm0): BatchNorm2d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">        (relu0): LeakyReLU(negative_slope=0.01)</span><br><span class="line">        (drop0): Dropout2d(p=0.1, inplace=False)</span><br><span class="line">        (conv1): Conv2d(200, 100, kernel_size=(1, 1), stride=(1, 1), bias=False)</span><br><span class="line">        (norm1): BatchNorm2d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">        (relu1): LeakyReLU(negative_slope=0.01)</span><br><span class="line">        (drop1): Dropout2d(p=0.1, inplace=False)</span><br><span class="line">        (conv_out): Conv2d(100, 1, kernel_size=(1, 1), stride=(1, 1))</span><br><span class="line">      )</span><br><span class="line">    )</span><br><span class="line">    (graph_pooling): Sequential(</span><br><span class="line">      (0): Linear(in_features=300, out_features=150, bias=True)</span><br><span class="line">      (1): ReLU()</span><br><span class="line">      (2): Linear(in_features=150, out_features=300, bias=True)</span><br><span class="line">    )</span><br><span class="line">    (classifier): Sequential(</span><br><span class="line">      (0): Linear(in_features=600, out_features=300, bias=True)</span><br><span class="line">      (1): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)</span><br><span class="line">      (2): ReLU()</span><br><span class="line">      (3): Linear(in_features=300, out_features=1, bias=True)</span><br><span class="line">    )</span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>子图调度器结构如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">TaskSelector(</span><br><span class="line">  (z1): Sequential(</span><br><span class="line">    (0): Linear(in_features=300, out_features=10, bias=True)</span><br><span class="line">    (1): ReLU()</span><br><span class="line">    (2): Linear(in_features=10, out_features=1, bias=True)</span><br><span class="line">    (3): Tanh()</span><br><span class="line">  )</span><br><span class="line">  (z2): Sequential(</span><br><span class="line">    (0): Linear(in_features=300, out_features=300, bias=True)</span><br><span class="line">    (1): ReLU()</span><br><span class="line">    (2): Linear(in_features=300, out_features=300, bias=True)</span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>训练 loss，测试得分如下图</p><p><img src="/blog/../images/GS-Meta/loss-cls.svg" alt="loss-cls"></p><p><img src="/blog/../images/GS-Meta/score.svg" alt="score"></p><p>最终得分 83.75，与论文基本一致</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="deep learning" scheme="https://kpmark.icu/tags/deep-learning/"/>
    
  </entry>
  
  <entry>
    <title>浅读 MAML</title>
    <link href="https://kpmark.icu/2025/12/01/MAML/"/>
    <id>https://kpmark.icu/2025/12/01/MAML/</id>
    <published>2025-12-01T03:43:47.000Z</published>
    <updated>2025-12-09T01:31:38.005Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="Model-Agnostic-Meta-Learning-for-Fast-Adaptation-of-Deep-Networks"><a href="#Model-Agnostic-Meta-Learning-for-Fast-Adaptation-of-Deep-Networks" class="headerlink" title="Model-Agnostic Meta-Learning for Fast Adaptation of Deep Networks"></a>Model-Agnostic Meta-Learning for Fast Adaptation of Deep Networks</h1><h2 id="Abstract"><a href="#Abstract" class="headerlink" title="Abstract"></a>Abstract</h2><p><strong>模型无关</strong>的元学习（meta-learning） 算法，兼容所有基于<strong>梯度下降</strong>训练的模型，适用于分类、回归和强化学习</p><p>通过对模型进行多任务训练（a variety of learning tasks），用少量训练样本解决新任务？</p><p>使用少量数据和训练就能在任务上得到很好的泛化性能</p><p>简单来说，MAML 赋予模型学习“如何学习”的能力，是的其在面对新任务时能够快速适应</p><h2 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h2><p><strong>算法核心：训练模型的初始参数，是的模型在之后遇到新的任务时，通过少量数据就能获得最大性能</strong></p><p>与先前提出的元学习算法不同，此算法不增加学习参数数量，不约束模型架构</p><p>从特征学习的角度，MAML 算法过程，可以被视为构建了一种广泛使用于多种模型的内部表征（Internal representation）</p><p>从动力系统角度，这个学习过程可以被视为最大化新任务损失函数<strong>对参数的敏感性</strong>，只要敏感度够高，参数的微小变化就能使任务损失大幅提升</p><p>本文评估结果：监督分类任务中，MAML 优于专门为小样本学习的最先进方法（17 年），在回归问题和强化学习中，性能远超直接使用预训练模型进行初始化的传统方法</p><blockquote><p>[!NOTE]</p><p>这里的<strong>显著优于</strong>适用于<strong>存在任务变异性</strong>的场景中<strong>强化学习</strong>进程的预训练初始化，对于普通的回归问题，原文为（can also be readily applied to）</p></blockquote><h2 id="Model-Agnostic-Meta-Learning"><a href="#Model-Agnostic-Meta-Learning" class="headerlink" title="Model-Agnostic Meta-Learning"></a>Model-Agnostic Meta-Learning</h2><p>这玩意通常在小样本学习的理论框架下</p><h3 id="Meta-Learning-Problem-Set-Up"><a href="#Meta-Learning-Problem-Set-Up" class="headerlink" title="Meta-Learning Problem Set-Up"></a>Meta-Learning Problem Set-Up</h3><p>考虑一个模型$f$，将观测$x$映射到$a$，因为框架是通用的，所以需要通用任务概念</p><p>$$<br>\mathcal{T} &#x3D; \left{ \mathcal{L}(\mathbf{x}_1, \mathbf{a}_1, \ldots, \mathbf{x}_H, \mathbf{a}_H),\ q(\mathbf{x}<em>1),\ q(\mathbf{x}</em>{t+1} \mid \mathbf{x}_t, \mathbf{a}_t),\ H \right}<br>$$</p><p>其中，包含一个损失函数$\mathcal{L}$，初始观测分布$q(\mathbf{x}<em>1)$，转移分布$q(\mathbf{x}</em>{t+1} \mid \mathbf{x}_t, \mathbf{a}_t)$，和情节长度$H$</p><p>独立同分布问题中，长度$H &#x3D; 1$，模型在每个时间步选择一个$a$来生成长度为$H$的样本，损失函数提供任务特定的反馈</p><p>在元学习场景中，考虑一个<strong>任务分布</strong>$p(\mathcal{T})$，模型需要尽快适应这个新的分布</p><h3 id="A-Model-Agnostic-Meta-Learning-Algorithm"><a href="#A-Model-Agnostic-Meta-Learning-Algorithm" class="headerlink" title="A Model-Agnostic Meta-Learning Algorithm"></a>A Model-Agnostic Meta-Learning Algorithm</h3><p>模型在新任务上使用基于梯度的规则进行微调，我们使用同样的方式学习一个模型，目标是找到<strong>对任务变化敏感的模型参数</strong></p><p><strong>这里的参数指的是内部参数，而不是超参数</strong></p><p><img src="/blog/../images/MAML/image-20251125083239877.png" alt="image-20251125083239877"></p><p>有一下两个假设</p><ul><li>模型由$\theta$参数化</li><li>损失函数足够平滑</li></ul><p>这里的损失通过任务的$\mathcal{L}$来计算，而$\theta$指代模型的内部参数，通过梯度下降更新</p><p>$$<br>\theta’<em>i &#x3D; \theta - \alpha \nabla</em>{\theta}\mathcal{L}_{\mathcal{T}<em>i}(f</em>{\theta})<br>$$</p><p>本节考虑一次梯度更新的情况，多次也行，反正就是计算单个任务的较优内部参数</p><p>训练方式就是优化其在不同任务上对应的性能，即最小化代价函数</p><p>$$<br>\min_{\theta} \sum_{\mathcal{T}<em>i \sim p(\mathcal{T})} \mathcal{L}</em>{\mathcal{T}<em>i} (f</em>{\theta_i’}) &#x3D; \sum_{\mathcal{T}<em>i \sim p(\mathcal{T})} \mathcal{L}</em>{\mathcal{T}<em>i} (f</em>{\theta - \alpha \nabla_\theta \mathcal{L}_{\mathcal{T}_i}(f_\theta)})<br>$$</p><p>通过随机梯度下降（SGD）执行优化迭代如下</p><p>$$<br>\theta \leftarrow \theta - \beta \nabla_{\theta} \sum_{\mathcal{T}<em>{i} \sim p(\mathcal{T})} \mathcal{L}</em>{\mathcal{T}<em>{i}}(f</em>{\theta_{i}^{\prime}})<br>$$</p><p>有形式化算法</p><p><img src="/blog/../images/MAML/image-20251125090919142.png" alt="image-20251125090919142"></p><p>对于单个损失的计算，使用了验证集和训练集</p><p>这里的梯度更新实际上涉及梯度的梯度，所以需要进行一次额外的反向传播，大多数库都支持</p><p>也有一种省略该传播的近似</p><hr><p>实际应用和实现先不看了</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="deep learning" scheme="https://kpmark.icu/tags/deep-learning/"/>
    
  </entry>
  
  <entry>
    <title>浅读 Flatten Transformer</title>
    <link href="https://kpmark.icu/2025/10/30/Flatten%20Transformer/"/>
    <id>https://kpmark.icu/2025/10/30/Flatten%20Transformer/</id>
    <published>2025-10-30T03:43:47.000Z</published>
    <updated>2025-11-13T02:39:25.878Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="Flatten-Transformer"><a href="#Flatten-Transformer" class="headerlink" title="Flatten Transformer"></a>Flatten Transformer</h1><h2 id="Abstract"><a href="#Abstract" class="headerlink" title="Abstract"></a>Abstract</h2><p><strong>二次计算复杂度</strong></p><p>来源于 Dot-Product Attention 计算</p><p>$$<br>\text{Attention}(Q, K, V) &#x3D; \text{Softmax}(\frac{QK^T}{\sqrt{d_k}})V<br>$$</p><p>Q，K 分别是 N X d 矩阵，导致计算注意力的时候时间复杂度为$n^2$d</p><p>Linear attentioin 通过映射函数近似 Softmax 操作，提供线性复杂度</p><p>要么性能显著下降，要么因为 mapping funciton 引入额外计算开销</p><p>引入了一种简单有效的映射函数和一个高效的秩恢复模块</p><p><a href="https://github.com/LeapLabTHU/FLatten-Transformer%E3%80%82">https://github.com/LeapLabTHU/FLatten-Transformer。</a></p><blockquote><p>[!NOTE]</p><p>查找，其他线性注意力机制</p></blockquote><h2 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h2><p><strong>线性注意力</strong></p><p>通过改变激活函数和映射函数替代 Softmax，使得矩阵计算顺序调换</p><p>先计算$K^TV$改变计算顺序，将复杂度变成$O(nd)$</p><p>有更大的感受野（receptive field）和吞吐量</p><blockquote><p>global reveptive field 指 Attention 建立联系的范围</p></blockquote><p>针对 Transformer 在 CV 领域的应用，相较于 CNN，存在计算复杂度为$O(n^2d)$的矩阵运算</p><p><img src="/blog/../images/Flatten_Transformer/image-20251008212646004.png" alt="image-20251008212646004"></p><p>linear attention 通过**适当的近似(proper approximation)**解耦了 Softmax，但是还是有性能下降和额外计算开销</p><hr><p>铺垫结束</p><p>本文提出模块 Focused Linear Attention</p><p>用 dual-pronged analysis 研究了线性注意力下降的因素</p><ul><li>现有的线性注意力模块的注意力权重相对平滑，缺乏对特征重点的关注能力<ul><li>使用 simple mapping function 调整查询和键的特征方向</li><li>rank restoration module，秩恢复模块，恢复矩阵的秩，保证特征多样性</li></ul></li></ul><p>总的来说，就是想办法替代 softmax</p><blockquote><p>[!NOTE]</p><p>从从原理角度探究，为什么线性注意力机制会让前后关联注意力下降，特征多样性减小</p></blockquote><h2 id="Related-Works"><a href="#Related-Works" class="headerlink" title="Related Works"></a>Related Works</h2><h3 id="Vision-Transformer"><a href="#Vision-Transformer" class="headerlink" title="Vision Transformer"></a>Vision Transformer</h3><blockquote><p>[!NOTE]</p><p>看一眼相关文献</p></blockquote><p>Transformer 在 CV 中有广泛应用</p><p>计算复杂度很高，所以限制了直接应用（CV 中输入很多，CNN 有相关优化</p><ul><li>Vision Transformer 合并相邻像素降低分辨率</li><li>PVT 稀疏注意力</li><li>DAT 设计可变形注意力模块</li><li>Swin Transformer 分割输入为独立窗口</li><li>NAT 为所有查询设计独立的注意力标记</li></ul><hr><ul><li>CMT 结合 Transformer 和 convolution（卷积）算子</li><li>ACmix 恭喜那个卷积和自注意力开销</li></ul><hr><ul><li>MobileFormer 维护卷积和 Transformer 两条路径</li><li>Dyn-Percriver 使用动态早退机制（dynamic early exiting</li><li>MobileViT 结合 MobileNet 和 Transformer</li></ul><p>以上所有算法最终都依赖于 softmax，复杂度仍为$n^2 d$</p><h3 id="Linear-Attention"><a href="#Linear-Attention" class="headerlink" title="Linear Attention"></a>Linear Attention</h3><p>这些方法就是试图替换 Softmax，这样交换矩阵计算顺序，复杂度即为$nd$</p><ul><li>Performer 使用正交随机特征</li><li>Efficient Attention 单独对 Q 和 K 应用 Softmax</li><li>Nyströmformer 和 SOFT 通过矩阵分解近似完整的自注意力矩阵</li><li>Hydra Attention 使用余弦相似度替换 Softmax，使用 Hydra 技巧将计算复杂度降低到$O(Nd)$</li><li>EfficientVit 使用深度可分离卷积来提升线性注意力的局部特征提取能力</li><li>Castling-ViT 提出了线性角度核来衡量每个 Q 和 K 之间的谱相似性</li></ul><p>上面那些都有点 🌽，所以本文提出 focused linear attention，计算复杂度更低，性能超过 Softmax（？</p><h2 id="Preliminaries"><a href="#Preliminaries" class="headerlink" title="Preliminaries"></a>Preliminaries</h2><h3 id="Vision-Transformer-and-Self-Attention"><a href="#Vision-Transformer-and-Self-Attention" class="headerlink" title="Vision Transformer and Self-Attention"></a>Vision Transformer and Self-Attention</h3><p>ViT（Vision Transformers)中，给定 n 个 token，$x \in R^{N \times C}$有以下公式</p><p>$$<br>Q &#x3D; xW_Q,K &#x3D; xW_K, V &#x3D; xW_V\<br>O_i &#x3D; \sum_{j&#x3D;1}^N \frac{\text{Sim}(Q_i,K_j)}{\sum^N_{j&#x3D;1}\text{Sim}(Q_i, K_j)}V_j\<br>\frac{\text{Sim}(Q_i,K_j)}{\sum^N_{j&#x3D;1}\text{Sim}(Q_i, K_j)}V_j&#x3D;\text{Attention}(Q, K, V) &#x3D; \text{Softmax}(\frac{QK^T}{\sqrt{d_k}})V<br>$$</p><p>其中，三个$W$是$R^{C \times C}$的矩阵，是需要训练求得的参数</p><p>$\text{Sim(·,·)}$是相似度函数，ViT 主要采用 Softmax 注意力，即$\text{Sim}(Q,K) &#x3D; \exp(\frac{QK^T}{\sqrt{d}})$</p><p>注意这里的$Q_i$表示矩阵的某一行（某一个向量</p><p>注意力需要计算所有 Q-K 对，导致$N^2$的复杂度</p><blockquote><p>[!NOTE]</p><p>Softmax 的核心思想在于将一个 K 维向量压缩成一个 K 维的概率函数</p></blockquote><blockquote><p>[!NOTE]</p><p>先前的工作主要通过 designing sqarse global attention pattern（设计稀疏全局注意力模式）或 applying smaler attention windows（应用较小注意力窗口）</p><p>受到额外注意力影响、或牺牲长距离依赖关系的能力</p></blockquote><h3 id="Linear-Attention-1"><a href="#Linear-Attention-1" class="headerlink" title="Linear Attention"></a>Linear Attention</h3><p>这种方法引入新的核函数替代 Softmax</p><p>$$<br>\text{Sim}(Q,K) &#x3D; \phi(Q)\phi(K)^T\<br>O_i &#x3D; \sum^N_{j&#x3D;1} \frac{\phi(Q_i)\phi(K_j)^T}{\sum^N_{j&#x3D;1}\phi(Q_i)\phi(K-J)^T}V_j\<br>O_i &#x3D; \frac{\phi(Q_i)(\sum^N_{j&#x3D;1}\phi(K_j)^TV_j)}{\phi(Q_i)(\sum^N_{j&#x3D;1}\phi(K_j)^T)}<br>$$</p><p>这样变换后，矩阵计算顺序变化，复杂度变为$N$</p><p>然而，核函数太简单了性能不够，太复杂了有额外计算开销</p><h3 id="Focused-Linear-Attention"><a href="#Focused-Linear-Attention" class="headerlink" title="Focused Linear Attention"></a>Focused Linear Attention</h3><h3 id="Focus-ability"><a href="#Focus-ability" class="headerlink" title="Focus ability"></a>Focus ability</h3><p>Softmax 本质上是一种<strong>非线性重加权机制</strong>，其产生的注意力在特定区域非常集中</p><blockquote><p>[!NOTE]</p><p>补充 Softmax 在 Transformer 中的作用</p><p>其实 Softmax 除了在注意力机制中起作用，原本在解码器的最后一层也要作为分类器的一部分计算概率分布</p><p>这里仅讨论其在编码器线性注意力中的作用</p><p>注意力机制首先为每个 token 计算一个查询向量 Q 和键向量 K，通过二者点积，可以计算出表示两个 token 之间<strong>相关性</strong>的<strong>原始得分矩阵</strong></p><p>这些得分非常抽象，根据原始 token 和参数莫名其妙就算出来了，无法直接表示所谓的<strong>注意力</strong></p><p>所以 Softmax 对矩阵的每一行都应用，将这一行归一化为一个总和为 1 的概率分布，且<strong>权重分布会非常清晰</strong>，之后这些权重分布会在和 V 矩阵点乘，表示值的加权求和</p></blockquote><p>线性注意力的分布较为平滑，其输出更接近所有特征的均值</p><p>下图是 DeiT-tiny 模型下，Softmax、linear 和 focus linear 的对比，红色色块的特征被用作查询</p><blockquote><p>[!NOTE]</p><p>尝试复现？</p></blockquote><p><img src="/blog/../images/Flatten_Transformer/image-20251028154418544.png" alt="image-20251028154418544"></p><p>$$<br>\text{Sim}(Q_i, K_j) &#x3D; \phi_p(Q_i)\phi_p(K_j)^T\<br>where \ \phi_p(x) &#x3D; f_p(ReLU(x)), f_p(x) &#x3D; \frac{||x||}{||x^{**p}||}x^{**p}<br>$$</p><p>上式中的$f_p$即为本文提出的<strong>聚焦函数</strong></p><p>$x^{**p}$表示 x 的逐元素 p 次幂运算</p><p>首先使用<strong>从前线性注意力模块</strong>的算法，使用 ReLU 算法保证特征的非负性和分母有效性</p><p>得到<strong>结论：特征经过该映射后范数保持不变</strong>（与 Softmax 变换就不改变范数 00 上述结论的目的，是为了表明该操作<strong>仅调整了该特征的方向</strong>，与其他线性注意力中的成功核函数设计理念保持一致</p><p><strong>由于其只改变特征向量的方向而非大小</strong>，所以其提升性能的根源在于对语义方向的优化</p><blockquote><p>[!NOTE]</p><p>$$<br>|x| &#x3D; \sqrt{x_1^2 + x_2^2 + … + x_n^2}<br>$$</p></blockquote><p>在此基础上，可以证明这里提出的映射函数$f_p$能够实际影响注意力的分布</p><blockquote><p>[!NOTE]</p><p>有严格的证明，但是这里先不看 🙈</p></blockquote><p>通过选择合适的 p 值，该文章提出的函数可以显著增强注意力的区分度，从而恢复与原始 Softmax 函数相似的尖锐注意力分布</p><p><img src="/blog/../images/Flatten_Transformer/image-20251028175136970.png" alt="image-20251028175136970"></p><p>通过图片可以看出，$f_p$可以将每个特征向量拉向最接近的坐标轴，p 值决定这种拉动的强度，也就是提高组内特征的相似性，降低组间特征的相似性</p><h4 id="Feature-diversity"><a href="#Feature-diversity" class="headerlink" title="Feature diversity"></a>Feature diversity</h4><p>除聚焦能力外，特征多样性也是限制线性注意力的关键因素</p><p>潜在原因可能与注意力矩阵的相关。以 DeiT-Tiny 中$N&#x3D;14 \times 14$的 Transformer 为例，其标准注意力矩阵具有满秩特性</p><p>然而，线性注意力中的注意力矩阵存在理论上限，由每个注意力头中的 N 和 d 共同决定（这里是线性代数推导</p><p>$$<br>\text{rank}(\phi(Q)\phi(K)^K)\le\min{\text{rank} (\phi(Q)), \text{rank}(\phi(K))} \le \min { N, d}<br>$$</p><p>在常见的 ViT 中，d 通常小于 T，注意力矩阵的上限被限制了，所以注意力权重的同质化不可避免的导致聚合之后的特征出现相似性</p><p><img src="/blog/../images/Flatten_Transformer/image-20251105173101942.png" alt="image-20251105173101942"></p><p>上图可以看出，Linear 使得注意力矩阵的许多行非常相似</p><p>所以这篇论文提出了如下公式，添加了一个 DWC（深度可分离卷积）模块</p><p>$$<br>O &#x3D; \phi(Q)\phi(K)^TV + \text{DWC}(V)<br>$$</p><p>DWC 可以被视为一种特殊的注意力机制，在这种机制中，每个 Q 关注空间中的几个相邻特征，而非所有特征 V</p><p>这样，即使两个查询对应的线性注意力相同，其 DWC 不同，导致其输出仍然不同</p><p>也可以从矩阵的秩的角度表示：</p><p>$$<br>O &#x3D; (\phi(Q)\phi(K)^T + M_{\text{DWC}})V &#x3D; M_{eq}V<br>$$</p><p>这里$M_{DWC}$表示深度卷积函数对应的稀疏矩阵，$M_{eq}$为等效的全注意力映射。因为$M_{DWC}$有成为满秩矩阵的潜力，所以这个做法极大的提升线性注意力的性能。</p><h4 id="Focused-linear-attention-module"><a href="#Focused-linear-attention-module" class="headerlink" title="Focused linear attention module"></a>Focused linear attention module</h4><p>上文已经成功提出了一种聚焦函数，可以用来替代 Softmax 函数中的基函数</p><p>这里结合 DWC，提出新的<strong>线性注意力模块</strong>，即<strong>聚焦线性注意力</strong></p><p>$$<br>O &#x3D; Sim(Q,K) &#x3D; \phi_p(Q)\phi_p(K)^TV + \text{DWC}(V)<br>$$</p><p><strong>优点</strong></p><ul><li>具备与线性注意力相同的低计算复杂度</li><li>具备与 Softmaz 注意力相当的高表达能力</li></ul><p>此外，该模型还具有适应更大感受野和不同模型架构的潜力，此论文已经在 DeiT、PVT、PVT-v2、Swin Transformer 和 CSwin Transformer 中进行了验证实验</p><hr><p>剩下部分暂时掠过</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="deep learning" scheme="https://kpmark.icu/tags/deep-learning/"/>
    
  </entry>
  
  <entry>
    <title>浅读 Attention is all your need</title>
    <link href="https://kpmark.icu/2025/10/03/transformer/"/>
    <id>https://kpmark.icu/2025/10/03/transformer/</id>
    <published>2025-10-03T03:43:47.000Z</published>
    <updated>2025-10-03T07:13:23.651Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="Transformer"><a href="#Transformer" class="headerlink" title="Transformer"></a>Transformer</h1><h2 id="Abstract"><a href="#Abstract" class="headerlink" title="Abstract"></a>Abstract</h2><p>主流 Seq2Seq 模型，基于 CNN 和 RNN，包含 encoder 和 decoder 的复杂模型，通过注意力机制连接解码器和编码器</p><p>Transformer，摆脱 CNN 与 RNN，仅使用注意力机制</p><p>好训练，精度高，可泛化</p><h2 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h2><p>当前状态依赖于上一次计算得到的状态，因此每个 token 无法进行并行计算，限制模型性能</p><p>Transformer 结构避免循环，通过 Self-attention 让序列中的每一个 token 都与其他所有 token 直接建立联系</p><p>模型第一层直接接收到的输入时序列中所有 token 嵌入向量（Embedding Vectors 和 位置编码（Positional encoding 组成的矩阵</p><p>平均注意力加权可能导致有效分辨率降低，但是可以通过 Multi-Head 抵消这种负面影响</p><blockquote><p>每个特征因为混入了上下文联系信息，导致其本身的细节特征在一定程度上被模糊</p></blockquote><p>recurrent attention instead of sequence-aligned recurrence</p><p>前者是 transformer 采用的，计算注意力矩阵的方式，后者是 RNN 使用的，步进迭代当前状态的</p><h2 id="Model-Architecture"><a href="#Model-Architecture" class="headerlink" title="Model Architecture"></a>Model Architecture</h2><p><img src="/blog/../images/transformer/image-20250930182412145.png" alt="image-20250930182412145"></p><h3 id="Encoder-and-Decoder"><a href="#Encoder-and-Decoder" class="headerlink" title="Encoder and Decoder"></a>Encoder and Decoder</h3><p>Encoder 包含 6 个完全相同的层，每层有一个 Multi-Head Attention 和前馈全连接，两个子层都做了<strong>残差连接和归一化(Add and Norm)</strong></p><blockquote><p>这个 Add &amp; Norm 有点神秘</p></blockquote><p>这里的 FFN，类似针对矩阵的全连接计算（这里因为上下文相关，所以不会混合位置信息</p><p>Decoder 同样由三个子层组成，这里每层第一个 self-attention 加上了掩码，这个需要结合预测和训练的不同行为来理解</p><ul><li>训练时，给 Decoder 的输入时正确答案，为了不让模型知道后续的正确答案（因为预测时不知道），需要掩码进行修饰</li><li>训练时，Decoder 的输入时自回归的输出，每一次输出的内容时与之前的输出有联系</li></ul><h3 id="Attention"><a href="#Attention" class="headerlink" title="Attention"></a>Attention</h3><p><img src="/blog/../images/transformer/image-20251002204744095.png" alt="image-20251002204744095"></p><p>query，key，value 三个向量，映射成一个 output。output 是通过对 value 加权求和算出来的。权重时通过 query 和 key，用 compatibility function 算出来的</p><blockquote><p>这里的 cpmatibility function 就是 Scaled Dot-Product Attention 算法</p></blockquote><blockquote><p>[!NOTE]</p><p>这里的三个向量是原来的输入点乘了神秘参数</p><p><img src="/blog/../images/transformer/image-20250930095707639.png" alt="image-20250930095707639"></p></blockquote><h4 id="Scaled-Dot-Product-Attention"><a href="#Scaled-Dot-Product-Attention" class="headerlink" title="Scaled Dot-Product Attention"></a>Scaled Dot-Product Attention</h4><p>实际上就是</p><p>$$<br>Attention(Q,K,V) &#x3D; softmax(\frac{QK^T}{\sqrt{d_k}})V<br>$$</p><p>Q 和 K 的转置点乘，得到 Q 和 K 的相关性值，除以纬度放缩，然后 softmax 输出一个权重矩阵，拿这个矩阵和 V 相乘，得到 V 得加权平均</p><blockquote><p>这个加权平均矩阵，每一个向量都融合了输入矩阵的特征</p></blockquote><blockquote><p>[!NOTE]</p><p>论文中的模型 self-attention 计算相关性的方式是 Dot-product，但是也可以使用矩阵和的形式 Additive，论文中提到这种形式可能在不进行缩放的情况下优于 Dot-product，所以这里会使用$\sqrt{d_k}$进行缩放</p><p>实际上 QK 两个向量都是用来计算 self-attention 中相关性引出的，其点积结果就是 Dot-product 的结果</p><p>这个结果表明了输入矩阵中的向量两两之间关系的权重，这里再乘以 V 向量就相当于求加权和，得到的结果就是矩阵中某个向量与其他矩阵的关系分数</p></blockquote><h4 id="Multi-Head-Attention"><a href="#Multi-Head-Attention" class="headerlink" title="Multi-Head Attention"></a>Multi-Head Attention</h4><p><img src="/blog/../images/transformer/image-20251003125031578.png" alt="image-20251003125031578"></p><p>就是通过不同的参数得出不同版本的 QKV，然后分别计算相关性得分</p><p>注意每个版本的 QKV 得到的都是一个向量，将这些向量拼接成一个矩阵，然后与可学习参数$W^o$点乘，得到最终 Multi-Head Attention 的计算结果</p><h2 id="FFN"><a href="#FFN" class="headerlink" title="FFN"></a>FFN</h2><p>两个线性全连接，中间夹着一个 ReLU</p><p>FFN（前馈层，又叫 MPL（多层感知 🐔</p><h2 id="Train"><a href="#Train" class="headerlink" title="Train"></a>Train</h2><p>优化器使用 Adam</p><h2 id="Code"><a href="#Code" class="headerlink" title="Code"></a>Code</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> torch.nn <span class="keyword">as</span> nn</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用 Transformer base 参数</span></span><br><span class="line">d_model = <span class="number">512</span>      <span class="comment"># 嵌入维度</span></span><br><span class="line">N = <span class="number">6</span>              <span class="comment"># 编码器和解码器的层数</span></span><br><span class="line">h = <span class="number">8</span>              <span class="comment"># 多头注意力的头数</span></span><br><span class="line">d_ff = <span class="number">2048</span>        <span class="comment"># 前馈神经网络的隐藏层维度</span></span><br><span class="line">dropout = <span class="number">0.1</span>      <span class="comment"># Dropout 概率</span></span><br><span class="line"></span><br><span class="line">model = nn.Transformer(</span><br><span class="line">    d_model=d_model,</span><br><span class="line">    nhead=h,</span><br><span class="line">    num_encoder_layers=N,</span><br><span class="line">    num_decoder_layers=N,</span><br><span class="line">    dim_feedforward=d_ff,</span><br><span class="line">    dropout=dropout,</span><br><span class="line">    batch_first=<span class="literal">True</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(model)</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">Transformer(</span><br><span class="line">  (encoder): TransformerEncoder(</span><br><span class="line">    (layers): ModuleList(</span><br><span class="line">      (0-5): 6 x TransformerEncoderLayer(</span><br><span class="line">        (self_attn): MultiheadAttention(</span><br><span class="line">          (out_proj): NonDynamicallyQuantizableLinear(in_features=512, out_features=512, bias=True)</span><br><span class="line">        )</span><br><span class="line">        (linear1): Linear(in_features=512, out_features=2048, bias=True)</span><br><span class="line">        (dropout): Dropout(p=0.1, inplace=False)</span><br><span class="line">        (linear2): Linear(in_features=2048, out_features=512, bias=True)</span><br><span class="line">        (norm1): LayerNorm((512,), eps=1e-05, elementwise_affine=True)</span><br><span class="line">        (norm2): LayerNorm((512,), eps=1e-05, elementwise_affine=True)</span><br><span class="line">        (dropout1): Dropout(p=0.1, inplace=False)</span><br><span class="line">        (dropout2): Dropout(p=0.1, inplace=False)</span><br><span class="line">      )</span><br><span class="line">    )</span><br><span class="line">    (norm): LayerNorm((512,), eps=1e-05, elementwise_affine=True)</span><br><span class="line">  )</span><br><span class="line">  (decoder): TransformerDecoder(</span><br><span class="line">    (layers): ModuleList(</span><br><span class="line">      (0-5): 6 x TransformerDecoderLayer(</span><br><span class="line">        (self_attn): MultiheadAttention(</span><br><span class="line">          (out_proj): NonDynamicallyQuantizableLinear(in_features=512, out_features=512, bias=True)</span><br><span class="line">        )</span><br><span class="line">        (multihead_attn): MultiheadAttention(</span><br><span class="line">          (out_proj): NonDynamicallyQuantizableLinear(in_features=512, out_features=512, bias=True)</span><br><span class="line">        )</span><br><span class="line">        (linear1): Linear(in_features=512, out_features=2048, bias=True)</span><br><span class="line">        (dropout): Dropout(p=0.1, inplace=False)</span><br><span class="line">        (linear2): Linear(in_features=2048, out_features=512, bias=True)</span><br><span class="line">        (norm1): LayerNorm((512,), eps=1e-05, elementwise_affine=True)</span><br><span class="line">        (norm2): LayerNorm((512,), eps=1e-05, elementwise_affine=True)</span><br><span class="line">        (norm3): LayerNorm((512,), eps=1e-05, elementwise_affine=True)</span><br><span class="line">        (dropout1): Dropout(p=0.1, inplace=False)</span><br><span class="line">        (dropout2): Dropout(p=0.1, inplace=False)</span><br><span class="line">        (dropout3): Dropout(p=0.1, inplace=False)</span><br><span class="line">      )</span><br><span class="line">    )</span><br><span class="line">    (norm): LayerNorm((512,), eps=1e-05, elementwise_affine=True)</span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="deep learning" scheme="https://kpmark.icu/tags/deep-learning/"/>
    
  </entry>
  
  <entry>
    <title>东莞和 vivo 和 aigc</title>
    <link href="https://kpmark.icu/2025/08/22/2025-8-22/"/>
    <id>https://kpmark.icu/2025/08/22/2025-8-22/</id>
    <published>2025-08-22T03:43:47.000Z</published>
    <updated>2025-08-25T14:10:38.501Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p>欢迎参加中国高校计算机大赛 · AIGC 创新赛</p><hr><p>吃了第二届参加人数少的福利，我们这种做了一个月的毛胚项目也能进国赛</p><p>vivo 给参与国赛的每个队伍提供了夏令营集训和 vivo vision 发布会暨影像盛典的名额，于是兴高采烈的和队友跑跑了两趟东莞（只是可惜去年答辩是在天津，今年去不了</p><h2 id="夏令营"><a href="#夏令营" class="headerlink" title="夏令营"></a>夏令营</h2><p>vivo 邀请我们参观了园区和他们的自动化车间</p><p>以前只在电视和报道中看过自动化车间，这次能够亲身感受自动化车间的工作氛围实在幸运</p><p>可惜 vivo 不让给设备拍照</p><p><img src="/blog/../images/2025-8-22/9f2896f2b06a4a95c31baa1e8aee0a20_720.jpg" alt="9f2896f2b06a4a95c31baa1e8aee0a20_720"></p><p>然后上了好多课，听了好多讲座，参加了模拟答辩</p><p>深切感觉到自己的项目能够挤进国赛有多么不可思议</p><p>其他团队的项目，要么是策划已久，方案齐全详尽的软件计划，要么是他们实验室的科研成果转化</p><p>相比之下我们的项目就很苍白了，正紧开发也就是不到一个月，正儿八经参与开发的也就两个人，PPT 也非常简陋，演示视频效果也不好……</p><p><img src="/blog/../images/2025-8-22/2bd9d056217cf20581712e6f488f431c.jpg" alt="2bd9d056217cf20581712e6f488f431c"></p><p>于是老实回家打磨软件，写屁屁踢</p><h2 id="发布会"><a href="#发布会" class="headerlink" title="发布会"></a>发布会</h2><p>因为暴雨和航空管制耽误半天之后，总算是又飞到了东莞</p><p>vivo 提前给了伴手礼，有一个挎包，一个水杯，一个创作者日记和一盒明信片</p><p><img src="/blog/../images/2025-8-22/b&bo=VQhABlUIQAYWECA!&rf=viewer_311.jpeg" alt="img"></p><p>隔天去会场参加正式的发布会</p><p>会场在东莞体育馆，中心看台被改成了发布会场地，四周的场馆做成了展馆</p><p>有 vivo vision 的展台，vivo 自家摄影比赛的展台，和<strong>茶歇（重点</strong></p><p>（拿到一杯他们家的 🍊 美式，太甜了</p><p><img src="/blog/../images/2025-8-22/b&ek=1&kp=1&pt=0&bo=QAZVCEAGVQgWECA!&tl=3&vuin=2585050765&tm=1755867600&dis_t=1755870784&dis_k=33f0a1bc9a9e82b6b38f7c754c1321f2&sce=60-4-3&rf=viewer_311.jpeg" alt="img"></p><p>正式的发布会上，其实大多数功能直接看隔壁 🍎 的 vision pro 就能知道个大概，比较值得一提的就是重量达到了 398g，很轻便</p><p>vivo 自己也知道这个产品的成熟度远没到可以售卖的阶段，所以发布的是所谓<strong>探索版</strong>，仅仅开放了线下授权店的体验</p><p>但是要我说他这个支持 windows 电脑串流和安卓手机投屏已经赢了 vision pro 太多了</p><p><img src="/blog/../images/2025-8-22/ef0fc7de9a0eb7a3e60464a2e6e0a9c4_720.jpg" alt="ef0fc7de9a0eb7a3e60464a2e6e0a9c4_720"></p><p>现场没开放体验，但是我们第二天有去园区体验的机会</p><p>vivo 的园区正好在 30 周年纪念活动，除了这些 vivo vision 的体验也有其他展览（比如把手机冻进冰块的神必操作</p><p><img src="/blog/../images/2025-8-22/98d972658da9b9f9193f78c54f9c4ed6_720.jpg" alt="98d972658da9b9f9193f78c54f9c4ed6_720"></p><p>碍于时间限制没法完整体验完所有流程，也不能看完 vivo park 的演出，但是我们仍然可以到二楼完整体验一下 vivo vision 的功能</p><p>vivo 为戴眼镜的朋友提供了度数测量和镜片匹配服务</p><p><img src="/blog/../images/2025-8-22/1345f2ef118484c8ccaf791e2c3169f1_720.jpg" alt="1345f2ef118484c8ccaf791e2c3169f1_720"></p><p><em>（虽然但是，我感觉他测量的并不是很准，我和队友两个人的镜片效果都差强人意）</em></p><p>在工作人员的帮助下，我把 vision 支持的功能基本都体验了一遍</p><p>首先通过 vision 扫描我的头型，选择合适我的面罩</p><p>然后是视频收看，他提供了一段常规的视频，但是可以通过头显进行全屏放映</p><p>实际体验下来非常类似电影的观感，我认为是这其中最让人惊艳的功能了</p><p>之后看了有 180° 和 270° 的视频，每个 VR&#x2F;MR 的必吃环节，不多赘述</p><p>所有应用窗口都是可以自定义在空间各处的，体验很不错，但是官方宣传里说是打工牛马专属工具……</p><p><img src="/blog/../images/2025-8-22/Snipaste_2025-08-22_22-47-52.png" alt="Snipaste_2025-08-22_22-47-52"></p><p>既然是 MR，肯定得有增强现实的部分，我没有体验他给出的神秘小音游，而是选择了一段交互演出，大致还是和虚拟人物互动，实际上现实的部分不多</p><p>当时给出的演示地点空间不大，不然可以走动观赏，据工作人员说可以看到建模很精密的地方，但是我持怀疑态度</p><hr><p>总体来讲，我觉得 vision 作为影音设备的功能是非常强大的</p><p>但是手眼交互这块，说实话没有达到发布会给我的预期</p><p>然后 MR 交互也差强人意</p><p>期待正式版这些问题能够得到改进</p><h2 id="决赛答辩"><a href="#决赛答辩" class="headerlink" title="决赛答辩"></a>决赛答辩</h2><p>发布会之后，我们从原来的酒店转移到了东莞迎宾馆</p><p><em>给我干那来了，这还是国内吗.jpg</em></p><p>我是真没想到还能住上一次五星级酒店，企业赞助的比赛就是有底气，你说是吧 xx</p><hr><p>止步国二，但是结果已经超出我的预料</p><p>从五月的一个想法，到小学期的正式开发，再到八月的夏令营和包装</p><p>满打满算一个多月的开发，有过因为赶进度一天只吃一口饭，也因为 ddl 之前的 bug 红温过，在模拟答辩被其他组比的一无是处，在电脑前对着不擅长的 ppt 抓耳挠腮</p><p>我们不像其他队伍有着祖传代码和科研底蕴，也没有博士生研究生过来代打。他们有产品，开发，UI，答辩，而我们正经参与开发的就两个大二，还要靠着自己稀碎的审美做屁屁踢剪视频</p><p>但是第一次，在正式比赛的答辩中，我没有紧张到手无足措</p><p>从需求，到架构，到开发，到美化，到包装</p><p>每一行代码，每一行字，每一处技术栈我都深度参与，没有 AI 给的乱七八糟的屎山，我头一次对每一处内容都有把握</p><p>这个结果，我心满意足</p><p>感谢队友，没有他的端侧我们甚至进不了决赛，感谢自己，没有在最红温的时候撂挑子不干</p><p>感谢焦头烂额的那一个个日日夜夜</p><p><img src="/blog/../images/2025-8-22/f3824d97711da954dcd2b2e9c26a4cc1.jpg" alt="f3824d97711da954dcd2b2e9c26a4cc1"></p><blockquote><p>修改于 8 月 25 日晚，东莞迎宾馆</p><p>kpmark</p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="杂谈" scheme="https://kpmark.icu/categories/%E6%9D%82%E8%B0%88/"/>
    
    
    <category term="vivo" scheme="https://kpmark.icu/tags/vivo/"/>
    
    <category term="AIGC" scheme="https://kpmark.icu/tags/AIGC/"/>
    
  </entry>
  
  <entry>
    <title>Tauri 安卓踩坑笔记</title>
    <link href="https://kpmark.icu/2025/07/17/2025-7-17/"/>
    <id>https://kpmark.icu/2025/07/17/2025-7-17/</id>
    <published>2025-07-17T03:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.265Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="Tauri-安卓踩坑日记"><a href="#Tauri-安卓踩坑日记" class="headerlink" title="Tauri 安卓踩坑日记"></a>Tauri 安卓踩坑日记</h1><p>简单记录一下使用 Tauri 2.0 开发安卓应用踩的小坑</p><h2 id="打包签名-apk"><a href="#打包签名-apk" class="headerlink" title="打包签名 apk"></a>打包签名 apk</h2><p>官网上其实写的很清楚，但是就是有两个 🐖 眼睛瞎了</p><p><img src="/blog/../images/2025-7-17/5c04b5d9bc3eb13564b908aae01ea8d3fd1f44b1.jpg" alt="img"></p><p>首先你要有一个密钥库，可以通过 Android Studio 生成密钥，也可以用 keytool 生成，这里我们通过命令行使用 keytool</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keytool <span class="literal">-genkey</span> <span class="literal">-v</span> <span class="literal">-keystore</span> <span class="variable">$env:USERPROFILE</span>\upload<span class="literal">-keystore</span>.jks <span class="literal">-storetype</span> JKS <span class="literal">-keyalg</span> RSA <span class="literal">-keysize</span> <span class="number">2048</span> <span class="literal">-validity</span> <span class="number">10000</span> <span class="literal">-alias</span> upload</span><br></pre></td></tr></table></figure><blockquote><p>你的 keytool 安装在 Android Studio 附带的 JDK 中，例如<code>C:\\Program Files\\Android\\Android Studio\\jbr\\bin\\keytool.exe</code></p></blockquote><p>然后在<code>[project]/src-tauri/gen/android/keystore.properties</code>位置创建文件，这个文件包含对你密钥库的引用信息</p><blockquote><p>注意你的密钥库和 keystore.properties 都应该是私密的</p></blockquote><p>最后，我们只需要在 Gradle 中配置签名选项即可</p><p>在**<code>[project]/src-tauri/gen/android/app/build.gradle.kts</code>**文件中，添加如下引用</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.Properties</span><br><span class="line"><span class="keyword">import</span> java.io.FileInputStream</span><br></pre></td></tr></table></figure><p>在<code>buildTypes</code>代码块之前的<code>signingConfigs </code>中添加<code>release</code>代码块</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">signingConfigs &#123;</span><br><span class="line">    create(<span class="string">&quot;release&quot;</span>) &#123;</span><br><span class="line">        <span class="keyword">val</span> keystorePropertiesFile = rootProject.file(<span class="string">&quot;keystore.properties&quot;</span>)</span><br><span class="line">        <span class="keyword">val</span> keystoreProperties = Properties()</span><br><span class="line">        <span class="keyword">if</span> (keystorePropertiesFile.exists()) &#123;</span><br><span class="line">            keystoreProperties.load(FileInputStream(keystorePropertiesFile))</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        keyAlias = keystoreProperties[<span class="string">&quot;keyAlias&quot;</span>] <span class="keyword">as</span> String</span><br><span class="line">        keyPassword = keystoreProperties[<span class="string">&quot;keyPassword&quot;</span>] <span class="keyword">as</span> String</span><br><span class="line">        storeFile = file(keystoreProperties[<span class="string">&quot;storeFile&quot;</span>] <span class="keyword">as</span> String)</span><br><span class="line">        storePassword = keystoreProperties[<span class="string">&quot;storePassword&quot;</span>] <span class="keyword">as</span> String</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">buildTypes &#123;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后在<code>buildTypes</code>块的<code>release</code>中使用新的配置</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">buildTypes &#123;</span><br><span class="line">    getByName(<span class="string">&quot;release&quot;</span>) &#123;</span><br><span class="line">        signingConfig = signingConfigs.getByName(<span class="string">&quot;release&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看上去很简单，但是就是有 🐖 分不清<code>src-tauri\gen\android\build.gradle.kts</code>和<code>src-tauri\gen\android\app\build.gradle.kts</code></p><p>注意着两个文件同名但路径不同，功能也不同</p><h2 id="使用-HTTP-客户端插件"><a href="#使用-HTTP-客户端插件" class="headerlink" title="使用 HTTP 客户端插件"></a>使用 HTTP 客户端插件</h2><p>这里官网的文档说的不是很详细</p><p>我们使用的是 JavaScript API 版本，通过</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run tauri add http</span><br></pre></td></tr></table></figure><p>安装插件</p><p>在官方文档中，给出的配置允许访问的 URL 的配置文件如下</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src-tauri/capabilities/base.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;permissions&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;identifier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;http:default&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;allow&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">&#123;</span> <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://*.tauri.app&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;deny&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">&#123;</span> <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://private.tauri.app&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>但是我们找到<code>src-tauri/capabilities</code>中，并没有发现<code>base.json</code>文件</p><p>直接创建一个<code>base.json</code>文件并粘贴代码肯定是错误的，我按照我对 Tauri 配置的诸多理解进行了修改，也依然报错</p><p>最简单的方式是直接在<code>default.json</code>中复制代码块</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;$schema&quot;</span><span class="punctuation">:</span> <span class="string">&quot;../gen/schemas/desktop-schema.json&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;identifier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;default&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Capability for the main window&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;windows&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;main&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;permissions&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;core:default&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;opener:default&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;identifier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;http:default&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;allow&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://*.*&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;http://*.*&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>然后就可以正常使用了</p><h2 id="使用-SQL-插件"><a href="#使用-SQL-插件" class="headerlink" title="使用 SQL 插件"></a>使用 SQL 插件</h2><h3 id="构建配置"><a href="#构建配置" class="headerlink" title="构建配置"></a>构建配置</h3><p>这个插件官方的文档也有点小问题</p><p>首先，在<code>Cargo.toml</code>文件中添加以下内容</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[dependencies.tauri-plugin-sql]</span><br><span class="line">features = [&quot;sqlite&quot;] # or &quot;postgres&quot;, or &quot;mysql&quot;</span><br><span class="line">git = &quot;https://github.com/tauri-apps/plugins-workspace&quot;</span><br><span class="line">branch = &quot;v2&quot;</span><br></pre></td></tr></table></figure><p>然后使用以下命令下载插件</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm add https://github.com/tauri<span class="literal">-apps</span>/tauri<span class="literal">-plugin-sql</span><span class="comment">#v2</span></span><br></pre></td></tr></table></figure><p>我们在 Tauri 中注册插件</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src-tauri/src/main.rs</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    tauri::Builder::<span class="title function_ invoke__">default</span>()</span><br><span class="line">        .<span class="title function_ invoke__">plugin</span>(tauri_plugin_sql::Builder::<span class="title function_ invoke__">default</span>().<span class="title function_ invoke__">build</span>()) <span class="comment">// 注册插件</span></span><br><span class="line">        .<span class="title function_ invoke__">run</span>(tauri::generate_context!())</span><br><span class="line">        .<span class="title function_ invoke__">expect</span>(<span class="string">&quot;error while running tauri application&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>官网的说明就到这里，实际上，你还需要在<code>src-tauri\src\lib.rs</code>中添加如下代码</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#[tauri::command]</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">greet</span>(name: &amp;<span class="type">str</span>) <span class="punctuation">-&gt;</span> <span class="type">String</span> &#123;</span><br><span class="line">    <span class="built_in">format!</span>(<span class="string">&quot;Hello, &#123;&#125;! You&#x27;ve been greeted from Rust!&quot;</span>, name)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#[cfg_attr(mobile, tauri::mobile_entry_point)]</span></span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">run</span>() &#123;</span><br><span class="line">    tauri::Builder::<span class="title function_ invoke__">default</span>()</span><br><span class="line">        .<span class="title function_ invoke__">plugin</span>(tauri_plugin_http::<span class="title function_ invoke__">init</span>())</span><br><span class="line">        .<span class="title function_ invoke__">plugin</span>(tauri_plugin_opener::<span class="title function_ invoke__">init</span>())</span><br><span class="line">        .<span class="title function_ invoke__">plugin</span>(tauri_plugin_sql::Builder::<span class="title function_ invoke__">default</span>().<span class="title function_ invoke__">build</span>()) <span class="comment">// 添加</span></span><br><span class="line">        .<span class="title function_ invoke__">invoke_handler</span>(tauri::generate_handler![greet])</span><br><span class="line">        .<span class="title function_ invoke__">run</span>(tauri::generate_context!())</span><br><span class="line">        .<span class="title function_ invoke__">expect</span>(<span class="string">&quot;error while running tauri application&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="类型转换"><a href="#类型转换" class="headerlink" title="类型转换"></a>类型转换</h3><p>不知道为什么，我们通过<code>execute</code>执行 sql 语句，拿回来的 Date 类型和 Boolean 类型都变成了字符串类型，而且 TypeScript 并看不出来变量的实际类型，导致类型转换的时候还要套一层<code>unkown</code></p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> completed = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(rawPlan.<span class="property">completed</span> <span class="keyword">as</span> <span class="built_in">unknown</span> <span class="keyword">as</span> <span class="built_in">string</span>);</span><br></pre></td></tr></table></figure><h2 id="使用自定义动态库"><a href="#使用自定义动态库" class="headerlink" title="使用自定义动态库"></a>使用自定义动态库</h2><p>先 🕊 着</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="杂谈" scheme="https://kpmark.icu/categories/%E6%9D%82%E8%B0%88/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
    <category term="Android" scheme="https://kpmark.icu/tags/Android/"/>
    
    <category term="Tauri" scheme="https://kpmark.icu/tags/Tauri/"/>
    
  </entry>
  
  <entry>
    <title>devzat 使用指北</title>
    <link href="https://kpmark.icu/2025/04/13/2025-4-13/"/>
    <id>https://kpmark.icu/2025/04/13/2025-4-13/</id>
    <published>2025-04-13T03:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.264Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p>项目地址：<a href="https://github.com/quackduck/devzat">quackduck&#x2F;devzat: The devs are over here at devzat, chat over SSH!</a></p><p>我进行了简单的汉化：<a href="https://github.com/markzhang12345/devzat">markzhang12345&#x2F;devzat: The devs are over here at devzat, chat over SSH!</a></p><p>聊天室太冷清了，快进来玩</p><hr><h1 id="devzat-简介"><a href="#devzat-简介" class="headerlink" title="devzat 简介"></a>devzat 简介</h1><p>devzat 是程序员专属的 SSH 聊天室。这是一个通过 SSH 连接的聊天室，用户无需安装客户端，仅需一条 SSH 命令即可登录。它支持私人消息、多聊天室、图片和代码高亮等功能，还可以集成第三方服务、自托管 SSH 聊天室。</p><h1 id="快速启动"><a href="#快速启动" class="headerlink" title="快速启动"></a>快速启动</h1><h2 id="环境要求"><a href="#环境要求" class="headerlink" title="环境要求"></a>环境要求</h2><p>首先，确保你的电脑有 SSH 客户端。正常用过 git 的电脑中都应该有 SSH 客户端了，没有的可以去下载 OpenSSH</p><p>然后，确保你的电脑中存在 SSH 密钥对，虽然我并未设置对密钥进行认证，但是你得有一个</p><p>可以使用以下命令创建 SSH 密钥对：</p><ul><li><code>ssh-keygen -t rsa -C &quot;youremail@example.com&quot;</code></li><li><code>ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;</code></li></ul><p>基本上对密钥版本没有特殊的要求，选一个创建就行，输完命令之后一路回车即可</p><h2 id="快速启动-1"><a href="#快速启动-1" class="headerlink" title="快速启动"></a>快速启动</h2><p>打开任意终端，输入命令</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh &lt;用户名&gt;@<span class="number">60.205</span>.<span class="number">131.158</span> <span class="literal">-p</span> <span class="number">11451</span></span><br></pre></td></tr></table></figure><p>即可连接至聊天室</p><p>有时候维护服务区重启项目，可能需要使用以下命令删去原先的指纹</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh<span class="literal">-keygen</span> <span class="literal">-R</span> <span class="string">&quot;[60.205.131.158]:11451&quot;</span></span><br></pre></td></tr></table></figure><p>用户名就是你的网名，随便起就行</p><h2 id="不快速的启动"><a href="#不快速的启动" class="headerlink" title="不快速的启动"></a>不快速的启动</h2><p>每次输入一长串命令太麻烦？可以直接在用户文件夹下的<code>.ssh/config</code>中添加以下配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Host devchat          # 自定义别名（可随意命名）</span><br><span class="line">    HostName 60.205.131.158</span><br><span class="line">    Port 11451</span><br><span class="line">    User &lt;用户名&gt;       # 你的用户名</span><br></pre></td></tr></table></figure><p><strong>现在只需输入 <code>ssh devchat</code> 即可自动连接</strong></p><p><img src="/blog/../images/2025-4-13/image-20250413092623376.png" alt="image-20250413092623376"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="杂谈" scheme="https://kpmark.icu/categories/%E6%9D%82%E8%B0%88/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
    <category term="Go" scheme="https://kpmark.icu/tags/Go/"/>
    
  </entry>
  
  <entry>
    <title>WebSocket 原理与 Go 语言实现</title>
    <link href="https://kpmark.icu/2025/03/17/2025-3-17/"/>
    <id>https://kpmark.icu/2025/03/17/2025-3-17/</id>
    <published>2025-03-17T03:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.263Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p><img src="/blog/../images/2025-3-17/bf67abd6912397ddba7b27271f82b2b7d0a28770.jpg" alt="img"></p><p>隔了半个月才发出来，咕咕咕</p><hr><p>WebSocket 这玩意用的不少了，从 JavaScript 到 Go，用过各种各样的包来实现这玩意的服务器或客户端。然而，直到现在我对这玩意的原理还是模模糊糊的，趁这个机会，好好学一下 WebSocket 的原理</p><h1 id="从-HTTP-到-Websocket"><a href="#从-HTTP-到-Websocket" class="headerlink" title="从 HTTP 到 Websocket"></a>从 HTTP 到 Websocket</h1><h2 id="二者关系"><a href="#二者关系" class="headerlink" title="二者关系"></a>二者关系</h2><p>虽然 WebSocket 区别于 HTTP，是全双工通信协议。但其并非完全独立于 HTTP，而是基于 HTTP 的升级机制</p><p>其核心原理，是通过一次 HTTP 的握手，建立持久连接，然后转变为双向二进制帧传输</p><h2 id="升级过程"><a href="#升级过程" class="headerlink" title="升级过程"></a>升级过程</h2><h3 id="握手请求"><a href="#握手请求" class="headerlink" title="握手请求"></a>握手请求</h3><p>首先，客户端应该先发送 HTTP 握手请求</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">GET</span> <span class="string">/ws</span> <span class="meta">HTTP/1.1</span></span><br><span class="line"><span class="attribute">Host</span><span class="punctuation">: </span>example.com</span><br><span class="line"><span class="attribute">Connection</span><span class="punctuation">: </span>Upgrade</span><br><span class="line"><span class="attribute">Upgrade</span><span class="punctuation">: </span>websocket</span><br><span class="line"><span class="attribute">Sec-WebSocket-Key</span><span class="punctuation">: </span>dGhlIHNhbXBsZSBub25jZQ==  # 随机 Base64 字符串</span><br><span class="line"><span class="attribute">Sec-WebSocket-Version</span><span class="punctuation">: </span>13</span><br></pre></td></tr></table></figure><p>其中，<code>Connection: Upgrade</code>和<code>Upgrade: websocket</code>字段，明确要求协议升级</p><p><code>Sec-WebSocekt-Key</code>用于服务端生成生成响应密钥，防止中间代理缓存 WebSocket 帧</p><p>最后那个字段用于指定协议版本</p><h3 id="响应"><a href="#响应" class="headerlink" title="响应"></a>响应</h3><p>成功响应的响应头如下所示</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">HTTP/1.1</span> <span class="number">101</span> Switching Protocols</span><br><span class="line"><span class="attribute">Upgrade</span><span class="punctuation">: </span>websocket</span><br><span class="line"><span class="attribute">Connection</span><span class="punctuation">: </span>Upgrade</span><br><span class="line"><span class="attribute">Sec-WebSocket-Accept</span><span class="punctuation">: </span>s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  # 基于客户端密钥生成</span><br></pre></td></tr></table></figure><p>这里唯一值得注意的操作，就是这里的密钥生成规则</p><p>服务端将客户端发送的<code>Sec-WebSocket-Key</code>字段内容，拼接在字符串常量后，计算 SHA-1 后，转为 Base64 编码，作为<code>Sec-WebSocket-Accept</code>字段的内容返回</p><p>在 Go 语言中，<code>girilla/websocket</code>库自动完成此计算，我们并不需要手动处理这块内容</p><h3 id="TCP-连接复用"><a href="#TCP-连接复用" class="headerlink" title="TCP 连接复用"></a>TCP 连接复用</h3><p>握手完成后，Websocket 接管刚刚完成的 TCP 连接，后续的数据将以 WebSocket 帧格式传输</p><p>Go 语言中，存在<code>net/http</code>包将原始 TCP 连接抽象为<code>http.ResponseWriter</code>和<code>http.Request</code>，我们用这两个对象向 Websocket 中读写消息</p><p>同时，在<code>girilla/websocket</code>库中存在<code>Upgrader.Upgrade()</code>方法，从<code>http.ResponseWriter</code>中提取底层的 TCP 连接<code>net.Conn</code>，并切换为 WebSocket 协议处理器</p><h1 id="Go-语言实例"><a href="#Go-语言实例" class="headerlink" title="Go 语言实例"></a>Go 语言实例</h1><p>以下提供一个简单的 Go 实现的 WebSocket 服务器：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;log&quot;</span></span><br><span class="line"><span class="string">&quot;net/http&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/gorilla/websocket&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> upgrader = websocket.Upgrader&#123;</span><br><span class="line">CheckOrigin: <span class="function"><span class="keyword">func</span><span class="params">(r *http.Request)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleWebSocket</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">conn, err := upgrader.Upgrade(w, r, <span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(<span class="string">&quot;WebSocket 升级失败:&quot;</span>, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> conn.Close()</span><br><span class="line"></span><br><span class="line">log.Println(<span class="string">&quot;客户端已连接:&quot;</span>, conn.RemoteAddr())</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">messageType, message, err := conn.ReadMessage()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(<span class="string">&quot;读取消息失败:&quot;</span>, err)</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">log.Printf(<span class="string">&quot;收到消息: %s&quot;</span>, message)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := conn.WriteMessage(messageType, message); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(<span class="string">&quot;发送消息失败:&quot;</span>, err)</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">log.Println(<span class="string">&quot;客户端已断开:&quot;</span>, conn.RemoteAddr())</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">http.HandleFunc(<span class="string">&quot;/ws&quot;</span>, handleWebSocket)</span><br><span class="line"></span><br><span class="line">log.Println(<span class="string">&quot;WebSocket 服务器已启动，监听端口 8080...&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err := http.ListenAndServe(<span class="string">&quot;:8080&quot;</span>, <span class="literal">nil</span>); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;服务器启动失败:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="来看代码"><a href="#来看代码" class="headerlink" title="来看代码"></a>来看代码</h2><h3 id="依赖"><a href="#依赖" class="headerlink" title="依赖"></a>依赖</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;log&quot;</span></span><br><span class="line"><span class="string">&quot;net/http&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/gorilla/websocket&quot;</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><ul><li><code>log</code>：用于记录日志(虽然但是，Go 的日志系统还是依托)</li><li><code>net/http</code>：提供 HTTP 服务器功能</li><li><code>github.com/gorilla/websocket</code>：提供 WebSocket 协议的实现</li></ul><h3 id="实例化-WebSocket-升级器"><a href="#实例化-WebSocket-升级器" class="headerlink" title="实例化 WebSocket 升级器"></a>实例化 WebSocket 升级器</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> upgrader = websocket.Upgrader&#123;</span><br><span class="line">CheckOrigin: <span class="function"><span class="keyword">func</span><span class="params">(r *http.Request)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将升级的关键函数<code>Upgrader</code>通过<code>upgrader</code>实例化，并重写了<code>CheckOrigin</code>字段中的函数</p><p>这玩意是用来控制跨域请求的，默认只允许同源请求，即 WebSocket 请求的 Origin 头与服务器主机<strong>完全匹配</strong>时，连接才会被允许，即</p><ul><li>相同的协议(http&#x2F;https)</li><li>相同的主机名</li><li>相同的端口号</li></ul><p>所以，不管时调试还是生产环境，一般都是要重写的</p><h3 id="处理-WebSocket-连接"><a href="#处理-WebSocket-连接" class="headerlink" title="处理 WebSocket 连接"></a>处理 WebSocket 连接</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleWebSocket</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">conn, err := upgrader.Upgrade(w, r, <span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(<span class="string">&quot;WebSocket 升级失败:&quot;</span>, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> conn.Close()</span><br><span class="line">...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们用实例化的<code>upgrader.Upgrade</code>，拿到 HTTP 中的 TCP 读写，然后交给 WebSocket 控制，使其升级为 WebSocket 连接<code>conn</code></p><p>吐槽一下 Go 的 err 机制，太繁琐了</p><p>记得使用<code>defer conn.Close</code>确保函数结束时连接关闭，释放资源</p><h3 id="消息处理循环"><a href="#消息处理循环" class="headerlink" title="消息处理循环"></a>消息处理循环</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">messageType, message, err := conn.ReadMessage()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(<span class="string">&quot;读取消息失败:&quot;</span>, err)</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">log.Printf(<span class="string">&quot;收到消息: %s&quot;</span>, message)</span><br><span class="line"><span class="keyword">if</span> err := conn.WriteMessage(messageType, message); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Println(<span class="string">&quot;发送消息失败:&quot;</span>, err)</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用<code>conn</code>的<code>ReadMessage</code>方法，从 WebSocket 客户端中读取消息，返回消息类型和内容</p><p>这里的<code>WriteMessage</code>方法可以将消息写入 WebSocket 客户端，这里为了演示，直接将接收到的消息原样返回了</p><h3 id="启动-HTTP-服务器"><a href="#启动-HTTP-服务器" class="headerlink" title="启动 HTTP 服务器"></a>启动 HTTP 服务器</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">http.HandleFunc(<span class="string">&quot;/ws&quot;</span>, handleWebSocket)</span><br><span class="line">log.Println(<span class="string">&quot;WebSocket 服务器已启动，监听端口 8080...&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err := http.ListenAndServe(<span class="string">&quot;:8080&quot;</span>, <span class="literal">nil</span>); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;服务器启动失败:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里使用<code>http.HandleFunc</code>方法，将<code>/ws</code>路径的请求交给<code>handleWebSocket</code>处理，并使用<code>http.ListenAndServe</code>方法，将服务启动在本机 8080 端口</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
    <category term="Go" scheme="https://kpmark.icu/tags/Go/"/>
    
  </entry>
  
  <entry>
    <title>尝试复现一个 vv 表情机器人 —— 基于 LagrangeGo 的实现</title>
    <link href="https://kpmark.icu/2025/03/13/2025-3-13/"/>
    <id>https://kpmark.icu/2025/03/13/2025-3-13/</id>
    <published>2025-03-13T03:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.263Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p><a href="https://github.com/markzhang12345/vvBot">markzhang12345&#x2F;vvBot: 一个基于 LagrangeGo-Template 和 vv433 的自动 vv 表情包 qq 机器人</a></p><p>之前看到一个有关 vv 的开源项目，感觉特别有意思</p><p><a href="https://github.com/Cicada000/VV">Cicada000&#x2F;VV: 你还在为自己存放的 VV 表情包不够多，使用时觉得不够贴切而感到烦恼吗？快来试试这个项目吧！</a></p><p>当时觉得实现一个能够自动发送 vv 表情包的机器人特别有意思，但是手头事情很多，就暂时搁置了</p><p>虽然当天就看到某个学长已经实现了类似的机器人，但还是想着自己能不能复现一次</p><p>于是这几天，忙里偷闲整出来一个</p><p><img src="/blog/../images/2025-3-13/e609246d6587187296f8d7147ff856f2.png" alt="img"></p><h1 id="框架选择"><a href="#框架选择" class="headerlink" title="框架选择"></a>框架选择</h1><h2 id="koishi"><a href="#koishi" class="headerlink" title="koishi"></a>koishi</h2><p>开始了解有关机器人的框架，发现好像 koishi 很火，而且需要的环境正好全部都有，甚至还有管理面板，于是打算使用 koishi</p><p><img src="/blog/../images/2025-3-13/image-20250313205719666.png" alt="image-20250313205719666"></p><p>然而，开始阅读文档之后，发现实现自己的自定义插件好像不如我想的那么简单，而且，最重要的是，我需要的第三方 Bot 插件已经不能用了</p><p><img src="/blog/../images/2025-3-13/2b36a13fa819b8718425bb33d90800ed.jpg" alt="img"></p><p>于是决定转战其他框架</p><h2 id="LagrangeGo"><a href="#LagrangeGo" class="headerlink" title="LagrangeGo"></a>LagrangeGo</h2><p>在使用搜索引擎简单检索之后，决定尝试使用 LagrangeGo 这款协议</p><p>这玩意是本质上是 Lagrange.Core 的 Go 语言实现，其中 Lagrange.Core 的官方文档中存在的实现方式有如下几种：</p><ul><li>C#</li><li>Python</li><li>Golang</li></ul><p>你说的对，但是我不会 C#，不如说最近看某个项目的 C# 代码看的我及其头大。Python 又总是给我一种效率低的<strong>错觉</strong>，可以的话还是想用编译型语言</p><p>所以最后选择用还算熟悉的 Golang 进行编写</p><p>LagrangeGo 的<a href="https://github.com/LagrangeDev/LagrangeGo">仓库</a>中，说明这个协议实现不推荐直接使用，可以用基于此项目的 <a href="https://github.com/LagrangeDev/go-cqhttp">go-cqhttp</a> 或者模板框架 <a href="https://github.com/ExquisiteCore/LagrangeGo-Template">ExquisiteCore&#x2F;LagrangeGo-Template</a> 实现</p><p>我一看，欸，go-cqhttp 也被大手 👊🐔 了， 而且我想实现的 bot 暂时也不需要集成那么多功能，于是选择了使用 LagrangeGo-Template</p><h1 id="功能实现"><a href="#功能实现" class="headerlink" title="功能实现"></a>功能实现</h1><p>想要在模板的基础上添加功能，我们需要简单了解这个模板的构成</p><p>让我来看一看源码，嗯…</p><p><img src="/blog/../images/2025-3-13/593933857b42d23ece26a891f6708ed2_720.jpg" alt="img"></p><p>好，让我们来编写这个 bot 的功能</p><h2 id="调用-API"><a href="#调用-API" class="headerlink" title="调用 API ?"></a>调用 API ?</h2><p>注意到，文章开头提到的 Cicada000&#x2F;VV 提供了一个具有模糊搜索功能的 API 调用， 我一开始的想法是直接调用这个 API，然后获取对应的表情包</p><p>折腾了一下，发现这玩意的响应竟然是一组 json 而不是一个，让我调试了半天</p><p>然后解析他给我的 json，发现只给出来对应表情视频帧的位置</p><p>没找到视频在哪里，所以开始想其他办法</p><p><img src="/blog/../images/2025-3-13/905044512ecac512dc994e26f1ce7c42.jpg" alt="img"></p><h2 id="使用-vv433"><a href="#使用-vv433" class="headerlink" title="使用 vv433"></a>使用 vv433</h2><p>某位好友在这个时候向我提供的他的武器库，vv433 —— 带有标题的 433 张 vv 表情包，于是决定直接调用武器库</p><p>简单整理 vv433，只保留 png 格式文件，改装成 vv428</p><p>考虑到功能对于文件访问的高频性，这里简单将武器库的文件名整理成 json 文件，以便直接放进 go 程序的内存，实现快速查询</p><p>写个 Python 脚本</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"></span><br><span class="line">source_dir = <span class="string">&#x27;vvSource&#x27;</span></span><br><span class="line">output_file = <span class="string">&#x27;filenames.json&#x27;</span></span><br><span class="line"></span><br><span class="line">png_files = []</span><br><span class="line"><span class="keyword">for</span> filename <span class="keyword">in</span> os.listdir(source_dir):</span><br><span class="line">    file_path = os.path.join(source_dir, filename)</span><br><span class="line">    <span class="keyword">if</span> os.path.isfile(file_path) <span class="keyword">and</span> filename.lower().endswith(<span class="string">&#x27;.png&#x27;</span>):</span><br><span class="line">        png_files.append(filename)</span><br><span class="line"></span><br><span class="line">png_files.sort()</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(output_file, <span class="string">&#x27;w&#x27;</span>, encoding=<span class="string">&#x27;utf-8&#x27;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    json.dump(png_files, f, indent=<span class="number">4</span>, ensure_ascii=<span class="literal">False</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;已生成 <span class="subst">&#123;output_file&#125;</span>，包含 <span class="subst">&#123;<span class="built_in">len</span>(png_files)&#125;</span> 个PNG文件。&quot;</span>)</span><br></pre></td></tr></table></figure><h2 id="搜索逻辑"><a href="#搜索逻辑" class="headerlink" title="搜索逻辑"></a>搜索逻辑</h2><p>实在没时间写模糊语义处理，而且也不会写，于是直接暴力查询</p><p>先全字匹配，再部分匹配，匹配不到就随机，匹配到多个也随机</p><p>只能说 vv 还是太全面了，随机出来的表情包意外的有节目效果</p><p><img src="/blog/../images/2025-3-13/b71f51e33abcee646b684a7bca368572.jpg" alt="img"></p><p>这里不再贴出源码了，想看的直接去仓库看就行</p><hr><p>修改于 2025-4-6</p><p>其实几周后就做了个简单的模糊语义处理，但是太懒了所以现在才发</p><p>也没有什么，因为我自己实在不会写，Go 有没有现成的好用的库，所以干脆直接把选择逻辑全扔给 AI 做了，顺便写了几个好玩的（神必的）AI 交互</p><h1 id="待实现"><a href="#待实现" class="headerlink" title="待实现"></a>待实现</h1><ul><li><p>模糊语义搜索（升级版）</p></li><li><p>绕开 QQ 的机器人检测</p></li></ul><p>你说的对，但是我头一天只是调试了一下 bot，第二天号就被封了</p><p>姑且是加了点随机时延，然后增加了手动发送文字功能，但还是觉得不保险</p><p>正在想怎么模拟正常用户的行为</p><p><img src="/blog/../images/2025-3-13/7679a7ef629c6fb2e3734d3cf0348e1b.jpg" alt="img"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="杂谈" scheme="https://kpmark.icu/categories/%E6%9D%82%E8%B0%88/"/>
    
    
    <category term="Linux" scheme="https://kpmark.icu/tags/Linux/"/>
    
    <category term="Go" scheme="https://kpmark.icu/tags/Go/"/>
    
  </entry>
  
  <entry>
    <title>使用 VirtualBox 虚拟机安装 openEuler</title>
    <link href="https://kpmark.icu/2025/03/09/2025-3-9/"/>
    <id>https://kpmark.icu/2025/03/09/2025-3-9/</id>
    <published>2025-03-09T03:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.264Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="下载-VirtualBox"><a href="#下载-VirtualBox" class="headerlink" title="下载 VirtualBox"></a>下载 VirtualBox</h1><p>首先，从<a href="https://www.virtualbox.org/">官网</a>下载 openEuler 安装程序</p><p><img src="/blog/../images/2025-3-9/image-20250309092356090.png" alt="image-20250309092356090"></p><p>最新版本是 7.1.6，然而，在 7.0.14 到 7.0.16 的版本更新中，VirtualBox 将安装路径默认固定在了系统盘中，以防止用户重新写入或者重命名文件夹。但是我的 C 盘容量告急，所以这里选择查找历史版本 7.0.12 来进行安装</p><p><img src="/blog/../images/2025-3-9/image-20250309093724057.png" alt="image-20250309093724057"></p><h1 id="下载-openEuler-镜像"><a href="#下载-openEuler-镜像" class="headerlink" title="下载 openEuler 镜像"></a>下载 openEuler 镜像</h1><p>进入<a href="https://www.openeuler.org/zh/download/">openEuler 下载</a>，选择合适的稳定版本</p><p>这里我暂时不在此环境中进行开发，所以选择进行最小安装</p><p><img src="/blog/../images/2025-3-9/image-20250309095227309.png" alt="image-20250309095227309"></p><p>耐心等待下载即可</p><h1 id="创建-openEuler-虚拟机"><a href="#创建-openEuler-虚拟机" class="headerlink" title="创建 openEuler 虚拟机"></a>创建 openEuler 虚拟机</h1><h2 id="新建虚拟机"><a href="#新建虚拟机" class="headerlink" title="新建虚拟机"></a>新建虚拟机</h2><p>进入 VirtualBox，点击新建，开始创建虚拟机<br><img src="/blog/../images/2025-3-9/image-20250309101326483.png" alt="image-20250309101326483"></p><p>为啥显示检测到了 Red Hat 呢，收集资料显示，openEuler 早期计数路线沿用了红帽系得 RPM 软件包管理系统和文件系统布局，和 CentOS 极为类似。这样，该系统能够兼容大量 CentOS&#x2F;RHEL 开发的应用</p><p>不过我这里选择了跳过自动安装，这个自动检测类型应该不会有太大影响</p><p><img src="/blog/../images/2025-3-9/image-20250309103712635.png" alt="image-20250309103712635"></p><p>分配 2GB 内存，给 2 个 CPU 核心，给 10GB 的虚拟硬盘</p><p><img src="/blog/../images/2025-3-9/image-20250309103721713.png" alt="image-20250309103721713"></p><h2 id="导入系统镜像"><a href="#导入系统镜像" class="headerlink" title="导入系统镜像"></a>导入系统镜像</h2><p>双击启动虚拟机，按照提示导入系统镜像</p><p><img src="/blog/../images/2025-3-9/image-20250309104022545.png" alt="image-20250309104022545"></p><p>出现如下页面，启动成功</p><p><img src="/blog/../images/2025-3-9/image-20250309104354885.png" alt="image-20250309104354885"></p><p>按照引导进行系统设置，预设好 root 账户和用户，就可以开始安装系统了，耐心等待即可</p><blockquote><p>使用鼠标在虚拟机中进行操作时，虚拟机会自动捕获鼠标，使用右侧的 CTRL 键即可解放鼠标</p></blockquote><p>安装好后，重启系统即可开始使用</p><blockquote><p>注意，这里按照默认提示，仍然会进入虚拟盘的安装流程，这里应该先弹出安装系统使用的镜像，再重启系统</p></blockquote><p><img src="/blog/../images/2025-3-9/image-20250309110218483.png" alt="image-20250309110218483"></p><p>使用预设的用户名和密码登录，即可进入命令行界面</p><p>输入<code>pwd</code>查看当前路径</p><p><img src="/blog/../images/2025-3-9/image-20250309110422742.png" alt="image-20250309110422742"></p><p>输入<code>ping www.bilibili.com</code>，显示如下响应</p><p><img src="/blog/../images/2025-3-9/image-20250309110600853.png" alt="image-20250309110600853"></p><p>网络功能正常，此虚拟机运行成功</p><h1 id="使用-SSH-连接虚拟机"><a href="#使用-SSH-连接虚拟机" class="headerlink" title="使用 SSH 连接虚拟机"></a>使用 SSH 连接虚拟机</h1><p>在虚拟机的命令窗口中，鼠标会被捕获，没有语法高亮，且不能复制粘贴一些常用命令，所以我尝试在宿主机中使用 SSH 来连接本地的虚拟机</p><p>虚拟机默认的网络环境是 NAT 环境，即 VirtualBox 内置的 NAT 引擎充当虚拟路由器，分配一个私有 IP，将所有流量转发至主机的网络接口，进行地址转换后再访问外网</p><p>我们可以使用<code>ip addr</code>命令找到这个私有 IP：</p><p><img src="/blog/../images/2025-3-9/image-20250309170548121.png" alt="image-20250309170548121"></p><p>但是，使用 NAT 模式是无法被宿主机访问的，所以我们尝试使用桥接模式</p><h2 id="桥接"><a href="#桥接" class="headerlink" title="桥接"></a>桥接</h2><p>桥接，是通过将虚拟机直接连接到主机物理端口，把虚拟机当成局域网中的独立设备</p><p>通过手动取得宿主机静态 IP，我们可以通过给虚拟机的网卡分配同一网段下的 IP 地址，从而通过宿主机 SSH 访问</p><p>在网卡设置<code>/etc/sysconfig/network-scripts/ifcfg-enp0s3</code>中，我进行了如下修改</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ifcfg-enp0s3（静态IP）</span></span><br><span class="line"><span class="attr">BOOTPROTO</span>=static</span><br><span class="line"><span class="attr">IPADDR</span>=<span class="number">192.168</span>.<span class="number">1.100</span></span><br><span class="line"><span class="attr">NETMASK</span>=<span class="number">255.255</span>.<span class="number">255.0</span></span><br><span class="line"><span class="attr">GATEWAY</span>=<span class="number">192.168</span>.<span class="number">1.1</span></span><br></pre></td></tr></table></figure><p>然而，可能是我的无线网卡(AX 210)不支持桥接需要的混杂模式，或是我的配置过程出了问题，桥接之后，即使分配了 IP，虚拟机也一直无法连接外网</p><p>考虑使用 NAT + Host-Only 双网卡实现 SSH 连接</p><h2 id="NAT-Host-Only"><a href="#NAT-Host-Only" class="headerlink" title="NAT + Host-Only"></a>NAT + Host-Only</h2><p>仍保持默认的 NAT 网卡不变，我们在虚拟机设置中添加一块新的虚拟网卡</p><p><img src="/blog/../images/2025-3-9/image-20250309171254718.png" alt="image-20250309171254718"></p><p>其中，这个 Host-Only 工具是在你下载 VirtualBox 的过程中自动下载的</p><p>然后，我们需要手动为自己的新网卡编写设置。首先使用<code>ip addr</code>找到新网卡的名称，我这里是<code>enp0s8</code></p><p>然后，在<code>/etc/sysconfig/network-scripts/</code>目录下编写<code>ifcfg-enp0s8</code>文件，我的内容如下</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">DEVICE</span>=enp0s8</span><br><span class="line"><span class="attr">NAME</span>=enp0s8</span><br><span class="line"><span class="attr">TYPE</span>=Ethernet</span><br><span class="line"><span class="attr">IPADDR</span>=<span class="number">192.168</span>.<span class="number">56.101</span></span><br><span class="line"><span class="attr">NETMASK</span>=<span class="number">255.255</span>.<span class="number">255.0</span></span><br><span class="line"><span class="attr">GATEWAY</span>=<span class="number">192.168</span>.<span class="number">56.1</span></span><br><span class="line"><span class="attr">DNS1</span>=<span class="number">8.8</span>.<span class="number">8.8</span></span><br><span class="line"><span class="attr">DNS2</span>=<span class="number">8.8</span>.<span class="number">4.4</span></span><br><span class="line"><span class="attr">ONBOOT</span>=<span class="literal">yes</span></span><br></pre></td></tr></table></figure><p>这样，我就可以在宿主机的命令行中通过<code> ssh 192.168.56.101</code>连接至虚拟机了</p><p><img src="/blog/../images/2025-3-9/image-20250309171702850.png" alt="image-20250309171702850"></p><h1 id="疑问"><a href="#疑问" class="headerlink" title="疑问"></a>疑问</h1><p>我因为使用 Docker 的原因，开启了 WSL2 服务，电脑的 hyperV 应该是正常开启的。然而，在虚拟机安装过程中，并没有像其他人一样遇到启动报错的问题</p><p>VirtualBox 从 6.0 版本开始支持与 Hyper-V 共存，但是我并没有使用过这项配置</p><p>网络上关于这两个兼容性的文章都比较久远，可能是某次稳定版本更新之后默认开启的兼容设置吧</p><p>然后，为什么我的桥接设置始终失败，这很奇怪</p><p>考虑我的宿主机静态 IP 设置有误，或者真的是这块无线网卡不支持 VirtualBox 的混杂模式</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Linux" scheme="https://kpmark.icu/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>写一个模拟 GPS 定位程序</title>
    <link href="https://kpmark.icu/2025/03/07/2025-3-7/"/>
    <id>https://kpmark.icu/2025/03/07/2025-3-7/</id>
    <published>2025-03-07T07:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.264Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p><a href="https://github.com/markzhang12345/MockLocation">markzhang12345&#x2F;MockLocation: 一个安卓 GPS 信号模拟工具</a></p><p>不想 Keep 打卡，所以尝试找出一个比较好的替代方案。原本的想法是写一个基于 adb 命令的简单脚本，然后对手机发送定位命令，如</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">adb shell geo fix <span class="number">116.397428</span> <span class="number">39.908717</span></span><br><span class="line"></span><br><span class="line">adb shell am startservice <span class="literal">-a</span> com.android.location.service.v3.NetworkLocationProvider <span class="literal">--es</span> network<span class="literal">-location</span> <span class="string">&quot;&#123;&#x27;latitude&#x27;:39.908717,&#x27;longitude&#x27;:116.397428,&#x27;accuracy&#x27;:10.0&#125;&quot;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>然而，第一种方式在<code>Android 14</code>中已经不再适用，第二种方式需要手机的 root 权限。手边并没有的设备给我折腾 root，所以考虑写一个安卓软件</p><h1 id="为什么要写安卓"><a href="#为什么要写安卓" class="headerlink" title="为什么要写安卓"></a>为什么要写安卓</h1><p><img src="/blog/../images/2025-3-7/85b7a94c0dd3be8ca2c80f81fe6c80aa169208916.gif@976w_974h.webp" alt="img"></p><p>你说的对，但是现在的安卓版本虽然禁用了<code>adb shell geo fix</code>方法，在开发者选项中仍然存在<strong>模拟位置信息应用</strong>，所以我们直接在应用中调用<code>ACCESS_MOCK_LOCATION</code>权限，就可以为手机提供位置模拟服务</p><p>电脑上还留着大一时候下载的 Android Studio，倒是也不用新增环境，直接新建项目开始琢磨</p><blockquote><p>这里想着直接用 Kotlin 一步到位，但是越写越发现自己根本没写过完整的面向对象的项目。之前写 C++ 的原因也只是 STL 好用，完全没有认真尝试过他的面向对象特性。连带着这两天看 C# 也非常头大</p><p>开始认真思考要不要看着 Java 好好学一下面向对象的写法</p></blockquote><p>项目新建后会出现一堆乱七八糟的文件结构，在刚开始开发的时候确实让人无从下手。简单分析之后，其实需要关注的文件只有以下三个：</p><ul><li>MainActivity.kt，入口文件</li><li>activity_main.xml，页面配置，类似前端的 HTML</li><li>AndroidManifest.xml，配置文件</li></ul><h2 id="AndroidManifest-xml"><a href="#AndroidManifest-xml" class="headerlink" title="AndroidManifest.xml"></a>AndroidManifest.xml</h2><p>既然我们软件的核心功能是通过<code>ACCESS_MOCK_LOCATION</code>实现的，那么当然首先应该在配置文件中声明该权限</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">&quot;android.permission.ACCESS_FINE_LOCATION&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">&quot;android.permission.ACCESS_COARSE_LOCATION&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">&quot;android.permission.ACCESS_MOCK_LOCATION&quot;</span> /&gt;</span></span><br></pre></td></tr></table></figure><ul><li><code>ACCESS_FINE_LOCATION</code>允许应用获取精确位置信息，这里就是使用 GPS 获取高精位置</li><li><code>ACCESS_COARSE_LOCATION</code>允许应用获取大致位置信息，例如基于网络的位置</li><li><code>ACCESS_MOCK_LOCATION</code>允许应用创建模拟位置并将其提供给系统</li></ul><p>其实理论上来讲，以上三个权限都是非必要的。我的软件只需要提供位置信息，并不用获取位置信息。而第三个权限在<code>Android 6.0</code>版本纸上已经被开发者模式的设置取代了……</p><p><img src="/blog/../images/2025-3-7/1ad0d7abe124c31edefa6ccadb578695186510120.jpg@1192w.webp" alt="img"></p><blockquote><p>写文章的时候意识到<code>ACCESS_MOCK_LOCATION</code>这个影响我软件打包的权限竟然是非必要的，删了之后果然能够正常打包了，并且不影响软件的正常运行</p><p>被自己气笑了</p><p>权限需要使用<code>adb shell appops set icu.kpmark.mocklocation android:mock_location allow</code>手动赋予，开发者选项中可能没有</p></blockquote><h2 id="activity-main-xml"><a href="#activity-main-xml" class="headerlink" title="activity_main.xml"></a>activity_main.xml</h2><p>然后就是简单设计软件页面，因为这个软件的功能实在是比较单一，暂时也没有什么扩展的接口，于是设置一个开始&#x2F;结束键用来绑定<code>stop()</code>和<code>start()</code>函数就好了</p><p>没了</p><p>以后考虑添加路径，速度选择功能，但是经纬度计算实在是有点难处理，所以暂时搁置</p><h2 id="MainActivity-kt"><a href="#MainActivity-kt" class="headerlink" title="MainActivity.kt"></a>MainActivity.kt</h2><p>这个是程序的入口文件，我们在这个文件中重写系统父类<code>AppCompatActivity</code>中的<code>onCreate</code>方法，启动视图模式，并使用根视图处理窗口插图</p><p>其实最主要的，处理初始化一堆必要的选项之外，只有将我主要逻辑的<code>stop()</code>和<code>start()</code>函数绑定到界面按钮的监听事件上，然后将软件终止事件也绑定至<code>stop()</code>，确保主要逻辑的正确进入与退出</p><p>当然不会把主要逻辑写在里面，不然真就编程依托莫名其妙的代码了，这里我们新建<code>MockLocation.kt</code>来存放我们的<code>EnhancedMockLocation</code>类（为什么当时脑子一抽起了这个名字呢）</p><h2 id="MockLocation-kt"><a href="#MockLocation-kt" class="headerlink" title="MockLocation.kt"></a>MockLocation.kt</h2><p>为了骗过软件，我们必须将模拟信号提供者的名字替换为系统已有的 GPS 提供者，这里我们给出一个提供者数组</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 所有可能的位置提供者</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> providers = arrayOf(</span><br><span class="line">LocationManager.GPS_PROVIDER,</span><br><span class="line">LocationManager.NETWORK_PROVIDER,</span><br><span class="line">LocationManager.PASSIVE_PROVIDER</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>分别是 GPS 传感器，网络定位以及被动身份，毕竟不是所有软件都像高德地图一样，对 GPS 信号毫不怀疑（笑），所以这些假身份越多越好</p><p>由于要进行应用的后台运行，这里需要开个协程来运行模拟位置逻辑，然后按照类中预设的经纬度点，力求还原的提供位置信息</p><p>这里我们使用<code>interpolationPosition</code>函数来实现两个经纬度点之间的平滑过渡，类似以下逻辑</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">进度条 += (速度 × 时间差) / 两点距离</span><br><span class="line"><span class="keyword">if</span> (进度条 &gt; <span class="number">1.0</span>) &#123;</span><br><span class="line">    切到下个点</span><br><span class="line">    进度条 = <span class="number">0.0</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当手机移动速度超过光速时，可以直接触发瞬移特效（误</p><p>你说的对，但是我们还要计算方位角来模拟更加真是的方向朝向，这里我们使用以下代码</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> R = <span class="number">6371000.0</span> <span class="comment">// 地球半径</span></span><br><span class="line"><span class="keyword">val</span> a = sin(Δ纬度/<span class="number">2</span>)^<span class="number">2</span> + cos(纬度<span class="number">1</span>)*cos(纬度<span class="number">2</span>)*sin(Δ经度/<span class="number">2</span>)^<span class="number">2</span></span><br></pre></td></tr></table></figure><p>尝试计算当前的方位角，但是显然，效果不是很好</p><p><img src="/blog/../images/2025-3-7/dbc1767e9e2f0708ead17b35af24b899a801f2cc.jpg" alt="img"></p><p>这里，我们做了三个比较特殊的算法来尝试骗过软件的自动识别</p><ul><li><code>warmupLocationServices()</code>会先发送三个抖动坐标，用来迷惑手机系统，效果类似：先让系统相信我在北京 → 上海 → 广州之间反复横跳，之后突然出现在纽约就不会被怀疑了</li><li>方向角的计算，我使用 5 个历史位置来计算平均方向</li><li>尝试速度和方向添加随机噪声，试图模拟手机在身上的随机扰动</li></ul><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> smoothedBearing = calculateSmoothedBearing()</span><br><span class="line"><span class="keyword">val</span> smoothedSpeed = <span class="keyword">if</span> (currentSpeed &gt; <span class="number">0.1</span>) currentSpeed <span class="keyword">else</span> <span class="number">0.0</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> randomizedSpeed = smoothedSpeed * (<span class="number">0.95</span> + <span class="number">0.1</span> * Random.nextDouble()) <span class="comment">// ±5%波动</span></span><br><span class="line"><span class="keyword">val</span> randomizedBearing = (smoothedBearing + Random.nextDouble() * <span class="number">3</span> - <span class="number">1.5</span>).toFloat()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (provider <span class="keyword">in</span> providers) &#123;</span><br><span class="line">    setEnhancedMockLocation(</span><br><span class="line">        locationManager,</span><br><span class="line">        provider,</span><br><span class="line">        location.latitude,</span><br><span class="line">        location.longitude,</span><br><span class="line">        randomizedSpeed,    <span class="comment">// 随机速度</span></span><br><span class="line">        randomizedBearing   <span class="comment">// 假装手抖的方向</span></span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你说的对，但是在一些运动软件中，这个随机扰动的检测是通过陀螺仪和水平仪之类的其他传感器来检测的，使用 GPS 模拟的效果，只能说聊胜于无</p><p><img src="/blog/../images/2025-3-7/-9lddQ7i87-68ivK1nT3cSqo-lk.jpg" alt="img"></p><blockquote><p>本程序仅供学习交流使用，请勿用作不良用途</p><p>目前此软件仅经过不完全测试，请确保安装软件的设备上不存在<strong>任何重要信息</strong></p><p>错误使用本工具可能导致<strong>账号封禁等风险</strong>，<strong>使用者需自行承担一切后果</strong></p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="杂谈" scheme="https://kpmark.icu/categories/%E6%9D%82%E8%B0%88/"/>
    
    
    <category term="Android" scheme="https://kpmark.icu/tags/Android/"/>
    
  </entry>
  
  <entry>
    <title>聊聊 Git 与 README</title>
    <link href="https://kpmark.icu/2025/02/19/2025-2-19/"/>
    <id>https://kpmark.icu/2025/02/19/2025-2-19/</id>
    <published>2025-02-19T03:43:47.000Z</published>
    <updated>2025-10-03T07:13:14.649Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p>本人对此的认知有限，故本文只会聊一些最基本的规范</p><hr><h1 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h1><p><code>Git</code>是一款免费且开源的分布式版本控制系统，旨在以高速和高效的方式处理从小型到超大型的各类项目。除去基本的版本管理，还可以借助 <code>ssh</code>等协议做到方便、安全的文件传输及代码共享</p><p>这里会简单介绍一些<strong>我认为比较重要的<code>Git</code>使用规范</strong></p><h2 id="Commit"><a href="#Commit" class="headerlink" title="Commit"></a>Commit</h2><p>是的，使用<code>Git</code>最重要的一件事就是进行版本控制，而<code>git commit -m &quot;&lt;信息&gt;&quot;</code>命令就是提交当前更改的重要方式、</p><p>而 Commit 中信息的编写规范<strong>非常重要</strong>，可以非常直观的提供此次更改内容的变化情况，<strong>尤其是在与合作者协同开发的情况下</strong>，可以帮助他人一眼看出你的改动内容，是代码写完之后<strong>非常重要</strong>的一项流程</p><blockquote><p>[!WARNING]</p><p>存在不包含相关信息的 commit 命令，这里为了诸位的身心健康，不做介绍</p></blockquote><p>有的同学可能会说了，我写完代码很累了，还要写 commit 不是折磨人吗？</p><p>想象一下，当您早上打开仓库开始工作，看的同事 commit 长成下面这样，您的血压会不会飙升</p><ul><li><code>fix</code></li><li><code>修改 bug</code></li><li><code>设计页面</code></li><li><code>2.12</code></li><li><code>添加用户个性化设置功能, 实现用户主题与图片定制功能, 添加四个新API端点, 实现文件管理和Token验证......(此处省略一万字)</code></li></ul><p>心脏强大的同学，或者说完全不关心别人干了什么的同学可能觉得这样无伤大雅，但是为了世界和平，我必须在这里介绍一下规范的 commit 写法，并推荐大家按照规范合理的编写自己的 commit</p><p>当然有些同学可能在一些大的开源项目中看到如下提交</p><p><img src="/blog/../images/2025-2-19/image-20250219182243281.png" alt="image-20250219182243281"></p><p>注意这里的提交是回应相关 issue 的，详细信息应转至对应页面查看</p><h3 id="Angular-规范"><a href="#Angular-规范" class="headerlink" title="Angular 规范"></a>Angular 规范</h3><p>目前最受开发人员肯定的规范是前端框架<code>Angular</code>提出的<a href="https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines">Angular 提交信息规范</a></p><p>这里给出提交格式</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="built_in">type</span>&gt;(&lt;scope&gt;): &lt;subject&gt;</span><br><span class="line">&lt;BLANK LINE&gt;</span><br><span class="line">&lt;body&gt;</span><br><span class="line">&lt;BLANK LINE&gt;</span><br><span class="line">&lt;footer&gt;</span><br></pre></td></tr></table></figure><p>每次提交<strong>必须包括</strong>页眉(<code>header</code>)内容，其余正文<code>body</code>和页脚<code>footer</code>视改动情况定</p><p>每次提交不超过<code>100</code>个字符</p><p>这里简单介绍一些<code>type</code>字段类型</p><ul><li><code>build</code>：对构建系统或者外部依赖项进行了修改</li><li><code>docs</code>：对文档进行了修改</li><li><code>feat</code>：增加新的特征</li><li><code>fix</code>：修复<code>bug</code></li><li><code>refactor</code>：既不是修复<code>bug</code>也不是添加特征的代码重构</li></ul><p>更多详细信息可以参看连接给出的官方文档<br>这里搬运一些规范的<code>Angular</code>提交信息</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">feat(UserProfile): add user profile editing feature</span><br><span class="line"></span><br><span class="line">This commit introduces a new feature that allows users to edit their profiles</span><br><span class="line">directly from the user interface. The motivation behind this change is to</span><br><span class="line">enhance user interaction and provide a more seamless experience.</span><br><span class="line"></span><br><span class="line">Previously, users had to navigate to a separate editing page to update their</span><br><span class="line">profile information. With this new feature, users can now make changes</span><br><span class="line">efficiently from their profile page, eliminating unnecessary steps <span class="keyword">in</span> the</span><br><span class="line">workflow.</span><br><span class="line"></span><br><span class="line">Changes included <span class="keyword">in</span> this commit:</span><br><span class="line">- Added a new <span class="string">&#x27;Edit Profile&#x27;</span> button on the user profile page.</span><br><span class="line">- Implemented frontend components <span class="keyword">for</span> profile editing.</span><br><span class="line">- Updated backend API to handle profile updates securely.</span><br><span class="line"></span><br><span class="line">By streamlining the profile editing <span class="keyword">process</span>, we aim to improve overall user</span><br><span class="line">satisfaction and make our application more user<span class="literal">-friendly</span>. This enhancement is</span><br><span class="line"><span class="keyword">in</span> response to user feedback, addressing the need <span class="keyword">for</span> a more intuitive and</span><br><span class="line">accessible way to modify profile details.</span><br><span class="line"></span><br><span class="line">Closes <span class="comment">#234</span></span><br></pre></td></tr></table></figure><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">fix(Validation): correct input validation logic</span><br><span class="line"></span><br><span class="line">This commit addresses an issue related to input validation logic <span class="keyword">in</span> the</span><br><span class="line">application. Previously, the validation <span class="keyword">process</span> was not handling certain edge</span><br><span class="line">cases correctly, leading to unexpected behavior <span class="keyword">in</span> specific scenarios.</span><br><span class="line"></span><br><span class="line">To resolve this issue, the validation logic has been revised to properly</span><br><span class="line">handle various input scenarios. This ensures that user input is thoroughly</span><br><span class="line">validated, reducing the likelihood of errors <span class="keyword">in</span> the application.</span><br><span class="line"></span><br><span class="line">The changes made <span class="keyword">in</span> this commit include:</span><br><span class="line">- Correcting boundary checks <span class="keyword">for</span> user input.</span><br><span class="line">- Improving error messages <span class="keyword">for</span> better user guidance.</span><br><span class="line"></span><br><span class="line">These adjustments align with our commitment to delivering a robust and</span><br><span class="line">reliable application experience.</span><br><span class="line"></span><br><span class="line">Closes <span class="comment">#123</span></span><br></pre></td></tr></table></figure><h3 id="个人理解"><a href="#个人理解" class="headerlink" title="个人理解"></a>个人理解</h3><p>有的同学可能会说了，小体量项目没必要写的这么规范吧，随便写写不就行了？</p><blockquote><p>[!WARNING]</p><p>以下内容仅代表我个人观点，若有不同意见，欢迎联系作者进行友好交流</p></blockquote><p>如果同事或者队友明确表示不在意的话，请自便</p><p>其余情况，我个人建议您的提交起码包括以下内容</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">git commit -m &quot;一句话交代大致工作</span><br><span class="line"></span><br><span class="line">- 详细条目1</span><br><span class="line">- 详细条目2</span><br><span class="line"> - 子条目21&quot;</span><br></pre></td></tr></table></figure><p>其中，若提交能够被简短的一句话描述，可省略详细后续内容(务必确保您的工作能被<strong>简短</strong>的一句话<strong>完全</strong>概括)</p><p>其中，若改动内容较多 ，建议可以<strong>稍微详细</strong>的对后续条目进行编写(务必确保您的工作<strong>不能</strong>被<strong>简短</strong>的一句话<strong>完全</strong>概括)</p><p>这里给出作者的 commit 以供参考</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">修复查询帖子 bug 逻辑，新增用户历史发布 API</span><br><span class="line"></span><br><span class="line">- 修复查询帖子(GET /search)空列表问题</span><br><span class="line"> - 修复空列表重复响应200状态码</span><br><span class="line"> - 新增响应 count 字段表示当前页数量</span><br><span class="line"></span><br><span class="line">- 新增用户历史发布接口(GET /user-history)</span><br><span class="line">  - 支持分页查询</span><br><span class="line">  - 包含帖子总数、当前页数量统计</span><br><span class="line">  - 返回帖子详情及关联图片</span><br><span class="line"></span><br><span class="line">- 增改相关 API 文档</span><br></pre></td></tr></table></figure><h2 id="可视化查看工具"><a href="#可视化查看工具" class="headerlink" title="可视化查看工具"></a>可视化查看工具</h2><p><code>Git</code>的基本操作是命令行命令，虽然对于基本的提交，拉取，推送之类的命令支持良好，但是当你需要查看具体改动的时候，命令行操作实在是相当不方便</p><p>这里提供几个<strong>不需要特意下载的可视化工具</strong></p><h3 id="Github"><a href="#Github" class="headerlink" title="Github"></a>Github</h3><p><code>Github</code>作为世界上最大的开源代码托管平台，其名称中的<code>Git</code>已然彰显了其与<code>Git</code>的密切关系</p><p>这里以<a href="https://github.com/Tencent/puerts">Tencent&#x2F;puerts: PUER(普洱) Typescript. Let’s write your game in UE or Unity with TypeScript.</a>仓库为例稍作演示</p><p>在仓库的代码主页，你能很轻松的看到 commit 标识</p><p><img src="/blog/../images/2025-2-19/image-20250219175958659.png" alt="image-20250219175958659"></p><p>点击左上角 Commit，进入提交详情</p><p><img src="/blog/../images/2025-2-19/image-20250219180049708.png" alt="image-20250219180049708"></p><p>这里已经能很详细的看到提交结果了，我们再点进详情页</p><p><img src="/blog/../images/2025-2-19/image-20250219182439638.png" alt="image-20250219182439638"></p><p>这里的红绿色块非常明显的提示了相应的增改内容</p><h3 id="Vs-code"><a href="#Vs-code" class="headerlink" title="Vs code"></a>Vs code</h3><p>你说的对，但是 vs code 内置了一款<code>Git</code>可视化工具，可以辅助进行更改的提交，推送</p><p>还有<strong>好用</strong>的冲突管理器，让你不用再<code>Git</code>默认的<code>Vim</code>编辑器中进行冲突的更改</p><p><img src="/blog/../images/2025-2-19/scm-staging.png" alt="img"></p><p>这里给出官方使用文档<a href="https://vscode.github.net.cn/docs/sourcecontrol/intro-to-git">Visual Studio Code 中的 Git 简介</a></p><h1 id="README"><a href="#README" class="headerlink" title="README"></a>README</h1><p>一个好的项目，必然有一份好的<code>README</code>，无论这份<code>README</code>是面向开发者还是使用者，都应该进行认真地进行撰写</p><p>一份好的<code>README</code>是项目标配，然而其作用<strong>并非</strong>彰显该项目的专业性或满足自己的<strong>展示欲</strong>，您的<code>README</code>应该以<strong>解决开发者或使用者可能存在的疑惑为目的</strong></p><p>古早的游戏文件中，常常能看见<code>txt</code>格式的<code>README</code>文件，直到现在有些游戏仍然使用<code>html</code>网页文件来进行项目描述</p><p>然而在如今的开源项目中，通常使用<code>markdown</code>语法来撰写<code>README.md</code>文件，GitHub 要求开源项目必须包含 README.md，否则会显示警告提示</p><p>那么，一份完整的项目<code>README</code>应该说明哪些内容呢？</p><ul><li>软件定位及基本功能</li><li>运行代码的方法：环境、启动命令…</li><li>简要使用说明</li><li>代码目录结构说明</li><li>常见问题说明</li></ul><p>作为一份完整的<code>README</code>，其还包括一些在代码<strong>早期开发</strong>中不需要填写的内容</p><ul><li>版权和许可信息</li><li>作者列表</li><li>社区介绍</li><li>联系信息</li><li>法律声明</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="杂谈" scheme="https://kpmark.icu/categories/%E6%9D%82%E8%B0%88/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
    <category term="Git" scheme="https://kpmark.icu/tags/Git/"/>
    
    <category term="Github" scheme="https://kpmark.icu/tags/Github/"/>
    
  </entry>
  
  <entry>
    <title>近期拾遗-关于建站</title>
    <link href="https://kpmark.icu/2025/02/17/2025-2-17/"/>
    <id>https://kpmark.icu/2025/02/17/2025-2-17/</id>
    <published>2025-02-17T03:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.262Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p><img src="/blog/../images/2025-2-17/740155938b89c62b9aebd83aa44156ae186510120.gif@1142w_936h.avif" alt="img"></p><h1 id="关于-Frp-的安全问题"><a href="#关于-Frp-的安全问题" class="headerlink" title="关于 Frp 的安全问题"></a>关于 Frp 的安全问题</h1><p>曾今心心念念想要有低成本的实体服务器，在了解树莓派之后觉得终于有一个可行的解决方案了，遂自购树莓派 4b 开始折腾</p><p>某位同学曾使用过 Sakura Frp 内网穿透进行过游戏联机，于是打算用这个方案进行内网穿透</p><p>本来以为将局域网内设备暴露至公网不会出什么大事，直到在研究校园网&#x2F;公司内网穿透方案时看到了如下两个帖子</p><p><img src="/blog/../images/2025-2-17/b&ek=1&kp=1&pt=0&bo=0AKOBdACjgUWECA!&tl=1&vuin=2585050765&tm=1739768400&dis_t=1739769509&dis_k=8739475423fb7a691834561c481e1f35&sce=50-1-1&rf=viewer_311.jpeg" alt="img"></p><p><img src="/blog/../images/2025-2-17/b&ek=1&kp=1&pt=0&bo=0AJBBdACQQUWECA!&tl=3&vuin=2585050765&tm=1739757600&dis_t=1739760662&dis_k=f2c890fd723ed71b76fa69ad0d941c53&sce=60-2-2&rf=viewer_311.jpeg" alt="img"></p><p>道心破碎，开始重视 Frp 的安全问题</p><h2 id="安全隐患"><a href="#安全隐患" class="headerlink" title="安全隐患"></a>安全隐患</h2><p>简单来讲，通过内网 Frp 客户端和外网 Frp 服务端之间的连接，可将内网打通，攻击者通过访问 frp 服务端主机的指定端口，可实现对内网路由可达的任意主机的访问</p><p>从影响上来讲，最坏的情况并不是进行穿透的主机被控制，毕竟个体身份的信息体量一般不会特意招致黑客攻击（笑），但是对局域网内其他设备的威胁确实是不容忽视的</p><p>尤其是在公司内网和校园网环境下，同局域网内的服务主机中存有重要信息的情况下，招致攻击的风险和后果严重程度都将大大提升</p><p>以校园网环境举例，一般<strong>网信中心</strong>都会对这类穿透服务进行拦截，然而，大多数 Frp 客户端，都有如下功能</p><p><img src="/blog/../images/2025-2-17/image-20250217110828947.png" alt="image-20250217110828947"></p><p>当然，在这里只放这一张图片是不负责任的，我们将原版网页的截图放出</p><p><img src="/blog/../images/2025-2-17/image-20250217110946104.png" alt="image-20250217110946104"></p><p>那么，为什么 Frp 方案有如此多的安全隐患，还能成为相当主流的一种公网方案呢？</p><p>除开低成本和众所周知的一些因素，其实是有对应措施缓解安全问题的，由于本人了解并不过深，这里仅列举几种</p><h2 id="Frp-安全方案"><a href="#Frp-安全方案" class="headerlink" title="Frp 安全方案"></a>Frp 安全方案</h2><h3 id="仅开放必要端口"><a href="#仅开放必要端口" class="headerlink" title="仅开放必要端口"></a>仅开放必要端口</h3><p>以 Web 网页为例，尽量只开放<strong>默认 80</strong>端口，该端口一般由 Nginx 之类的反向代理工具提供服务，可以较为安全的进行代理转发，**较难(?)**被渗入</p><p>同时，<strong>千万不要</strong>轻易将 22&#x2F;3389 端口暴露，默认 ssh 连接的 22 端口一旦被攻入，很容易取得主机的 root 权限，远程桌面的 3389 端口同理</p><p>若因实际需求<strong>不得不</strong>暴露这两个端口，请务必限制访问 ip 并设置<strong>复杂度高</strong>的相关密钥</p><h3 id="强制使用-TLS-加密通信"><a href="#强制使用-TLS-加密通信" class="headerlink" title="强制使用 TLS 加密通信"></a>强制使用 TLS 加密通信</h3><p>你说的对，但是 TLS 不仅能绕过监管，其本身也是一种加密通信协议，这种服务能相当有效的阻挡中间人攻击</p><p>默认情况下的 Frp TCP 流量是明文传输的，攻击者可窃听或篡改传输数据(http 请求、数据库连接)，使用强制 HTTPS 和 TLS 服务能有效的对流量进行加密</p><h3 id="使用高位端口"><a href="#使用高位端口" class="headerlink" title="使用高位端口"></a>使用高位端口</h3><p>常见端口如 3000、5000、8080 等端口尽量避免开放，这些端口比较容易被工具扫描，引发暴力破解或漏洞引用</p><h3 id="隔离环境"><a href="#隔离环境" class="headerlink" title="隔离环境"></a>隔离环境</h3><p>主播主播，你的环境确实很危险，但是我就在家庭环境下开个 NAS，有没有什么强势又简单的安全措施</p><p>有的兄弟，有的</p><p>使用 DMZ 服务，可以在家庭网络下创建一个单独的区域，这个区域内的设备映射到网络中，即使被攻击或入侵，也不会影响到区域外的其他设备</p><p>具体操作涉及路由器操作，可以参照以下 up 的视频进行配置</p><p><a href="https://www.bilibili.com/video/BV1S14y1P7SN/?share_source=copy_web&vd_source=6b438c91e58eb29d4b7f190b8674a85f">家庭网络下，玩内网穿透的服务器，是不是应该隔离防护一下</a></p><h2 id="忠告"><a href="#忠告" class="headerlink" title="忠告"></a>忠告</h2><p>保险起见，无论诸位大神的网安技能多优秀，我不建议任何人在校园网&#x2F;公司内网环境下使用 Frp 进行内网穿透</p><p>实在想进行远程控制的同学们，可以研究更加安全的 VPN 方案</p><h1 id="Linux-云端操作感想"><a href="#Linux-云端操作感想" class="headerlink" title="Linux 云端操作感想"></a>Linux 云端操作感想</h1><p>在折腾云服务器的时候，大致流程和树莓派差不多，少了 Frp 的一些奇怪问题后，Nginx 的配置简单了很多</p><p>但是，由于云服务器中定制 Ubuntu 系统相当安全(雾)的用户权限管理，网站搭建中 90% 的原因都来自于文件权限不足，剩下 10% 来自于用户服务的混乱</p><p>建议同学们在使用 root 进行操作和其他用户进行操作时遵循一定的规则逻辑，尽可能减少因为用户权限不同造成的各种奇怪 bug</p><p>apt 作为包管理器确实相当方便，定制系统已经预先将镜像源换成了阿里云，在下载各种环境的时候并不需要像在 win 上一样折磨</p><p>考虑到代理问题，使用 apt 部署简单网页环境的时候确实比 Docker 部署还好用，毕竟一 pull 半小时、一看已失败的效果非常容易让人红温</p><p>云服务的安全组规则和免费的 ddos 防护确实让人无比安心，愿意的话花点小钱(并非小)还能享受企业级的安全防护，不过 2G2 核的便宜服务器确实没什么黑入的价值就是了</p><p>vsc 的远程接入后 2G 的内存几乎能被吃满，我也不知道为什么 vsc-server 这么吃内存，感觉不如学着用一下 Vim，直接 powershell 进行 ssh 连接了</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="杂谈" scheme="https://kpmark.icu/categories/%E6%9D%82%E8%B0%88/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
  </entry>
  
  <entry>
    <title>写一个树莓派状态检测服务</title>
    <link href="https://kpmark.icu/2025/02/15/2025-2-15/"/>
    <id>https://kpmark.icu/2025/02/15/2025-2-15/</id>
    <published>2025-02-15T03:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.262Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p>好想给自己的网页加点功能</p><h1 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h1><p>考虑到树莓派对 python 的支持良好，且 python 的各种包实在是非常好用，这里用 python 的 Flask 框架作为网页后端</p><p>树莓派 raspberryPi OS 中的 python 环境作为系统服务，是不允许直接使用 pip 进行包管理的，这里我们使用 Debian 维护的，<strong>非常好用</strong>的 <strong>apt</strong> 包管理器来安装需要的 python 包</p><p><em>有良好习惯的同学们可能有疑问，为什么不使用 python 进行虚拟环境的隔离呢？因为虚拟环境中无法直接调用树莓派有关系统环境的 API，需要使用<code>python -m venv --system-site-packages &lt;虚拟环境路径&gt;</code>参数在虚拟环境中调用全局系统包，那我为什么不直接用全局环境呢？我暂时没有发现比较好的办法</em></p><p>我们使用以下命令下载需要的包</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">更新系统包列表</span></span><br><span class="line">sudo apt update</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装所需的 Python 包</span></span><br><span class="line">sudo apt install python3-flask python3-psutil python3-flask-cors</span><br></pre></td></tr></table></figure><p>这里，<code>pyhton3-flask</code>和<code>python-flask-cors</code>分别是 Flask 框架和对应的跨域服务，<code>python3-psutil</code>则是调用系统状态的 API 所必要的包</p><h1 id="服务器实现"><a href="#服务器实现" class="headerlink" title="服务器实现"></a>服务器实现</h1><p>在你喜欢的地方，写一个后端提供系统数据的 API，这里我在 <code>/home/kpmark/nav/status.py</code>中进行编写</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> psutil</span><br><span class="line"><span class="keyword">from</span> flask <span class="keyword">import</span> Flask, jsonify</span><br><span class="line"><span class="keyword">from</span> flask_cors <span class="keyword">import</span> CORS</span><br><span class="line"></span><br><span class="line">app = Flask(name)</span><br><span class="line"><span class="comment"># 允许特定域名的跨域请求</span></span><br><span class="line">CORS(app, resources=&#123;</span><br><span class="line">    <span class="string">r&quot;/status&quot;</span>: &#123;</span><br><span class="line">        <span class="string">&quot;origins&quot;</span>: [<span class="string">&quot;https://www-blog.u2330056.nyat.app:49191&quot;</span>],</span><br><span class="line">        <span class="string">&quot;methods&quot;</span>: [<span class="string">&quot;GET&quot;</span>],</span><br><span class="line">        <span class="string">&quot;allow_headers&quot;</span>: [<span class="string">&quot;Content-Type&quot;</span>]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">&#x27;/status&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_status</span>():</span><br><span class="line">    cpu_percent = psutil.cpu_percent(interval=<span class="number">1</span>)</span><br><span class="line">    memory = psutil.virtual_memory()</span><br><span class="line">    disk = psutil.disk_usage(<span class="string">&#x27;/&#x27;</span>)</span><br><span class="line">    temperatures = psutil.sensors_temperatures()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 获取CPU温度（针对树莓派）</span></span><br><span class="line">    cpu_temp = <span class="literal">None</span></span><br><span class="line">    <span class="keyword">if</span> <span class="string">&#x27;cpu_thermal&#x27;</span> <span class="keyword">in</span> temperatures:</span><br><span class="line">        cpu_temp = temperatures[<span class="string">&#x27;cpu_thermal&#x27;</span>][<span class="number">0</span>].current</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> jsonify(&#123;</span><br><span class="line">        <span class="string">&#x27;cpu_percent&#x27;</span>: cpu_percent,</span><br><span class="line">        <span class="string">&#x27;memory_percent&#x27;</span>: memory.percent,</span><br><span class="line">        <span class="string">&#x27;disk_percent&#x27;</span>: disk.percent,</span><br><span class="line">        <span class="string">&#x27;cpu_temp&#x27;</span>: cpu_temp,</span><br><span class="line">        <span class="string">&#x27;memory_total&#x27;</span>: memory.total,</span><br><span class="line">        <span class="string">&#x27;memory_used&#x27;</span>: memory.used,</span><br><span class="line">        <span class="string">&#x27;disk_total&#x27;</span>: disk.total,</span><br><span class="line">        <span class="string">&#x27;disk_used&#x27;</span>: disk.used</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> name == <span class="string">&#x27;main&#x27;</span>:</span><br><span class="line">    app.run(host=<span class="string">&#x27;127.0.0.1&#x27;</span>, port=<span class="number">3001</span>)</span><br></pre></td></tr></table></figure><p>这样，就有一个后端可以挂在你树莓派的 3001 端口了，但是我们想让他自动在后台启动，需要配置相应的系统服务</p><h1 id="配置系统服务"><a href="#配置系统服务" class="headerlink" title="配置系统服务"></a>配置系统服务</h1><p>这里，使用以下命令创建 systemd 服务文件</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">cd</span> /etc/systemd/system/</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">touch</span> raspstatus.service</span><br></pre></td></tr></table></figure><p>然后填入以下内容</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Raspberry Pi Status API</span><br><span class="line"><span class="attr">After</span>=network.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">User</span>=kpmark</span><br><span class="line"><span class="attr">WorkingDirectory</span>=/home/kpmark/nav</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/bin/python3 /home/kpmark/nav/status.py</span><br><span class="line"><span class="attr">Restart</span>=always</span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure><ul><li><strong>Description</strong>：描述服务的名称和用途，这里是 “Raspberry Pi Status API”</li><li><strong>After</strong>：指定服务启动的依赖顺序，表示该服务将在 <code>network.target</code>（网络服务）启动后才启动，确保网络可用</li><li><strong>User</strong>：指定运行该服务的用户，这里是 <code>kpmark</code>。服务会以该用户的权限运行</li><li><strong>WorkingDirectory</strong>：设置服务的工作目录，这里是 <code>/home/kpmark/nav</code>。服务启动时会切换到该目录</li><li><strong>ExecStart</strong>：指定启动服务的命令，这里是使用 Python 3 执行 <code>/home/kpmark/nav/status.py</code> 脚本</li><li><strong>Restart</strong>：定义服务在什么情况下重启。<code>always</code> 表示无论服务因何原因退出，都会自动重启，确保服务持续运行</li><li><strong>WantedBy</strong>：指定服务的启动级别或目标。<code>multi-user.target</code> 是多用户环境的目标，表示该服务会在系统启动时自动启动</li></ul><p>然后，我们通过以下命令启动该服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl daemon-reload</span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> raspstatus</span><br><span class="line"><span class="built_in">sudo</span> systemctl start raspstatus</span><br></pre></td></tr></table></figure><p>出现问题的话，可以通过以下命令调出服务日志进行查看</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看服务日志</span></span><br><span class="line"><span class="built_in">sudo</span> journalctl -u raspstatus -f</span><br></pre></td></tr></table></figure><h1 id="Nginx-配置"><a href="#Nginx-配置" class="headerlink" title="Nginx 配置"></a>Nginx 配置</h1><p>由于涉及到后端的跨域请求，这里需要对 Nginx 进行一些配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"># 状态API代理</span><br><span class="line">location /status &#123;</span><br><span class="line">        proxy_pass http://127.0.0.1:3001/status;</span><br><span class="line">        proxy_set_header Host $host;</span><br><span class="line">        proxy_set_header X-Real-IP $remote_addr;</span><br><span class="line"></span><br><span class="line">        # CORS 配置</span><br><span class="line">        add_header &#x27;Access-Control-Allow-Origin&#x27; &#x27;https://www-blog.u2330056.nyat.app:49191&#x27;;</span><br><span class="line">        add_header &#x27;Access-Control-Allow-Methods&#x27; &#x27;GET, OPTIONS&#x27;;</span><br><span class="line">        add_header &#x27;Access-Control-Allow-Headers&#x27; &#x27;DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range&#x27;;</span><br><span class="line">        add_header &#x27;Access-Control-Expose-Headers&#x27; &#x27;Content-Length,Content-Range&#x27;;</span><br><span class="line"></span><br><span class="line">        if ($request_method = &#x27;OPTIONS&#x27;) &#123;</span><br><span class="line">            add_header &#x27;Access-Control-Allow-Origin&#x27; &#x27;https://www-blog.u2330056.nyat.app:49191&#x27;;</span><br><span class="line">            add_header &#x27;Access-Control-Allow-Methods&#x27; &#x27;GET, OPTIONS&#x27;;</span><br><span class="line">            add_header &#x27;Access-Control-Allow-Headers&#x27; &#x27;DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range&#x27;;</span><br><span class="line">            add_header &#x27;Access-Control-Max-Age&#x27; 1728000;</span><br><span class="line">            add_header &#x27;Content-Type&#x27; &#x27;text/plain; charset=utf-8&#x27;;</span><br><span class="line">            add_header &#x27;Content-Length&#x27; 0;</span><br><span class="line">            return 204;</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure><p>这里我们直接将 <code>/status</code>转发至后端，这样就不用在前端写出完整的请求路由了</p><ul><li><strong>proxy_pass</strong>：将匹配的请求代理到 <code>http://127.0.0.1:3001/status</code>，即转发到本地的 3001 端口上</li><li><strong>proxy_set_header</strong>：设置代理请求的 <code>Host</code> 头为原始请求的 <code>Host</code> 值，确保后端服务能正确识别请求的域名</li><li><strong>proxy_set_header</strong>：设置代理请求的 <code>X-Real-IP</code> 头为客户端 IP 地址，方便后端服务获取真实 IP</li><li><strong>add_header</strong>：设置响应头 <code>Access-Control-Allow-Origin</code>，允许来自 <code>https://www-blog.u2330056.nyat.app:49191</code> 的跨域请求</li><li><strong>add_header</strong>：设置响应头 <code>Access-Control-Allow-Methods</code>，允许的 HTTP 方法为 <code>GET</code> 和 <code>OPTIONS</code></li><li><strong>add_header</strong>：设置响应头 <code>Access-Control-Allow-Headers</code>，允许的请求头包括 <code>DNT</code>、<code>User-Agent</code> 等</li><li><strong>add_header</strong>：设置响应头 <code>Access-Control-Expose-Headers</code>，允许客户端访问 <code>Content-Length</code> 和 <code>Content-Range</code> 头信息<br>还有一堆 <code>OPTIONS</code>请求处理，这里不再赘述其含义</li></ul><h1 id="前端实现"><a href="#前端实现" class="headerlink" title="前端实现"></a>前端实现</h1><p>这里给出我的前端 demo，仅供参考</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;card status-card&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">h2</span>&gt;</span>树莓派系统状态<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>实时系统资源监控<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-grid&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-item&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">h3</span>&gt;</span>CPU 使用率<span class="tag">&lt;/<span class="name">h3</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">p</span> <span class="attr">id</span>=<span class="string">&quot;cpu-percent&quot;</span>&gt;</span>--<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-bar&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-bar-fill&quot;</span> <span class="attr">id</span>=<span class="string">&quot;cpu-bar&quot;</span> <span class="attr">style</span>=<span class="string">&quot;width: 0%&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-item&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">h3</span>&gt;</span>内存使用率<span class="tag">&lt;/<span class="name">h3</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">p</span> <span class="attr">id</span>=<span class="string">&quot;memory-percent&quot;</span>&gt;</span>--<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-bar&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-bar-fill&quot;</span> <span class="attr">id</span>=<span class="string">&quot;memory-bar&quot;</span> <span class="attr">style</span>=<span class="string">&quot;width: 0%&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-item&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">h3</span>&gt;</span>CPU 温度<span class="tag">&lt;/<span class="name">h3</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">p</span> <span class="attr">id</span>=<span class="string">&quot;cpu-temp&quot;</span>&gt;</span>--<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-bar&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-bar-fill&quot;</span> <span class="attr">id</span>=<span class="string">&quot;temp-bar&quot;</span> <span class="attr">style</span>=<span class="string">&quot;width: 0%&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-item&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">h3</span>&gt;</span>磁盘使用率<span class="tag">&lt;/<span class="name">h3</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">p</span> <span class="attr">id</span>=<span class="string">&quot;disk-percent&quot;</span>&gt;</span>--<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-bar&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status-bar-fill&quot;</span> <span class="attr">id</span>=<span class="string">&quot;disk-bar&quot;</span> <span class="attr">style</span>=<span class="string">&quot;width: 0%&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">function</span> <span class="title function_">formatPercent</span>(<span class="params">value</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">return</span> <span class="string">`<span class="subst">$&#123;value.toFixed(<span class="number">1</span>)&#125;</span>%`</span>;</span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">function</span> <span class="title function_">formatTemp</span>(<span class="params">value</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">return</span> <span class="string">`<span class="subst">$&#123;value.toFixed(<span class="number">1</span>)&#125;</span>°C`</span>;</span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">function</span> <span class="title function_">updateStatus</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="comment">// 使用当前域名的status路径</span></span></span><br><span class="line"><span class="language-javascript">    <span class="title function_">fetch</span>(<span class="string">&quot;/status&quot;</span>)</span></span><br><span class="line"><span class="language-javascript">      .<span class="title function_">then</span>(<span class="function">(<span class="params">response</span>) =&gt;</span> response.<span class="title function_">json</span>())</span></span><br><span class="line"><span class="language-javascript">      .<span class="title function_">then</span>(<span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="comment">// Update CPU</span></span></span><br><span class="line"><span class="language-javascript">        <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;cpu-percent&quot;</span>).<span class="property">textContent</span> = <span class="title function_">formatPercent</span>(data.<span class="property">cpu_percent</span>);</span></span><br><span class="line"><span class="language-javascript">        <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;cpu-bar&quot;</span>).<span class="property">style</span>.<span class="property">width</span> = <span class="string">`<span class="subst">$&#123;data.cpu_percent&#125;</span>%`</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">        <span class="comment">// Update Memory</span></span></span><br><span class="line"><span class="language-javascript">        <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;memory-percent&quot;</span>).<span class="property">textContent</span> = <span class="title function_">formatPercent</span>(data.<span class="property">memory_percent</span>);</span></span><br><span class="line"><span class="language-javascript">        <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;memory-bar&quot;</span>).<span class="property">style</span>.<span class="property">width</span> = <span class="string">`<span class="subst">$&#123;data.memory_percent&#125;</span>%`</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">        <span class="comment">// Update Temperature</span></span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">if</span> (data.<span class="property">cpu_temp</span> !== <span class="literal">null</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;cpu-temp&quot;</span>).<span class="property">textContent</span> = <span class="title function_">formatTemp</span>(data.<span class="property">cpu_temp</span>);</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;temp-bar&quot;</span>).<span class="property">style</span>.<span class="property">width</span> = <span class="string">`<span class="subst">$&#123;(data.cpu_temp / <span class="number">80</span>) * <span class="number">100</span>&#125;</span>%`</span>;</span></span><br><span class="line"><span class="language-javascript">        &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">        <span class="comment">// Update Disk</span></span></span><br><span class="line"><span class="language-javascript">        <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;disk-percent&quot;</span>).<span class="property">textContent</span> = <span class="title function_">formatPercent</span>(data.<span class="property">disk_percent</span>);</span></span><br><span class="line"><span class="language-javascript">        <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;disk-bar&quot;</span>).<span class="property">style</span>.<span class="property">width</span> = <span class="string">`<span class="subst">$&#123;data.disk_percent&#125;</span>%`</span>;</span></span><br><span class="line"><span class="language-javascript">      &#125;)</span></span><br><span class="line"><span class="language-javascript">      .<span class="title function_">catch</span>(<span class="function">(<span class="params">error</span>) =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&quot;Error fetching status:&quot;</span>, error));</span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="comment">// Update every 2 seconds</span></span></span><br><span class="line"><span class="language-javascript">  <span class="title function_">updateStatus</span>();</span></span><br><span class="line"><span class="language-javascript">  <span class="built_in">setInterval</span>(updateStatus, <span class="number">2000</span>);</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>这里我使用原生页面简单的实现了卡片效果如下，也可以使用现代前端框架实现更加美观的界面</p><p><img src="/blog/../images/2025-2-15/image-20250215225153644.png" alt="image-20250215225153644"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
    <category term="Linux" scheme="https://kpmark.icu/tags/Linux/"/>
    
    <category term="RasberryPi" scheme="https://kpmark.icu/tags/RasberryPi/"/>
    
  </entry>
  
  <entry>
    <title>如何在 windows 中使用 Docker —— 以 Redis 为例</title>
    <link href="https://kpmark.icu/2025/02/03/2025-2-3/"/>
    <id>https://kpmark.icu/2025/02/03/2025-2-3/</id>
    <published>2025-02-03T03:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.263Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p>Docker 是一个轻量化的容器技术，它提供了一个独立的环境容器，用于打包和运行应用程序及其依赖。虽然 Docker 容器与虚拟机在功能上有相似之处（如提供隔离的运行环境），但 Docker 容器直接运行在宿主机的操作系统内核上，而不是通过虚拟化硬件来实现，因此更加轻量、快速和高效。Docker 镜像可以在任何支持 Docker 的环境中运行，确保开发、测试和生产环境的一致性</p><ul><li><p>Docker 用于将应用程序及其依赖打包成一个轻量化的容器，实现跨环境的一致性和快速部署</p></li><li><p>通过 Docker，开发者可以在隔离的环境中运行应用，确保开发、测试和生产环境的无缝迁移。</p></li></ul><h1 id="开启-WSL-环境"><a href="#开启-WSL-环境" class="headerlink" title="开启 WSL 环境"></a>开启 WSL 环境</h1><p>很不幸，Docker 原生支持的环境为 linux 和 mac OS，无法直接在 windows 系统中运行，我们需要开启 windows 自带的 linux 子系统</p><p>在 win10 环境中，只需要开启 hyperV，即可正常使用 Docker 的客户端(在系统设置中搜索即可)</p><p>而在 win11 中，我们需要开启 WSL 服务（WSL2 具有更好的新性能与兼容性，win10 用户在版本允许的情况下也推荐使用 WSL）</p><p>首先，打开 windows 设置中以下两项服务</p><ul><li>适用于 Linux 的 Windows 子系统</li><li>虚拟机平台</li></ul><p><img src="/blog/../images/2025-2-3/image-20250203183155290.png" alt="image-20250203183155290"></p><p>然后，重启电脑，以管理员打开 CMD 或者 PowerShell，输入以下命令将 WSL 的默认版本切换至 WSL2</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wsl <span class="literal">--set-default-version</span> <span class="number">2</span></span><br></pre></td></tr></table></figure><p>然后，输入以下命令下载 WSL</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wsl <span class="literal">--update</span> <span class="literal">--web-download</span></span><br></pre></td></tr></table></figure><p>祈祷成功即可</p><p>理论上以上操作之后即可正常下载 Docker 客户端，但是你也可以在 WSL 中下载 Ubantu 或者 Arch 这种 linux 发行版当成虚拟机折腾一下</p><h1 id="下载-Docker-客户端"><a href="#下载-Docker-客户端" class="headerlink" title="下载 Docker 客户端"></a>下载 Docker 客户端</h1><p>推荐直接使用神秘力量进入 Docker 官网下载 docker_desktop_installer_windows_x86_64.exe，当然如果没有什么力量也可以在国内下载到各种镜像下载器</p><p>注意通过安装包引导安装是不能指定安装位置的，这里建议在命令行中通过以下命令指定安装路径</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">start</span> /w <span class="string">&quot;&quot;</span> <span class="string">&quot;Docker Desktop Installer.exe&quot;</span> install <span class="literal">--installation-dir</span>=D:\Docker</span><br></pre></td></tr></table></figure><p>打开 Docker Desktop 的客户端，成功运行即可</p><p>输入<code>docker version</code>，得到以下结果即安装成功</p><p><img src="/blog/../images/2025-2-3/image-20250203190448517.png" alt="image-20250203190448517"></p><p>这里的客户端提供的云服务与图形化界面，由于十分便于理解这里不做介绍，以下操作仍使用命令行</p><h1 id="使用-Docker-运行-Redis-服务"><a href="#使用-Docker-运行-Redis-服务" class="headerlink" title="使用 Docker 运行 Redis 服务"></a>使用 Docker 运行 Redis 服务</h1><p>Redis 是一种基于内存的高性能键值存储系统，广泛用作缓存、消息队列和会话存储，以提升应用性能</p><p>由于不明原因，其最新版本只能运行于 mac OS 和 linux 上，windows 只存在一个非常古老的版本，这里我们使用 Docker 运行 Redis 服务</p><p>在具体项目中要配置的环境不止一种，建议使用 docker-compose 的方式编写配置文件，这里以 MySQL 和 Redis 为例</p><p>在配置根目录下新建文件 <code>docker-compose.yml</code>，写入如下内容</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">version: <span class="string">&#x27;3.8&#x27;</span></span><br><span class="line"></span><br><span class="line">services:</span><br><span class="line">  mysql:  <span class="comment"># MySQL 服务定义</span></span><br><span class="line">    image: mysql:<span class="number">8.0</span>  <span class="comment"># 使用官方 MySQL 8.0 镜像</span></span><br><span class="line">    environment:</span><br><span class="line">      MYSQL_ROOT_PASSWORD: root   <span class="comment"># 设置 root 用户密码</span></span><br><span class="line">      MYSQL_DATABASE: marketplace <span class="comment"># 自动创建数据库</span></span><br><span class="line">    ports:</span><br><span class="line">      - <span class="string">&quot;3307:3306&quot;</span>  <span class="comment"># 将容器内的3306端口映射到宿主机的3307</span></span><br><span class="line">    <span class="keyword">healthcheck</span><span class="language-bash">:  <span class="comment"># 健康检查（可选）</span></span></span><br><span class="line">      test: [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;mysqladmin&quot;</span>, <span class="string">&quot;ping&quot;</span>, <span class="string">&quot;-h&quot;</span>, <span class="string">&quot;localhost&quot;</span>]</span><br><span class="line">      interval: <span class="number">5</span>s</span><br><span class="line">      timeout: <span class="number">10</span>s</span><br><span class="line">      retries: <span class="number">5</span></span><br><span class="line"></span><br><span class="line">  redis:  <span class="comment"># Redis 服务定义</span></span><br><span class="line">    image: redis:alpine  <span class="comment"># 使用轻量级 Redis 镜像</span></span><br><span class="line">    ports:</span><br><span class="line">      - <span class="string">&quot;6379:6379&quot;</span>  <span class="comment"># 端口映射</span></span><br><span class="line">    command: redis-server --save <span class="number">60</span> <span class="number">1</span> --loglevel warning  <span class="comment"># 每60秒保存一次</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>其中两项服务为了便于开发测试，均未设置持久卷</p><p>以上文件其实包含了两个容器的配置，一旦启动该 docker 服务，对应得镜像便会依照配置运行在容器内部，我们可以通过其映射的宿主机端口访问服务</p><ul><li>在根目录下使用<code>docker-compose up -d</code>运行容器，<code>-d</code>参数使其在后台运行</li><li>使用<code>docker-compose down -v</code>删除容器，<code>-v</code>同时删除其临时卷</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Docker" scheme="https://kpmark.icu/tags/Docker/"/>
    
  </entry>
  
  <entry>
    <title>关于在 Github 上合作开发</title>
    <link href="https://kpmark.icu/2025/01/17/2025-1-17-2/"/>
    <id>https://kpmark.icu/2025/01/17/2025-1-17-2/</id>
    <published>2025-01-16T20:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.261Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p>之前只在自己的 github 仓库中使用 git 工具进行过开发和远程管理，正好这两天有合作开发的需求，学习一下在 github 上进行合作开发的规范</p><h1 id="使用-Fork"><a href="#使用-Fork" class="headerlink" title="使用 Fork"></a>使用 Fork</h1><h2 id="Fork-公共仓库"><a href="#Fork-公共仓库" class="headerlink" title="Fork 公共仓库"></a>Fork 公共仓库</h2><p>进入想要参与开发的公共仓库后，点击右上角 Fork 图标，此时该仓库的一个副本被复制到你的 github 仓库中</p><p>这一步不需要原仓库主的同意</p><p>然后将该仓库 Clone 到本地，即可对仓库内容进行修改</p><p>这是你所有提交的修改都会发生在你的远程仓库中，源代码作者的仓库内容不会发生修改</p><h2 id="与原仓库保持同步"><a href="#与原仓库保持同步" class="headerlink" title="与原仓库保持同步"></a>与原仓库保持同步</h2><p>有时在你修改代码的过程中原始仓库发生了更新，这里我们需要同步这种更新</p><p>当前我们本地仓库的远程仓库应该是 fork 至我们 github 上的仓库</p><p>这里我们输入</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git remote add upstream url</span><br></pre></td></tr></table></figure><p>来将本地仓库连接至原仓库</p><p>可以使用以下命令确认自己的远程库</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git remote -v</span><br></pre></td></tr></table></figure><p>这里有几个命令来帮助你将远程仓库的内容更新至本地</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">// 从远程仓库获取最新版本到本地</span><br><span class="line">git fetch upstream master</span><br><span class="line"></span><br><span class="line">// 保证当前位于 master 分支</span><br><span class="line">git checkout master</span><br><span class="line"></span><br><span class="line">// 将最新版本整合到 master 分支</span><br><span class="line">git merge upstream/master</span><br><span class="line"></span><br><span class="line">// 将最新版本发送至自己的 github 仓库中</span><br><span class="line">git push origin master</span><br></pre></td></tr></table></figure><h2 id="创建-Pull-Request"><a href="#创建-Pull-Request" class="headerlink" title="创建 Pull Request"></a>创建 Pull Request</h2><p>将修改好的代码提交至自己的代码仓库后，可以在仓库页面点击 <strong>New pull request</strong></p><p>将修改提交给仓库的管理者</p><h1 id="直接使用多人合作仓库"><a href="#直接使用多人合作仓库" class="headerlink" title="直接使用多人合作仓库"></a>直接使用多人合作仓库</h1><p>这种可能在共同开发中更常用一点</p><p>首先你应该创建公共仓库，然后你可以邀请其他开发这作为合作者</p><ol><li>进入仓库</li><li>点击 setting</li><li>在左侧菜单中选择 Manage access</li><li>点击 Invite a collaborator，输入合作者的 Github 用户名或邮箱发出邀请</li></ol><p>这样可以实现多人共同管理一个远程仓库，并且每个人的管理权限一致</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
  </entry>
  
  <entry>
    <title>使用 Typora beta 版</title>
    <link href="https://kpmark.icu/2025/01/16/2025-1-16-2/"/>
    <id>https://kpmark.icu/2025/01/16/2025-1-16-2/</id>
    <published>2025-01-15T17:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.261Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><p>markdown 源码敲起来太麻烦？vscode 双窗口看起来太累人？别怕孩子，我带着 Typora 来救你了</p><h1 id="为什么用-Typora"><a href="#为什么用-Typora" class="headerlink" title="为什么用 Typora"></a>为什么用 Typora</h1><ul><li>即时渲染，随写随读</li><li>界面极简，主题丰富</li><li>占用小，14M</li><li>丰富的快捷键，丰富的自定义功能</li></ul><p>我最喜欢图片的是<strong>自动生成链接功能</strong>，写博客时能省不少力气</p><p><img src="/blog/../images/2025-1-16-2/image-20250116164438721.png" alt="image-20250116164438721"></p><h1 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h1><h2 id="付费使用"><a href="#付费使用" class="headerlink" title="付费使用"></a>付费使用</h2><p>正式版 89r，可供三台设备使用，找两个人拼一下 30r，也算对得起这个价</p><p>当然也有免费使用的方法</p><h2 id="免费使用"><a href="#免费使用" class="headerlink" title="免费使用"></a>免费使用</h2><p>在几年前的 beta 版中，Typora 是不用收费的，这里我使用的就是最后一代 beta 版 0.11.18</p><p>虽然直接使用会报错并提醒你强制更新，但是我们可以通过一些手段规避这个报错</p><p>首先，你应该充分使用<strong>搜索引擎</strong>，下载到如下文件</p><p><img src="/blog/../images/2025-1-16-2/image-20250116165006279.png" alt="image-20250116165006279"></p><p>双击安装，然后运行，发现报错</p><p>这里使用 win + R 调出<strong>运行</strong>界面，输入<strong>regedit</strong>打开注册表</p><p>依次找到如下文件</p><p><img src="/blog/../images/2025-1-16-2/image-20250116165338170.png" alt="image-20250116165338170"></p><p><img src="/blog/../images/2025-1-16-2/image-20250116165349362.png" alt="image-20250116165349362"></p><p><img src="/blog/../images/2025-1-16-2/image-20250116165400133.png" alt="image-20250116165400133"></p><p>右键点击，找到<strong>权限</strong>，进入界面</p><p><img src="/blog/../images/2025-1-16-2/image-20250116165525158.png" alt="image-20250116165525158"></p><p>将所有用户的<strong>完全控制</strong>和<strong>读取</strong>权限禁用</p><p>然后你就可以正常使用这个版本的 Typora 了</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="杂谈" scheme="https://kpmark.icu/categories/%E6%9D%82%E8%B0%88/"/>
    
    
    <category term="Typora" scheme="https://kpmark.icu/tags/Typora/"/>
    
  </entry>
  
  <entry>
    <title>Frp导致的一些神必问题</title>
    <link href="https://kpmark.icu/2025/01/14/2025-1-14/"/>
    <id>https://kpmark.icu/2025/01/14/2025-1-14/</id>
    <published>2025-01-14T12:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.261Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="与-hexo-有关的神必重定向"><a href="#与-hexo-有关的神必重定向" class="headerlink" title="与 hexo 有关的神必重定向"></a>与 hexo 有关的神必重定向</h1><p>昨天在测验网站的时候，发现一个在任何界面点击<strong>友链</strong>和<strong>关于</strong>链接时会神秘的重定向到错误地址<code>frp-act.top:3000/links/</code>。而正确地址<code>https://frp-act.top:49191/links/</code>是可以访问的</p><p>于是我尝试在<code>nginx</code>配置中修改重定向规则，强制将<code>frp-act.top:3000/links/</code>重定向至<code>https://frp-act.top:49191/links/</code>，结果是两个网址都 404 了</p><p>尝试寻找最开始出错的原因，发现<code>hexo</code>在生成静态文件时给出的相对路径是<code>/links</code>，并不包含<strong>尾斜杠</strong>，在不开隧道的情况下，一般浏览器会自动补上<strong>尾斜杠</strong>以访问正确地址</p><p>在<code>nginx</code>进行反代的环境下，理应也会进行补全，事实上，<code>nginx</code>确实给出了带上尾斜杠的重定向地址，然而其却将<code>frp-act.top:49191</code>这个隧道域名+隧道口重定向为了<code>frp-act.top:3000</code>这种隧道域名+本地端口这种不伦不类的东西</p><p>在对 nginx 反代进行了时长 3h 的各种配置组合后，我放弃了对 nginx 的重新配置，完全不能理解其中为什么会出现莫名其妙的 404、403 和 500</p><p>于是决定直接修改服务器中生成的静态文件</p><h2 id="使用-vscode-通过-ssh-连接服务器"><a href="#使用-vscode-通过-ssh-连接服务器" class="headerlink" title="使用 vscode 通过 ssh 连接服务器"></a>使用 vscode 通过 ssh 连接服务器</h2><p>当然不可能用 nano 或者 vim 这种编辑器去进行 160 处修改，这里我们选择天下第一的 vscode</p><p>在 vscode 上下载 remote-ssh 插件，点击左侧边栏的<strong>远程资源管理器</strong><br><img src="/blog/images/%E8%BF%9C%E7%A8%8B%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86%E5%99%A8.png" alt="图片" title="程资源管理"></p><p>点击<strong>新建远程</strong>，在上边栏中输入</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh 用户名@服务器ip</span><br></pre></td></tr></table></figure><p>按下 enter,系统会自动进行配置，按照引导一路向下，即可在服务器中安装 vscode server，从而使用 vscode 进行远程操作</p><p>这里注意给电脑开上代理，不然无法下载 vscode server</p><p>然后可以通过 vscode 的查找功能统一替换错误的相对地址</p><h1 id="使用-git-上传静态文件"><a href="#使用-git-上传静态文件" class="headerlink" title="使用 git 上传静态文件"></a>使用 git 上传静态文件</h1><p>直接通过<code>hexo d</code>上传静态文件非常方便，我们对服务器端 git 稍作配置</p><p>首先，如果你的服务器没有 git（树莓派就没有），可以运行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt-get install git</span><br></pre></td></tr></table></figure><p>下载 git</p><p>然后在一个合适的地方，比如&#x2F;home&#x2F;kpmark&#x2F;（kpmark 是我的用户名 ）新建一个 git 仓库存放我需要的 hooks</p><p>使用以下命令新建文件夹（或者你已经用 vscode 远程控制了，直接右键新建即可）,并在文件夹中初始化一个 git 裸仓库</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mkdir /homek/kpmark/git</span><br><span class="line">cd /homek/kpmark/git</span><br><span class="line">git init --bare hexo.git</span><br></pre></td></tr></table></figure><p>进入该仓库，在 hooks 文件夹中新建文件<code>post-receive</code>，并写入以下内容</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git --work-tree=/home/kpmark/hexo-blog --git-dir=/home/kpmark/git/hexo.git checkout -f</span><br></pre></td></tr></table></figure><p>这里前面的地址是你博客静态文件所在的地址，是该 git 仓库的工作地址，后面是你 git 仓库的位置，这条命令会强制更新你工作目录的文件</p><p>在&#x2F;home&#x2F;kpmark&#x2F;下创建<code>.ssh</code>文件夹（如果你没有的话），然后将你 windows 电脑的 ssh 公钥复制到其中的 authorized_keys 文件中</p><p>最后修改 hexo 配置，在<code>_config.yml</code>文件中，修改 url 为你的域名，修改 deploy 为</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">deploy:</span><br><span class="line">  type: git</span><br><span class="line">  repository: kpmark@ip:/home/kpmark/git/hexo.git</span><br><span class="line">  branch: master</span><br></pre></td></tr></table></figure><p>这里如果你有公网 ip 就直接写上去，非常方便</p><p>如果开隧道的话，必须对 ssh 进行额外配置，这里不赘述，因为会非常影响你其他需要 ssh 的仓库，建议之间局域网 ip 算了</p><p>至此，配置成功</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
    <category term="Frp" scheme="https://kpmark.icu/tags/Frp/"/>
    
  </entry>
  
  <entry>
    <title>搭建自己的 web 服务器</title>
    <link href="https://kpmark.icu/2025/01/13/2025-1-13/"/>
    <id>https://kpmark.icu/2025/01/13/2025-1-13/</id>
    <published>2025-01-13T12:43:47.000Z</published>
    <updated>2025-08-22T09:06:45.260Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>正好手头有一个树莓派，想着加一个 web 服务器的功能就折腾了一下，如果只是想搭载自己的博客建议还是使用云服务器，毕竟树莓派确实不便宜(</p><h1 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h1><ul><li>树莓派（我用的型号是 4b）</li><li>sd 卡（建议 32G 以上）</li><li>读卡器</li><li>一台能正常工作的电脑</li></ul><p>这里 web 服务器中搭载的内容是简单的静态博客页面，使用 hexo 搭建博客网站的方法这里不再赘述，可以在本站的<strong>关于</strong>中查看相关教程</p><p>如果不使用树莓派等本地 linux 服务器进行配置，可以跳过<strong>树莓派</strong>配置部分</p><h1 id="树莓派配置"><a href="#树莓派配置" class="headerlink" title="树莓派配置"></a>树莓派配置</h1><h2 id="系统烧录"><a href="#系统烧录" class="headerlink" title="系统烧录"></a>系统烧录</h2><p>我们拿到树莓派时是一个空板，所以我们需要自行烧录系统</p><p>理论上可以烧录任何你喜欢的系统，这里我们为了配置方便与以后的其他用途选择树莓派官方的 RaspberryPi OS ——一个基于 debian 的 linux 系统</p><p>进入<a href="https://www.raspberrypi.com/">树莓派官网</a>，下载烧录工具，我们在 windows 设备上进行烧录</p><p><img src="/blog/images/rasb1.png" alt="图片" title="烧录工具"></p><p>按照引导一路安装，完成后打开烧录程序<br><img src="/blog/images/rasb2.png" alt="图片" title="烧录程序"></p><p>在 CHOOSE DECVICE 中选择自己的树莓派型号，这里选择<strong>Raspberry Pi 4</strong></p><p>然后选择你需要的操作系统，这里一般为 Raspberry Pi OS(64 bit)，若不需要图形界面的话可以选择对用的 lite 版本</p><p>这里我觉得命令行很帅，所以使用的 lite 版本，后续内容均基于命令行操作演示</p><p>由于我手边并没有显示器，因此选择在自己的 windows 系统上使用 ssh 进行连接，在烧录系统时注意设置自己的用户名、密码与 wifi，方便使用在终端使用 ssh 进行连接</p><p>然后选择你自己的 sd 卡，进行烧录即可</p><h2 id="ssh-连接"><a href="#ssh-连接" class="headerlink" title="ssh 连接"></a>ssh 连接</h2><p>首先确认你的电脑上有 ssh.exe 这个文件，一般电脑上有 git，文件中是自带 ssh.exe 的，关于 git 的使用请参考<strong>关于</strong>中的教程</p><p>这里为了方便我们使用 cmd 或者 powershell 进行命令行操作（git bash 确实不太方便），我们将 git 文件夹中的 ssh.exe 加入环境变量</p><p><img src="/blog/images/rasb3.png" alt="图片" title="ssh位置"><br><img src="/blog/images/rasb4.png" alt="图片" title="环境变量"></p><p>然后，打开你的终端，输入</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh &lt;树莓派的ip&gt;</span><br></pre></td></tr></table></figure><p>如果不知道 ip 是什么，可以输入</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ping 主机名 // 未修改默认raspberrypi</span><br></pre></td></tr></table></figure><p>然后输入你烧录系统时设定的密码，注意输入的密码不会显示在终端中</p><p>出现以下提示</p><p><img src="/blog/images/rasb5.png" alt="图片" title="提示"></p><p>表示你已经成功通过 ssh 连接至你的树莓派了</p><h1 id="配置-Ningx"><a href="#配置-Ningx" class="headerlink" title="配置 Ningx"></a>配置 Ningx</h1><h2 id="下载"><a href="#下载" class="headerlink" title="下载"></a>下载</h2><p>将自己的服务器用作 web 服务器，我们需要对其进行反向代理，这里我们使用 Ningx 来进行配置</p><p>首先在服务器中下载 Nginx</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt -y Nginx // -y表示自动确认</span><br></pre></td></tr></table></figure><p>然后启动</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl start nginx</span><br></pre></td></tr></table></figure><p>现在你通过同局域网的设备访问树莓派 ip,可以看到如下页面<br><img src="/blog/images/rasb6.png" alt="图片" title="成功页面"></p><p>代表 Nginx 下载成功</p><h2 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h2><p>这里假设我要将我的博客文件放在<code>/home/username/hexo</code>中，我需要将对应监听的端口指向<code>blog</code>中的<code>index.html</code>文件</p><p>这里我们在<code>etc/nginx</code>文件夹下新建一个<code>vhost</code>文件夹来存放我们自定义的配置信息，例如，我想将 3000 端口对应至我的博客页面</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">    listen 3000;</span><br><span class="line">    location / &#123;</span><br><span class="line">            root /home/yourname/hexo; //注意将yourname换成自己的用户名</span><br><span class="line">            index index.html;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后，我们需要在<code>/etc/nginx/nginx.conf</code>文件中的 http 代码块内加入<code>include /etc/nginx/vhost/*.conf;</code>来添加所有 vhost 下的配置文件</p><p>此外，我们将需要将<code>nginx.conf</code>文件中的<code>user</code>字段设为自己的用户名，以解决访问限制问题</p><p>使用<code>sudo nginx -t</code>来检测是否有语法错误，无误后使用<code>sudo systemctl restart nginx</code>重启 nginx 代理</p><p>现在访问你树莓派对应的端口，就可以定向至树莓派内文件所在的位置，接下来我们将 windows 上的静态博客页面迁移至树莓派</p><h1 id="博客迁移"><a href="#博客迁移" class="headerlink" title="博客迁移"></a>博客迁移</h1><p>这里埋个坑，目前我直接将 hexo 的静态文件手动转移至树莓派，实际使用 git 进行管理会更加方便</p><p>在电脑中的 hexo 文件架内，使用<code>hexo generate</code>在<code>public</code>中生成对应的静态文件，运行以下命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cd public</span><br><span class="line">tar -czvf hexo-blog.tar.gz *</span><br></pre></td></tr></table></figure><p>将对应的静态文件打包</p><p>然后，使用 ssh 自带的 scp 工具将压缩包转移至树莓派。假设树莓派 ip 为<code>192.168.1.100</code>，目标目录是 &#x2F;home&#x2F;kpmark&#x2F;hexo:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">scp hexo-blog.tar.gz kpmark@192.168.1.100:/home/kpmark/hexo</span><br></pre></td></tr></table></figure><p>然后，在树莓派中运行以下命令解压文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cd /home/kpmark/hexo</span><br><span class="line">tar -xzvf hexo-blog.tar.gz</span><br></pre></td></tr></table></figure><p>此时通过局域网访问树莓派对应的端口，即可看见你的博客页面</p><h1 id="内网穿透"><a href="#内网穿透" class="headerlink" title="内网穿透"></a>内网穿透</h1><p>为了在公网中能够访问自己的博客，我们需要一个公网 ip 以及对应的域名</p><p>然而目前国内公网 ip 的获取较为麻烦，我们可以采用内网穿透式服务器能够面向公网，这里我选择<code>Sakura Frp</code></p><p>首先，进入官网，注册账号，进行实名认证（需要一元巨款）<br><img src="/blog/images/rasb7.png" alt="图片" title="注册账号"></p><p>然后点击<strong>服务</strong>，进入软件下载</p><p>这里我们跳过启动器，直接安装<code>frpc</code>使用命令行进行操作<br><img src="/blog/images/rasb8.png" alt="图片" title="下载"></p><p>找到你服务器设备架构对应的软件版本，如若不确定可以在服务器端输入<code>uname -u</code>来进行确认</p><p>这里我的树莓派是<code>Linux AArch64(arm64)</code>版本，注意不要和<code>amd64</code>搞混</p><p>不想折腾也可以使用<code>Docker</code>进行部署，但是我比较喜欢手操的感觉<br><img src="/blog/images/rasb9.png" alt="图片" title="版本"></p><p>以下操作需要取得<code>root</code>权限，请使用<code>sudo -s</code>命令进行切换</p><p>对照官方文档，我们使用<code>cd /usr/local/bin</code>进入对应的文件夹，然后使用以下命令进行下载:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"># 一般来说只需要使用这条命令:</span><br><span class="line">wget -O frpc &lt;下载地址&gt;</span><br><span class="line"></span><br><span class="line"># 如果上面的命令报错，请尝试这条:</span><br><span class="line">curl -Lo frpc &lt;下载地址&gt;</span><br><span class="line"></span><br><span class="line"># Linux frpc 通常已经过 UPX 压缩，如需下载未压缩的版本请在下载地址尾部加上 _noupx</span><br></pre></td></tr></table></figure><p>下载地址直接在版本页面复制连接即可</p><p>然后设置权限并校验文件完整性</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">chmod 755 frpc</span><br><span class="line">ls -ls frpc</span><br><span class="line">md5sum frpc</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/blog/images/rasb10.png" alt="图片" title="完整性"></p><p>如果操作全部正确，此时可以运行<code>frpc -v</code>来查看版本</p><p>在服务页面的隧道列表中新建列表，注意输入你服务器的 ip 和要监听的端口号<br><img src="/blog/images/rasb11.png" alt="图片" title="隧道"></p><p>再右侧操作中找到配置文件，复制密钥即可，然后再服务器中运行：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">frpc &lt;复制内容&gt;</span><br></pre></td></tr></table></figure><p>即可开启隧道，此时可直接通过隧道给出的 ip 或域名从公网访问网站</p><h1 id="https-与-http"><a href="#https-与-http" class="headerlink" title="https 与 http"></a>https 与 http</h1><p>如果你的网站申请过 https，那应该是可有照常访问的，如若没有，请在隧道设置中开启自动 https 选项</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
    <category term="Linux" scheme="https://kpmark.icu/tags/Linux/"/>
    
    <category term="RasberryPi" scheme="https://kpmark.icu/tags/RasberryPi/"/>
    
  </entry>
  
  <entry>
    <title>服务器编程概述</title>
    <link href="https://kpmark.icu/2024/06/22/6-22/"/>
    <id>https://kpmark.icu/2024/06/22/6-22/</id>
    <published>2024-06-22T02:16:46.000Z</published>
    <updated>2025-08-22T09:06:45.265Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/blog/assets/css/APlayer.min.css"><script src="/blog/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="/blog/assets/js/Meting.min.js"></script><h1 id="互联网是如何工作的"><a href="#互联网是如何工作的" class="headerlink" title="互联网是如何工作的"></a>互联网是如何工作的</h1><p>当两台电脑需要工作的时候，我们必须以有线或无线的方式将其连接(两重方式本质上并无不同)</p><p>通常一个网络不限于两个电脑，所以我们可以将所有电脑连接只一个叫<strong>路由器</strong>的特殊小电脑上。由<strong>路由器</strong>来接受并转发信息。</p><p><img src="/blog/images/Router.png" alt="图片" title="路由器"></p><h2 id="构成网络"><a href="#构成网络" class="headerlink" title="构成网络"></a>构成网络</h2><p>我们将路由器之间再相互连接，规模会不断的扩大，越来越接近我们脑海中<strong>互联网</strong>的规模。</p><p>然而，在现实中为每个家庭每个人架设专用的电缆将不同的网络连接起来是不可能的，那么我们改怎么处理这件事？</p><p>电话基础设施已将每家每户进行覆盖了，我们可以使用<strong>调制解调器</strong>将网络信息转变为电话设施可处理的信息来利用这些设备。</p><p><img src="/blog/images/Modem.png" alt="图片" title="所谓的‘猫’"></p><p><em>(由于科技的发展，很多地区已经实现光纤入户)</em></p><p>我们将自己的网络连接至互联网服务提供商(ISP)—-一家可以管理一些特殊的路由器的公司。这些路由器连接至其他<strong>ISP</strong>的路由器，进而实现信息的交互。</p><p><img src="/blog/images/ISP.png" alt="图片" title="ISP"></p><p><em>(国内的电信、移动、联通均为 ISP 供应商)</em></p><h1 id="什么是-web-服务器"><a href="#什么是-web-服务器" class="headerlink" title="什么是 web 服务器"></a>什么是 web 服务器</h1><p>硬件部分，web 服务器是一台存储了 web 服务器软件以及网站的组成文件的计算机。</p><p>软件部分，web 服务器包括控制网络用户如何访问托管文件的几个部分，至少是一台 HTTP 服务器。</p><h2 id="文件托管"><a href="#文件托管" class="headerlink" title="文件托管"></a>文件托管</h2><p>严格来说，你可以在你自己的计算机上托管所有的这些文件，但是在一个专用的 web 服务器上存储它们会方便得多。</p><ul><li>专用 web 服务器可用性更强（会一直启动和运行）</li><li>除去停机时间和系统故障，专用 web 服务器总是连接到互联网。</li><li>专用 web 服务器可以一直拥有一样的 IP 地址，这也称为专有 IP 地址（不是所有的 ISP 都会为家庭线提供一个固定的 IP 地址）</li><li>专用 web 服务器往往由第三方提供者维护</li></ul><h2 id="通过-HTTPS-交流"><a href="#通过-HTTPS-交流" class="headerlink" title="通过 HTTPS 交流"></a>通过 HTTPS 交流</h2><p>web 通过**超文本传输协议(http)**来和 web 服务器进行交流。当你在网页上点击一个链接，或提交一个表单，再或进行一次搜索时，一个 HTTP 请求就从你的浏览器发送到了目标服务器。</p><p><strong>一个 HTTP 请求</strong>包含所请求资源的<strong>URL</strong>，定义所需操作的<strong>方法</strong>，编码在 URL 参数的<strong>附加信息</strong>。</p><p>Web 服务器等待客户端的请求信息，在它们到达的时候处理它们，并且回复 Web 浏览器一个 HTTP 响应信息。</p><p><strong>一个 HTTP 相应</strong>包含一个显示请求是否成功的<strong>状态行</strong>(比如 404 not found)。</p><p>相应一个请求的成功回应包含被请求的资源，然后这些会被展示在客户端的 Web 浏览器上。</p><h2 id="动态网站"><a href="#动态网站" class="headerlink" title="动态网站"></a>动态网站</h2><p>一些相应内容只有在被需要的时候才会在网站中生成，在一个动态网站中，页面通常是通过将数据库的数据植入到 HTML 模板中的占位符而产生的。</p><p>大多数支持动态网站的代码必须运行在服务器上。编写这些代码就是所谓的<strong>服务器端编程</strong>(有些时候也称<strong>后端脚本编写</strong>)。</p><p><img src="/blog/images/server.png" alt="图片" title="ISP"></p><p>对于动态资源的请求则会指向(2)<strong>服务器端代码</strong> (在图中显示为 Web Application)。在处理<strong>动态请求</strong>时，服务器会首先解释请求，从数据库中读取被请求的信息，然后将这些被检索的信息组合到<strong>HTML 模板</strong>中(4),最后返回一个包含所生成的<strong>HTML 页面</strong>的回应(5,6)。</p><h1 id="web-服务器编程"><a href="#web-服务器编程" class="headerlink" title="web 服务器编程"></a>web 服务器编程</h1><p>服务器端的代码可以使用任何一种语言编程，比较受欢迎的包括 PHP(<strong>狗都不用</strong>)、Python、Ruby 和 C#。服务器端代码有充分的权限访问服务器的操作系统。</p><p>开发者通常使用 web 框架来编写代码。</p><p>客户端 web 框架主要简化布局和演示任务，服务器 web 框架提供大量的普通 Web 服务功能，比如支持会话、支持用户和身份验证、简单的数据访问、模板库等。</p><p>客户端框架通常被用来帮助加速客户端代码的开发，但是你也可以选择手写所有的代码；事实上，如果你只需要一个小型的、简单的网站 UI，手写自己的代码可能更快并且更高效。</p><p>相反的，你应该从来没有考虑过不使用框架而直接编写 web 应用程序的服务器端组件——实现一个重要的功能比如 HTTP 服务器真的很难直接从头开始用 、Python 语言构建，但是一些用 Python 语言写的 web 框架，比如 Django 提供了开箱即用的功能，同时还包含其他很多有用的工具。</p><h2 id="web-服务器端可以做什么？"><a href="#web-服务器端可以做什么？" class="headerlink" title="web 服务器端可以做什么？"></a>web 服务器端可以做什么？</h2><ul><li><p>信息的高效存储和传输</p></li><li><p>定制用户体验</p></li><li><p>控制对内容的访问</p></li><li><p>存储会话和状态信息</p></li><li><p>通知和通讯</p></li><li><p>数据分析</p></li></ul><p>详见<a href="https://developer.mozilla.org/zh-CN/docs/Learn/Server-side/First_steps/Introduction">MDN 服务端编程介绍</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/blog/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/blog/assets/js/APlayer.m</summary>
      
    
    
    
    <category term="笔记" scheme="https://kpmark.icu/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Web" scheme="https://kpmark.icu/tags/Web/"/>
    
  </entry>
  
</feed>
