<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>AEnjoy</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://www.a-enjoy.top/</id>
  <link href="https://www.a-enjoy.top/" rel="alternate"/>
  <link href="https://www.a-enjoy.top/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, AEnjoy</rights>
  <subtitle>AEnjoy's Blog</subtitle>
  <title>Enjoy your life</title>
  <updated>2026-08-09T10:09:31.413Z</updated>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="分布式系统" scheme="https://www.a-enjoy.top/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    <content>
      <![CDATA[<p>最近运维同事提出了一个需求：<br>在上层，我们目前有一个中央配置中心（依托于我们内部的CMDB）用于管理DHCP服务器和PXE相关的的配置和策略（运维平台），以便在需要时可以动态调整；<br>在下层一个大型数据中心网络中，需要设计一个能够支持十万级节点的DHCP服务器系统，同时还需要兼具PXEServer用于提供PXE启动服务（包括：裸金属装机、无盘启动）。<br>这个系统需要具备高可用性、可扩展性和负载均衡能力，以确保在高并发环境下能够稳定运行。</p><p>在这个量级，传统的单体架构已经无法满足需求，因此需要采用分布式架构来实现dhcp服务的可扩展性和高可用性。</p><p>目前，有两类ip分配：纯动态：在地址池中随机分配；纯静态：根据mac地址分配固定ip。</p><p>Pxe server：对于特定mac+来自pxe client的请求，我们还需要修改本机的配置文件（grub）用于实现特定配置(如ubuntu，windows，centos)pxe启动，这又涉及到了文件的并发读写。</p><p>整体系统实现思路很简单，但要保证一致性还是有一定难度的，下面我将详细描述这个系统的设计思路。</p><span id="more"></span><h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><pre class="mermaid">graph TB    subgraph Center[中心管理服务]        direction TB        A[统一IP地址池管理] --> B[动态/静态IP分配引擎]        B --> C[gRPC服务端]        D[(IP地址池数据库)]        A -.-> D    end    subgraph Room1[机房A]        direction LR        S1[DHCP Server Agent 1] --> G1[gRPC客户端]    end    subgraph Room2[机房B]        direction LR        S2[DHCP Server Agent 2] --> G2[gRPC客户端]    end    subgraph RoomN[机房N]        direction LR        S3[DHCP Server Agent N] --> G3[gRPC客户端]    end    C <-->|双向流式gRPC| G1    C <-->|双向流式gRPC| G2    C <-->|双向流式gRPC| G3    classDef center fill:#e1f5fe,stroke:#01579b,stroke-width:2px    classDef room fill:#f3e5f5,stroke:#4a148c,stroke-width:2px    classDef agent fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px    class Center center    class Room1,Room2,RoomN room    class S1,S2,S3 agent</pre><p>中心管理(我们内部的运维平台)：作为大脑，维护全局IP地址池数据库，并内置动态&#x2F;静态IP分配引擎。所有分配决策均由中心服务做出。</p><p>通信机制：通过双向流式gRPC连接，支持中心服务实时推送配置变更，同时各机房Agent可上报状态与请求。</p><p>机房集群：每个机房内可部署多个DHCP Server Agent实例，形成高可用集群。所有Agent均以客户端身份与中心建立长连接。</p><h2 id="需求核心策略"><a href="#需求核心策略" class="headerlink" title="需求核心策略"></a>需求核心策略</h2><p>在接到一个需求时，我们首先要想的是这个需求的目的是什么：很明显，各业务节点都需要从dhcp server获取IP地址用于上网(当前，<br>有的节点在系统上配置了静态ip，但绝大多数仍然需要动态分配，无论是mac-ip静态绑定还是从地址池中随机分配，都是为了让节点能够上网)。</p><p>那很显然，系统要是崩了（含超时、故障等），节点就无法上网了，这就是我们要解决的核心问题 -&gt; 稳定性问题。</p><p>如果ip分配冲突了，那么节点受路由，arp策略自然也无法上网了。<br>这个问题会出现的时机：agent与配置中心配置版本不一致、本地dhcp ippool分配逻辑错误，这就是我们要解决的第二个核心问题 -&gt; 配置一致性问题（边缘节点与配置中心）。</p><p>Grub配置文件读写竞态：有的节点正在开机-&gt;读取grub配置；此时若云端下发其它节点的pxe配置 -&gt; 并发读写问题。</p><p>约束点：配置都放在配置中心，所有的dhcp server agent都从配置中心拉取配置，这基本能够保证一致性，不需要担心传统分布式系统去中心化共识带来的一系列问题，<br>在纯分布式架构中，各dhcp server节点 需要抢锁选举leader作为事实来源。但在中心-边缘架构中，中央服务是唯一的事实来源。</p><p>性能在这个系统里不是主要的考虑点，毕竟请求是很轻量的，对请求响应耗时要求也不高。</p><hr><p>好，现在我们开始解决 <code>需求核心策略</code> 提到的关注点。</p><h2 id="稳定性问题："><a href="#稳定性问题：" class="headerlink" title="稳定性问题："></a>稳定性问题：</h2><h3 id="服务自动拉起"><a href="#服务自动拉起" class="headerlink" title="服务自动拉起"></a>服务自动拉起</h3><p>这个很好办，搭建一个dhcp 多节点server集群，每节点只负责特定mac段段处理请求，避免冲突。每个节点都可以独立处理请求，互不干扰。<br>dhcpserver交由systemd托管，宿主机开机、服务挂掉可以自动拉起重试。</p><h3 id="断网时网络分区自保"><a href="#断网时网络分区自保" class="headerlink" title="断网时网络分区自保"></a>断网时网络分区自保</h3><p>网络断连时，agent无法与中心服务通信，但仍然可以继续提供dhcp服务。<br>因为agent在本地维护了静态mac-ip映射表和dhcp ip池，能够独立处理请求，但是这有一个期限：在配置中心不可用期间，agent只能基于本地配置提供服务<br>（时长为2分钟），直到配置中心恢复；如若两分钟内配置中心恢复，agent会立即与中心服务重新建立连接，并同步最新配置；若未恢复，则停止服务并重启。</p><p>配置中心：检测到 agent grpc流断开，中央服务立即回收之前授予该 agent 的 ip token池，将其标记为“待分配”，并在 120 秒后授权给其他存活 agent。</p><h2 id="配置一致性问题："><a href="#配置一致性问题：" class="headerlink" title="配置一致性问题："></a>配置一致性问题：</h2><p>配置中心管理 静态macip映射、dhcp ip池、pxe参数，GRUB 模板。由于是双向流，下发必须保证幂等性和顺序性。</p><h3 id="静态策略"><a href="#静态策略" class="headerlink" title="静态策略"></a>静态策略</h3><p>同步机制：agent启动建立连接时，会携带本地配置版本号，<br>中心服务会根据版本号判断是否需要下发最新配置。若版本不一致，中心服务会将最新配置推送给agent，确保所有agent的配置保持一致（配置分为全量下发和增量下发）。</p><p>双向ack：agent收到配置后，会返回ack给中心服务，中心服务在收到ack后才会认为该agent的配置已更新成功。若未收到ack，中心服务会重试下发，直到成功为止<br>（对于网络抖动，下发重复配置，agent 依据配置版本号进行幂等忽略）。</p><p>直至配置同步完成，agent才会正式对外提供dhcp服务，避免配置不一致导致的ip冲突。</p><h3 id="pxe参数与grub"><a href="#pxe参数与grub" class="headerlink" title="pxe参数与grub"></a>pxe参数与grub</h3><p>pxe策略：pxe策略不存储于本地，毕竟数十万节点中，pxe dchp请求是少数，我这里采用的策略是收到请求后再去同步拉取响应。</p><p>当agent收到请求时，会携带请求的mac地址和pxe请求类型（如uefi&#x2F;legacy等），<br>中心服务会根据mac地址查询pxe策略，并将对应的grub配置下发给agent，agent再将配置写入本地grub文件中（需要加文件锁），<br>确保pxe启动的正确性（读修复）。</p><h3 id="动态ip-dhcp地址池分配"><a href="#动态ip-dhcp地址池分配" class="headerlink" title="动态ip dhcp地址池分配"></a>动态ip dhcp地址池分配</h3><p>虽然只有少部分节点是这种分配方式：比如我们有的客户，他们自己管自己虚拟机，没有报mac给我们，但要我们提供分配：即未知mac-&gt;动态分配</p><p>如果每个请求都同步阻塞等待中央服务，性能虽不至于雪崩，但也绝对不高效。我才用的策略是双向流异步批量仲裁。</p><p>在agent启动时，服务端会下发特定ip段（你可以理解为访客段）IP租赁许可证。<code>&#123;Agent_ID: AgentA, 授权IP段: 10.0.1.1~10.0.1.100, 排除: [10.0.1.7], 有效期: 120s, 版本号: 5&#125;</code><br>IP 冲突彻底消失，因为中央服务将 IP 段切成互不重叠的“时间片”分配给不同 Agent。<br>若 Agent A 宕机，其持有的 Token 在 60s 后过期，中央服务可立即将该 IP 段授权给 Agent B（故障转移），且绝不重叠。</p><p>agent 会在 pool 内存中做快速fifo分配，毫秒级响应，然后再上报ip-mac分配.<br>配置中心收到后，会将该分配结果写入数据库，并在下次配置下发时同步给所有agent，确保全局一致性。</p><p>当时设计时询问了AI，AI说还有一种极少数情况：若中央服务发现批量日志中的某个 ip 已被其它 agent 的 token 占用<br>（极少数时钟&#x2F;逻辑 BUG 导致）， 中央服务会立即在 gRPC 流中下发 Force_Revoke(IP, MAC)<br>指令给当前 Agent。Agent 收到后，必须对该 MAC 主动发送 DHCP FORCERENEW 或拒绝续租，强制纠错（最终一致性兜底），不过我感觉这是不必要的。</p><h2 id="并发读写："><a href="#并发读写：" class="headerlink" title="并发读写："></a>并发读写：</h2><p>PXE启动时，grub配置文件的读写是并发的。为了解决这个问题，我们在agent中实现了文件锁机制。<br>这样，就算同时收到dhcp请求、grub请求和配置下发，所有操作依然是串行化的，避免了并发读写导致的配置文件损坏或不一致问题。<br>毕竟，就算pxe dhcp请求再怎么多，量级也不会特别大；而且pxe dhcp请求的bios中设置的超时往往特别长，所以只要加了锁，这个问题的影响是可控的。</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2026/08/08/%E5%8D%81%E4%B8%87%E7%BA%A7%E8%8A%82%E7%82%B9dhcp-server%E8%AE%BE%E8%AE%A1/</id>
    <link href="https://www.a-enjoy.top/2026/08/08/%E5%8D%81%E4%B8%87%E7%BA%A7%E8%8A%82%E7%82%B9dhcp-server%E8%AE%BE%E8%AE%A1/"/>
    <published>2026-08-08T08:42:26.000Z</published>
    <summary>
      <![CDATA[<p>最近运维同事提出了一个需求：<br>在上层，我们目前有一个中央配置中心（依托于我们内部的CMDB）用于管理DHCP服务器和PXE相关的的配置和策略（运维平台），以便在需要时可以动态调整；<br>在下层一个大型数据中心网络中，需要设计一个能够支持十万级节点的DHCP服务器系统，同时还需要兼具PXEServer用于提供PXE启动服务（包括：裸金属装机、无盘启动）。<br>这个系统需要具备高可用性、可扩展性和负载均衡能力，以确保在高并发环境下能够稳定运行。</p>
<p>在这个量级，传统的单体架构已经无法满足需求，因此需要采用分布式架构来实现dhcp服务的可扩展性和高可用性。</p>
<p>目前，有两类ip分配：纯动态：在地址池中随机分配；纯静态：根据mac地址分配固定ip。</p>
<p>Pxe server：对于特定mac+来自pxe client的请求，我们还需要修改本机的配置文件（grub）用于实现特定配置(如ubuntu，windows，centos)pxe启动，这又涉及到了文件的并发读写。</p>
<p>整体系统实现思路很简单，但要保证一致性还是有一定难度的，下面我将详细描述这个系统的设计思路。</p>]]>
    </summary>
    <title>中心化的，边缘十万级节点 DHCP Server 设计</title>
    <updated>2026-08-09T10:09:31.413Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="golang" scheme="https://www.a-enjoy.top/tags/golang/"/>
    <category term="消息队列" scheme="https://www.a-enjoy.top/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <category term="nats" scheme="https://www.a-enjoy.top/tags/nats/"/>
    <content>
      <![CDATA[<p>试着回答一下这个有趣的消费-生产问题吧:</p><ol><li>这是一个使用消息队列解耦多个生产者和多个消费者的系统。</li><li>生产者生产的消息是有分区的(按<code>project_id</code>分区),分区也会动态分配。</li><li>消费者需要竞争消费消息,每个消费者可以消费多个分区。</li><li>对于一个分区,<strong>最多</strong>有不超过<code>1/n</code>个消费者在消费。</li><li>生产者和消费者根据性能和数据量,会在kubernetes中进行动态的扩缩容。</li></ol><p>消息需要尽可能的被均匀地分配到消费者上,以实现负载均衡。</p><p>消息需要尽可能的N等分，这N份消息将分配给消费者服务A(A的数量N&gt;&#x3D;2A)</p><p>e.g: 1&#x2F;10 情景: 消息需要尽可能的10等分，这10份消息将分配给消费者服务A(A的数量&gt;&#x3D;5),对于不满足5的倍数情况，如23个消费者，则余下3个随机分配</p><p>在这种动态输入输出的情况下,我们该如何设计一个消息队列负载均衡系统呢，如何保证3个9的SLA呢?</p><span id="more"></span><h2 id="流量路径"><a href="#流量路径" class="headerlink" title="流量路径"></a>流量路径</h2><p>客户的IoT Devices -&gt; StreamGateway(nginx)四层负载均衡 -&gt; DataGateway(生产者集群) -&gt; NATS JetStream(消息队列集群) -&gt; DataHandler(消费者数据存储集群)</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>上述问题就是我在工作中遇到的一个实际场景。我们需要设计一个系统来处理多个生产者和消费者之间的消息传递，同时确保负载均衡和动态扩缩容。</p><p>这套系统使用mermaid图来描述其架构和工作流程如下:</p><pre class="mermaid">graph TD    subgraph Producers["生产者"]        P1[生产者 1]        P2[生产者 2]        P3[生产者 ...]    end    subgraph MessageQueue["消息队列 (按 project_id 分区)"]        Partition1[分区 1]        Partition2[分区 2]        Partition3[分区 ...]    end    subgraph Consumers["消费者"]        C1[消费者 1]        C2[消费者 2]        C3[消费者 ...]    end    subgraph Kubernetes["Kubernetes 集群"]        ScaleUpDown[动态扩缩容根据性能和数据量]    end    P1 -->|生产消息| Partition1    P2 -->|生产消息| Partition2    P3 -->|生产消息| Partition3    Partition1 -->|竞争消费最多1/n消费者| C1    Partition1 -->|竞争消费最多1/n消费者| C2    Partition2 -->|竞争消费最多1/n消费者| C2    Partition2 -->|竞争消费最多1/n消费者| C3    Partition3 -->|竞争消费最多1/n消费者| C1    Partition3 -->|竞争消费最多1/n消费者| C3    Producers -->|动态扩缩容| Kubernetes    Consumers -->|动态扩缩容| Kubernetes</pre><p>你可能会注意到,在这个系统中,生产者和消费者都是动态的,并且消息队列是按<code>project_id</code>分区的。这意味着我们需要一个灵活的机制来处理消息的分发和消费。</p><p>那么,为什么他们是动态的呢?</p><p>在实际应用中,生产者和消费者的数量会根据业务需求和系统负载进行调整。例如,在高峰期可能需要增加更多的消费者来处理大量的消息，而在低峰期则可以减少消费者以节省资源。</p><h3 id="扩缩容策略："><a href="#扩缩容策略：" class="headerlink" title="扩缩容策略："></a>扩缩容策略：</h3><p>消费者leader会根据性能(Cpu&gt;&#x3D;90%)或数据量(单个消费者消费的消息条数超过消费者数量的50倍(这个数据也可以动态配置)，如100个消费者消费超过5000条),leader会调用kubernetes api进行动态的扩容。</p><p>当每个消费者平均消费的消息小于2条时缩容。</p><h3 id="工作背景"><a href="#工作背景" class="headerlink" title="工作背景:"></a>工作背景:</h3><p>系统是一个物联网系统,需要处理不同客户上报的数据,并对其进行处理.</p><p>所有客户上报的数据(消息)会遵循一定的规律.在这里,它是一个<code>project</code>, 因此会有唯一的<code>project_id</code></p><p>所有的消息以<code>projectid</code>进行分区，<br>生产者(数据网关)会将<code>projectid</code>投送至<code>DATASTORE_PROJECT</code>，<br>消费者在通过订阅获取到<code>DATASTORE_PROJECT</code>后，<br>发送成功请求至<code>DATASTORE_PROJECT_REG_RESP</code>，并订阅<code>DATASTORE_PROJECT_DATA</code>数据开始消费</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> (</span><br><span class="line">    DATASTORE_PROJECT = <span class="string">&quot;/handler/data/partitions/p%d&quot;</span> <span class="comment">// e.g., /handler/data/partitions/p0, ... p127</span></span><br><span class="line">    DATASTORE_PROJECT_REG_RESP = DATASTORE_PROJECT + <span class="string">&quot;/%s/reg/response&quot;</span></span><br><span class="line">    DATASTORE_PROJECT_CLOSE = DATASTORE_PROJECT + <span class="string">&quot;/%s/close&quot;</span></span><br><span class="line">    DATASTORE_PROJECT_DATA= DATASTORE_PROJECT + <span class="string">&quot;/%s/data&quot;</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><h2 id="Language-And-Tools"><a href="#Language-And-Tools" class="headerlink" title="Language And Tools"></a>Language And Tools</h2><ul><li>Golang</li><li>NATS(JetStream)</li><li>Kubernetes</li><li>Etcd</li></ul><h2 id="Solution"><a href="#Solution" class="headerlink" title="Solution"></a>Solution</h2><p>我们的解决目标是构建一个能够动态伸缩、自动负载均衡的消费者集群。</p><p>我们将采用 <strong>分区(Partitioning)和服务协调(Coordination)</strong> 相结合的策略。</p><h3 id="核心思路"><a href="#核心思路" class="headerlink" title="核心思路"></a>核心思路</h3><p>我们将放弃 <strong>“所有消费者都去订阅同一个发现Topic”</strong> 的模式，因为这会导致所有消费者产生 <strong>惊群效应</strong> ,并需要复杂的分布式锁来争抢和校验projectId</p><p>取而代之的核心思想是：</p><ol><li><p>固定分区：</p><p>我们将projectid的空间<strong>预先划分成一个固定的、数量较多的分区</strong>，例如 P &#x3D; 128 个。每个projectid通过哈希计算后，将永远属于同一个分区。</p></li><li><p>动态分配：</p><p>通过一个中心化的协调服务（etcd），将这 P 个分区动态地、均衡地分配给当前所有存活的 N 个消费者。</p></li><li><p>领导者选举：</p><p>在所有消费者中，选举出一个 Leader。这个 Leader 负责监控消费者的增减，并执行分区的重新分配和动态扩缩容决策。</p></li></ol><p><strong>这个设计类似于 Kafka 的 Consumer Group 机制，具有高可用性和水平扩展性</strong>。</p><h3 id="生产者逻辑（分区划分）"><a href="#生产者逻辑（分区划分）" class="headerlink" title="生产者逻辑（分区划分）"></a>生产者逻辑（分区划分）</h3><p>当一个<code>projectid</code>需要被处理时，生产者会：</p><ol><li>计算该<code>projectid</code>的哈希值。</li><li>对哈希值取模，得到分区号 <code>partition_id = hash(projectid) % P</code>。</li><li>将该projectid投递到对应的分区Topic，例如 <code>/handler/data/partitions/p42</code>。</li><li>生产者不需要关心消费者的数量和状态，只需将消息投递到正确的分区。</li></ol><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;crypto/sha256&quot;</span></span><br><span class="line">  <span class="string">&quot;math/big&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetPartition</span><span class="params">(id <span class="type">string</span>, totalPartitions <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">  hash := sha256.Sum256([]<span class="type">byte</span>(id))</span><br><span class="line">  hashInt := <span class="built_in">new</span>(big.Int).SetBytes(hash[:])</span><br><span class="line">  <span class="keyword">return</span> <span class="type">int</span>(hashInt.Mod(hashInt, big.NewInt(<span class="type">int64</span>(totalPartitions))).Int64())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="服务协调和leader选举"><a href="#服务协调和leader选举" class="headerlink" title="服务协调和leader选举"></a>服务协调和leader选举</h3><p>etcd 终于登场了。在这个部分，它扮演了项目里至关重要的<strong>协调角色</strong>。在项目里，我利用它的几个特性：</p><ul><li>键值存储：存储分区分配状态和消费者信息。</li><li>Watch机制：实时监控消费者变化。</li><li>Lease机制(TTL)：实现消费者心跳和故障检测。</li><li>分布式锁：实现<strong>领导选举</strong>。</li></ul><h4 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h4><ul><li><p><code>/consumers/</code></p><p>&#x2F;{consumer-id} -&gt; {“cpu”: 25.5, “project_count”: 30, “last_message_id”: “uuid” } (Lease attached)</p><p>每个消费者启动后，都会在这个目录下创建一个以自己唯一ID为名的key，并关联一个Lease（租约，带TTL）。消费者需要定期<strong>续约</strong>（”心跳”），如果Lease过期，key会自动删除，代表该消费者已下线。值用来存放该消费者的性能指标。</p></li><li><p><code>/partitions/</code></p><p>&#x2F;{partition_id} -&gt; {consumer-id}</p><p>这个目录存储了每个分区当前被哪个消费者占有。例如 <code>/partitions/42 -&gt; consumer-abc</code>。</p></li><li><p><code>/leader</code></p><p>一个用于分布式锁的key，成功写入该key的消费者成为Leader,同样需要关联Lease。</p></li></ul><h3 id="消费者逻辑（Leader）"><a href="#消费者逻辑（Leader）" class="headerlink" title="消费者逻辑（Leader）"></a>消费者逻辑（Leader）</h3><p>Leader消费者负责以下任务：</p><ol><li><p>赢得选举：启动后，所有消费者实例尝试获取 <code>/leader</code> 这个key上的锁。成功的成为<code>Leader</code>。</p></li><li><p>监控消费者变化：Leader WATCH etcd中的 <code>/consumers/</code> 目录，获取事件变化。</p><ul><li><p>新消费者加入 (PUT)：触发再均衡 (Rebalance)。</p></li><li><p>消费者下线 (DELETE)：触发再均衡。</p></li><li><p>?什么时候消费者数量会变化？</p><ul><li>一般是消费者崩溃时（数据处理有可能导致崩溃，不过崩溃并不会导致数据丢失，数据会因为没有ACK重新回到分区）</li></ul></li></ul></li><li><p>执行再均衡：</p><ul><li><p>获取当前所有存活的消费者列表 C (from &#x2F;consumers&#x2F;)。</p></li><li><p>获取当前所有的分区列表 P (0 to 127)。</p></li><li><p>执行分配算法：为每个消费者 c in C 分配 P&#x2F;N 个分区。</p><ul><li><p>最简单的分配方式是 partition_id % len(C)，将分区 i 分配给 C[i % len(C)]。</p></li><li><p>为了最小化分区迁移，可以采用更优化的算法（如一致性哈希环的变种），不过，取模方式已足够均衡。</p></li></ul></li><li><p>更新etcd：将新的分配结果 map[partition_id]consumer_id 写入到 <code>/partitions/</code> 目录。</p></li></ul></li><li><p>执行扩缩容：Leader 定期（在代码里，项目里设置了30s）执行一次监控和扩缩容决策。</p></li></ol><h3 id="消费者逻辑（Worker【include-leader】）"><a href="#消费者逻辑（Worker【include-leader】）" class="headerlink" title="消费者逻辑（Worker【include leader】）"></a>消费者逻辑（Worker【include leader】）</h3><ol><li><p>注册和发送心跳：启动后，生成唯一ID，在etcd <code>/consumers/&#123;consumer-id&#125;</code> 创建自己的key，并持续续约Lease。</p></li><li><p>监控任务分配：WATCH etcd中的 <code>/partitions/</code> 目录。</p></li><li><p>响应任务变更：</p><ul><li>当发现某个分区 p 的负责人变成了自己 (&#x2F;partitions&#x2F;p -&gt; my-id)，并且自己之前未订阅该分区：</li><li>a. 立刻通过NATS订阅对应的分区Topic: &#x2F;handler&#x2F;data&#x2F;partitions&#x2F;p{p}。</li><li>当发现某个分区 p 的负责人不再是自己 (&#x2F;partitions&#x2F;p 被删除或值变为其他id)，并且自己正在订阅该分区：</li><li>a. 立刻取消NATS对该分区Topic的订阅，并完成正在处理的消息后，停止处理来自该分区的任何新消息。</li></ul></li><li><p>消费数据：从已订阅的分区Topic中接收到 projectid 后，执行原有的业务逻辑：</p><ul><li><p>发送成功请求至 <code>DATASTORE_PROJECT_REG_RESP</code>。</p></li><li><p>订阅 <code>DATASTORE_PROJECT_DATA</code>。</p></li></ul></li><li><p>上报状态：定期更新自己在etcd中 &#x2F;consumers&#x2F;{consumer-id} 的值，报告信息，供Leader决策。</p></li></ol><h3 id="动态扩缩容策略（Leader）"><a href="#动态扩缩容策略（Leader）" class="headerlink" title="动态扩缩容策略（Leader）"></a>动态扩缩容策略（Leader）</h3><p>数据定义：</p><ul><li>cpu: cpu使用率</li><li>cpu_max_usage: 节点触发扩容的CPU使用率</li><li>project_count: 消费者当前处理的项目数量</li><li>max_project_count： 触发扩容的项目数量阈值</li><li>N: 消费者数量</li></ul><p>Leader会定期（30秒）检查所有消费者的状态，并根据以下规则决定是否扩缩容：</p><ul><li>超过CPU上限： 如果有任何消费者的 cpu 使用率超过 cpu_max_usage（90%），则触发扩容。</li><li>超过负载上限：<ol><li>计算N.</li><li>获取所有消费者的 project_count，并计算平均值 <code>avg_project_count = sum(project_count) / N</code>。</li><li>如果 <code>avg_project_count &gt; max_project_count（50）</code></li><li>触发扩容:<ul><li>Leader调用Go的Kubernetes客户端，更新对应Deployment的 replicas 字段，使其 replicas &#x3D; replicas + 1</li><li>冷静期： 为防止频繁扩缩容，一次操作后，进入一个冷却期（10分钟），在此期间不执行扩缩容轮询操作。</li></ul></li></ol></li><li>平均负载过低：<ol><li>计算N。</li><li>计算总项目数 <code>avg_project_count = sum(project_counts) / N</code>。</li><li><code>if N &gt; min_replicas (e.g., 2) &amp;&amp; avg_project_count / N &lt; 2</code>。</li><li>触发缩容:<ul><li>Leader调用Go的Kubernetes客户端，更新对应Deployment的 replicas 字段，使其 replicas &#x3D; replicas - 1</li><li>冷静期：同样，为防止频繁扩缩容，一次操作后，进入一个冷却期（10分钟），在此期间不执行扩缩容轮询操作。</li></ul></li></ol></li></ul><h2 id="工作流程："><a href="#工作流程：" class="headerlink" title="工作流程："></a>工作流程：</h2><ol><li><p>K8s启动消费Pod。</p></li><li><p>该Pod内的服务实例启动，生成唯一ID <code>consumer-hostname</code>。</p></li><li><p>consumer-hostname 连接etcd，在 <code>/consumers/consumer-hostname</code> 创建了一个带10秒Lease的key，并开始每5秒续约一次。</p></li><li><p>Leader正在 <code>WATCH /consumers/</code>，收到 <code>consumer-hostname</code> 的 PUT 事件。</p></li><li><p>Leader触发再均衡逻辑。假设之前有3个消费者 c1, c2, c3，现在有4个了。分区总数是128。</p><ul><li><p>旧分配：c1 (0,3,6,…), c2 (1,4,7,…), c3 (2,5,8,…)。</p></li><li><p>新分配：c1 (0,4,8,…), c2 (1,5,9,…), c3 (2,6,10,…), consumer-hostname (3,7,11,…)。</p></li></ul></li><li><p>Leader将新的分配结果批量更新到etcd的 &#x2F;partitions&#x2F; 目录下。</p></li><li><p>所有消费者（包括c1,c2,c3,consumer-hostname）都在WATCH &#x2F;partitions&#x2F;。</p></li><li><p>c1, c2, c3 发现自己的一些分区被剥夺了，它们会取消对这些分区Topic的NATS订阅。</p></li><li><p><code>consumer-hostname</code> 发现自己被分配了一批新的分区，它会去订阅这些分区对应的NATS Topic。</p></li><li><p>系统在没有停机的情况下，自动将负载均衡到了新的消费者上。</p></li></ol><hr><h2 id="数据一致性保障："><a href="#数据一致性保障：" class="headerlink" title="数据一致性保障："></a>数据一致性保障：</h2><p>你可能会担心：在消费者宕机时（这系统在消费者因为外部逻辑的问题，存在处理数据时崩溃的可能），数据的一致性和完整性如何保障。<br>在这套系统里，我们采用以下策略来处理这些情况：</p><h3 id="JetStream"><a href="#JetStream" class="headerlink" title="JetStream"></a>JetStream</h3><p>NATS JetStream支持一种数据应答机制，确保消息在被消费者处理后，才会从队列中删除。<br>消费者在处理完数据后，必须发送一个确认消息（ACK）给JetStream，只有收到ACK后，JetStream才会将该消息从队列中移除。</p><p>Stream定义：</p><ul><li>Subjects: “&#x2F;handler&#x2F;data&#x2F;partitions&#x2F;*”</li><li>Storage: “Memory” (Quickly)</li><li>Retention Policy: WorkQueuePolicy (消息被成功消费后即可删除)</li><li>Replicas: 3</li><li>Ack Policy: AckExplicit</li><li>Ack Wait Timeout: 5s(我们经过统计，5秒足够处理客户的数据了，数据SLA已达到6个9要求[99.9999%]，再长的等待时间会导致资源浪费)</li></ul><h3 id="消费者宕机时的数据处理"><a href="#消费者宕机时的数据处理" class="headerlink" title="消费者宕机时的数据处理"></a>消费者宕机时的数据处理</h3><p>如果消费者在处理数据(已拉取，发送uuid响应，但没写入最终结果)时宕机，系统会自动将其数据重新投入队列，其触发流程如下：</p><ol><li>消费者在处理数据时，发送了成功请求至 DATASTORE_PROJECT_REG_RESP，但未完成最终数据处理。</li><li>NATS JetStream在 <strong>5秒</strong> 后未收到ACK时，将该消息重新放回队列。</li></ol><hr><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>通过上述设计，我们项目组实现了一个高效、动态的消息队列负载均衡系统。</p><p>它能够在生产者和消费者数量变化时，自动调整分区分配，确保负载均衡，并且在消费者宕机时，能够保证数据的一致性和完整性。</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2025/06/27/%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AAnats%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E7%B3%BB%E7%BB%9F/</id>
    <link href="https://www.a-enjoy.top/2025/06/27/%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AAnats%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E7%B3%BB%E7%BB%9F/"/>
    <published>2025-06-27T10:18:29.000Z</published>
    <summary>试着回答一下这个有趣的消费-生产问题吧:  这是一个使用消息队列解耦多个生产者和多个消费者的系统。 生产者生产的消息是有分区的(按project_id分区),分区也会动态分配。 消费者需要竞争消费消息,每个消费者可以消费多个分区。 对于一个分区,最多有不超过1/n个消费者在消费。 生产者和消费者根据性能和数据量,会在kubernetes中进行动态的扩缩容。  消息需要尽可能的被均匀地分配到消费者上</summary>
    <title>设计一个nats消息队列负载均衡系统</title>
    <updated>2025-06-29T08:07:43.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <content>
      <![CDATA[<p>最近有朋友问 我去哪了，为什么不更新文章了…</p><span id="more"></span><p>时光飞逝，转眼间已经2025年了，距离上次更新文章已经过去了一年有余。</p><p>在这一年里经历了很多事情，生活和工作上也有了新的挑战和成长。</p><p>虽然没有时间更新文章，但我一直在思考如何更好地分享我的一切。</p><p>这一年…</p><p>从南昌到杭州, 从杭州到北京, 从南到北, 走过喧嚣的大城市…</p><p>在福州平潭的小岛上, 领略惬意的海风</p><p>在南宁品鉴水果,与故友相见</p><p>在广州的珠江新城, 奔波于各大写字楼</p><p>我似乎老了, 但更多的是释然</p><p>祖国的大好河山还等着我们去探索,</p><p>放下眼前的苦楚吧,睁开眼拥抱世界</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2025/06/25/%E6%88%91%E5%8E%BB%E5%93%AA%E4%BA%86/</id>
    <link href="https://www.a-enjoy.top/2025/06/25/%E6%88%91%E5%8E%BB%E5%93%AA%E4%BA%86/"/>
    <published>2025-06-25T10:09:32.000Z</published>
    <summary>最近有朋友问 我去哪了，为什么不更新文章了…</summary>
    <title>我去哪了...</title>
    <updated>2025-06-25T10:38:19.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="vue" scheme="https://www.a-enjoy.top/tags/vue/"/>
    <category term="golang" scheme="https://www.a-enjoy.top/tags/golang/"/>
    <content>
      <![CDATA[<p>有时候我们需要需要实现通过浏览器选择文件获取文件信息而不上传文件, 这个时候JavaScript的内置api就足以完成需求. 但是,如果我们需要获取到文件的完整路径, 那么对不起, 由于为了安全避免xss攻击, 现代的浏览器仅能获取到一个虚假的路径或者根本无法获取(IE10之前的浏览器可以获取到完整路径).</p><p>如果我们是本地Web应用, 这个问题就可以得到解决.解决方案有Electron封装,或者提供一个运行在本地的Agent用于获取文件路径,再由前端选择文件提交至 Agent或后端, 如果选择前者,再由Agent将路径间接传递至后端.</p><p>本文分享使用Go作为后端,Vue作为前端的,一起运行在本地的Web应用解决方案.</p><p>(附完整代码)</p><span id="more"></span><h1 id="效果图"><a href="#效果图" class="headerlink" title="效果图"></a>效果图</h1><p><img src="/images/52.png" class="lazyload" data-srcset="/images/52.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p><img src="/images/53.png" class="lazyload" data-srcset="/images/53.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p><img src="/images/54.png" class="lazyload" data-srcset="/images/54.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><h1 id="我们的需求"><a href="#我们的需求" class="headerlink" title="我们的需求"></a>我们的需求</h1><p>后端需要获取到选择路径&#x3D;&gt;通过图形化&#x2F;命令行获取</p><p>图形化获取&#x3D;&gt;需要选择实现图形化的方案&#x3D;&gt;Web方案</p><p>Web需要获取本地路径&#x3D;&gt;通过Api获取&#x3D;&gt;方案行不通X 通过后端返回路径供前端选择,再由前端选择后提交回后端:行得通√</p><h1 id="后端设计"><a href="#后端设计" class="headerlink" title="后端设计"></a>后端设计</h1><h2 id="数据类型定义"><a href="#数据类型定义" class="headerlink" title="数据类型定义"></a>数据类型定义</h2><p>在前后端交互过程中,我们需要规定一个表单格式</p><p>前端向后端发出获取目录文件列表的请求体</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    id<span class="punctuation">:</span><span class="string">&quot;xxx&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    nowDir<span class="punctuation">:</span><span class="string">&quot;./&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>后端向前端返回的目录格式</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    File<span class="punctuation">:</span><span class="punctuation">[</span>name<span class="punctuation">:</span><span class="string">&quot;&quot;</span><span class="punctuation">,</span>time<span class="punctuation">:</span><span class="string">&quot;&quot;</span><span class="punctuation">,</span>isDir<span class="punctuation">:</span><span class="literal"><span class="keyword">false</span></span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  allowBack<span class="punctuation">:</span><span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  status<span class="punctuation">:</span><span class="string">&quot;selectSuccess/ok/fail&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h2><p>Golang-Gin</p><p>路由路径</p><p>api&#x2F;v1&#x2F;file&#x2F;xxx</p><p>Gin初始化和中间件设置</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">var</span> RouterGroupApp []CommonRouter</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">InitRouter</span><span class="params">()</span></span> &#123;</span><br><span class="line">  <span class="comment">// 初始化路由</span></span><br><span class="line">  global.Router = gin.Default()</span><br><span class="line">  global.Router.MaxMultipartMemory = <span class="number">50</span> &lt;&lt; <span class="number">20</span> <span class="comment">//设置最大上传文件大小50 Gb</span></span><br><span class="line">  global.Router.Use( <span class="comment">/*使用中间件*/</span> )</span><br><span class="line">  static()</span><br><span class="line">  <span class="comment">//路由组</span></span><br><span class="line">  RouterGroupApp = commonGroups()</span><br><span class="line">  PrivateGroup := global.Router.Group(<span class="string">&quot;/api/v1&quot;</span>)</span><br><span class="line">  <span class="keyword">for</span> _, router := <span class="keyword">range</span> RouterGroupApp &#123;</span><br><span class="line">    router.InitRouter(PrivateGroup)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">//启动</span></span><br><span class="line">  err := global.Router.Run(<span class="string">&quot;:18088&quot;</span>) <span class="comment">//设置侦听端口</span></span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    logrus.Errorln(<span class="string">&quot;Web Server start error, error Info is: &quot;</span>, err)</span><br><span class="line">    logrus.Info(<span class="string">&quot;Web Server will not start, please check the configuration or error Info.&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">static</span><span class="params">()</span></span> &#123; </span><br><span class="line">  <span class="comment">//设置静态资源</span></span><br><span class="line">  global.Router.Static(<span class="string">&quot;/static&quot;</span>, <span class="string">&quot;./res&quot;</span>)</span><br><span class="line">  global.Router.Static(<span class="string">&quot;/assets&quot;</span>, <span class="string">&quot;./res&quot;</span>)</span><br><span class="line">  global.Router.Static(<span class="string">&quot;/index.html&quot;</span>, <span class="string">&quot;./index.html&quot;</span>)</span><br><span class="line">  global.Router.GET(<span class="string">&quot;/favicon.ico&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(context *gin.Context)</span></span> &#123;</span><br><span class="line">    context.Redirect(<span class="number">302</span>, <span class="string">&quot;/static/favicon.ico&quot;</span>)</span><br><span class="line">  &#125;)</span><br><span class="line">  global.Router.LoadHTMLGlob(<span class="string">&quot;res/html/*.html&quot;</span>)</span><br><span class="line">  global.Router.NoRoute(<span class="function"><span class="keyword">func</span><span class="params">(c *gin.Context)</span></span> &#123;</span><br><span class="line">    c.HTML(http.StatusOK, <span class="string">&quot;404.html&quot;</span>, gin.H&#123;</span><br><span class="line">      <span class="string">&quot;title&quot;</span>: <span class="string">&quot;404&quot;</span>,</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>通过实现接口和对接口的调用快速初始化各路由组</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> CommonRouter <span class="keyword">interface</span> &#123;</span><br><span class="line">  InitRouter(Router *gin.RouterGroup)</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">commonGroups</span><span class="params">()</span></span> []CommonRouter &#123;</span><br><span class="line">  <span class="keyword">return</span> []CommonRouter&#123;</span><br><span class="line">    &amp;FileRouter&#123;&#125;,</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实现文件路由组功能:</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> FileRouter <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *FileRouter)</span></span> InitRouter(Router *gin.RouterGroup) &#123;</span><br><span class="line">  fileRouter := Router.Group(<span class="string">&quot;file&quot;</span>)</span><br><span class="line">  fileApi := v1.ApiGroupApp.FileApi</span><br><span class="line">  fileRouter.POST(<span class="string">&quot;/list&quot;</span>, fileApi.DirList)</span><br><span class="line">  fileRouter.POST(<span class="string">&quot;/select&quot;</span>, fileApi.FileSelect)</span><br><span class="line">  fileRouter.POST(<span class="string">&quot;/pwd&quot;</span>, fileApi.GetNowDir)</span><br><span class="line">  fileRouter.GET(<span class="string">&quot;/pwd&quot;</span>, fileApi.GetNowDir)</span><br><span class="line">  fileRouter.POST(<span class="string">&quot;/chdir&quot;</span>, fileApi.Chdir)</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>路由处理:</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> FileApi <span class="keyword">struct</span> &#123;</span><br><span class="line">  dir  <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span> <span class="comment">//id(task) path</span></span><br><span class="line">  init <span class="type">bool</span></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="params">(f *FileApi)</span></span> DirList(context *gin.Context) &#123; </span><br><span class="line">  <span class="comment">//列出目录信息</span></span><br><span class="line">  <span class="keyword">if</span> !f.init &#123;</span><br><span class="line">    f.dir = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)</span><br><span class="line">    f.init = <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">//解析请求体</span></span><br><span class="line">  id := context.PostForm(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">  nowDir := context.PostForm(<span class="string">&quot;nowDir&quot;</span>)</span><br><span class="line">  <span class="keyword">if</span> id == <span class="string">&quot;&quot;</span> || nowDir == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">    context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  _, ok := f.dir[id]</span><br><span class="line">  <span class="keyword">if</span> !ok &#123;</span><br><span class="line">    f.dir[id] = <span class="string">&quot;./&quot;</span> <span class="comment">//第一次访问</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    _, err := os.Stat(path.Join(f.dir[id], nowDir))</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">      <span class="comment">//路径不存在 不允许继续</span></span><br><span class="line">      context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;now dir not exist&quot;</span>&#125;)</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    f.dir[id] = path.Join(f.dir[id], nowDir)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">type</span> file <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name  <span class="type">string</span> <span class="string">`json:&quot;name&quot;`</span></span><br><span class="line">    IsDir <span class="type">bool</span>   <span class="string">`json:&quot;isDir&quot;`</span></span><br><span class="line">    Time  <span class="type">string</span> <span class="string">`json:&quot;time&quot;`</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">var</span> Files []file</span><br><span class="line">  files, _ := os.ReadDir(f.dir[id])</span><br><span class="line">  <span class="keyword">for</span> _, v := <span class="keyword">range</span> files &#123;</span><br><span class="line">    t, _ := v.Info()</span><br><span class="line">    Files = <span class="built_in">append</span>(Files, file&#123;v.Name(), v.IsDir(), t.ModTime().Format(<span class="string">&quot;2006-01-02 15:04:05&quot;</span>)&#125;)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">//todo 判断是否允许上一级目录</span></span><br><span class="line">  context.JSON(<span class="number">200</span>, gin.H&#123;<span class="string">&quot;File&quot;</span>: Files, <span class="string">&quot;allowBack&quot;</span>: <span class="literal">true</span>, <span class="string">&quot;status&quot;</span>: <span class="string">&quot;ok&quot;</span>&#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="params">(f *FileApi)</span></span> FileSelect(context *gin.Context) &#123;</span><br><span class="line">  <span class="comment">//目录或文件选择</span></span><br><span class="line">  <span class="comment">//&#123;id:&quot;&quot;, nowDir:&quot;&quot;,&#125;</span></span><br><span class="line">  <span class="keyword">if</span> !f.init &#123;</span><br><span class="line">    f.dir = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)</span><br><span class="line">    f.init = <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">  id := context.PostForm(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">  nowDir := context.PostForm(<span class="string">&quot;nowDir&quot;</span>)</span><br><span class="line">  <span class="keyword">if</span> id == <span class="string">&quot;&quot;</span> || nowDir == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">    context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  info, err := os.Stat(path.Join(f.dir[id], nowDir))</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="comment">//文件不存在(一般来说不应该有)</span></span><br><span class="line">    context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;file or dir not exist. fail&quot;</span>&#125;)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    _, err = os.Stat(path.Join(f.dir[id], nowDir, <span class="string">&quot;go.mod&quot;</span>))</span><br><span class="line">    mod := <span class="literal">false</span></span><br><span class="line">    <span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">      mod = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> !info.IsDir() || mod &#123;</span><br><span class="line">      <span class="comment">//</span></span><br><span class="line">      t := builder.UsingAuto(path.Join(f.dir[id], nowDir), <span class="string">&quot;build from web&quot;</span>)</span><br><span class="line">      <span class="keyword">if</span> <span class="built_in">len</span>(t) != <span class="number">0</span> &#123;</span><br><span class="line">        builder.Tasks[id] = t[<span class="number">0</span>]</span><br><span class="line">        logrus.Infoln(<span class="string">&quot;select success. Path:&quot;</span>, path.Join(f.dir[id], nowDir))</span><br><span class="line">        context.JSON(<span class="number">200</span>, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;selectSuccess.ID=&quot;</span> + id&#125;)</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail. Not Config File / Dir?&quot;</span>&#125;)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      f.DirList(context)</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></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="params">(f *FileApi)</span></span> GetNowDir(context *gin.Context) &#123;</span><br><span class="line">  <span class="keyword">if</span> !f.init &#123;</span><br><span class="line">    f.dir = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)</span><br><span class="line">    f.init = <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">var</span> id <span class="type">string</span></span><br><span class="line">  <span class="keyword">if</span> context.Request.Method == <span class="string">&quot;POST&quot;</span> &#123;</span><br><span class="line">    id = context.PostForm(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> context.Request.Method == <span class="string">&quot;GET&quot;</span> &#123;</span><br><span class="line">    id = context.Query(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> id == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">    context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  abs, err := filepath.Abs(f.dir[id])</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  context.JSON(<span class="number">200</span>, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;success&quot;</span>, <span class="string">&quot;dir&quot;</span>: abs&#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="params">(f *FileApi)</span></span> Chdir(context *gin.Context) &#123;</span><br><span class="line">  <span class="keyword">if</span> !f.init &#123;</span><br><span class="line">    f.dir = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)</span><br><span class="line">    f.init = <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">  id := context.PostForm(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">  nowDir := context.PostForm(<span class="string">&quot;nowDir&quot;</span>)</span><br><span class="line">  <span class="keyword">if</span> id == <span class="string">&quot;&quot;</span> || nowDir == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">    context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  t, _ := os.Getwd()</span><br><span class="line">  err := os.Chdir(nowDir)</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;PathError.&quot;</span>&#125;)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  os.Chdir(t)</span><br><span class="line">  <span class="comment">//valid dir</span></span><br><span class="line">  f.dir[id] = filepath.Dir(nowDir)</span><br><span class="line">  f.DirList(context)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="前端设计"><a href="#前端设计" class="headerlink" title="前端设计"></a>前端设计</h1><p>前端的实现:Vue3+ElementPlus</p><p>界面组成</p><blockquote><p>一个网页组件容器{</p><p>​ 顶端{目录显示框}</p><p>​ 主功能区{</p><p>​ 一个表格:展示路径</p><p>​ 表头:</p><p>​ 表格内容{</p><p>​ 列1:图标 列2:文件名 要求点击文件可以选择或者进入目录 列3:时间 列4:功能区</p><p>​ }</p><p>​ }</p><p>}</p></blockquote><h2 id="接口Api设计"><a href="#接口Api设计" class="headerlink" title="接口Api设计"></a>接口Api设计</h2><p>接口是前端用于访问后端的桥梁,适当的对接口进行封装可以方便各页面的调用,减少页面端的代码量,减少冗余.</p><p>src&#x2F;api&#x2F;axiosInstance.js 用于实现axios的各功能</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">&#x27;axios&#x27;</span></span><br><span class="line"><span class="keyword">let</span> <span class="title class_">Port</span>=<span class="string">&#x27;18088&#x27;</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">validateStatus</span>(<span class="params">status</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> status&lt;=<span class="number">500</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">API</span> = axios.<span class="title function_">create</span>(&#123;</span><br><span class="line">  <span class="attr">baseURL</span>:<span class="string">&#x27;http://localhost:&#x27;</span>+<span class="title class_">Port</span>, <span class="comment">//</span></span><br><span class="line">  <span class="attr">timeout</span>: <span class="number">2000</span>,                   <span class="comment">//ms</span></span><br><span class="line">  <span class="attr">withCredentials</span>:<span class="literal">true</span>,</span><br><span class="line">  validateStatus</span><br><span class="line">&#125;)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="variable constant_">API</span></span><br></pre></td></tr></table></figure><p>src&#x2F;api&#x2F;file.js 用于调用后端接口</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="variable constant_">API</span> <span class="keyword">from</span> <span class="string">&quot;./axiosInstance.js&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">DirList</span>(<span class="params">data</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable constant_">API</span>.<span class="title function_">post</span>(<span class="string">&#x27;/api/v1/file/list&#x27;</span>, data)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">FileSelect</span>(<span class="params">data</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable constant_">API</span>.<span class="title function_">post</span>(<span class="string">&#x27;/api/v1/file/select&#x27;</span>, data)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">Pwd</span>(<span class="params">data</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable constant_">API</span>.<span class="title function_">post</span>(<span class="string">&#x27;/api/v1/file/pwd&#x27;</span>, data)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">Chdir</span>(<span class="params">data</span>)&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable constant_">API</span>.<span class="title function_">post</span>(<span class="string">&#x27;/api/v1/file/chdir&#x27;</span>, data)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="页面内方法"><a href="#页面内方法" class="headerlink" title="页面内方法"></a>页面内方法</h2><p>适用于页面内的JavaScript方法</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> regex = <span class="regexp">/^[^.]+(\.yaml|\.json|\.yml)$/i</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="title function_">data</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">tableData</span>: <span class="title function_">ref</span>([]),</span><br><span class="line">      <span class="attr">loading</span>: <span class="title function_">ref</span>(<span class="literal">true</span>),</span><br><span class="line">      <span class="attr">dir</span>: <span class="title function_">ref</span>(<span class="string">&#x27;&#x27;</span>),</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="title function_">mounted</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">start</span>();</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">methods</span>: &#123;</span><br><span class="line">    <span class="title function_">flashTable</span>(<span class="params">data</span>) &#123; <span class="comment">//&#x27;id=xxx&amp;nowDir=./&#x27;</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">loading</span> = <span class="literal">true</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">tableData</span> = []</span><br><span class="line">      <span class="title class_">DirList</span>(data).<span class="title function_">then</span>(<span class="function"><span class="params">response</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (response.<span class="property">status</span> === <span class="number">200</span>) &#123;</span><br><span class="line">          <span class="keyword">let</span> data = response.<span class="property">data</span></span><br><span class="line">          <span class="keyword">if</span> (data.<span class="property">allowBack</span> === <span class="literal">true</span>) &#123;</span><br><span class="line">            <span class="variable language_">this</span>.<span class="property">tableData</span>.<span class="title function_">push</span>(&#123; <span class="attr">name</span>: <span class="string">&quot;../&quot;</span>, <span class="attr">time</span>: <span class="string">&quot;&quot;</span>, <span class="attr">isDir</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; data.<span class="property">File</span>.<span class="property">length</span>; i++) &#123;</span><br><span class="line">            <span class="variable language_">this</span>.<span class="property">tableData</span>.<span class="title function_">push</span>(&#123; <span class="attr">name</span>: data.<span class="property">File</span>[i].<span class="property">name</span>, <span class="attr">time</span>: data.<span class="property">File</span>[i].<span class="property">time</span>, <span class="attr">isDir</span>: data.<span class="property">File</span>[i].<span class="property">isDir</span> &#125;)</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">loading</span> = <span class="literal">false</span></span><br><span class="line">      &#125;).<span class="title function_">catch</span>(<span class="function"><span class="params">error</span> =&gt;</span> &#123;</span><br><span class="line">      &#125;)</span><br><span class="line">      <span class="title class_">Pwd</span>(data).<span class="title function_">then</span>(<span class="function"><span class="params">response</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (response.<span class="property">status</span> === <span class="number">200</span>) &#123;</span><br><span class="line">          <span class="comment">//console.log(response.data.dir)</span></span><br><span class="line">          <span class="variable language_">this</span>.<span class="property">dir</span> = response.<span class="property">data</span>.<span class="property">dir</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="title function_">select</span>(<span class="params">row</span>) &#123;</span><br><span class="line">      <span class="keyword">let</span> result = regex.<span class="title function_">test</span>(row.<span class="property">name</span>) || row.<span class="property">isDir</span></span><br><span class="line">      <span class="keyword">if</span> (!result) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;请选择json/yaml或目录&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">let</span> params = <span class="keyword">new</span> <span class="title class_">URLSearchParams</span>();</span><br><span class="line">      params.<span class="title function_">append</span>(<span class="string">&#x27;id&#x27;</span>, <span class="variable language_">this</span>.<span class="property">id</span>);</span><br><span class="line">      params.<span class="title function_">append</span>(<span class="string">&#x27;nowDir&#x27;</span>, row.<span class="property">name</span>);</span><br><span class="line">      <span class="title class_">FileSelect</span>(params).<span class="title function_">then</span>(<span class="function"><span class="params">response</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">//todo</span></span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="title function_">entry</span>(<span class="params">row</span>) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">loading</span> = <span class="literal">true</span></span><br><span class="line">      <span class="keyword">if</span> (row.<span class="property">isDir</span> === <span class="literal">true</span>) &#123;</span><br><span class="line">        <span class="keyword">let</span> params = <span class="keyword">new</span> <span class="title class_">URLSearchParams</span>();</span><br><span class="line">        params.<span class="title function_">append</span>(<span class="string">&#x27;id&#x27;</span>, <span class="variable language_">this</span>.<span class="property">id</span>);</span><br><span class="line">        params.<span class="title function_">append</span>(<span class="string">&#x27;nowDir&#x27;</span>, row.<span class="property">name</span>);</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">flashTable</span>(params);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">//todo 打开文件</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">loading</span> = <span class="literal">false</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="title function_">start</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="keyword">let</span> params = <span class="keyword">new</span> <span class="title class_">URLSearchParams</span>();</span><br><span class="line">      params.<span class="title function_">append</span>(<span class="string">&#x27;id&#x27;</span>, <span class="variable language_">this</span>.<span class="property">id</span>);</span><br><span class="line">      params.<span class="title function_">append</span>(<span class="string">&#x27;nowDir&#x27;</span>, <span class="string">&quot;./&quot;</span>);</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">flashTable</span>(params);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="title function_">enterClick</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="keyword">let</span> params = <span class="keyword">new</span> <span class="title class_">URLSearchParams</span>();</span><br><span class="line">      params.<span class="title function_">append</span>(<span class="string">&#x27;id&#x27;</span>, <span class="variable language_">this</span>.<span class="property">id</span>);</span><br><span class="line">      params.<span class="title function_">append</span>(<span class="string">&#x27;nowDir&#x27;</span>, <span class="variable language_">this</span>.<span class="property">dir</span>);</span><br><span class="line">      <span class="title class_">Chdir</span>(params)</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">flashTable</span>(params)</span><br><span class="line">    &#125;</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="Vue3变更注意"><a href="#Vue3变更注意" class="headerlink" title="Vue3变更注意:"></a>Vue3变更注意:</h3><p>从网上找到了很多Vue获取表格行内容的博文,但是都不是针对Vue3的.目前Vue3对表格行绑定做出了变更</p><p>我们需要在表格标记中添加 <strong>slot-scope&#x3D;”scope”</strong></p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">&lt;el-table v-loading=&quot;loading&quot; element-loading-text=&quot;数据正在加载中...&quot; :data=&quot;tableData&quot; border stripe</span><br><span class="line">    highlight-current-row slot-scope=&quot;scope&quot; style=&quot;width: 100%&quot; max-height=&quot;600px&quot;/&gt;</span><br></pre></td></tr></table></figure><p>然后才能在表格行中获取行.</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">&lt;el-table-column label=&quot;Type&quot; prop=&quot;isDir&quot; width=&quot;80&quot;&gt;</span><br><span class="line">          &lt;template v-slot=&quot;scope&quot;&gt;</span><br><span class="line">            &lt;el-icon v-if=&quot;scope.row.isDir&quot; size=&quot;40&quot;&gt;&lt;el-tooltip class=&quot;box-item&quot; effect=&quot;dark&quot; content=&quot;文件夹&quot;</span><br><span class="line">                placement=&quot;top&quot;&gt;</span><br><span class="line">                &lt;Folder /&gt;</span><br><span class="line">              &lt;/el-tooltip&gt;&lt;/el-icon&gt;</span><br><span class="line">            &lt;el-icon v-else size=&quot;40&quot;&gt;&lt;el-tooltip class=&quot;box-item&quot; effect=&quot;dark&quot; content=&quot;文件&quot; placement=&quot;top&quot;&gt;</span><br><span class="line">                &lt;Document /&gt;</span><br><span class="line">              &lt;/el-tooltip&gt; &lt;/el-icon&gt;</span><br><span class="line">          &lt;/template&gt;</span><br><span class="line">        &lt;/el-table-column&gt;</span><br></pre></td></tr></table></figure><h3 id="界面部分"><a href="#界面部分" class="headerlink" title="界面部分"></a>界面部分</h3><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">&lt;el-container&gt;</span><br><span class="line">  &lt;el-header&gt;&lt;el-input v-model=&quot;dir&quot; @keyup.enter=&quot;enterClick&quot; style=&quot;width: 480px&quot; placeholder=&quot;localDir&quot; readonly&gt;</span><br><span class="line">      &lt;el-button slot=&quot;append&quot; @click=&quot;enterClick&quot;&gt;Enter&lt;/el-button&gt;</span><br><span class="line">    &lt;/el-input&gt;&lt;/el-header&gt;</span><br><span class="line">  &lt;el-main&gt;&lt;el-table v-loading=&quot;loading&quot; element-loading-text=&quot;数据正在加载中...&quot; :data=&quot;tableData&quot; border stripe</span><br><span class="line">      highlight-current-row slot-scope=&quot;scope&quot; style=&quot;width: 100%&quot; max-height=&quot;600px&quot;&gt;</span><br><span class="line">      &lt;el-table-column label=&quot;Type&quot; prop=&quot;isDir&quot; width=&quot;80&quot;&gt;</span><br><span class="line">        &lt;template v-slot=&quot;scope&quot;&gt;</span><br><span class="line">          &lt;el-icon v-if=&quot;scope.row.isDir&quot; size=&quot;40&quot;&gt;&lt;el-tooltip class=&quot;box-item&quot; effect=&quot;dark&quot; content=&quot;文件夹&quot;</span><br><span class="line">              placement=&quot;top&quot;&gt;</span><br><span class="line">              &lt;Folder /&gt;</span><br><span class="line">            &lt;/el-tooltip&gt;&lt;/el-icon&gt;</span><br><span class="line">          &lt;el-icon v-else size=&quot;40&quot;&gt;&lt;el-tooltip class=&quot;box-item&quot; effect=&quot;dark&quot; content=&quot;文件&quot; placement=&quot;top&quot;&gt;</span><br><span class="line">              &lt;Document /&gt;</span><br><span class="line">            &lt;/el-tooltip&gt; &lt;/el-icon&gt;</span><br><span class="line">        &lt;/template&gt;</span><br><span class="line">      &lt;/el-table-column&gt;</span><br><span class="line">      &lt;el-table-column prop=&quot;name&quot; sortable label=&quot;文件名&quot; width=&quot;180&quot;&gt;</span><br><span class="line">        &lt;template v-slot=&quot;scope&quot;&gt;</span><br><span class="line">          &lt;el-button type=&quot;text&quot; @click=&quot;entry(scope.row)&quot;&gt;&#123;&#123; scope.row.name &#125;&#125;&lt;/el-button&gt;</span><br><span class="line">        &lt;/template&gt;</span><br><span class="line">      &lt;/el-table-column&gt;</span><br><span class="line">      &lt;el-table-column prop=&quot;time&quot; label=&quot;修改时间&quot; width=&quot;180&quot; /&gt;</span><br><span class="line">      &lt;el-table-column label=&quot;操作&quot; align=&quot;center&quot; width=&quot;180&quot;&gt;</span><br><span class="line">        　　&lt;template v-slot=&quot;scope&quot;&gt;</span><br><span class="line">          &lt;!--              &lt;el-button type=&quot;primary&quot; v-show=&quot;scope.row.isDir&quot; @click=&quot;entry(scope.row)&quot;&gt;进入&lt;/el-button&gt;--&gt;</span><br><span class="line">          &lt;el-button type=&quot;info&quot; @click=&quot;select(scope.row)&quot;&gt;选择&lt;/el-button&gt;</span><br><span class="line">          　　&lt;/template&gt;</span><br><span class="line">      &lt;/el-table-column&gt;</span><br><span class="line">    &lt;/el-table&gt;&lt;/el-main&gt;</span><br><span class="line">&lt;/el-container&gt;</span><br></pre></td></tr></table></figure><h1 id="完整代码参考"><a href="#完整代码参考" class="headerlink" title="完整代码参考:"></a>完整代码参考:</h1><h2 id="前端部分"><a href="#前端部分" class="headerlink" title="前端部分:"></a>前端部分:</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">&lt;script setup&gt;</span><br><span class="line">import &#123;</span><br><span class="line">  Document, Folder</span><br><span class="line">&#125; from &#x27;@element-plus/icons-vue&#x27;</span><br><span class="line">const props = defineProps(&#123;</span><br><span class="line">  ID: &quot;String&quot;</span><br><span class="line">&#125;)</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;el-container&gt;</span><br><span class="line">    &lt;el-header&gt;&lt;el-input v-model=&quot;dir&quot; @keyup.enter=&quot;enterClick&quot; style=&quot;width: 480px&quot; placeholder=&quot;localDir&quot; readonly&gt;</span><br><span class="line">        &lt;el-button slot=&quot;append&quot; @click=&quot;enterClick&quot;&gt;Enter&lt;/el-button&gt;</span><br><span class="line">      &lt;/el-input&gt;&lt;/el-header&gt;</span><br><span class="line">    &lt;el-main&gt;&lt;el-table v-loading=&quot;loading&quot; element-loading-text=&quot;数据正在加载中...&quot; :data=&quot;tableData&quot; border stripe</span><br><span class="line">        highlight-current-row slot-scope=&quot;scope&quot; style=&quot;width: 100%&quot; max-height=&quot;600px&quot;&gt;</span><br><span class="line">        &lt;el-table-column label=&quot;Type&quot; prop=&quot;isDir&quot; width=&quot;80&quot;&gt;</span><br><span class="line">          &lt;template v-slot=&quot;scope&quot;&gt;</span><br><span class="line">            &lt;el-icon v-if=&quot;scope.row.isDir&quot; size=&quot;40&quot;&gt;&lt;el-tooltip class=&quot;box-item&quot; effect=&quot;dark&quot; content=&quot;文件夹&quot;</span><br><span class="line">                placement=&quot;top&quot;&gt;</span><br><span class="line">                &lt;Folder /&gt;</span><br><span class="line">              &lt;/el-tooltip&gt;&lt;/el-icon&gt;</span><br><span class="line">            &lt;el-icon v-else size=&quot;40&quot;&gt;&lt;el-tooltip class=&quot;box-item&quot; effect=&quot;dark&quot; content=&quot;文件&quot; placement=&quot;top&quot;&gt;</span><br><span class="line">                &lt;Document /&gt;</span><br><span class="line">              &lt;/el-tooltip&gt; &lt;/el-icon&gt;</span><br><span class="line">          &lt;/template&gt;</span><br><span class="line">        &lt;/el-table-column&gt;</span><br><span class="line">        &lt;el-table-column prop=&quot;name&quot; sortable label=&quot;文件名&quot; width=&quot;180&quot;&gt;</span><br><span class="line">          &lt;template v-slot=&quot;scope&quot;&gt;</span><br><span class="line">            &lt;el-button type=&quot;text&quot; @click=&quot;entry(scope.row)&quot;&gt;&#123;&#123; scope.row.name &#125;&#125;&lt;/el-button&gt;</span><br><span class="line">          &lt;/template&gt;</span><br><span class="line">        &lt;/el-table-column&gt;</span><br><span class="line">        &lt;el-table-column prop=&quot;time&quot; label=&quot;修改时间&quot; width=&quot;180&quot; /&gt;</span><br><span class="line">        &lt;el-table-column label=&quot;操作&quot; align=&quot;center&quot; width=&quot;180&quot;&gt;</span><br><span class="line">          　　&lt;template v-slot=&quot;scope&quot;&gt;</span><br><span class="line">            &lt;!--              &lt;el-button type=&quot;primary&quot; v-show=&quot;scope.row.isDir&quot; @click=&quot;entry(scope.row)&quot;&gt;进入&lt;/el-button&gt;--&gt;</span><br><span class="line">            &lt;el-button type=&quot;info&quot; @click=&quot;select(scope.row)&quot;&gt;选择&lt;/el-button&gt;</span><br><span class="line">            　　&lt;/template&gt;</span><br><span class="line">        &lt;/el-table-column&gt;</span><br><span class="line">      &lt;/el-table&gt;&lt;/el-main&gt;</span><br><span class="line">  &lt;/el-container&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;style scoped&gt;&lt;/style&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line">import &#123; ref &#125; from &quot;vue&quot;;</span><br><span class="line">import &#123; DirList, FileSelect, Pwd &#125; from &quot;../api/file.js&quot;;</span><br><span class="line">const regex = /^[^.]+(\.yaml|\.json|\.yml)$/i;</span><br><span class="line">export default &#123;</span><br><span class="line">  data() &#123;</span><br><span class="line">    return &#123;</span><br><span class="line">      tableData: ref([]),</span><br><span class="line">      loading: ref(true),</span><br><span class="line">      dir: ref(&#x27;&#x27;),</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  mounted() &#123;</span><br><span class="line">    this.start();</span><br><span class="line">  &#125;,</span><br><span class="line">  methods: &#123;</span><br><span class="line">    flashTable(data) &#123; //&#x27;id=xxx&amp;nowDir=./&#x27;</span><br><span class="line">      this.loading = true</span><br><span class="line">      this.tableData = []</span><br><span class="line">      DirList(data).then(response =&gt; &#123;</span><br><span class="line">        if (response.status === 200) &#123;</span><br><span class="line">          let data = response.data</span><br><span class="line">          //console.log(data)</span><br><span class="line">          if (data.allowBack === true) &#123;</span><br><span class="line">            this.tableData.push(&#123; name: &quot;../&quot;, time: &quot;&quot;, isDir: true &#125;)</span><br><span class="line">          &#125;</span><br><span class="line">          for (let i = 0; i &lt; data.File.length; i++) &#123;</span><br><span class="line">            this.tableData.push(&#123; name: data.File[i].name, time: data.File[i].time, isDir: data.File[i].isDir &#125;)</span><br><span class="line">          &#125;</span><br><span class="line">          //console.log(&quot;1&quot;)</span><br><span class="line">          //File:[] ,allowBack:bool,status</span><br><span class="line">        &#125;</span><br><span class="line">        this.loading = false</span><br><span class="line">      &#125;).catch(error =&gt; &#123;</span><br><span class="line">      &#125;)</span><br><span class="line">      Pwd(data).then(response =&gt; &#123;</span><br><span class="line">        if (response.status === 200) &#123;</span><br><span class="line">          //console.log(response.data.dir)</span><br><span class="line">          this.dir = response.data.dir</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;,</span><br><span class="line">    select(row) &#123;</span><br><span class="line">      let result = regex.test(row.name) || row.isDir</span><br><span class="line">      if (!result) &#123;</span><br><span class="line">        console.log(&quot;请选择json/yaml或目录&quot;)</span><br><span class="line">        return</span><br><span class="line">      &#125;</span><br><span class="line">      let params = new URLSearchParams();</span><br><span class="line">      params.append(&#x27;id&#x27;, this.id);</span><br><span class="line">      params.append(&#x27;nowDir&#x27;, row.name);</span><br><span class="line">      FileSelect(params).then(response =&gt; &#123;</span><br><span class="line">        //todo</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;,</span><br><span class="line">    entry(row) &#123;</span><br><span class="line">      this.loading = true</span><br><span class="line">      if (row.isDir === true) &#123;</span><br><span class="line">        let params = new URLSearchParams();</span><br><span class="line">        params.append(&#x27;id&#x27;, this.id);</span><br><span class="line">        params.append(&#x27;nowDir&#x27;, row.name);</span><br><span class="line">        this.flashTable(params);</span><br><span class="line">      &#125; else &#123;</span><br><span class="line">        //todo 打开文件</span><br><span class="line">      &#125;</span><br><span class="line">      this.loading = false</span><br><span class="line">    &#125;,</span><br><span class="line">    start() &#123;</span><br><span class="line">      let params = new URLSearchParams();</span><br><span class="line">      params.append(&#x27;id&#x27;, this.id);</span><br><span class="line">      params.append(&#x27;nowDir&#x27;, &quot;./&quot;);</span><br><span class="line">      this.flashTable(params);</span><br><span class="line">    &#125;,</span><br><span class="line">    enterClick() &#123;</span><br><span class="line">      let params = new URLSearchParams();</span><br><span class="line">      params.append(&#x27;id&#x27;, this.id);</span><br><span class="line">      params.append(&#x27;nowDir&#x27;, this.dir);</span><br><span class="line">      Chdir(params)</span><br><span class="line">      this.flashTable(params)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><p>以及上面的两个JavaScript文件</p><h2 id="后端部分"><a href="#后端部分" class="headerlink" title="后端部分"></a>后端部分</h2><p>app&#x2F;api&#x2F;v1&#x2F;entry.go</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> v1</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> ApiGroup <span class="keyword">struct</span> &#123;</span><br><span class="line">    FileApi</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> ApiGroupApp = <span class="built_in">new</span>(ApiGroup)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>app&#x2F;api&#x2F;v1&#x2F;file.go</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> v1</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;github.com/aenjoy/BuilderX-go/app/builder&quot;</span></span><br><span class="line">    <span class="string">&quot;github.com/gin-gonic/gin&quot;</span></span><br><span class="line">    <span class="string">&quot;github.com/sirupsen/logrus&quot;</span></span><br><span class="line">    <span class="string">&quot;net/http&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line">    <span class="string">&quot;path&quot;</span></span><br><span class="line">    <span class="string">&quot;path/filepath&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> FileApi <span class="keyword">struct</span> &#123;</span><br><span class="line">    dir  <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span> <span class="comment">//id(task) path</span></span><br><span class="line">    init <span class="type">bool</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//curl http://localhost:18088/api/v1/file/select  -X POST -d &#x27;id=xxx&amp;nowDir=./&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(f *FileApi)</span></span> DirList(context *gin.Context) &#123; <span class="comment">//目录</span></span><br><span class="line">    <span class="keyword">if</span> !f.init &#123;</span><br><span class="line">       f.dir = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)</span><br><span class="line">       f.init = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//POST</span></span><br><span class="line">    <span class="comment">//&#123;id:&quot;&quot;, nowDir:&quot;&quot;,&#125; id用于记录当前访问时的路径</span></span><br><span class="line">    <span class="comment">//outPut &#123;File:[</span></span><br><span class="line">    <span class="comment">//name:&quot;&quot;,</span></span><br><span class="line">    <span class="comment">//time:&quot;&quot;,</span></span><br><span class="line">    <span class="comment">//isDir:false,</span></span><br><span class="line">    <span class="comment">//],</span></span><br><span class="line">    <span class="comment">//allowBack:true,</span></span><br><span class="line">    <span class="comment">//status:&quot;selectSuccess/ok/fail&quot;,</span></span><br><span class="line">    <span class="comment">//</span></span><br><span class="line">    <span class="comment">//&#125;</span></span><br><span class="line">    id := context.PostForm(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">    nowDir := context.PostForm(<span class="string">&quot;nowDir&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> id == <span class="string">&quot;&quot;</span> || nowDir == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">       context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">       <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    _, ok := f.dir[id]</span><br><span class="line">    <span class="keyword">if</span> !ok &#123;</span><br><span class="line">       f.dir[id] = <span class="string">&quot;./&quot;</span> <span class="comment">//第一次访问</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">       _, err := os.Stat(path.Join(f.dir[id], nowDir))</span><br><span class="line">       <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">          <span class="comment">//路径不存在 不允许继续</span></span><br><span class="line">          context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;now dir not exist&quot;</span>&#125;)</span><br><span class="line">          <span class="keyword">return</span></span><br><span class="line">       &#125;</span><br><span class="line">       f.dir[id] = path.Join(f.dir[id], nowDir)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">type</span> file <span class="keyword">struct</span> &#123;</span><br><span class="line">       Name  <span class="type">string</span> <span class="string">`json:&quot;name&quot;`</span></span><br><span class="line">       IsDir <span class="type">bool</span>   <span class="string">`json:&quot;isDir&quot;`</span></span><br><span class="line">       Time  <span class="type">string</span> <span class="string">`json:&quot;time&quot;`</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">var</span> Files []file</span><br><span class="line">    files, _ := os.ReadDir(f.dir[id])</span><br><span class="line">    <span class="keyword">for</span> _, v := <span class="keyword">range</span> files &#123;</span><br><span class="line">       t, _ := v.Info()</span><br><span class="line">       Files = <span class="built_in">append</span>(Files, file&#123;v.Name(), v.IsDir(), t.ModTime().Format(<span class="string">&quot;2006-01-02 15:04:05&quot;</span>)&#125;)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//todo 判断是否允许上一级目录</span></span><br><span class="line">    context.JSON(<span class="number">200</span>, gin.H&#123;<span class="string">&quot;File&quot;</span>: Files, <span class="string">&quot;allowBack&quot;</span>: <span class="literal">true</span>, <span class="string">&quot;status&quot;</span>: <span class="string">&quot;ok&quot;</span>&#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="params">(f *FileApi)</span></span> FileSelect(context *gin.Context) &#123;</span><br><span class="line">    <span class="comment">//目录或文件选择</span></span><br><span class="line">    <span class="comment">//&#123;id:&quot;&quot;, nowDir:&quot;&quot;,&#125;</span></span><br><span class="line">    <span class="keyword">if</span> !f.init &#123;</span><br><span class="line">       f.dir = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)</span><br><span class="line">       f.init = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">    id := context.PostForm(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">    nowDir := context.PostForm(<span class="string">&quot;nowDir&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> id == <span class="string">&quot;&quot;</span> || nowDir == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">       context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">       <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    info, err := os.Stat(path.Join(f.dir[id], nowDir))</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">       <span class="comment">//文件不存在(一般来说不应该有)</span></span><br><span class="line">       context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;file or dir not exist. fail&quot;</span>&#125;)</span><br><span class="line">       <span class="keyword">return</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">       _, err = os.Stat(path.Join(f.dir[id], nowDir, <span class="string">&quot;go.mod&quot;</span>))</span><br><span class="line">       mod := <span class="literal">false</span></span><br><span class="line">       <span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">          mod = <span class="literal">true</span></span><br><span class="line">       &#125;</span><br><span class="line">       <span class="keyword">if</span> !info.IsDir() || mod &#123;</span><br><span class="line">          <span class="comment">//</span></span><br><span class="line">          t := builder.UsingAuto(path.Join(f.dir[id], nowDir), <span class="string">&quot;build from web&quot;</span>)</span><br><span class="line">          <span class="keyword">if</span> <span class="built_in">len</span>(t) != <span class="number">0</span> &#123;</span><br><span class="line">             builder.Tasks[id] = t[<span class="number">0</span>]</span><br><span class="line">             logrus.Infoln(<span class="string">&quot;select success. Path:&quot;</span>, path.Join(f.dir[id], nowDir))</span><br><span class="line">             context.JSON(<span class="number">200</span>, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;selectSuccess.ID=&quot;</span> + id&#125;)</span><br><span class="line">          &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">             context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail. Not Config File / Dir?&quot;</span>&#125;)</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">return</span></span><br><span class="line">       &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          f.DirList(context)</span><br><span class="line">          <span class="keyword">return</span></span><br><span class="line">       &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></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="params">(f *FileApi)</span></span> GetNowDir(context *gin.Context) &#123;</span><br><span class="line">    <span class="keyword">if</span> !f.init &#123;</span><br><span class="line">       f.dir = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)</span><br><span class="line">       f.init = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">var</span> id <span class="type">string</span></span><br><span class="line">    <span class="keyword">if</span> context.Request.Method == <span class="string">&quot;POST&quot;</span> &#123;</span><br><span class="line">       id = context.PostForm(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> context.Request.Method == <span class="string">&quot;GET&quot;</span> &#123;</span><br><span class="line">       id = context.Query(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> id == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">       context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">       <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    abs, err := filepath.Abs(f.dir[id])</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">       context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">       <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    context.JSON(<span class="number">200</span>, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;success&quot;</span>, <span class="string">&quot;dir&quot;</span>: abs&#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="params">(f *FileApi)</span></span> Chdir(context *gin.Context) &#123;</span><br><span class="line">    <span class="keyword">if</span> !f.init &#123;</span><br><span class="line">       f.dir = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)</span><br><span class="line">       f.init = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">    id := context.PostForm(<span class="string">&quot;id&quot;</span>)</span><br><span class="line">    nowDir := context.PostForm(<span class="string">&quot;nowDir&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> id == <span class="string">&quot;&quot;</span> || nowDir == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">       context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;fail&quot;</span>&#125;)</span><br><span class="line">       <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    t, _ := os.Getwd()</span><br><span class="line">    err := os.Chdir(nowDir)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">       context.JSON(http.StatusBadRequest, gin.H&#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;PathError.&quot;</span>&#125;)</span><br><span class="line">       <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    os.Chdir(t)</span><br><span class="line">    <span class="comment">//valid dir</span></span><br><span class="line">    f.dir[id] = filepath.Dir(nowDir)</span><br><span class="line">    f.DirList(context)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>路由部分</p><p>route&#x2F;init.go</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> router</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;github.com/aenjoy/BuilderX-go/global&quot;</span></span><br><span class="line">    <span class="string">&quot;github.com/gin-gonic/gin&quot;</span></span><br><span class="line">    <span class="string">&quot;github.com/sirupsen/logrus&quot;</span></span><br><span class="line">    <span class="string">&quot;net/http&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> RouterGroupApp []CommonRouter</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">InitRouter</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="comment">// 初始化路由</span></span><br><span class="line">    global.Router = gin.Default()</span><br><span class="line">    global.Router.MaxMultipartMemory = <span class="number">50</span> &lt;&lt; <span class="number">20</span> <span class="comment">//50 Gb</span></span><br><span class="line">    global.Router.Use(<span class="function"><span class="keyword">func</span><span class="params">(c *gin.Context)</span></span> &#123;</span><br><span class="line">       method := c.Request.Method</span><br><span class="line">       origin := c.Request.Header.Get(<span class="string">&quot;Origin&quot;</span>) <span class="comment">//请求头部</span></span><br><span class="line">       <span class="keyword">if</span> origin != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">          <span class="comment">//接收客户端发送的origin （重要！）</span></span><br><span class="line">          <span class="comment">//Access-Control-Allow-Origin是必须的,他的值要么是请求Origin字段的值,要么是一个*, 表示接受任意域名的请求</span></span><br><span class="line">          c.Writer.Header().Set(<span class="string">&quot;Access-Control-Allow-Origin&quot;</span>, origin)</span><br><span class="line">          <span class="comment">//服务器支持的所有跨域请求的方法</span></span><br><span class="line">          c.Header(<span class="string">&quot;Access-Control-Allow-Methods&quot;</span>, <span class="string">&quot;POST, GET, PUT, DELETE,UPDATE&quot;</span>)</span><br><span class="line">          <span class="comment">//允许跨域设置可以返回其他子段，可以自定义字段</span></span><br><span class="line">          <span class="comment">//该字段可选。CORS请求时，XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段：Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。</span></span><br><span class="line">          <span class="comment">//如果想拿到其他字段，就必须在Access-Control-Expose-Headers里面指定。上面的例子指定，getResponseHeader(&#x27;FooBar&#x27;)可以返回FooBar字段的值。</span></span><br><span class="line">          c.Header(<span class="string">&quot;Access-Control-Allow-Headers&quot;</span>, <span class="string">&quot;Authorization, Content-Length, X-CSRF-Token, Token, session, IOMAuth&quot;</span>)</span><br><span class="line">          <span class="comment">// 允许浏览器（客户端）可以解析的头部 （重要）</span></span><br><span class="line">          c.Header(<span class="string">&quot;Access-Control-Expose-Headers&quot;</span>, <span class="string">&quot;Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers&quot;</span>)</span><br><span class="line">          <span class="comment">//设置缓存时间</span></span><br><span class="line">          <span class="comment">//该字段可选，用来指定本次预检请求的有效期，单位为秒。有效期是20天（1728000秒），即允许缓存该条回应1728000秒（即20天），在此期间，不用发出另一条预检请求。</span></span><br><span class="line">          c.Header(<span class="string">&quot;Access-Control-Max-Age&quot;</span>, <span class="string">&quot;172800&quot;</span>)</span><br><span class="line">          <span class="comment">//允许客户端传递校验信息比如 cookie (重要)</span></span><br><span class="line">          c.Header(<span class="string">&quot;Access-Control-Allow-Credentials&quot;</span>, <span class="string">&quot;true&quot;</span>)</span><br><span class="line">       &#125;</span><br><span class="line">       <span class="comment">//允许类型校验</span></span><br><span class="line">       <span class="keyword">if</span> method == <span class="string">&quot;OPTIONS&quot;</span> &#123;</span><br><span class="line">          c.JSON(http.StatusOK, <span class="string">&quot;ok!&quot;</span>)</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">          <span class="keyword">if</span> err := <span class="built_in">recover</span>(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">             logrus.Panic(<span class="string">&quot;Panic info is: %v&quot;</span>, err)</span><br><span class="line">          &#125;</span><br><span class="line">       &#125;()</span><br><span class="line"></span><br><span class="line">       c.Next()</span><br><span class="line">    &#125;)</span><br><span class="line">    static()</span><br><span class="line">    <span class="comment">//路由组</span></span><br><span class="line">    RouterGroupApp = commonGroups()</span><br><span class="line">    PrivateGroup := global.Router.Group(<span class="string">&quot;/api/v1&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, router := <span class="keyword">range</span> RouterGroupApp &#123;</span><br><span class="line">       router.InitRouter(PrivateGroup)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//启动</span></span><br><span class="line">    err := global.Router.Run(<span class="string">&quot;:&quot;</span> + global.WebPort)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">       logrus.Errorln(<span class="string">&quot;Web Server start error, error Info is: &quot;</span>, err)</span><br><span class="line">       logrus.Info(<span class="string">&quot;Web Server will not start, please check the configuration or error Info.&quot;</span>)</span><br><span class="line">       <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">static</span><span class="params">()</span></span> &#123;</span><br><span class="line">    global.Router.Static(<span class="string">&quot;/static&quot;</span>, <span class="string">&quot;./res&quot;</span>)</span><br><span class="line">    global.Router.Static(<span class="string">&quot;/assets&quot;</span>, <span class="string">&quot;./res&quot;</span>)</span><br><span class="line">    global.Router.Static(<span class="string">&quot;/index.html&quot;</span>, <span class="string">&quot;./index.html&quot;</span>)</span><br><span class="line">    global.Router.GET(<span class="string">&quot;/favicon.ico&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(context *gin.Context)</span></span> &#123;</span><br><span class="line">       context.Redirect(<span class="number">302</span>, <span class="string">&quot;/static/favicon.ico&quot;</span>)</span><br><span class="line">    &#125;)</span><br><span class="line">    global.Router.LoadHTMLGlob(<span class="string">&quot;res/html/*.html&quot;</span>)</span><br><span class="line">    global.Router.NoRoute(<span class="function"><span class="keyword">func</span><span class="params">(c *gin.Context)</span></span> &#123;</span><br><span class="line">       c.HTML(http.StatusOK, <span class="string">&quot;404.html&quot;</span>, gin.H&#123;</span><br><span class="line">          <span class="string">&quot;title&quot;</span>: <span class="string">&quot;404&quot;</span>,</span><br><span class="line">       &#125;)</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>route&#x2F;common.go</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">package router</span><br><span class="line"></span><br><span class="line">import (</span><br><span class="line">    &quot;github.com/gin-gonic/gin&quot;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">type CommonRouter interface &#123;</span><br><span class="line">    InitRouter(Router *gin.RouterGroup)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">func commonGroups() []CommonRouter &#123;</span><br><span class="line">    return []CommonRouter&#123;</span><br><span class="line">       &amp;FileRouter&#123;&#125;,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>route&#x2F;ro_file.go</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> router</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    v1 <span class="string">&quot;github.com/aenjoy/BuilderX-go/app/api/v1&quot;</span></span><br><span class="line">    <span class="string">&quot;github.com/gin-gonic/gin&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> FileRouter <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *FileRouter)</span></span> InitRouter(Router *gin.RouterGroup) &#123;</span><br><span class="line">    fileRouter := Router.Group(<span class="string">&quot;file&quot;</span>)</span><br><span class="line">    fileApi := v1.ApiGroupApp.FileApi</span><br><span class="line">    fileRouter.POST(<span class="string">&quot;/list&quot;</span>, fileApi.DirList)</span><br><span class="line">    fileRouter.POST(<span class="string">&quot;/select&quot;</span>, fileApi.FileSelect)</span><br><span class="line">    fileRouter.POST(<span class="string">&quot;/pwd&quot;</span>, fileApi.GetNowDir)</span><br><span class="line">    fileRouter.GET(<span class="string">&quot;/pwd&quot;</span>, fileApi.GetNowDir)</span><br><span class="line">    fileRouter.POST(<span class="string">&quot;/chdir&quot;</span>, fileApi.Chdir)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>完整代码请访问:<a href="https://github.com/aenjoy/BuilderX-go">https://github.com/aenjoy/BuilderX-go</a></p>]]>
    </content>
    <id>https://www.a-enjoy.top/2024/03/13/%E6%9C%AC%E5%9C%B0Web%E5%BA%94%E7%94%A8-%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6-%E7%9B%AE%E5%BD%95%E9%80%89%E6%8B%A9%E5%99%A8/</id>
    <link href="https://www.a-enjoy.top/2024/03/13/%E6%9C%AC%E5%9C%B0Web%E5%BA%94%E7%94%A8-%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6-%E7%9B%AE%E5%BD%95%E9%80%89%E6%8B%A9%E5%99%A8/"/>
    <published>2024-03-13T14:34:07.000Z</published>
    <summary>有时候我们需要需要实现通过浏览器选择文件获取文件信息而不上传文件, 这个时候JavaScript的内置api就足以完成需求. 但是,如果我们需要获取到文件的完整路径, 那么对不起, 由于为了安全避免xss攻击, 现代的浏览器仅能获取到一个虚假的路径或者根本无法获取(IE10之前的浏览器可以获取到完整路径). 如果我们是本地Web应用, 这个问题就可以得到解决.解决方案有Electron封装,或者</summary>
    <title>本地Web应用-实现文件/目录选择器-获取文件完整路径</title>
    <updated>2024-03-13T15:54:09.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="golang" scheme="https://www.a-enjoy.top/tags/golang/"/>
    <content>
      <![CDATA[<p>本文介绍golang中的返回值有一个语法糖,可以帮助我们减少代码量;</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">loadConfigYaml</span><span class="params">(f <span class="type">string</span>)</span></span> (defaultConfig yamlConfig)</span><br></pre></td></tr></table></figure><p>同时还介绍func (c *Struct)FunName与func (c Struct)FunName区别</p><span id="more"></span><h1 id="golang返回值变量"><a href="#golang返回值变量" class="headerlink" title="golang返回值变量:"></a>golang返回值变量:</h1><p>golang可以将欲返回的变量值写在函数末尾,并作为一个作用域为函数体内的变量</p><p>例如:</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">loadConfigYaml</span><span class="params">(f <span class="type">string</span>)</span></span> (defaultConfig yamlConfig) &#123;</span><br><span class="line">    file, err := os.Open(f)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">       fmt.Println(<span class="string">&quot;Error opening file:&quot;</span>, err)</span><br><span class="line">       <span class="keyword">return</span> yamlConfig&#123;&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">defer</span> file.Close()</span><br><span class="line">    decoder := yaml.NewDecoder(file)</span><br><span class="line">    err = decoder.Decode(&amp;defaultConfig)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此时,我们在函数的末尾使用return时可以不带变量名(返回的值).</p><p>这是常规写法,也是大多数编程语言的思维 :定义一个返回内容的变量,赋值后返回</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">loadConfigYaml</span><span class="params">(f <span class="type">string</span>)</span></span> yamlConfig &#123;</span><br><span class="line">    <span class="keyword">var</span> defaultConfig yamlConfig</span><br><span class="line">    file, err := os.Open(f)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">       fmt.Println(<span class="string">&quot;Error opening file:&quot;</span>, err)</span><br><span class="line">        <span class="keyword">return</span> yamlConfig&#123;&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">defer</span> file.Close()</span><br><span class="line">    decoder := yaml.NewDecoder(file)</span><br><span class="line">    err = decoder.Decode(&amp;defaultConfig)</span><br><span class="line">    <span class="keyword">return</span> defaultConfig</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="func-后接的括号里带-与不带-的区别"><a href="#func-后接的括号里带-与不带-的区别" class="headerlink" title="func 后接的括号里带*与不带*的区别"></a>func 后接的括号里带*与不带*的区别</h1><p>在golang中,func 后接一个括号代表是这个结构体(接口)的方法,类似于C++的 classA::func(string a);</p><p>即func (b className)func(a string)&lt;&#x3D;&#x3D;&gt;classA::func(string a);</p><p>其中golang代码中的b指代这个结构体对象,类似于Python中的self,c++&#x2F;java的this</p><p>带*表示传递这个对象中的一切通过指针传递,最常用的特性就是可以修改值;不带*表示使用值传递方式,<strong>如果要修改结构体中属性的值,则不会生效.</strong></p><figure class="highlight go"><table><tr><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">type</span> myStruct <span class="keyword">struct</span> &#123;</span><br><span class="line">  a <span class="type">string</span></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="params">(s myStruct)</span></span> setA1(a <span class="type">string</span>) &#123;</span><br><span class="line">  s.a = a</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s myStruct)</span></span> readA1() &#123;</span><br><span class="line">  <span class="built_in">println</span>(s.a)</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *myStruct)</span></span> setA2(a <span class="type">string</span>) &#123;</span><br><span class="line">  s.a = a</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *myStruct)</span></span> readA2() &#123;</span><br><span class="line">  <span class="built_in">println</span>(s.a)</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">  s := myStruct&#123;&#125;</span><br><span class="line">    s.a = <span class="string">&quot;dddd&quot;</span></span><br><span class="line">  s.setA1(<span class="string">&quot;hello&quot;</span>)</span><br><span class="line">  s.readA1() <span class="comment">//将不会输出 hello , 而是输出dddd</span></span><br><span class="line">  s.setA2(<span class="string">&quot;hello2&quot;</span>)</span><br><span class="line">  s.readA2() <span class="comment">//输出 hello2</span></span><br><span class="line">  s.readA1() <span class="comment">//输出 hello2</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>执行代码:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">$ go run main.go</span><br><span class="line">dddd</span><br><span class="line">hello2</span><br><span class="line">hello2</span><br></pre></td></tr></table></figure><p>在go中,并不推荐同时使用func (s myStruct) 与func (s *myStruct),这两种选其一即可.</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2024/02/22/golang%E5%B0%8F%E6%8A%80%E5%B7%A7-%E8%BF%94%E5%9B%9E%E5%80%BC%E7%AE%80%E5%86%99-func-c-Struct-FunName%E4%B8%8Efunc-c-Struct-FunName%E5%8C%BA%E5%88%AB/</id>
    <link href="https://www.a-enjoy.top/2024/02/22/golang%E5%B0%8F%E6%8A%80%E5%B7%A7-%E8%BF%94%E5%9B%9E%E5%80%BC%E7%AE%80%E5%86%99-func-c-Struct-FunName%E4%B8%8Efunc-c-Struct-FunName%E5%8C%BA%E5%88%AB/"/>
    <published>2024-02-22T09:13:38.000Z</published>
    <summary>本文介绍golang中的返回值有一个语法糖,可以帮助我们减少代码量; 1func loadConfigYaml(f string) (defaultConfig yamlConfig)  同时还介绍func (c *Struct)FunName与func (c Struct)FunName区别</summary>
    <title>golang小技巧:返回值简写,func (c *Struct)FunName与func (c Struct)FunName区别</title>
    <updated>2024-02-22T10:40:55.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="network" scheme="https://www.a-enjoy.top/tags/network/"/>
    <content>
      <![CDATA[<p>使用最新的nginx主线版本1.25，给网站开启quic支持。</p><p>本文不探讨手动编译，而是使用主线安装直接启用quic特性</p><p>环境：Debian12+nginx1.25</p><span id="more"></span><h1 id="HTTP-3-QUIC介绍"><a href="#HTTP-3-QUIC介绍" class="headerlink" title="HTTP 3&#x2F;QUIC介绍"></a>HTTP 3&#x2F;QUIC介绍</h1><p>QUIC&#x2F;HTTP3 (Quick UDP Internet Connections), 快速 UDP 互联网连接，由Google在2015年提出的SPDY v3演化而来的新协议，传统的HTTP协议是基于传输层TCP的协议，而QUIC是基于传输层UDP上的协议，可以定义成：HTTP3.0基于UDP的安全可靠的HTTP2.0协议。 QUIC是基于UDP协议的，与传统的HTTP1.1&#x2F;HTTP2不兼容，当浏览器不支持quic&#x2F;http3时，会自动尝试使用http2、http1.1连接。</p><p>HTTP&#x2F;3 基于 QUIC 传输协议，该协议专门用于支持多路复用连接，并摆脱了对单个 TCP 连接的依赖。QUIC 使用 UDP 作为在客户端和服务器之间的数据包底层传输机制，并为HTTP 请求提供可靠连接。值得注意的是，从HTTP2起，包括HTTP3&#x2F;QUIC ，都将 TLS作为必选项（即强制加密），如果站点不支持ssl&#x2F;tls，那就只能回到http1.1了。</p><p>主要特性：</p><ul><li>基于UDP减少了TCP三次握手及TLS握手时间</li><li>解决多路复用丢包时的线头阻塞问题</li><li>优化重传策略</li><li>流量控制</li><li>连接迁移</li></ul><h1 id="安装主线nginx1-25版本"><a href="#安装主线nginx1-25版本" class="headerlink" title="安装主线nginx1.25版本"></a>安装主线nginx1.25版本</h1><p>由于各大发行版（截止2024年1月9日，我测试了archLinux，openSUSE，Fedora，Debian，ubuntu23.10）目前还没有将1.25合并进存储库，因此我们需要手动添加1.25版nginx源.</p><p>参考<a href="https://nginx.org/en/linux_packages.html#instructions">nginx: Linux packages</a></p><p>本机环境：Debian 12</p><p>其它发行版操作类似</p><p>安装先决依赖：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">apt install curl gnupg2 ca-certificates lsb-release debian-archive-keyring</span><br></pre></td></tr></table></figure><p>下载nginx官方签名</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg &gt;/dev/null</span><br></pre></td></tr></table></figure><p>导入签名</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg</span><br></pre></td></tr></table></figure><p>输出的内容应包含“<strong>573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62</strong>”</p><p>例如：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub   rsa2048 2011-08-19 [SC] [expires: 2024-06-14]</span><br><span class="line">      573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62</span><br><span class="line">uid                      nginx signing key &lt;signing-key@nginx.com&gt;</span><br></pre></td></tr></table></figure><p>如果不是，那么你需要重新下载并重新导入。</p><p>导入主线分支版本库：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">echo &quot;deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \</span><br><span class="line">http://nginx.org/packages/mainline/debian `lsb_release -cs` nginx&quot; \</span><br><span class="line">    | sudo tee /etc/apt/sources.list.d/nginx.list</span><br></pre></td></tr></table></figure><p>固定使用nginx官方的包：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">echo -e &quot;Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n&quot; \</span><br><span class="line">    | sudo tee /etc/apt/preferences.d/99nginx</span><br></pre></td></tr></table></figure><p>（如果你已经安装了系统源带的nginx，请先卸载（你应该手动备份配置））</p><p>卸载系统自带的nginx</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo apt remove nginx</span><br></pre></td></tr></table></figure><p>安装nginx官方的nginx</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo apt update</span><br><span class="line">sudo apt install nginx</span><br></pre></td></tr></table></figure><h1 id="配置nginx-conf"><a href="#配置nginx-conf" class="headerlink" title="配置nginx.conf"></a>配置nginx.conf</h1><p>注意：如果你从nginx1.24或更早的版本升级到1.25，应该注意几个重要变更，否则可能会因为nginx版本升级，配置部分选项弃用导致的error。</p><h2 id="NGINX1-25重要变更"><a href="#NGINX1-25重要变更" class="headerlink" title="NGINX1.25重要变更"></a>NGINX1.25重要变更</h2><p>1.不再支持“http2”标记，已经默认ssl站点全部启用http2（该参数已弃用，但继续使用不会报错，只会报warning，你应该将站点中所有的http2参数移除）</p><p>listen 443 ssl http2；会出现警告</p><p>2.“ssl”参数完全丢弃，如果你的站点参数中有ssl参数，则会报error</p><p>如果有ssl on;则会报错</p><h2 id="配置项"><a href="#配置项" class="headerlink" title="配置项"></a>配置项</h2><p>启用SSL：</p><p>由于http2、http3&#x2F;quic强制加密，如果你的站点没有SSL支持，将不支持启用http2、http3&#x2F;quic。</p><p>什么？你还没有ssl证书？你可以了解下acme.sh。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">ssl_protocols  TLSv1.3; #启用TLS1.3加密（如果你考虑支持老旧浏览器和系统，则可以增加TLSv1.2，TLSv1.1）</span><br><span class="line">      ssl_certificate /root/uhttpd.crt;#SSL证书</span><br><span class="line">      ssl_certificate_key /root/uhttpd.key;#SSL密钥</span><br><span class="line">      ssl_ecdh_curve secp384r1;#SSL加密算法</span><br><span class="line">      ssl_stapling on;</span><br><span class="line">      # Path of the file with Diffie-Hellman parameters for EDH ciphers.</span><br><span class="line">      # TIP: Generate with: `sudo openssl dhparam -out /etc/nginx/dh2048.pem 2048`</span><br><span class="line">      #ssl_dhparam /etc/ssl/nginx/dh2048.pem;</span><br><span class="line">      ssl_dhparam /etc/nginx/dh2048.pem;#使用ssl_dhparam扩展，可以根据以上命令创建</span><br><span class="line">      # Specifies that our cipher suits should be preferred over client ciphers.</span><br><span class="line">      # Default is &#x27;off&#x27;.</span><br><span class="line">      ssl_prefer_server_ciphers on; </span><br><span class="line">      # Enables a shared SSL cache with size that can hold around 8000 sessions.</span><br><span class="line">      # Default is &#x27;none&#x27;.</span><br><span class="line">      ssl_session_cache shared:SSL:2m;</span><br><span class="line">      # Specifies a time during which a client may reuse the session parameters.</span><br><span class="line">      # Default is &#x27;5m&#x27;.</span><br><span class="line">      ssl_session_timeout 1h; </span><br><span class="line">      # Disable TLS session tickets (they are insecure). Default is &#x27;on&#x27;.</span><br><span class="line">      ssl_session_tickets off;</span><br><span class="line">      ssl_early_data on; #开启 0-RTT的支持 quic部分特性</span><br></pre></td></tr></table></figure><p>在全局段启用http3&#x2F;quic支持：</p><p>在http{}内增加</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">quic_retry on;</span><br><span class="line">      quic_gso on;</span><br><span class="line">      http3 on;</span><br></pre></td></tr></table></figure><p>在server{}段中有关quic的配置</p><p>增加单独的quic标记</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">listen 443 quic;</span><br><span class="line">      listen [::]:443 quic;</span><br></pre></td></tr></table></figure><p>当然，如果只有这样的配置，那么站点就是只支持quic访问。我们可以增加传统http2支持</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">listen 443 ssl;</span><br><span class="line">      listen [::]:443 ssl;</span><br></pre></td></tr></table></figure><p>这两端代码合在一起是可以的，不会报错。毕竟，quic监听的是443udp，ssl侦听的是443tcp。</p><p>此外，我们还要增加标头，向浏览器报告我们支持quic</p><p>在location中增加如下内容：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">add_header Alt-Svc &#x27;h3=&quot;:443&quot;; ma=2592000,h3-29=&quot;:443&quot;; ma=2592000,h3-Q050=&quot;:443&quot;; ma=2592000,h3-Q046=&quot;:443&quot;; ma=2592000,h3-Q043=&quot;:443&quot;; ma=2592000,quic=&quot;:443&quot;; ma=2592000; v=&quot;46,43&quot;&#x27;;</span><br></pre></td></tr></table></figure><p>这段代码的意思是，在443端口启用http3的以下草案支持：h3，h3-29,h3-q050,h3-q046,h3-q043,quic。</p><p>一段完整的示例：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">server&#123;</span><br><span class="line">        #listen 80; #站点不支持传统http（可以设置一个默认监听80端口跳转至443）</span><br><span class="line">        #listen [::]:80; </span><br><span class="line">        </span><br><span class="line">        listen 443 quic;  #在IPv4/IPv6的443端口监听udp连接（quic/http3）</span><br><span class="line">        listen [::]:443 quic;</span><br><span class="line">        </span><br><span class="line">        listen 443 ssl;  #在IPv4/IPv6的443端口监听传统httptcp连接（默认http2、如若客户端不支持则变为http1.1）</span><br><span class="line">        listen [::]:443 ssl;</span><br><span class="line">        </span><br><span class="line">        server_name xxx.sample.com;</span><br><span class="line">        location /&#123;</span><br><span class="line">        add_header Alt-Svc &#x27;h3=&quot;:443&quot;; ma=2592000,h3-29=&quot;:443&quot;; ma=2592000,h3-Q050=&quot;:443&quot;; ma=2592000,h3-Q046=&quot;:443&quot;; ma=2592000,h3-Q043=&quot;:443&quot;; ma=2592000,quic=&quot;:443&quot;; ma=2592000; v=&quot;46,43&quot;&#x27;;</span><br><span class="line">                proxy_pass http://192.168.31.1;</span><br><span class="line">        &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>保存，退出，重启nginx服务器</p><h1 id="浏览器测试"><a href="#浏览器测试" class="headerlink" title="浏览器测试"></a>浏览器测试</h1><p>使用最新版edge&#x2F;chrome 打开F12，在网络栏的协议可以看到，已经启用http3&#x2F;quic</p><p><img src="/images/50.png" class="lazyload" data-srcset="/images/50.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="h3"></p><p>手机浏览器访问：（Chrome）</p><p><img src="/images/51.png" class="lazyload" data-srcset="/images/51.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="quic"></p><h1 id="Quic站点的好处："><a href="#Quic站点的好处：" class="headerlink" title="Quic站点的好处："></a>Quic站点的好处：</h1><p>1.加速访问</p><p>2.过滤GFW的TCP-SNI阻断（GFW目前还没有识别http3站点的技术），可以在DNS配置正确的情况下直接打开诸如 <a href="http://www.v2ex.com/">www.v2ex.com</a> 这类的，只被DNS污染和SNI打断但又支持QUIC的站点。</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2024/01/09/%E4%B8%BA%E4%BD%A0%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%99%A8Nginx%E6%94%AF%E6%8C%81%E6%9C%80%E6%96%B0%E7%9A%84quic/</id>
    <link href="https://www.a-enjoy.top/2024/01/09/%E4%B8%BA%E4%BD%A0%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%99%A8Nginx%E6%94%AF%E6%8C%81%E6%9C%80%E6%96%B0%E7%9A%84quic/"/>
    <published>2024-01-09T05:12:15.000Z</published>
    <summary>使用最新的nginx主线版本1.25，给网站开启quic支持。 本文不探讨手动编译，而是使用主线安装直接启用quic特性 环境：Debian12+nginx1.25</summary>
    <title>为你的服务器Nginx支持最新的quic</title>
    <updated>2024-01-09T06:23:02.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="network" scheme="https://www.a-enjoy.top/tags/network/"/>
    <content>
      <![CDATA[<p>在某些特殊时期，我们的科学上网工具不太好使的时候，我们还有要经常上github找资源、浏览v2ex和查询维基百科的需求，我们可以通过绕过GFW的SNI阻断来避免一些简易网站访问难的问题。</p><span id="more"></span><h1 id="支持的站点"><a href="#支持的站点" class="headerlink" title="支持的站点"></a>支持的站点</h1><blockquote><ul><li><a href="https://www.wikipedia.org/">维基百科</a></li><li><a href="https://www.pixiv.net/">Pixiv</a>（支持登录，注册暂不支持）</li><li><a href="https://steamcommunity.com/">Steam社区</a></li><li>GitHub</li><li><a href="https://www.tumblr.com/">Tumblr</a></li><li><a href="https://www.instagram.com/">Instagram</a></li><li><a href="https://www.quora.com/">Quora</a></li><li>Google reCAPTCHA</li><li><a href="https://www.reddit.com/">reddit</a></li><li><a href="https://www.apkmirror.com/">APKMirror</a></li><li><a href="https://vimeo.com/">vimeo</a></li><li><a href="https://www.amazon.co.jp/">日本亚马逊</a></li><li><a href="https://onedrive.live.com/">Microsoft OneDrive</a></li><li>Niconico（部分页面有问题，见<a href="https://kkgithub.com/URenko/Accesser/issues/19">#19</a>）</li><li>BBC</li><li>Dropbox</li><li>日本时报</li><li>Kobo</li><li>Medium</li><li>Pinterest</li><li>TheTVDB</li><li>雅虎搜索</li><li>Imgur</li><li><a href="https://mega.nz/">MEGA</a></li><li>OpenAI API</li></ul></blockquote><p>完整列表<a href="https://github.com/URenko/Accesser/blob/master/accesser/pac">https://github.com/URenko/Accesser/blob/master/accesser/pac</a></p><h1 id="GFW的过滤规则"><a href="#GFW的过滤规则" class="headerlink" title="GFW的过滤规则"></a>GFW的过滤规则</h1><h2 id="轻度污染级别"><a href="#轻度污染级别" class="headerlink" title="轻度污染级别"></a>轻度污染级别</h2><h3 id="DNS污染："><a href="#DNS污染：" class="headerlink" title="DNS污染："></a>DNS污染：</h3><p>我兔通过通知运营商、国内的DNS服务商（运营商DNS、公共DNS等）对指定的域名请求进行污染，使之查询不到一个正确的IP，从而使我们没法访问。</p><p><img src="/images/44.jpg" class="lazyload" data-srcset="/images/44.jpg" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="DNS污染"></p><p>你可能说我们在电脑、路由器里设置国外的dns就能解决？NO，没用。</p><p>国外的DNS只会徒增你的查询时间，降低网络体验，而且污染问题得不到解决。因为dns查询所用的53端口已经被劫持了，所以你用国外dns和运营商dns是一样的（传统dns查询）。解决方法：使用<strong>国外、权威和加密的</strong>DNS查询。如使用DoT，DoH，DoQ等代替传统dns查询，解决污染。</p><h3 id="解决方案："><a href="#解决方案：" class="headerlink" title="解决方案："></a>解决方案：</h3><p>部署、使用加密DNS服务器最简单的方法是在局域网内搭建一个加密DNS服务器的中转站，局域网下的其它设备依然使用传统dns查询，但是传统dns查询的ip地址指向的是局域网dns服务器。</p><p>一般我们使用 <a href="https://adguard.info/zh_cn/welcome.html">AdGuard Home</a></p><p>它的用法可参考这几篇文章：</p><p><a href="https://www.timochan.cn/posts/any_pen/adguard_home_for_me">使用 AdGuard Home 来守护自己的 DNS 查询</a></p><p><a href="https://www.timochan.cn/posts/any_pen/adguard_home_for_setting">AdGuard Home 调整</a></p><p><a href="https://www.timochan.cn/posts/jc/adguard_home_configuration_guide">AdGuard Home 中文指南 - TimochanのBlog</a></p><p>由于这种污染级别实在是太低了，我们绕过它也极其容易，目前已经很少单独使用了。往往是轻度污染级别+中度污染级别一起出现使用。</p><h2 id="中度打断级别"><a href="#中度打断级别" class="headerlink" title="中度打断级别"></a>中度打断级别</h2><p>如果你侥幸绕过了DNS污染，正确获取到了服务器的IP，GFW就拿你没办法了吗，你就可以名言正顺的访问被墙的网页？NO！只要我检测到你在访问黑名单页面，我一样可以Ban掉。</p><p>怎么发现我们在访问黑名单网站呢？</p><p>通过对请求头的host进行判断在不在白名单内的IP+黑名单的域名。</p><p>curl 172.67.3.188 -H “host:<a href="http://www.v2ex.com/">www.v2ex.com</a>“</p><p>如果IP在白名单内（对应的域名肯定都在白名单），那当然皆大欢喜，全部放行。</p><p>如果是非黑白名单IP+非白名单域名，那就给你定期打断。如github.com的定时抽风。</p><p>如果是非黑白名单IP+黑名单域名，那就给你一直打断TLS握手，让你没办法完成TLS握手，重置连接，无法访问。</p><p>对于一般的网站，这种方法足够了。</p><p>如果是黑名单IP，那就丢进路由黑洞，下文会有解释。</p><h3 id="SNI阻断："><a href="#SNI阻断：" class="headerlink" title="SNI阻断："></a>SNI阻断：</h3><p>SNI域名阻断（Server Name Indication Domain Blocking）是一种用于在网络上屏蔽特定网站的技术手段。SNI是Transport Layer Security（TLS）协议的一部分，它允许客户端在与服务器建立加密连接时传输所需访问的网站域名信息。SNI域名阻断利用这一特性，通过检查网络传输中的SNI字段，阻止用户访问特定的网站。</p><p>SNI域名阻断通常由网络管理员、政府或互联网服务提供商（ISP）实施，目的是限制或屏蔽特定网站的访问。这可以用于防止用户访问敏感内容、限制言论自由、打击非法网站或控制互联网使用等目的。然而，SNI域名阻断也引发了一些争议，因为它可能影响用户隐私、言论自由和互联网开放性。</p><p>HTTPS（更准确来讲是TLS）有一个扩展，使得在Client Hello阶段，浏览器会向服务器明文发送要访问网站的域名。而GFW根据这一原理对特定的SNI进行阻断，这就是俗称的SNI阻断。更多的可以去看：<a href="https://zh.wikipedia.org/wiki/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%90%8D%E7%A7%B0%E6%8C%87%E7%A4%BA">维基百科</a>，<a href="https://zhuanlan.zhihu.com/p/547260827">看完秒懂什么是SNI - 知乎 (zhihu.com)</a></p><h3 id="解决方案：域前置-Domain-Fronting-代理技术。"><a href="#解决方案：域前置-Domain-Fronting-代理技术。" class="headerlink" title="解决方案：域前置(Domain Fronting)代理技术。"></a>解决方案：域前置(Domain Fronting)代理技术。</h3><p>就是利用有的服务器不验证SNI的这一特点，给GFW和服务器一个假的SNI进行前置代理。因为GFW没有私钥，只能通过SNI和IP判断你要访问的网站，而假的SNI就能欺骗GFW。但服务器本身有私钥，解开后，服务器是通过HTTP请求头判断你要访问哪个网站的。（但注意，特殊的比如虚拟主机、Cloudflare免费版等能在一个IP上部署多个证书的，是一定要SNI的，否则会出错。这种唯一的绕过办法就是ESNI，如果有空的话，以后再讲）因此可以通过这种方式绕过GFW的SNI阻断。</p><h2 id="重度黑洞级别"><a href="#重度黑洞级别" class="headerlink" title="重度黑洞级别"></a>重度黑洞级别</h2><p>GFW针对特定的IP段直接进行路由黑洞处理（BGP路由劫持的最高形式）。</p><p>因为 BGP 是基于互连网络真实表述它们拥有的 IP 地址这一事实，所以 BGP 劫持几乎是<strong>无法停止</strong>的。</p><h3 id="路由劫持"><a href="#路由劫持" class="headerlink" title="路由劫持"></a>路由劫持</h3><p><a href="https://cloud.tencent.com/developer/article/2233355">什么是 BGP 劫持？-腾讯云开发者社区-腾讯云 (tencent.com)</a></p><p><img src="/images/42.png" class="lazyload" data-srcset="/images/42.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="路由劫持"></p><h3 id="路由黑洞"><a href="#路由黑洞" class="headerlink" title="路由黑洞"></a>路由黑洞</h3><p>一条路由无论是静态还是动态，都需要关联到一个出接口，在众多的出接口中，有一种接口非常特殊，即Null（无效）接口，这种类型的接口只有一个编号0，类似（交换机、路由器）的出接口Interface g或e 0&#x2F;0&#x2F;0; Null0是系统保留的逻辑接口，当转发网络设备在转发某些数据包时，如果使用出接口为Null0的路由，那么这些报文会被直接丢弃，就像直接丢进一个黑洞里，永远无法到达，因此出接口为Null0的路由被称为黑洞路由。</p><p><img src="/images/43.png" class="lazyload" data-srcset="/images/43.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="路由黑洞"></p><p>到了这一级别的最强黑名单，黑上黑级别的，那甚至不需要污染域名和sni了，根本连不上！（dns污染也经常是将被污染的域名直接指向这一类IP以达到污染的目的）。当然，这种方法封禁是最彻底的，成本也是最高的。需要在路由器上设置跳过路由的自动学习，手动添加黑名单IP段、出口。</p><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>使用科学上网工具代理（代理的IP需要不在黑名单内，且可以访问目标IP）</p><h1 id="使用工具绕过SNI阻断"><a href="#使用工具绕过SNI阻断" class="headerlink" title="使用工具绕过SNI阻断"></a>使用工具绕过SNI阻断</h1><h2 id="下载和运行URenko-Accesser"><a href="#下载和运行URenko-Accesser" class="headerlink" title="下载和运行URenko&#x2F;Accesser:"></a>下载和运行URenko&#x2F;Accesser:</h2><p><a href="https://github.com/URenko/Accesser">URenko&#x2F;Accesser: 🌏一个解决SNI RST导致维基百科、Pixiv等站点无法访问的工具 | A tool for solving SNI RST (github.com)</a></p><p>需要先安装Python3.10及以上更高版本</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">pip3 install -U accesser[doh,doq]</span><br></pre></td></tr></table></figure><p>如果我们不需要 DNS-over-HTTPS 和 DNS-over-QUIC，则可以不用带<code>[doh,doq]</code>。</p><p>然后通过如下命令启动：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">accesser</span><br></pre></td></tr></table></figure><p>对于Windows系统，默认情况下（没有指定<code>--notsetproxy</code>）会设置PAC代理为<code>http://localhost:7654/pac/?t=&lt;随机数&gt;</code>，如果没有可以手动设置。</p><p>此外，对于Windows系统，默认情况下（没有指定<code>--notimportca</code>）会自动导入证书至系统（第一次运行命令会弹框询问，选择是），如果没有可以手动导入，请看<a href="https://github.com/URenko/Accesser/wiki/FAQ#q-windows%E8%AE%BF%E9%97%AE%E7%9B%B8%E5%85%B3%E7%BD%91%E7%AB%99%E5%87%BA%E7%8E%B0%E8%AF%81%E4%B9%A6%E9%94%99%E8%AF%AF%E6%82%A8%E7%9A%84%E8%BF%9E%E6%8E%A5%E4%B8%8D%E6%98%AF%E7%A7%81%E5%AF%86%E8%BF%9E%E6%8E%A5neterr_cert_invalid%E4%B9%8B%E7%B1%BB%E7%9A%84%E6%80%8E%E4%B9%88%E5%8A%9E%E8%AF%81%E4%B9%A6%E5%AF%BC%E5%85%A5%E9%94%99%E8%AF%AF%E6%80%8E%E4%B9%88%E5%8A%9E%E5%A6%82%E4%BD%95%E5%8D%B8%E8%BD%BD%E8%AF%81%E4%B9%A6">这里</a>。</p><p>支持的站点列表：</p><h2 id="编辑配置文件"><a href="#编辑配置文件" class="headerlink" title="编辑配置文件"></a>编辑配置文件</h2><p>编辑命令执行路径下的config.toml</p><p>我们一般只需要修改DNS部分即可，当然，如果你没有部署无污染的DNS服务器，或者说它预置的DNS服务器你（有其一）都可以访问也是不需要修改的。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">nameserver = [</span><br><span class="line">  #&quot;https://185.222.222.222/dns-query&quot;,# https://host|IP[:port]/path # DNS-over-HTTPS (DoH)</span><br><span class="line">  #&quot;https://45.11.45.11/dns-query&quot;,</span><br><span class="line">  #&quot;https://149.112.112.112/dns-query&quot;,</span><br><span class="line">  #&quot;https://149.112.112.10/dns-query&quot;,</span><br><span class="line">  #&quot;tls://149.112.112.112&quot;,            # tls://host|IP[:port]  # DNS-overTLS (DoT)</span><br><span class="line">  #&quot;tls://149.112.112.10&quot;,</span><br><span class="line">  #&quot;tls://dot.sb&quot;,</span><br><span class="line">  # &quot;quic://dns.adguard-dns.com&quot;,     # quic://host|IP[:port] # DNS-over-QUIC (DoQ)</span><br><span class="line">  &quot;192.168.31.53&quot;                       # host|IP[:port]        # 传统 UDP DNS | traditional UDP DNS</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>因为我的内网有一台安全的DNS服务器，IP是192.168.31.53，所以这里我就把其它的dns全部注释了，使用内网dns服务器加快查询速度</p><p><img src="/images/45.png" class="lazyload" data-srcset="/images/45.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="AdGHome"></p><p><img src="/images/46.png" class="lazyload" data-srcset="/images/46.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="SafeDNS"></p><p>保存设置，重启服务 Enjoy it！</p><h3 id="附：在手机上使用："><a href="#附：在手机上使用：" class="headerlink" title="附：在手机上使用："></a>附：在手机上使用：</h3><p>确保手机与服务器同属于一个局域网。</p><p>编辑命令执行路径下的config.toml</p><p>找到[server]的address，改为</p><p>address &#x3D; “0.0.0.0”</p><p>将pac_host &#x3D; “127.0.0.1”设置为代理的局域网ip地址。</p><p>在手机上的WiFi进行如下设置：</p><p><img src="/images/49.png" class="lazyload" data-srcset="/images/49.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><h1 id="效果图"><a href="#效果图" class="headerlink" title="效果图"></a>效果图</h1><p><img src="/images/47.png" class="lazyload" data-srcset="/images/47.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p><img src="/images/48.png" class="lazyload" data-srcset="/images/48.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/11/12/%E5%88%A9%E7%94%A8%E5%9F%9F%E5%89%8D%E7%BD%AE%E6%8A%80%E6%9C%AF%E7%BB%95%E8%BF%87GFW%E7%9A%84SNI%E9%98%BB%E6%96%AD/</id>
    <link href="https://www.a-enjoy.top/2023/11/12/%E5%88%A9%E7%94%A8%E5%9F%9F%E5%89%8D%E7%BD%AE%E6%8A%80%E6%9C%AF%E7%BB%95%E8%BF%87GFW%E7%9A%84SNI%E9%98%BB%E6%96%AD/"/>
    <published>2023-11-11T16:29:15.000Z</published>
    <summary>在某些特殊时期，我们的科学上网工具不太好使的时候，我们还有要经常上github找资源、浏览v2ex和查询维基百科的需求，我们可以通过绕过GFW的SNI阻断来避免一些简易网站访问难的问题。</summary>
    <title>利用域前置技术绕过GFW的SNI阻断</title>
    <updated>2023-11-11T18:09:08.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="vue" scheme="https://www.a-enjoy.top/tags/vue/"/>
    <category term="golang" scheme="https://www.a-enjoy.top/tags/golang/"/>
    <category term="全栈笔记" scheme="https://www.a-enjoy.top/tags/%E5%85%A8%E6%A0%88%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<p>自己在写一个项目，目前遇到的问题是：根据csdn和axios官网的说明，发送简单POST请求的时候，后端可能没办法正确处理数据，浅浅记录一下解决方案。</p><span id="more"></span><h1 id="传递类型对比"><a href="#传递类型对比" class="headerlink" title="传递类型对比"></a>传递类型对比</h1><p>即key:value 键值对，不使用JSON封装</p><p>类似于get请求，可以很简单的从命令行中看出key:value</p><p>我们进行下对比</p><h2 id="curl发送对比"><a href="#curl发送对比" class="headerlink" title="curl发送对比"></a>curl发送对比</h2><p>1.简单POST</p><blockquote><p>curl <a href="http://localhost:8088/api/auth/signin">http://localhost:8088/api/auth/signin</a> -X POST -d ‘username&#x3D;admin&amp;password&#x3D;passwords’</p></blockquote><p>2.简单GET</p><blockquote><p>curl “<a href="http://localhost:8088/api/auth/signin?username=admin&password=passwords">http://localhost:8088/api/auth/signin?username=admin&amp;password=passwords</a>“</p></blockquote><p>3.JSON型POST</p><blockquote><p>curl -X POST -H “Content-Type: application&#x2F;json” <a href="http://localhost:8088/api/auth/signin">http://localhost:8088/api/auth/signin</a> -d ‘{“username”:”admin”,”password”:”passwords”}’</p></blockquote><h2 id="接收端对比"><a href="#接收端对比" class="headerlink" title="接收端对比"></a>接收端对比</h2><p>1.处理简单POST请求</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">method</span><span class="params">(context *gin.Context)</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> global.IsCookieAuthValid(context) &#123; <span class="comment">//防止反复登录授权</span></span><br><span class="line">    <span class="comment">//context.String(http.StatusOK, &quot;ok&quot;)</span></span><br><span class="line">    context.String(http.StatusOK, <span class="string">&quot;Verified&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">//println(context.Request.Body)</span></span><br><span class="line">  un := context.PostForm(<span class="string">&quot;username&quot;</span>)</span><br><span class="line">  pw := context.PostForm(<span class="string">&quot;password&quot;</span>)</span><br><span class="line">    <span class="comment">//Code</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2.处理简单GET请求</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">method</span><span class="params">(context *gin.Context)</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> global.IsCookieAuthValid(context) &#123; <span class="comment">//防止反复登录授权</span></span><br><span class="line">    <span class="comment">//context.String(http.StatusOK, &quot;ok&quot;)</span></span><br><span class="line">    context.Redirect(<span class="number">302</span>, <span class="string">&quot;/dashboard&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  un := context.DefaultQuery(<span class="string">&quot;username&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">  pw := context.DefaultQuery(<span class="string">&quot;password&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">  <span class="comment">//Code</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以发现 简单方法获取参数都是一行代码的事，比较简单</p><p>而</p><p>3.JSON数据处理…</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">method</span><span class="params">(context *gin.Context)</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> global.IsCookieAuthValid(context) &#123; <span class="comment">//防止反复登录授权</span></span><br><span class="line">    <span class="comment">//context.String(http.StatusOK, &quot;ok&quot;)</span></span><br><span class="line">    context.Redirect(<span class="number">302</span>, <span class="string">&quot;/dashboard&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;    </span><br><span class="line">  json := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="keyword">interface</span>&#123;&#125;) <span class="comment">//注意该结构接受的内容</span></span><br><span class="line">  context.BindJSON(&amp;json)</span><br><span class="line">    un := json[<span class="string">&quot;username&quot;</span>]</span><br><span class="line">  pw := json[<span class="string">&quot;password&quot;</span>]</span><br><span class="line">    <span class="comment">//Code</span></span><br><span class="line">   </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>好吧 也还算简单，但是我们想实现简单的数据传递，而根据axios官网的数据传递方法：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="title function_">axios</span>(&#123;</span><br><span class="line">  <span class="attr">method</span>: <span class="string">&#x27;post&#x27;</span>,</span><br><span class="line">  <span class="attr">url</span>: <span class="string">&#x27;/user/12345&#x27;</span>,</span><br><span class="line">  <span class="attr">data</span>: &#123;</span><br><span class="line">    <span class="attr">firstName</span>: <span class="string">&#x27;Fred&#x27;</span>,</span><br><span class="line">    <span class="attr">lastName</span>: <span class="string">&#x27;Flintstone&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>这种传递方法会使传递的值后端想进行简单处理是不行的，后端没办法获取到数据，或者直接线程panic了，返回500。</p><h1 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h1><p>我们需要使用标准的JavaScript方法</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="title function_">login</span>(<span class="params">formName</span>) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(formName)</span><br><span class="line">      <span class="keyword">var</span> params = <span class="keyword">new</span> <span class="title class_">URLSearchParams</span>();</span><br><span class="line">      params.<span class="title function_">append</span>(<span class="string">&#x27;username&#x27;</span>, formName.<span class="property">name</span>);<span class="comment">//标准简单请求参数</span></span><br><span class="line">      params.<span class="title function_">append</span>(<span class="string">&#x27;password&#x27;</span>, formName.<span class="property">password</span>);<span class="comment">//标准简单请求参数</span></span><br><span class="line">      axios.<span class="title function_">post</span>(<span class="string">&#x27;/api/api/auth/signin&#x27;</span>, params).<span class="title function_">then</span>(<span class="function"><span class="params">response</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (response.<span class="property">status</span> == <span class="number">200</span>) &#123;</span><br><span class="line">          <span class="title function_">goRouter</span>(&#123; <span class="attr">name</span>: <span class="string">&#x27;Dashboard&#x27;</span> &#125;);</span><br><span class="line">          <span class="comment">//ok</span></span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (response.<span class="property">status</span> == <span class="number">401</span>) &#123;</span><br><span class="line">          <span class="title class_">ElMessageBox</span>.<span class="title function_">alert</span>(<span class="string">&quot;用户名或密码错误&quot;</span>, <span class="string">&quot;登录失败&quot;</span>, &#123;&#125;)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;, <span class="function"><span class="params">error</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;E&#x27;</span>, error.<span class="property">message</span>)</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>问题解决，后端可以按（1.）正常获取数据。</p><p>或者转换为标准json，按（3.）处理</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/11/02/axios%E5%8F%91%E9%80%81%E7%AE%80%E5%8D%95POST%E8%AF%B7%E6%B1%82%EF%BC%8CGin%E7%AE%80%E5%8D%95%E5%A4%84%E7%90%86/</id>
    <link href="https://www.a-enjoy.top/2023/11/02/axios%E5%8F%91%E9%80%81%E7%AE%80%E5%8D%95POST%E8%AF%B7%E6%B1%82%EF%BC%8CGin%E7%AE%80%E5%8D%95%E5%A4%84%E7%90%86/"/>
    <published>2023-11-01T16:51:26.000Z</published>
    <summary>自己在写一个项目，目前遇到的问题是：根据csdn和axios官网的说明，发送简单POST请求的时候，后端可能没办法正确处理数据，浅浅记录一下解决方案。</summary>
    <title>axios发送简单POST请求，Gin简单处理</title>
    <updated>2023-11-01T17:19:55.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="vue" scheme="https://www.a-enjoy.top/tags/vue/"/>
    <category term="golang" scheme="https://www.a-enjoy.top/tags/golang/"/>
    <category term="全栈笔记" scheme="https://www.a-enjoy.top/tags/%E5%85%A8%E6%A0%88%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<p>自己在写一个项目，目前遇到了问题：前端需要向后端执行ajax请求获取数据，需要传递cookie。如果使用默认的代码不进行配置则会出现跨域问题cors报错，本文将解决问题.</p><span id="more"></span><p>前端(node.js+Vite)：<a href="http://localhost:5173/">http://localhost:5173</a></p><p>后端(golang + gin)：<a href="http://localhost:8088/">http://localhost:8088/</a></p><h1 id="问题出现"><a href="#问题出现" class="headerlink" title="问题出现"></a>问题出现</h1><p>前端需要向后端请求数据&#x2F;api&#x2F;devices&#x2F;snlspjrnzxcevyfk&#x2F;info,后端通过前端传递过来的cookie进行身份识别，授权成功返回200，JSON数据, 失败返回401未授权或404.</p><p>当我们使用默认参数不做配置时，直接请求会被拦截，进而无法获取数据。参考网上的教程在解决请求跨域后，又少有解决cookie传递的，参考几篇资料后，结合本人项目，目前问题已解决。</p><p>cookie:IOMAuth</p><h1 id="解决措施"><a href="#解决措施" class="headerlink" title="解决措施"></a>解决措施</h1><h2 id="前端部分"><a href="#前端部分" class="headerlink" title="前端部分"></a>前端部分</h2><h3 id="vite-config-js"><a href="#vite-config-js" class="headerlink" title="vite.config.js"></a>vite.config.js</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(&#123;</span><br><span class="line">  <span class="attr">plugins</span>: [</span><br><span class="line">    <span class="title function_">vue</span>(),</span><br><span class="line">    <span class="title class_">AutoImport</span>(&#123;</span><br><span class="line">      <span class="attr">resolvers</span>: [<span class="title class_">ElementPlusResolver</span>()],</span><br><span class="line">    &#125;),</span><br><span class="line">    <span class="title class_">Components</span>(&#123;</span><br><span class="line">      <span class="attr">resolvers</span>: [<span class="title class_">ElementPlusResolver</span>()],</span><br><span class="line">    &#125;),</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">resolve</span>: &#123;</span><br><span class="line">    <span class="attr">alias</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;@&#x27;</span>: <span class="title function_">fileURLToPath</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">&#x27;./src&#x27;</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>))</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">server</span>: &#123;</span><br><span class="line">    <span class="attr">host</span>: <span class="string">&#x27;0.0.0.0&#x27;</span>,</span><br><span class="line">    <span class="comment">//port: 8088,</span></span><br><span class="line">    <span class="attr">proxy</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;/api&#x27;</span>: &#123;</span><br><span class="line">        <span class="comment">// 后台地址</span></span><br><span class="line">        <span class="attr">target</span>: <span class="string">&#x27;http://localhost:8088/&#x27;</span>,</span><br><span class="line">        <span class="attr">changeOrigin</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">secure</span>: <span class="literal">false</span>,</span><br><span class="line">        <span class="attr">ws</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">followRedirects</span>: <span class="literal">true</span>, <span class="comment">//cookie自动跳转传递</span></span><br><span class="line">        <span class="attr">rewrite</span>: <span class="function"><span class="params">path</span> =&gt;</span> path.<span class="title function_">replace</span>(<span class="regexp">/^\/api/</span>, <span class="string">&#x27;&#x27;</span>),</span><br><span class="line">        <span class="title function_">onProxyReq</span>(<span class="params">proxyReq, req, res</span>) &#123;</span><br><span class="line">          originHost = req.<span class="property">headers</span>[<span class="string">&#x27;x-forwarded-for&#x27;</span>]</span><br><span class="line">          <span class="keyword">const</span> cookie = req.<span class="property">headers</span>[<span class="string">&#x27;cookie&#x27;</span>]</span><br><span class="line">          <span class="keyword">if</span> (cookie) &#123;</span><br><span class="line">            proxyReq.<span class="title function_">setHeader</span>(<span class="string">&#x27;cookie&#x27;</span>, cookie)</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="title function_">onProxyRes</span>(<span class="params">proxyRes, req, res</span>) &#123;</span><br><span class="line">          <span class="keyword">if</span> (proxyRes.<span class="property">headers</span>[<span class="string">&#x27;set-cookie&#x27;</span>]) &#123;</span><br><span class="line">            <span class="comment">// cookie 传递重点</span></span><br><span class="line">            proxyRes.<span class="property">headers</span>[<span class="string">&#x27;Access-Control-Allow-Credentials&#x27;</span>] = <span class="string">&#x27;true&#x27;</span>;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="axios配置-axiosInstance-js"><a href="#axios配置-axiosInstance-js" class="headerlink" title="axios配置 axiosInstance.js"></a>axios配置 axiosInstance.js</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">&#x27;axios&#x27;</span></span><br><span class="line"></span><br><span class="line">axios.<span class="property">defaults</span>.<span class="property">withCredentials</span> = <span class="literal">true</span>; <span class="comment">//重点</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">API</span> = axios.<span class="title function_">create</span>(&#123;</span><br><span class="line">  <span class="attr">baseURL</span>:<span class="string">&#x27;http://localhost:8088&#x27;</span>, </span><br><span class="line">  <span class="attr">timeout</span>: <span class="number">2000</span>,                   </span><br><span class="line">  <span class="attr">withCredentials</span>:<span class="literal">true</span> <span class="comment">//重点 这参数可以放上边，也可以放里面</span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="variable constant_">API</span></span><br></pre></td></tr></table></figure><h2 id="后端"><a href="#后端" class="headerlink" title="后端"></a>后端</h2><h3 id="go-gin路由设置"><a href="#go-gin路由设置" class="headerlink" title="go gin路由设置"></a>go gin路由设置</h3><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">RouterInit</span><span class="params">()</span></span> &#123;</span><br><span class="line">  global.Router = gin.Default()</span><br><span class="line">  global.Router.Use(<span class="function"><span class="keyword">func</span><span class="params">(c *gin.Context)</span></span> &#123;</span><br><span class="line">    method := c.Request.Method</span><br><span class="line">    origin := c.Request.Header.Get(<span class="string">&quot;Origin&quot;</span>) <span class="comment">//请求头部</span></span><br><span class="line">    <span class="keyword">if</span> origin != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">      <span class="comment">//接收客户端发送的origin （重要！）</span></span><br><span class="line">      <span class="comment">//Access-Control-Allow-Origin是必须的,他的值要么是请求Origin字段的值,要么是一个*, 表示接受任意域名的请求</span></span><br><span class="line">      c.Writer.Header().Set(<span class="string">&quot;Access-Control-Allow-Origin&quot;</span>, origin) <span class="comment">//设置为*会报错</span></span><br><span class="line">      <span class="comment">//服务器支持的所有跨域请求的方法</span></span><br><span class="line">      c.Header(<span class="string">&quot;Access-Control-Allow-Methods&quot;</span>, <span class="string">&quot;POST, GET, OPTIONS, PUT, DELETE,UPDATE&quot;</span>)</span><br><span class="line">      <span class="comment">//允许跨域设置可以返回其他子段，可以自定义字段</span></span><br><span class="line">      <span class="comment">//该字段可选。CORS请求时，XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段：Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。</span></span><br><span class="line">      <span class="comment">//如果想拿到其他字段，就必须在Access-Control-Expose-Headers里面指定。上面的例子指定，getResponseHeader(&#x27;FooBar&#x27;)可以返回FooBar字段的值。</span></span><br><span class="line">      c.Header(<span class="string">&quot;Access-Control-Allow-Headers&quot;</span>, <span class="string">&quot;Authorization, Content-Length, X-CSRF-Token, Token, session, IOMAuth&quot;</span>)</span><br><span class="line">      <span class="comment">// 允许浏览器（客户端）可以解析的头部 （重要）</span></span><br><span class="line">      c.Header(<span class="string">&quot;Access-Control-Expose-Headers&quot;</span>, <span class="string">&quot;Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers&quot;</span>)</span><br><span class="line">      <span class="comment">//设置缓存时间</span></span><br><span class="line">      <span class="comment">//该字段可选，用来指定本次预检请求的有效期，单位为秒。有效期是20天（1728000秒），即允许缓存该条回应1728000秒（即20天），在此期间，不用发出另一条预检请求。</span></span><br><span class="line">      c.Header(<span class="string">&quot;Access-Control-Max-Age&quot;</span>, <span class="string">&quot;172800&quot;</span>)</span><br><span class="line">      <span class="comment">//允许客户端传递校验信息比如 cookie (重要)</span></span><br><span class="line">      c.Header(<span class="string">&quot;Access-Control-Allow-Credentials&quot;</span>, <span class="string">&quot;true&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//允许类型校验</span></span><br><span class="line">    <span class="keyword">if</span> method == <span class="string">&quot;OPTIONS&quot;</span> &#123;</span><br><span class="line">      c.JSON(http.StatusOK, <span class="string">&quot;ok!&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">      <span class="keyword">if</span> err := <span class="built_in">recover</span>(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        logrus.Panic(<span class="string">&quot;Panic info is: %v&quot;</span>, err)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    c.Next()</span><br><span class="line">  &#125;)</span><br><span class="line">  regRouter()</span><br><span class="line">  global.Router.Run(<span class="string">&quot;:&quot;</span> + global.WebPort)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/10/29/%E8%A7%A3%E5%86%B3%E5%89%8D%E7%AB%AFVue3-vite-axios-cookie%E8%B7%A8%E5%9F%9F%E6%8A%A5%E9%94%99%E9%97%AE%E9%A2%98-%E5%90%8E%E7%AB%AFGo/</id>
    <link href="https://www.a-enjoy.top/2023/10/29/%E8%A7%A3%E5%86%B3%E5%89%8D%E7%AB%AFVue3-vite-axios-cookie%E8%B7%A8%E5%9F%9F%E6%8A%A5%E9%94%99%E9%97%AE%E9%A2%98-%E5%90%8E%E7%AB%AFGo/"/>
    <published>2023-10-29T09:50:57.000Z</published>
    <summary>自己在写一个项目，目前遇到了问题：前端需要向后端执行ajax请求获取数据，需要传递cookie。如果使用默认的代码不进行配置则会出现跨域问题cors报错，本文将解决问题.</summary>
    <title>解决前端Vue3+vite+axios+cookie跨域报错问题 后端Go+gin</title>
    <updated>2023-10-29T10:09:38.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="OpenWrt" scheme="https://www.a-enjoy.top/tags/OpenWrt/"/>
    <content>
      <![CDATA[<p>rootfs版CatWrt(OpenWrt)启动镜像的定制</p><p>包括镜像扩容，源修改，软件包定制</p><span id="more"></span><p>​ 熟悉软路由的朋友们都知道我们在下载固件(镜像)的时候，一般都是选择带有squashfs字样的镜像，然而往往还有另一个神秘的选择- rootfs。它却很少被人们提起，它的一般用途是拿来做docker的镜像，但是我们还可以拿做另一件事，OpenWrt的系统分区制作。</p><p>​ 我们平时接触到的OpenWrt的根文件系统一般都是 squashfs，这个文件系统给我们带来一定的安全感，我们在软路由上装插件、修改配置导致OpenWrt出现异常时可以通过清除挂载的文件修改操作来还原初始状态；在硬路由上，squashfs+配置分区的外挂载使我们可以轻易的通过外置按键还原对路由器的更改。虽然squashfs很好，但是它也阻止了我们直接往系统分区增添、删除、修改我们想要的文件、配置和程序。</p><p>想要解决系统分区不能修改的解决方法很简单，也是OpenWrt系默认的操作，</p><p>第一种方式：建立一个overlay分区，将所有修改后的文件存放于此，使用挂载的方式直接覆盖系统分区，实现修改，这种操作类似建立一个镜像，通过对镜像的修改而改动系统。</p><p>当我们清空&#x2F;overlay分区的时候，便实现了对系统分区修改的还原。</p><p>第二种方式，就是将系统分区的squashfs文件系统中的文件移动到其它可写的文件系统，如f2fs，ext4，xfs等，并建立启动信息启动系统。OpenWrtx86可以在系统-&gt;挂载点-&gt;&#x2F;-&gt;修改，通过拷贝&#x2F;下的文件到新的可写路径，并建立启动项来实现对&#x2F;的可读写，但是这种方式对新手并不友好，甚至一些老手都在此栽过跟头。</p><p>除此之外，我们还有第三种方式：<strong>使用Linux kernel+rootfs+grub(bootlodaer)制作一个可启动的Linux系统，这种方式建立的系统，根路径在root权限下是可读写的，本文将介绍此种方法</strong>。</p><p>实现的原理：同<a href="https://cloud.tencent.com/developer/article/1114481">Linux启动过程</a></p><h2 id="第一步：准备阶段"><a href="#第一步：准备阶段" class="headerlink" title="第一步：准备阶段"></a>第一步：准备阶段</h2><h3 id="准备内核-kernel-、引导-grub-和rootfs。"><a href="#准备内核-kernel-、引导-grub-和rootfs。" class="headerlink" title="准备内核(kernel)、引导(grub))和rootfs。"></a>准备内核(kernel)、引导(grub))和rootfs。</h3><p>启动引导(bootlodaer)启动引导程序(grub），引导程序启动内核(kernel)，内核初始化(mount and init)rootfs，启动rootfs里的进程。</p><p>下载启动分区 <a href="https://github.com/miaoermua/CatWrt/releases/download/v23.8/CatWrt.v23.8.x86_64-squashfs-combined-efi.img">EFI引导的</a> <a href="https://github.com/miaoermua/CatWrt/releases/download/v23.8/CatWrt.v23.8.x86_64-squashfs-combined.img">传统BIOS引导方式</a></p><p>由于一般的OpenWrt都不提供单独的内核和引导，我们只能下载我们要制作的目标版本镜像来做底包修改。 选择适当引导方式的底包制作镜像将可以很方便的在对应目标的系统启动openwrt系统。</p><p>下载rootfs <a href="https://github.com/miaoermua/CatWrt/releases/download/v23.8/CatWrt.v23.8.x86_64-rootfs.tar.gz">链接</a> 一般rootfs都是以 xxx-rootfs.tar.gz结尾</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">wget https://github.com/miaoermua/CatWrt/releases/download/v23.8/CatWrt.v23.8.x86_64-squashfs-combined-efi.img</span><br><span class="line"></span><br><span class="line">wget https://github.com/miaoermua/CatWrt/releases/download/v23.8/CatWrt.v23.8.x86_64-rootfs.tar.gz</span><br></pre></td></tr></table></figure><h3 id="准备多分区挂载工具-kpartx"><a href="#准备多分区挂载工具-kpartx" class="headerlink" title="准备多分区挂载工具 kpartx"></a>准备多分区挂载工具 kpartx</h3><p>Kpartx is a tool that creating device mappings for partitions.</p><p>Debian&#x2F;Ubuntu:</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">apt install kpartx -y </span><br></pre></td></tr></table></figure><p>alpineLinux:</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">apk add kpartx</span><br></pre></td></tr></table></figure><p>CentOS&#x2F;Fedora&#x2F;OpenRuler:</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">yum install kpartx -y</span><br></pre></td></tr></table></figure><p><img src="/images/25.png" class="lazyload" data-srcset="/images/25.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><h2 id="第二步：镜像处理"><a href="#第二步：镜像处理" class="headerlink" title="第二步：镜像处理"></a>第二步：镜像处理</h2><blockquote><p>1.使用cfdisk(fdisk)工具删掉镜像里的系统分区</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">cfdisk CatWrt.v23.8.x86_64-squashfs-combined-efi.img</span><br></pre></td></tr></table></figure><p><img src="/images/26.png" class="lazyload" data-srcset="/images/26.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>完成Delete后，选择Write，输入yes，然后Quit退出</p><p>等待同步完成</p><p><img src="/images/27.png" class="lazyload" data-srcset="/images/27.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><blockquote><p>2.使用dd命令给镜像增加空间</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">dd if=/dev/zero bs=1M count=256 &gt;&gt;CatWrt.v23.8.x86_64-squashfs-combined-efi.img </span><br></pre></td></tr></table></figure><p>bs参数 数字+单位 count参数 数量 &gt;&gt; 代表追加</p><p>意思是从&#x2F;dev&#x2F;zero中取出1M*256的容量追加到镜像中（扩容256M）</p><p>我们可以根据需要给镜像扩容任意大小</p><p>比如 dd if&#x3D;&#x2F;dev&#x2F;zero bs&#x3D;1G count&#x3D;5 &gt;&gt;CatWrt.v23.8.x86_64-squashfs-combined-efi.img 代表扩容5G</p><blockquote><p>3.使用cfdisk给扩容后的镜像尾建立分区</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">cfdisk CatWrt.v23.8.x86_64-squashfs-combined-efi.img</span><br></pre></td></tr></table></figure><p>选择Free space</p><p><img src="/images/28.png" class="lazyload" data-srcset="/images/28.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>接着，会询问我们要创建的分区大小，这里直接回车就行</p><p>完成后选择Write，yes，Quit</p><p>fdisk操作类似</p><p><img src="/images/29.png" class="lazyload" data-srcset="/images/29.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><blockquote><p>4.使用kpartx挂载镜像为硬盘回环设备</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">losetup /dev/loop7 CatWrt.v23.8.x86_64-squashfs-combined-efi.img</span><br></pre></td></tr></table></figure><p>如果&#x2F;dev&#x2F;loop7被占用就换其它的loop0 loop1什么的</p><p>查看映射的分区表</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">kpartx -av /dev/loop7</span><br></pre></td></tr></table></figure><p><img src="/images/30.png" class="lazyload" data-srcset="/images/30.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>其中 loop7p2就是我们的目标</p><p>格式化该分区</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">mkfs.ext4 /dev/mapper/loop7p2</span><br></pre></td></tr></table></figure><p>!CatWrt内核支持f2fs，格式化成f2fs格式也可以(f2fs适合将镜像刷写在闪存存储介质上 如U盘，移动硬盘SSD,固态硬盘(包括nvme固态，m2SATA，SATASSD等)) 命令mkfs.f2fs &#x2F;dev&#x2F;mapper&#x2F;loop7p2</p><p><img src="/images/31.png" class="lazyload" data-srcset="/images/31.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><blockquote><p>5.查看分区UUID （在格式化之后分区UUID会改变，因而需要先格式化再查看）</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">cfdisk /dev/loop7</span><br></pre></td></tr></table></figure><p><img src="/images/32.png" class="lazyload" data-srcset="/images/32.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>分区UUID用于在后续引导系统</p><blockquote><p>6.建立一个临时挂载路径并挂载系统分区</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">mkdir /mnt/mount</span><br><span class="line">mount /dev/mapper/loop7p2 /mnt/mount</span><br></pre></td></tr></table></figure><p><img src="/images/33.png" class="lazyload" data-srcset="/images/33.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><blockquote><p>7.解压rootfs</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">tar -xzvf CatWrt.v23.8.x86_64-rootfs.tar.gz -C /mnt/mount</span><br><span class="line">cd /mnt/mount</span><br><span class="line">ls</span><br></pre></td></tr></table></figure><p>可以看到 我们已经制作完成启动镜像了</p><p><img src="/images/34.png" class="lazyload" data-srcset="/images/34.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p><strong>第七步结束。如果不需要修改系统，请转到第八步</strong></p><hr><p>此时这个镜像的系统分区就已经处理好了 。当然，我们还可以优化一下镜像</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">chroot /mnt/mount</span><br></pre></td></tr></table></figure><p>编辑源，更新部分软件等 (CatWrt具有)</p><p><img src="/images/35.png" class="lazyload" data-srcset="/images/35.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">bash-5.2# vim /etc/opkg/distfeeds.conf</span><br></pre></td></tr></table></figure><p>换腾讯源</p><blockquote><p>src&#x2F;gz openwrt_core <a href="https://mirrors.cloud.tencent.com/lede/snapshots/targets/x86/64/packages">https://mirrors.cloud.tencent.com/lede/snapshots/targets/x86/64/packages</a></p><p>src&#x2F;gz openwrt_base <a href="https://mirrors.cloud.tencent.com/lede/snapshots/packages/x86_64/base">https://mirrors.cloud.tencent.com/lede/snapshots/packages/x86_64&#x2F;base</a></p><p>src&#x2F;gz openwrt_luci <a href="https://mirrors.cloud.tencent.com/lede/releases/18.06.9/packages/x86_64/luci">https://mirrors.cloud.tencent.com/lede/releases/18.06.9/packages/x86_64&#x2F;luci</a></p><p>src&#x2F;gz openwrt_packages <a href="https://mirrors.cloud.tencent.com/lede/snapshots/packages/x86_64/packages">https://mirrors.cloud.tencent.com/lede/snapshots/packages/x86_64&#x2F;packages</a></p><p>src&#x2F;gz openwrt_routing <a href="https://mirrors.cloud.tencent.com/lede/snapshots/packages/x86_64/routing">https://mirrors.cloud.tencent.com/lede/snapshots/packages/x86_64&#x2F;routing</a></p><p>src&#x2F;gz openwrt_telephony <a href="https://mirrors.cloud.tencent.com/lede/snapshots/packages/x86_64/telephony">https://mirrors.cloud.tencent.com/lede/snapshots/packages/x86_64&#x2F;telephony</a></p></blockquote><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">bash-5.2# mkdir /var/lock -p</span><br><span class="line">bash-5.2# opkg update</span><br></pre></td></tr></table></figure><p>安装程序包等…（可看文末）</p><p>退出</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">bash-5.2# exit</span><br><span class="line"></span><br><span class="line">exit</span><br></pre></td></tr></table></figure><blockquote><p>8.卸载系统分区</p></blockquote><p>root@pve:~# umount &#x2F;mnt&#x2F;mount</p><blockquote><p>9.挂载boot分区，修改启动项</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">mount /dev/mapper/loop7p1 /mnt/mount</span><br><span class="line"></span><br><span class="line">ls /mnt/mount</span><br></pre></td></tr></table></figure><p><img src="/images/36.png" class="lazyload" data-srcset="/images/36.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">root@pve:~/OpenWrt# cd /mnt/mount/boot/grub/</span><br><span class="line"></span><br><span class="line">root@pve:/mnt/mount/boot/grub# ls</span><br><span class="line"></span><br><span class="line">boot.img core.img grub.cfg</span><br><span class="line"></span><br><span class="line">root@pve:/mnt/mount/boot/grub# vim grub.cfg</span><br></pre></td></tr></table></figure><blockquote><p>10.将前面记录的UUID填入(替换)到PARTUUID中</p></blockquote><p><img src="/images/37.png" class="lazyload" data-srcset="/images/37.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><blockquote><p>11.保存文件，卸载分区</p></blockquote><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">umount /mnt/mount</span><br></pre></td></tr></table></figure><blockquote><p>12.卸载回环设备</p></blockquote><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">root@pve:~# losetup -d /dev/loop7</span><br></pre></td></tr></table></figure><h2 id="完成"><a href="#完成" class="headerlink" title="完成"></a>完成</h2><p>将新的镜像（CatWrt.v23.8.x86_64-squashfs-combined-efi.img）刷入启动盘，启动。（PVE添加硬盘）</p><blockquote><p>这个时候应该改名叫CatWrt.v23.8.x86_64-6G-ext4-rootfs-efi.img才对</p></blockquote><p>Enjoy Image！</p><h2 id="截图"><a href="#截图" class="headerlink" title="截图"></a>截图</h2><p>以下是10G版本扩容的截图</p><p>10G扩容版的下载链接：<a href="http://v4.iotroom.top:5244/CatWrt/v23.8/CatWrt.v23.8.x86_64-10Grootfs-ext4.img.7z">Here</a></p><p><img src="/images/38.png" class="lazyload" data-srcset="/images/38.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p><img src="/images/39.png" class="lazyload" data-srcset="/images/39.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><h2 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h2><blockquote><p>虚拟化环境下卸载amdGPU、IntelGPU驱动</p></blockquote><p>opkg remove amdgpu-firmware</p><p>opkg remove i915-firmware</p><blockquote><p>更新curl</p></blockquote><p>opkg upgrade libcurl4 curl</p><p><img src="/images/40.png" class="lazyload" data-srcset="/images/40.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>可安全更新的程序列表(等)</p><blockquote><p>xray-core v2ray-core v2ray-geoip fdisk blkid mount-utils libopenssl-conf node libfdisk1 samba4-libs cfdisk parted samba4-server tailscale ethtool libblkid1 lsblk libmount1 tailscaled zerotier libopenssl3 terminfo</p></blockquote><p>不建议更新的 :kmod-xxx uci docker(d) dnsmasq-full containerd strongswan等</p><p>安装apk包管理器&amp;&amp;neofetch</p><p>*显示 Linux 系统信息和发行版图标</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">opkg install apk alpine-keys alpine-repositories</span><br><span class="line">apk update --allow-untrusted</span><br><span class="line">apk add neofetch --allow-untrusted</span><br><span class="line">neofetch</span><br></pre></td></tr></table></figure><p><img src="/images/41.png" class="lazyload" data-srcset="/images/41.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>使用apk包管理器，我们可以安装一些其它东西，比如java，可以搭建mc服务器。安装装glibc支持 运行其它程序，安装完整开发环境、做远程测试服务器等…</p><blockquote><p>CatWrt扩展</p></blockquote><p><a href="https://www.miaoer.xyz/posts/network/catwrt-install-application">CatWrt 扩展应用安装，全系生态通用 - 喵二の小博客 (miaoer.xyz)</a></p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/10/17/rootfs%E7%89%88CatWrt-OpenWrt-%E5%90%AF%E5%8A%A8%E9%95%9C%E5%83%8F%E5%AE%9A%E5%88%B6/</id>
    <link href="https://www.a-enjoy.top/2023/10/17/rootfs%E7%89%88CatWrt-OpenWrt-%E5%90%AF%E5%8A%A8%E9%95%9C%E5%83%8F%E5%AE%9A%E5%88%B6/"/>
    <published>2023-10-17T09:38:16.000Z</published>
    <summary>rootfs版CatWrt(OpenWrt)启动镜像的定制 包括镜像扩容，源修改，软件包定制</summary>
    <title>rootfs版CatWrt(OpenWrt)启动镜像定制</title>
    <updated>2023-10-17T12:45:21.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="PVE" scheme="https://www.a-enjoy.top/tags/PVE/"/>
    <category term="OpenWrt" scheme="https://www.a-enjoy.top/tags/OpenWrt/"/>
    <content>
      <![CDATA[<p>在PVE下部署安装和配置CatWrt，实现软路由的虚拟化。</p><span id="more"></span><h1 id="CatWrt是什么？"><a href="#CatWrt是什么？" class="headerlink" title="CatWrt是什么？"></a>CatWrt是什么？</h1><p>CatWrt 是基于开源的 Lean <a href="https://github.com/coolsnowwolf/lede">LEDE</a> 仓库编译的 OpenWrt 发行版，LEDE相较于OpenWRT官方添加了更多符合中国用户的插件，其更符合中国用户的操作习惯。</p><p>CatWrt是由饱受好评的LEDE源代码<strong>直接</strong>*编译而来，其提供了相似于LEDE的硬路由私有化付费版本QWRT的体验，同时提供了较为完善的社区支持和多种硬件及架构的支持。</p><p>项目地址：</p><p><a href="https://github.com/miaoermua/CatWrt/">GitHub - miaoermua&#x2F;CatWrt: 🌠 客制化 OpenWrt 智能网关</a></p><p><img src="/images/CatWrt_gh.jpg" class="lazyload" data-srcset="/images/CatWrt_gh.jpg" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="img"></p><p>官网地址：<a href="https://www.miaoer.xyz/posts/network/catwrt.v23.8">CatWrt.v23.8 超强性能超稳固件，支持多款热门 mt798x 路由 - 喵二の小博客 (miaoer.xyz)</a></p><p>而但免费的<strong>技术支持</strong>*和更新。</p><h1 id="在PVE上安装OpenWrt"><a href="#在PVE上安装OpenWrt" class="headerlink" title="在PVE上安装OpenWrt"></a>在PVE上安装OpenWrt</h1><h3 id="1-创建虚拟机"><a href="#1-创建虚拟机" class="headerlink" title="1.创建虚拟机"></a>1.创建虚拟机</h3><p><img src="/images/13.png" class="lazyload" data-srcset="/images/13.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image-20230921173655356"></p><p><img src="/images/14.png" class="lazyload" data-srcset="/images/14.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>此处选择<strong>不使用任何介质</strong>，客户机操作系统选择Linux，版本默认</p><p><img src="/images/15.png" class="lazyload" data-srcset="/images/15.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>这里的BIOS选项根据自己的喜好选择默认(SeaBIOS)，或是OVMF(UEFI)，关系到后面添加镜像时选择的版本。</p><p>如果选择UEFI，<strong>不勾选 添加EFI磁盘</strong>。显卡建议<strong>保持默认</strong>或者标准VGA,VMware兼容</p><p><img src="/images/16.png" class="lazyload" data-srcset="/images/16.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>此处，将磁盘删除。</p><p><img src="/images/17.png" class="lazyload" data-srcset="/images/17.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>此处，根据自己电脑的情况设置CPU核心数，类别可以保持默认，也可以根据自己的情况选择。为了提升性能，这里选择host，并设置了CPU权重以提升调度积极性-&gt;提升性能。</p><p><img src="/images/18.png" class="lazyload" data-srcset="/images/18.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>内存设置：如果不跑代理类软件，仅用于拨号，设置512MB，768MB即可，如果需要跑代理类软件，建议设置更高，如1024.如果需要跑容器（docker），建议设置2048或更高。</p><p>勾选Ballooning设备将使内存变更为动态分配，如果需要使用监控内存类的软件(如htop，pcdn内存检测)，则不建议勾选。部分软件会因为此无法判断内存大小而出现内存泄露。</p><p><img src="/images/19.png" class="lazyload" data-srcset="/images/19.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>网络设置：我们知道，非(单臂、庞)路由器是有两类接口的，一类WAN，一类LAN。</p><p>而CatWrt的第一个接口默认设置为LAN接口，第二个接口默认为WAN，因此在此处应谨慎操作，避免因接口顺序设置出错导致前期配置困难。</p><p>在完成配置后，添加第二个网口。</p><p><img src="/images/20.png" class="lazyload" data-srcset="/images/20.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>找到刚刚创建的设备，点击之，选择硬件，添加-&gt;网络设备-&gt;选择另一接口(WAN)</p><p><img src="/images/21.png" class="lazyload" data-srcset="/images/21.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><h3 id="2-导入CatWrt硬盘"><a href="#2-导入CatWrt硬盘" class="headerlink" title="2.导入CatWrt硬盘"></a>2.导入CatWrt硬盘</h3><p>大部分的OpenWrtx86都提供.img格式磁盘镜像或相应的打包压缩文件，这里需要对应系统夹选择BIOS或EFI而选择相应系统。</p><p>比如，我前面选择的是OVMF(EFI)则我应该下载efi的镜像，选择SeaBIOS的，则因下载不带EFI的镜像。</p><p>下载地址：<a href="https://github.com/miaoermua/CatWrt/releases">Releases · miaoermua&#x2F;CatWrt (github.com)</a></p><p><img src="/images/22.png" class="lazyload" data-srcset="/images/22.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>使用ssh连接PVE主机，或是在网页的Shell中</p><p>下载镜像并导入虚拟磁盘。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">root@pve:~# wget https://github.com/miaoermua/CatWrt/releases/download/v23.8/CatWrt.v23.8.x86_64-squashfs-combined.img</span><br><span class="line">root@pve:~# qm importdisk 104 ./CatWrt.v23.8.x86_64-squashfs-combined.img local-lvm</span><br></pre></td></tr></table></figure><p>qm importdisk是命令</p><p>104是虚拟机ID</p><p>.&#x2F;CatWrt.v23.8.x86_64-squashfs-combined.img是镜像路径。</p><p>如果是通过浏览器上传的，则在&#x2F;var&#x2F;lib&#x2F;vz&#x2F;template&#x2F;iso&#x2F;xxx.img下。</p><p>local-lvm是存储位置，选用本地存储</p><p>导入成功后显示successful</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Successfully imported disk as &#x27;unused0:local-lvm:vm-104-disk-0&#x27;</span><br><span class="line">root@pve:~#</span><br></pre></td></tr></table></figure><p>接着，按如下步骤导入磁盘</p><p><img src="/images/23.png" class="lazyload" data-srcset="/images/23.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>完成后启动。</p><p>使用网线通过与vmbr0桥接的交换机相连，在浏览器输入192.168.1.4进入，进行后续拨号，DHCP，更高网口信息等配置。</p><p><img src="/images/24.png" class="lazyload" data-srcset="/images/24.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image"></p><p>用户名root，密码password.</p><p>1)*.CatWrt相较于LEDE有一部分软件包的修改和配置的修改</p><p>2)*.基础问题提供免费技术支持，特殊问题除外</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/09/21/PVE%E9%83%A8%E7%BD%B2CatWrt-OpenWrt/</id>
    <link href="https://www.a-enjoy.top/2023/09/21/PVE%E9%83%A8%E7%BD%B2CatWrt-OpenWrt/"/>
    <published>2023-09-21T09:17:57.000Z</published>
    <summary>在PVE下部署安装和配置CatWrt，实现软路由的虚拟化。</summary>
    <title>PVE部署CatWrt(OpenWrt)</title>
    <updated>2023-09-21T10:49:37.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="PVE" scheme="https://www.a-enjoy.top/tags/PVE/"/>
    <content>
      <![CDATA[<p>Debian 12已经发布了，紧跟Debian发布节奏的PVE也随之发布了8.0（Beta）[目前已经正式版了2023-6-24 15:30:36]。本文将展示PVE8的一些新特性和如何升级至最新版本。</p><span id="more"></span><h1 id="主要的更新内容"><a href="#主要的更新内容" class="headerlink" title="主要的更新内容"></a>主要的更新内容</h1><h2 id="1-从5-15-x更新到Linux-6-2-16的新内核"><a href="#1-从5-15-x更新到Linux-6-2-16的新内核" class="headerlink" title="1.从5.15.x更新到Linux 6.2.16的新内核"></a>1.从5.15.x更新到Linux 6.2.16的新内核</h2><p>PVE8(Beta)默认搭载Linux6.2.16新内核，只有待下一个LinuxKernelLTS时才会在正式版中搭载长期服务分支内核。</p><p>6.2.x内核相较于5.15.x内核的主要变化</p><blockquote><p>1.添加更多设备支持：优化Intel13代等。几乎支持至2023-06的所有硬件</p><p>2.内核支持Rust</p><p>3.Btrfs，EXT4文件系统性能改进</p></blockquote><h2 id="PVE7-4-8"><a href="#PVE7-4-8" class="headerlink" title="PVE7.4-&gt;8"></a>PVE7.4-&gt;8</h2><p>主要变化</p><blockquote><p>1.自动优化Ceph集群配置</p><p>2.完善笔记对markdown的支持</p><p><strong>3.手机访问管理页面UI优化</strong></p><p>4.QEMU8</p></blockquote><h1 id="通过包管理器更新到最新版本"><a href="#通过包管理器更新到最新版本" class="headerlink" title="通过包管理器更新到最新版本"></a>通过包管理器更新到最新版本</h1><p>首先，通过ssh或者在网页终端输入命令，将PVE7.x更新到7.4.x最新版本</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">apt update</span><br><span class="line">apt dist-upgrade</span><br></pre></td></tr></table></figure><p>接着，运行</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pve7to8 --full</span><br></pre></td></tr></table></figure><p>检查有没有什么不兼容的部分（主要是各种软件包）</p><p>如果输出的内容包含FAILURES: 0，则可以更新，否则，你可以根据提示排障。</p><p>然后，在终端执行以下命令，切换到新的存储库</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sed -i <span class="string">&#x27;s/bullseye/bookworm/g&#x27;</span> /etc/apt/sources.list</span><br></pre></td></tr></table></figure><p>使用ls命令查看&#x2F;etc&#x2F;apt&#x2F;sources.list.d&#x2F;路径的各种文件，将里面的其它版本库文件一同切换到新版本。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">root@pve:/etc/apt/sources.list.d# <span class="built_in">ls</span></span><br><span class="line">docker.list  pve-enterprise.list  pve-no-subscription.list</span><br><span class="line">root@pve:/etc/apt/sources.list.d# sed -i <span class="string">&#x27;s/bullseye/bookworm/g&#x27;</span> docker.list</span><br><span class="line">root@pve:/etc/apt/sources.list.d# sed -i <span class="string">&#x27;s/bullseye/bookworm/g&#x27;</span> pve-no-subscription.list</span><br></pre></td></tr></table></figure><p>如果不需要使用PVE8的测试版，以下切换分支内容可以忽略（2023-6-24 15:33:37）</p><p>由于目前PVE8还是测试版我们还必须将PVE版本库分支切换为测试分支。</p><p>（pve-no-subscription为我的PVE包存储库仓库文件，可能和你的不一样）</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sed -i <span class="string">&#x27;s/pve-no-subscription/pvetest/g&#x27;</span> pve-no-subscription.list</span><br></pre></td></tr></table></figure><p>然后，更新包存储器和系统 并重启</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">apt update</span><br><span class="line">apt full-upgrade -y</span><br><span class="line">reboot</span><br></pre></td></tr></table></figure><p>重启后，你将会在左上角看到PVE8.</p><p><img src="/2023/06/16/Proxmox-Virtual-Environment-7-x%E5%8D%87%E7%BA%A78-0%E6%95%99%E7%A8%8B/11.png" class="lazyload" data-srcset="/2023/06/16/Proxmox-Virtual-Environment-7-x%E5%8D%87%E7%BA%A78-0%E6%95%99%E7%A8%8B/11.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="PVE8"></p><p><img src="/2023/06/16/Proxmox-Virtual-Environment-7-x%E5%8D%87%E7%BA%A78-0%E6%95%99%E7%A8%8B/12.png" class="lazyload" data-srcset="/2023/06/16/Proxmox-Virtual-Environment-7-x%E5%8D%87%E7%BA%A78-0%E6%95%99%E7%A8%8B/12.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="PVE8"></p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/06/16/Proxmox-Virtual-Environment-7-x%E5%8D%87%E7%BA%A78-0%E6%95%99%E7%A8%8B/</id>
    <link href="https://www.a-enjoy.top/2023/06/16/Proxmox-Virtual-Environment-7-x%E5%8D%87%E7%BA%A78-0%E6%95%99%E7%A8%8B/"/>
    <published>2023-06-16T14:16:14.000Z</published>
    <summary>Debian 12已经发布了，紧跟Debian发布节奏的PVE也随之发布了8.0（Beta）[目前已经正式版了2023-6-24 15:30:36]。本文将展示PVE8的一些新特性和如何升级至最新版本。</summary>
    <title>Proxmox Virtual Environment 7.x升级8.0教程</title>
    <updated>2023-06-24T07:40:40.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="vue" scheme="https://www.a-enjoy.top/tags/vue/"/>
    <category term="web" scheme="https://www.a-enjoy.top/tags/web/"/>
    <content>
      <![CDATA[<p>现在的应用基本都是前后端分离，在国内，Vue相对来说更受欢迎一些，我们很多后端系统的前端都是Vue来开发的。那么Vue UI框架就显得尤为重要。Vue UI框架提供了一套丰富的用户界面组件及完整的前端UI解决方案，如按钮、表单、导航、布局等，使得开发者可以专注于业务逻辑开发，而不用从零构建这些常用的界面元素，帮助开发者更快速地构建高质量的用户界面，对于初学者，我们想要从0开始手撸显然是不现实的，在学习如何使用Vue后，最好就是从各种开源框架开始使用，快速提升自己的开发能力。</p><span id="more"></span><h2 id="Element-Plus"><a href="#Element-Plus" class="headerlink" title="Element Plus"></a>Element Plus</h2><p>Element Plus是一套为开发者、设计师和产品经理准备的基于Vue 3的组件库，提供了丰富的组件和实用的工具，更快的渲染性能等。它是国内饿了么前端团队推出的一款基于Vue.js 2.0 的桌面端UI框架的延伸和更新。它包括基本布局、表单元素、导航、数据表格、消息通知等，涵盖了开发中大部分场景的需求，这也是国内比较火的一套框架了，网上的相关教程最多，极力推荐。</p><p><img src="/images/6.png" class="lazyload" data-srcset="/images/6.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image-20230508234758274"></p><p><a href="https://element-plus.gitee.io/zh-CN/">一个 Vue 3 UI 框架 | Element Plus (gitee.io)</a></p><h2 id="Vuetify"><a href="#Vuetify" class="headerlink" title="Vuetify"></a>Vuetify</h2><p>Vuetify是一个基于Material Design风格的Vue 3组件库，提供了丰富的布局、组件和功能，可以帮助开发人员快速构建现代化的响应式应用程序。如果你熟悉和喜欢AndroidM+开发风格，相信你也一定会喜欢这个库。此外，它的质量、更新频率和文档完整程度都要略优于国内的库。</p><p><img src="/images/7.png" class="lazyload" data-srcset="/images/7.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image7"></p><p><a href="https://vuetifyjs.com/en/">Vuetify — A Vue Component Framework (vuetifyjs.com)</a></p><h2 id="TinyVue"><a href="#TinyVue" class="headerlink" title="TinyVue"></a>TinyVue</h2><p>它是华为云出品的企业级设计体系。由大厂背书应用的框架不会缺乏各种各样的组件，它的文档也是相当完善，简单易用。</p><p><img src="/images/8.png" class="lazyload" data-srcset="/images/8.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image8"></p><p><a href="https://opentiny.design/">OpenTiny - 企业级产品的完整设计和开发解决方案</a></p><h2 id="Naive-UI"><a href="#Naive-UI" class="headerlink" title="Naive UI"></a>Naive UI</h2><p>Naive UI 是一个基于 Vue 3 的现代化的、模块化的 UI 组件库，它的特点就是轻便，快，开发也简单，面向本地化，中文文档教完善。</p><p><img src="/images/9.png" class="lazyload" data-srcset="/images/9.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image9"></p><p><a href="https://www.naiveui.com/zh-CN">Naive UI: 一个 Vue 3 组件库</a></p><h2 id="vue-material"><a href="#vue-material" class="headerlink" title="vue-material"></a>vue-material</h2><p>又是一款符合Vue.js和Google的Material Design规范开发的UI框架，对于移动界面的适配非常友好。</p><p><img src="/images/10.png" class="lazyload" data-srcset="/images/10.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image10"></p><p><a href="https://www.creative-tim.com/vuematerial">Vue Material - Material Design for Vue.js (creative-tim.com)</a></p><h2 id="iView"><a href="#iView" class="headerlink" title="iView"></a>iView</h2><p>iView是一个基于Vue.js的高质量UI组件库，拥有丰富的组件和方便的主题定制功能。</p><p><img src="/images/admin-preview.png" class="lazyload" data-srcset="/images/admin-preview.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="img"></p><p><a href="https://github.com/iview/iview">GitHub - iview&#x2F;iview: A high quality UI Toolkit built on Vue.js 2.0</a></p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/05/09/%E6%8E%A8%E8%8D%906%E4%B8%AA%E5%BC%80%E6%BA%90Vue%E6%A1%86%E6%9E%B6-%E5%BF%AB%E9%80%9F%E6%9E%84%E5%BB%BA%E5%87%BA%E7%BE%8E%E8%A7%82%E7%9A%84Web%E5%BA%94%E7%94%A8/</id>
    <link href="https://www.a-enjoy.top/2023/05/09/%E6%8E%A8%E8%8D%906%E4%B8%AA%E5%BC%80%E6%BA%90Vue%E6%A1%86%E6%9E%B6-%E5%BF%AB%E9%80%9F%E6%9E%84%E5%BB%BA%E5%87%BA%E7%BE%8E%E8%A7%82%E7%9A%84Web%E5%BA%94%E7%94%A8/"/>
    <published>2023-05-08T16:12:57.000Z</published>
    <summary>现在的应用基本都是前后端分离，在国内，Vue相对来说更受欢迎一些，我们很多后端系统的前端都是Vue来开发的。那么Vue UI框架就显得尤为重要。Vue UI框架提供了一套丰富的用户界面组件及完整的前端UI解决方案，如按钮、表单、导航、布局等，使得开发者可以专注于业务逻辑开发，而不用从零构建这些常用的界面元素，帮助开发者更快速地构建高质量的用户界面，对于初学者，我们想要从0开始手撸显然是不现实的，在</summary>
    <title>推荐6个开源Vue框架-快速构建出美观的Web应用</title>
    <updated>2023-05-08T16:13:21.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="PVE" scheme="https://www.a-enjoy.top/tags/PVE/"/>
    <category term="N5105" scheme="https://www.a-enjoy.top/tags/N5105/"/>
    <content>
      <![CDATA[<p>换源加快PVE更新&#x2F;下载包速度，更新内核和Intel微码提升天坑N5105工控机跑PVE虚拟机的稳定性</p><p>康耐信N5105 0x0步进第一版i225-v3</p><span id="more"></span><p>本文阅读约12分钟。</p><p>本文最后更新于2023-5-8 23:26:13 版本v2。</p><h1 id="一-N5105机器自身问题带来的噩梦"><a href="#一-N5105机器自身问题带来的噩梦" class="headerlink" title="一.N5105机器自身问题带来的噩梦"></a>一.N5105机器自身问题带来的噩梦</h1><p>我手上有两台N5105工控机，一台是康耐信的，一台是畅网的。</p><p>前者用来跑虚拟化，简直是噩梦，后者裸机单跑<a href="https://github.com/miaoermua/CatWrt">CatWrt</a>(OpenWrt)，两台设备都是<strong>第一版</strong>N5105设备。如果你不使用虚拟化技术，那么本文对你参考意义不大。</p><p>说说我2022年10月左右玩虚拟化遇到的问题吧</p><p>1.Windows Server 2022：网卡直通失败</p><p>2.PVE异常</p><p>后来，实在忍受不了N5105的虚拟化问题，在网上看到大家都抱怨N5105虚拟化稳定性后，我选择了单跑了近半年的OpenWrt。直至看了网友们说的，新版本固件软件都优化了，遂于五一节重新搞起了虚拟化，确实坑变少了许多。最核心的解决方案还是更换硬件，如果不能更换，那用软件精修一下毛坯房也不是不行(</p><blockquote><p>2023-5-8 23:19:37 更新： 目前所有的虚拟机已经稳定运行四天，PCDN也保持了四天在线的记录。</p></blockquote><h2 id="N5105虚拟化的问题"><a href="#N5105虚拟化的问题" class="headerlink" title="N5105虚拟化的问题"></a>N5105虚拟化的问题</h2><p>看了网上很多关于IntelN5105虚拟化踩坑的帖子，无外乎就是这两点：</p><p><strong>虚拟机死机，PCIe设备直通失败</strong></p><p>目前给出的回复大致都是这样的</p><p>第一版N5105工控机存在缺陷（他们往往配置的都是i225-v3网卡，CPU步进0x0），如果是第一代，还是放弃直通吧，真没办法(</p><blockquote><p>问题列表：</p><p>1.虚拟机频繁死机，重启</p><p>2.Host死机，未响应</p><p>3.网卡直通错乱（i226一般没这个问题）</p><p>4.无网络</p><p>5.管理页面打不开</p><p>6.虚拟机无法重启，只能关闭PVE重新启动设备。</p></blockquote><p>在没有更新系统前，我的OpenWrt虚拟机一天死机一次，网心云x86几个小时就死机一次，更新后两者都已经稳定运行两天多了（5-5到5-7）</p><p><img src="/images/5.png" class="lazyload" data-srcset="/images/5.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="CatWrt稳定运行"></p><p>第三版及之后的，由于使用了更新的BIOS固件，更新的CPU，以及改善的i226，解决了很多问题。据chiphell论坛网友们的评论，基本上解决了N5105虚拟化死机和直通异常的问题。</p><h2 id="查看自己的硬件在不在天坑里"><a href="#查看自己的硬件在不在天坑里" class="headerlink" title="查看自己的硬件在不在天坑里"></a>查看自己的硬件在不在天坑里</h2><p>最简单的，在你购买的渠道询问：这是第几版的N5105工控机 如果卖家回答你：第一版的</p><p>那么，恭喜你！中奖了 ！早期天坑版本。</p><p>或者是查看商品信息，一般会写着网卡型号：如果是i255，一般是第一版的；i226一般是较新的，但不好判断，主要还是得手动查看。</p><p>一般来说，只有第一版的N5105是0x0CPU步进的，非常容易辨识。</p><p>在Linux终端里输入如下命令</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cat</span> /proc/cpuinfo | grep stepping</span><br></pre></td></tr></table></figure><p>如果输出的有</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">stepping        : 0</span><br></pre></td></tr></table></figure><p>那么，恭喜你！中奖了 ！早期天坑版本。</p><p>天坑们往往会携带着又一天坑网卡：<strong>I225-V (rev 03)</strong></p><p>在Linux终端里输入如下命令</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">lspci | grep <span class="string">&quot;Ethernet controller&quot;</span></span><br></pre></td></tr></table></figure><p>如果输出的有</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">03:00.0 Ethernet controller: Intel Corporation Ethernet Controller I225-V (rev 03)</span><br><span class="line">04:00.0 Ethernet controller: Intel Corporation Ethernet Controller I225-V (rev 03)</span><br><span class="line">05:00.0 Ethernet controller: Intel Corporation Ethernet Controller I225-V (rev 03)</span><br><span class="line">06:00.0 Ethernet controller: Intel Corporation Ethernet Controller I225-V (rev 03)</span><br></pre></td></tr></table></figure><p>那么，恭喜你！中奖了 ！该网卡不适合虚拟机直通，不过，可以通过创建网桥&#x2F;半虚拟化方式来使用。</p><p>（曲线救国方案：不支持模拟mac设备，不能修改mac地址，不支持在系统内创建桥接）</p><p><strong>总而言之，查看是不是第一版的天坑版本有两种有效的方法：查看CPU步进，询问卖家。</strong></p><h1 id="二-更新PVE以填部分坑"><a href="#二-更新PVE以填部分坑" class="headerlink" title="二.更新PVE以填部分坑"></a>二.更新PVE以填部分坑</h1><p><strong>PVE 是什么</strong></p><p>据 PVE 的<a href="https://pve.proxmox.com/wiki/Main_Page">官网介绍</a>，PVE 是Proxmox Virtual Environment 的缩写， 是一个完整的开源服务器管理平台，用于企业虚拟化。它将 KVM 管理程序和 Linux 容器 (LXC)、软件定义的存储和网络功能紧密集成在一个平台上。借助基于 Web 的集成用户界面，您可以轻松管理 VM 和容器、集群的高可用性或集成的灾难恢复工具。</p><p>它是基于Debian11的系统，因此，所有的包管理操作同Debian一致，可玩性也十分强大。</p><p><img src="/images/Proxmox-VE-Cluster-Summary.png" class="lazyload" data-srcset="/images/Proxmox-VE-Cluster-Summary.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="File:Proxmox-VE-Cluster-Summary.png"></p><p><strong>PVE下载与安装：</strong></p><p>网上教程较多，本文不再赘述</p><p>在新版的PVE内核中（推荐5.17+），优化了N5105的整体稳定性和核显稳定性，更好用了。</p><h2 id="换源"><a href="#换源" class="headerlink" title="换源"></a>换源</h2><h3 id="一、更换阿里云的源"><a href="#一、更换阿里云的源" class="headerlink" title="一、更换阿里云的源"></a>一、更换阿里云的源</h3><p>在终端中输入</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">vi /etc/apt/sources.list</span><br></pre></td></tr></table></figure><p>将里面的内容全部替换为：（换成其它源也可以）</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib`</span><br><span class="line"></span><br><span class="line">deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib</span><br><span class="line"></span><br><span class="line">deb http://mirrors.aliyun.com/debian-security/ bullseye-security main</span><br><span class="line"></span><br><span class="line">deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main</span><br><span class="line"></span><br><span class="line">deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib</span><br><span class="line"></span><br><span class="line">deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib</span><br><span class="line"></span><br><span class="line">deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib</span><br><span class="line"></span><br><span class="line">deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib</span><br></pre></td></tr></table></figure><h3 id="二、去除企业版源并增加社区源"><a href="#二、去除企业版源并增加社区源" class="headerlink" title="二、去除企业版源并增加社区源"></a>二、去除企业版源并增加社区源</h3><p>在终端中输入</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> /etc/apt/sources.list.d</span><br><span class="line">vi pve-enterprise.list</span><br></pre></td></tr></table></figure><p>注释掉：（增加#号）</p><p>找到对应行，输入i，然后注释</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">#deb https://enterprise.proxmox.com/debian/pve buster pve-enterprise</span><br></pre></td></tr></table></figure><p>按ESC -&gt; :wq 退出</p><p>接着：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">touch</span> pve-no-subscription.list</span><br><span class="line">vi pve-no-subscription.list</span><br></pre></td></tr></table></figure><p>然后输入i -&gt;</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">deb https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/pve bullseye pve-no-subscription</span><br></pre></td></tr></table></figure><p>ESC -&gt; :wq 退出</p><h3 id="三、清理、更新系统"><a href="#三、清理、更新系统" class="headerlink" title="三、清理、更新系统"></a>三、清理、更新系统</h3><p>使用下面命令清理系统冗余并更新软件包列表</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">apt clean </span><br><span class="line">apt autoclean </span><br><span class="line">apt autoremove --purge</span><br><span class="line">apt update</span><br></pre></td></tr></table></figure><p>注释掉5.15内核，否则在全部更新的时候会更新5.15版本</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">apt-mark hold pve-kernel-5.15</span><br></pre></td></tr></table></figure><p>安装最新内核及更新系统：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">apt install pve-kernel-6.2.11-1-pve -y</span><br><span class="line"><span class="comment">#截至当前发文，最新版本内核已经更新到6.2.11了，而我的是两天前安装的6.2.9</span></span><br><span class="line">apt dist-upgrade -y</span><br></pre></td></tr></table></figure><p>安装&#x2F;更新Intel微码</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">apt install intel-microcode -y</span><br></pre></td></tr></table></figure><p>接着输入reboot命令重启系统等待微码更新和内核启动即可。</p><p>重启后使用uname -a查看内核信息</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">root@pve:~# uname -a</span><br><span class="line">Linux pve 6.2.9-1-pve #1 SMP PREEMPT_DYNAMIC PVE 6.2.9-1 (2023-03-31T10:48Z) x86_64 GNU/Linux</span><br></pre></td></tr></table></figure><h1 id="三、PVE虚拟机注意事项（对于第一版）"><a href="#三、PVE虚拟机注意事项（对于第一版）" class="headerlink" title="三、PVE虚拟机注意事项（对于第一版）"></a>三、PVE虚拟机注意事项（对于第一版）</h1><p>本段是根据个人所遇到的情况及相应的处理方案，不一定适用于每个人。</p><p>由于我购置的康耐信N5105 r86s工控机是四网口的，可以通过单独为PVE准备一个WAN口解决PVE内无法上网的问题，还可以为此创建docker的macvlan网络。双网口可以参考双网口的文章。</p><h2 id="个人网络情况"><a href="#个人网络情况" class="headerlink" title="个人网络情况"></a>个人网络情况</h2><p><img src="/images/1.png" class="lazyload" data-srcset="/images/1.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image-20230506234046359"></p><p>网口1 enp3s0：接上游网络设备(光猫拨号设备&#x2F;另一个网关) <em><strong>已创建桥接</strong></em>（相当于虚拟机的WAN）</p><p>网口2 enp4s0：接路由器（AP及下游网络设备）<em><strong>已创建桥接</strong></em> （为虚拟化路由器系统（<a href="https://github.com/miaoermua/CatWrt">CatWrt</a>）准备的LAN）</p><p>网口3 enp5s0：接上游网络设备（用于做管理口和PVE上网的网口 相当于PVE的WAN）</p><p>网口4 闲置</p><h2 id="1-使用半虚拟化代替硬件直通"><a href="#1-使用半虚拟化代替硬件直通" class="headerlink" title="1.使用半虚拟化代替硬件直通"></a>1.使用半虚拟化代替硬件直通</h2><p>由于第一版N5105及其i225网卡的缺陷，直通将可能导致虚拟机内掉网卡，掉速和拔插网线死机，虚拟机外（Host）网口错乱等问题，故不宜选用此方案。</p><p>但我们总得解决设备的网卡需求，因此，我们选用半虚拟化方案。</p><p>首先，在数据中心-&gt;pve-&gt;系统-&gt;网络-&gt;创建-&gt;Linux Bridge</p><p>我为我的网络创建了两个bridge，分别对应WAN(vmbr0,enp3s0)和LAN(vmbr1,enp4s0)</p><p><img src="/images/2.png" class="lazyload" data-srcset="/images/2.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image-20230506235614503"></p><p>名称填自己喜欢的，桥接端口自己选择网口，<strong>勾选自动启动</strong></p><p>注意：<strong>在创建完网络设备和设置网关等信息后需要点击应用配置以使设置生效</strong></p><p>在虚拟机上，添加网络设备</p><p><img src="/images/3.png" class="lazyload" data-srcset="/images/3.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image-20230507000430199"></p><p>选择自己刚刚添加的设备，模型选择 <strong>VirtIO(半虚拟化)</strong> <strong>这对稳定性非常重要！</strong></p><p>防火墙选项：如果没有在PVE下管理流量的需求，可以不勾选。</p><h2 id="2-为PVE配置一个单独的WAN口"><a href="#2-为PVE配置一个单独的WAN口" class="headerlink" title="2.为PVE配置一个单独的WAN口"></a>2.为PVE配置一个单独的WAN口</h2><p>为什么需要为PVE单独创建一个WAN口而不复用vmbr0呢？</p><h3 id="问题1：PVE无法上网"><a href="#问题1：PVE无法上网" class="headerlink" title="问题1：PVE无法上网"></a>问题1：PVE无法上网</h3><p>如果在没有创建使用vmbr0的虚拟机前，当然是没有问题的，但当我们创建虚拟机后，就会遇到同网上一样的问题：</p><blockquote><p>PVE Host无法上网：网关，IP设置正确也无效</p></blockquote><p>如果PVE_WAN复用的是LAN（OpenWrt的LAN）</p><blockquote><p>有可能出现以下两种错误：</p><p>1 Temporary failure in name resolution</p><p>直接报dns的错误</p><p>2 failed: Network is unreachable 或Destination Host Unreachable</p></blockquote><p>参考CSDN文章：<a href="https://blog.csdn.net/IcarusAegis/article/details/125823095">Here</a></p><p>如果PVE_WAN复用的是WAN（vmbr0）,参考下文。</p><h3 id="问题2：无法在网卡上再创建桥接网络设备（vlan，macvlan）"><a href="#问题2：无法在网卡上再创建桥接网络设备（vlan，macvlan）" class="headerlink" title="问题2：无法在网卡上再创建桥接网络设备（vlan，macvlan）"></a>问题2：无法在网卡上再创建桥接网络设备（vlan，macvlan）</h3><blockquote><p>vmbr0，vmbr1 对应enp3s0,enp4s0</p></blockquote><p>假如需要创建多个虚拟WAN，那么在如上（vmbr0，vmbr1 ，enp3s0,enp4s0）设备无法创建虚拟的VLAN设备以达到桥接创建虚拟网口的目的，包括docker的macvlan，当我们指定这些网络设备时会提示设备繁忙。</p><p>比如</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker network create -d macvlan --subnet=192.168.31.0/24 --gateway=192.168.31.1 -o parent=enp3s0 <span class="built_in">test</span></span><br></pre></td></tr></table></figure><p>会提示设备繁忙无法创建（或者创建成功，但我们在添加容器时提示繁忙）。</p><h3 id="创建WAN的方法："><a href="#创建WAN的方法：" class="headerlink" title="创建WAN的方法："></a>创建WAN的方法：</h3><p>有两种途径创建WAN。</p><h4 id="1-使用PVE的Web图形化页面创建WAN口"><a href="#1-使用PVE的Web图形化页面创建WAN口" class="headerlink" title="1.使用PVE的Web图形化页面创建WAN口"></a>1.使用PVE的Web图形化页面创建WAN口</h4><p>在数据中心-&gt;pve-&gt;系统-&gt;网络，选择一个未桥接的接口，设置静态IP信息。（网关，IP地址，子网长度）</p><p><img src="/images/4.png" class="lazyload" data-srcset="/images/4.png" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="image-20230507002048100"></p><p>然后点击<strong>应用配置</strong></p><h4 id="2-在终端中配置（推荐动手能力高的）"><a href="#2-在终端中配置（推荐动手能力高的）" class="headerlink" title="2.在终端中配置（推荐动手能力高的）"></a>2.在终端中配置（推荐动手能力高的）</h4><p>输入命令</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">vi /etc/network/interfaces</span><br></pre></td></tr></table></figure><p>手动配置网卡信息。</p><p>在enp5s0区段下修改如下信息：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">auto enp5s0</span><br><span class="line">iface enp5s0 inet static</span><br><span class="line">        address 192.168.31.41/24</span><br><span class="line">        gateway 192.168.31.1</span><br></pre></td></tr></table></figure><p>重启网络</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">service networking restart</span><br></pre></td></tr></table></figure><h3 id="配置路由、DNS信息"><a href="#配置路由、DNS信息" class="headerlink" title="配置路由、DNS信息"></a>配置路由、DNS信息</h3><p>如果不执行这一步，你会发现PVE已经出现在了192.168.31.1&#x2F;24网段了，即已经连接上路由器了，但依然无法访问外网。这是因为路由信息异常导致的，需要单独设置默认网卡和默认路由。</p><p>设置enp5s0为默认路由</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ip route add default via 192.168.100.1 dev enp5s50</span><br><span class="line"><span class="comment">#或</span></span><br><span class="line">route add default gw 192.168.31.1 enp5s0</span><br></pre></td></tr></table></figure><p>查看配置结果：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">ip route show</span><br></pre></td></tr></table></figure><p>如果出现</p><blockquote><p>default via 192.168.31.1 dev enp5s0 proto kernel onlink</p><p>即成功</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">root@pve:~# ip route show</span><br><span class="line">default via 192.168.31.1 dev enp5s0 proto kernel onlink</span><br><span class="line">172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1</span><br><span class="line">192.168.31.0/24 dev enp5s0 proto kernel scope link src 192.168.31.41</span><br></pre></td></tr></table></figure></blockquote><p>接着配置dns</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">vi /etc/resolv.conf</span><br></pre></td></tr></table></figure><p>添加dns</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">search com</span><br><span class="line">nameserver 192.168.31.1</span><br><span class="line">nameserver 8.8.8.8</span><br></pre></td></tr></table></figure><p>有关PVE的IPv6配置将在后续到来。</p><p>测试：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">root@pve:~# curl ip.3322.net</span><br><span class="line">61.180.102.151</span><br></pre></td></tr></table></figure><p>成功连接Internet。</p><p>当然，此后，我们可以在enp5s0 直接创建macvlan网络，vlan设备了</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">root@pve:~# docker network create -d macvlan --subnet=192.168.31.0/24 --gateway=192.168.31.1 -o parent=enp5s0 dianxin</span><br><span class="line">0ebcd6c39b7e812665ba5176c088b715e196923fca1d3f75aef842dc2f1861d0</span><br><span class="line">root@pve:~# docker network ls</span><br><span class="line">NETWORK ID     NAME      DRIVER    SCOPE</span><br><span class="line">c1a286028953   bridge    bridge    local</span><br><span class="line">0ebcd6c39b7e   dianxin   macvlan   local</span><br><span class="line">1babb0e980ee   host      host      local</span><br><span class="line">ecb39f108081   none      null      local</span><br></pre></td></tr></table></figure><p>End.</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/05/07/ProxmoxVE-7-x-%E6%8D%A2%E6%BA%90%E5%8F%8A%E6%9B%B4%E6%96%B0%E7%B3%BB%E7%BB%9F%E5%86%85%E6%A0%B8%E4%BB%A5%E6%8F%90%E5%8D%87%E6%95%B4%E4%BD%93%E7%A8%B3%E5%AE%9A%E6%80%A7/</id>
    <link href="https://www.a-enjoy.top/2023/05/07/ProxmoxVE-7-x-%E6%8D%A2%E6%BA%90%E5%8F%8A%E6%9B%B4%E6%96%B0%E7%B3%BB%E7%BB%9F%E5%86%85%E6%A0%B8%E4%BB%A5%E6%8F%90%E5%8D%87%E6%95%B4%E4%BD%93%E7%A8%B3%E5%AE%9A%E6%80%A7/"/>
    <published>2023-05-06T17:10:39.000Z</published>
    <summary>换源加快PVE更新/下载包速度，更新内核和Intel微码提升天坑N5105工控机跑PVE虚拟机的稳定性 康耐信N5105 0x0步进第一版i225-v3</summary>
    <title>ProxmoxVE 7.x 换源及更新系统内核以提升IntelN5105机器的整体稳定性</title>
    <updated>2023-05-08T15:26:33.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <category term="Diary" scheme="https://www.a-enjoy.top/categories/Diary/"/>
    <category term="Move" scheme="https://www.a-enjoy.top/tags/Move/"/>
    <content>
      <![CDATA[<p>记录一次自己发文地点变更想说的话</p><span id="more"></span><p>哈，呼~！已经很久没有写东西了，包括在酷安，IT之家上都很少发消息了。现在总要写点什么吧，证明自己在996下制度依然挣扎着，活了下来(不是)。</p><p>时间匆匆而过，人已无少年。时不用以往，现是早上8时起床，开始一整天的工作与学习的生活(摸鱼)，中午，晚上在移动公司兼职，没什么时间搭理自己过去写的东西，也没兴趣写东西（其实是懒了）。</p><p>在很久很久之前（2013~2018），我在自己的站点上（shenguo.esy.es）写文章，分享一些Windows系统的小工具，不过主机与域名都已经收回了，况且那段时间写的东西也不堪入目，就不想再拿出来了，也没什么值得发出来的了。</p><p>在后来，2020年疫情居家，在酷安上写过几篇很长的图文，主要是Android 搞机有关的文章，最近我会抽时间把文章给搬回来。</p><p>五一期间，有了不少时间可以做自己想做的事。看着朋友们都在CSDN、自己个人站点、知乎发文，觉得自己不该摆烂了，才重新拾起了写博文的想法。</p><p>我还是我，人已随着年龄改变了，不变的是对互联网的热情。</p><p>2020.3~2022.2：</p><p>自学Python，结合自己想做一个Android ADB工具包的idea，写了两个项目</p><p><a href="https://github.com/AEnjoy/adbshellpy">adbshellpy</a></p><p><a href="https://github.com/AEnjoy/unpackandroidrom">Android ROM解包工具</a></p><p>2022.6:</p><p>自学C++DuiLib框架，基于vspoy的Adobe安装包，做了一个<a href="https://github.com/AEnjoy/Adobeproductioninstalltool">联网安装器</a></p><p>2022.10买了N5105四网口工控机，开始了噩梦般的软路由&#x2F;NAS&#x2F;PCDN&#x2F;虚拟化搞机生活。</p><p>还好，在众览网友们的N5105玩机经验后，前几天完成了在天坑的Intel N5105平台上玩虚拟化的骚操作。</p><p>蓦然回首，畅享IT之海依然是生活最重要的一部分。我会在这里分享我个人的玩机过程，分享生活中的杂碎。</p><p>MySkills&amp;Love</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">C,C++,Java,C#,Python,PyQt,C++Qt,PyTkinter,DuiLib,SpringBoot and IoTcommunication(MQTT,MODBUS)</span><br></pre></td></tr></table></figure><p>MyLearning:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Go,Web前后端,Docker&amp;k8s集群 and 云计算</span><br></pre></td></tr></table></figure><p>By AEnjoy，2023-5-3 00:29:10.</p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/05/02/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E5%8D%9A%E5%AE%A2%E7%9A%84%E8%BF%81%E7%A7%BB/</id>
    <link href="https://www.a-enjoy.top/2023/05/02/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E5%8D%9A%E5%AE%A2%E7%9A%84%E8%BF%81%E7%A7%BB/"/>
    <published>2023-05-02T15:49:07.000Z</published>
    <summary>记录一次自己发文地点变更想说的话</summary>
    <title>
      <![CDATA[About me & My Blog]]>
    </title>
    <updated>2023-05-02T16:32:18.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>AEnjoy</name>
    </author>
    <content>
      <![CDATA[<p>Welcome to <a href="https://hexo.io/">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/">documentation</a> for more info. If you get any problems when using Hexo, you can find the answer in <a href="https://hexo.io/docs/troubleshooting.html">troubleshooting</a> or you can ask me on <a href="https://github.com/hexojs/hexo/issues">GitHub</a>.</p><h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><h3 id="Create-a-new-post"><a href="#Create-a-new-post" class="headerlink" title="Create a new post"></a>Create a new post</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ hexo new <span class="string">&quot;My New Post&quot;</span></span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/writing.html">Writing</a></p><h3 id="Run-server"><a href="#Run-server" class="headerlink" title="Run server"></a>Run server</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ hexo server</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/server.html">Server</a></p><h3 id="Generate-static-files"><a href="#Generate-static-files" class="headerlink" title="Generate static files"></a>Generate static files</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ hexo generate</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/generating.html">Generating</a></p><h3 id="Deploy-to-remote-sites"><a href="#Deploy-to-remote-sites" class="headerlink" title="Deploy to remote sites"></a>Deploy to remote sites</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ hexo deploy</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/one-command-deployment.html">Deployment</a></p>]]>
    </content>
    <id>https://www.a-enjoy.top/2023/05/02/hello-world/</id>
    <link href="https://www.a-enjoy.top/2023/05/02/hello-world/"/>
    <published>2023-05-02T15:23:35.000Z</published>
    <summary>Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick</summary>
    <title>Hello World</title>
    <updated>2023-05-02T15:23:35.000Z</updated>
  </entry>
</feed>
