<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>panghutx&#39;s Blog</title>
  
  
  <link href="https://youngsay.cn/atom.xml" rel="self"/>
  
  <link href="https://youngsay.cn/"/>
  <updated>2026-05-06T12:19:02.185Z</updated>
  <id>https://youngsay.cn/</id>
  
  <author>
    <name>panghutx</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>我这该死的责任感</title>
    <link href="https://youngsay.cn/2026/05/06/my-damn-sense-of-responsibility/"/>
    <id>https://youngsay.cn/2026/05/06/my-damn-sense-of-responsibility/</id>
    <published>2026-05-06T11:00:00.000Z</published>
    <updated>2026-05-06T12:19:02.185Z</updated>
    
    <content type="html"><![CDATA[<h2 id="我这该死的责任感">我这该死的责任感</h2><p>五一小长假就这样过去了，好在多请了一天假，今天不用去上班。但节前留了一个小bug，自动化执行工具会偶现账号不足的问题，因为节前敏感时期没有上线，心里一直不得劲，怕节后有更多的人反馈。我这该死的责任感。本该好好享受最后一天的假期，最终还是用了一上午的时间，测试并上线了。</p><p>Bug修复上线，测试通过后，第一感觉是有成就感，而不是说浪费了半天的休息时间。但又进一步想想，<strong>自己的价值、成就感更多是从工作上获取，是件可悲的事</strong>。你的辛苦付出也许不会被人看到，你的价值、成就也许在老板面前不值一提。在公司面前，你是成本，是人力，是创造生产力的工具。</p><p><strong>与其说是责任感过剩，不如说是被工作侵蚀，逐渐失去自己</strong>。不上班的日子偶尔竟然会感到无聊，失去了探索世界、提升自己的心气。每次工作日请假，只要是呆在家里，都把手机放在手边，甚至关闭静音模式，生怕错过重要的事。<strong>真是不值得</strong>。疫情之后，企业都在降本增效，又有了AI的加持，很多公司都在减员。当那一刻来临，没有几家企业会考虑你的付出、考虑你的处境，甚至他们觉得给足n+1都是福报了。</p><h2 id="了结了一件-大-事">了结了一件&quot;大&quot;事</h2><p>3月中旬，一张香港银行卡通过平信寄往大陆，因为平信没有手机号、没有物流追踪，小区又没有信箱，迟迟没有收到。4月中旬在邮政微邮局公众号反馈后有邮局工作人员联系我，询问了相关信息，说会告知投递员，帮我留意信件。直到今天，还是没有收到信件。</p><p>于是自己去居委会找了一下，找到了平信投递的地点。进门，值班室的盒子里，堆满了大大小小、无人领取的信件。在众多信件中，找到了我的，历时一个多月，终于拿到了自己的平信。在大陆，平信业务几乎名存实亡，在我的小区，邮递员不会投递上门，哪怕写了地址、联系方式。网上很多人反馈自己收不到信件、信件丢失，也许是被丢弃在某个被人遗忘的信箱里。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;我这该死的责任感&quot;&gt;我这该死的责任感&lt;/h2&gt;
&lt;p&gt;五一小长假就这样过去了，好在多请了一天假，今天不用去上班。但节前留了一个小bug，自动化执行工具会偶现账号不足的问题，因为节前敏感时期没有上线，心里一直不得劲，怕节后有更多的人反馈。我这该死的责任感。本该好好享</summary>
      
    
    
    
    <category term="随笔" scheme="https://youngsay.cn/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://youngsay.cn/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>躺盈记账：一个开源的个人资产追踪工具</title>
    <link href="https://youngsay.cn/2026/05/06/finance-aggregator-introduction/"/>
    <id>https://youngsay.cn/2026/05/06/finance-aggregator-introduction/</id>
    <published>2026-05-06T02:00:00.000Z</published>
    <updated>2026-05-06T12:55:13.757Z</updated>
    
    <content type="html"><![CDATA[<p>你是否曾经困惑于自己的投资到底赚了多少钱？</p><p>存入本金、取出资金、账户间转账……这些操作让简单的&quot;期末减期初&quot;计算变得毫无意义。如果你也有这样的烦恼，那么「躺盈记账」可能正是你需要的工具。</p><h2 id="项目简介">项目简介</h2><p><strong>躺盈记账</strong>（Finance Aggregator）是一个开源的个人资产追踪应用，核心功能是帮助用户记录多账户资产、追踪资金流水，并计算出<strong>真实的投资收益</strong>。</p><p>项目提供两个版本：</p><ul><li><strong>Web 版</strong>：基于 Next.js 16，部署在 Vercel，支持多用户</li><li><strong>桌面版</strong>：基于 Tauri 2，本地运行，数据完全离线</li></ul><h2 id="核心功能">核心功能</h2><h3 id="真实收益计算">真实收益计算</h3><p>这是项目最核心的功能。传统的收益计算方式是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">收益 = 期末资产 - 期初资产</span><br></pre></td></tr></table></figure><p>但这忽略了资金进出对资产变化的影响。躺盈记账采用的是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">真实收益 = 资产变动 - 净流入</span><br><span class="line">净流入 = 入金 + 转入 - 出金 - 转出</span><br></pre></td></tr></table></figure><p>这样，无论你中途存取多少次，都能准确知道自己的投资到底赚了多少。</p><h3 id="多账户管理">多账户管理</h3><p>支持四种账户类型：</p><ul><li><strong>国内平台</strong>：支付宝、微信理财等</li><li><strong>银行理财</strong>：各类银行理财产品</li><li><strong>券商账户</strong>：股票、基金投资</li><li><strong>海外平台</strong>：美股、港股等</li></ul><p>每个账户可设置不同币种，系统自动换算成人民币汇总。</p><h3 id="多币种支持">多币种支持</h3><p>支持 CNY、USD、HKD、EUR、JPY、GBP 六种货币，实时汇率来自 <a href="http://exchangerate-api.com">exchangerate-api.com</a>，本地缓存 1 小时。</p><h3 id="数据可视化">数据可视化</h3><ul><li>资产趋势图：展示近 30 天的资产变化曲线</li><li>币种分布图：饼图展示各币种资产占比</li><li>收益报表：按日/周/月/年/全部时间段查看收益</li></ul><h3 id="批量录入">批量录入</h3><p>支持一次性录入多个账户的资产数据，适合定期（如每日/每周）记录的场景。</p><p><img src="https://offer.youngsay.cn/blog/Snipaste_2026-05-06_20-53-21.png" alt=""></p><h2 id="技术架构">技术架构</h2><h3 id="Web-版">Web 版</h3><table><thead><tr><th>层级</th><th>技术选型</th></tr></thead><tbody><tr><td>框架</td><td>Next.js 16 (App Router)</td></tr><tr><td>前端</td><td>React 19 + TypeScript</td></tr><tr><td>样式</td><td>Tailwind CSS 4</td></tr><tr><td>UI 组件</td><td>Radix UI + shadcn/ui</td></tr><tr><td>图表</td><td>Recharts</td></tr><tr><td>数据库</td><td>PostgreSQL (Supabase)</td></tr><tr><td>ORM</td><td>Prisma</td></tr><tr><td>认证</td><td>NextAuth.js v5</td></tr></tbody></table><p>Web 版采用现代化的全栈架构，部署在 Vercel 上，数据库使用 Supabase 提供的 PostgreSQL。支持多用户注册登录，每个用户的数据完全隔离。</p><h3 id="桌面版">桌面版</h3><table><thead><tr><th>层级</th><th>技术选型</th></tr></thead><tbody><tr><td>桌面框架</td><td>Tauri 2</td></tr><tr><td>前端</td><td>React 19 + Vite</td></tr><tr><td>后端</td><td>Rust</td></tr><tr><td>数据库</td><td>SQLite (本地)</td></tr><tr><td>样式</td><td>Tailwind CSS 4</td></tr><tr><td>UI 组件</td><td>Radix UI</td></tr></tbody></table><p>桌面版使用 Tauri 而非 Electron，优势明显：</p><ul><li><strong>体积小</strong>：约 10MB vs Electron 的 150MB+</li><li><strong>内存占用低</strong>：使用系统 WebView，无需打包 Chromium</li><li><strong>安全性高</strong>：Rust 后端，内存安全有保障</li><li><strong>性能好</strong>：Rust 比 Node.js 后端更快</li></ul><p>数据完全存储在本地 SQLite 数据库中，无需联网即可使用（汇率查询除外）。</p><h2 id="两个版本如何选择？">两个版本如何选择？</h2><table><thead><tr><th>维度</th><th>Web 版</th><th>桌面版</th></tr></thead><tbody><tr><td><strong>数据存储</strong></td><td>云端（Supabase）</td><td>本地（SQLite）</td></tr><tr><td><strong>用户模式</strong></td><td>多用户，需注册登录</td><td>单用户，无需登录</td></tr><tr><td><strong>网络依赖</strong></td><td>必须联网</td><td>离线可用</td></tr><tr><td><strong>数据隐私</strong></td><td>数据存储在云端</td><td>数据完全本地</td></tr><tr><td><strong>跨设备</strong></td><td>任意设备浏览器访问</td><td>需安装应用</td></tr><tr><td><strong>部署成本</strong></td><td>Vercel 免费额度足够</td><td>无需服务器</td></tr></tbody></table><p><strong>推荐选择</strong>：</p><ul><li>如果你有多个设备需要同步数据，或希望随时随地访问 → <strong>Web 版</strong></li><li>如果你对数据隐私敏感，不想把财务数据放到云端 → <strong>桌面版</strong></li><li>如果你希望离线使用，不依赖网络 → <strong>桌面版</strong></li><li>如果你是开发者想快速部署体验 → <strong>Web 版</strong></li></ul><p>当然，两个版本可以同时使用，互不干扰。</p><h2 id="开源地址">开源地址</h2><ul><li>Web 版：<a href="https://github.com/panghutx/tangying">https://github.com/panghutx/tangying</a></li><li>桌面版：<a href="https://github.com/panghutx/finance-aggregator-desktop">https://github.com/panghutx/finance-aggregator-desktop</a></li></ul><h2 id="写在最后">写在最后</h2><p>这个项目诞生于一个简单的需求：<strong>我想知道我的投资到底赚了多少钱</strong>。市面上的记账软件要么功能过于复杂，要么无法处理资金进出的影响。于是五一期间闲来无事，vibe coding了这个工具。</p><p>如果你也有类似的需求，欢迎试用、反馈、贡献代码。希望「躺盈记账」能帮你更清晰地了解自己的财富增长。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;你是否曾经困惑于自己的投资到底赚了多少钱？&lt;/p&gt;
&lt;p&gt;存入本金、取出资金、账户间转账……这些操作让简单的&amp;quot;期末减期初&amp;quot;计算变得毫无意义。如果你也有这样的烦恼，那么「躺盈记账」可能正是你需要的工具。&lt;/p&gt;
&lt;h2 id=&quot;项目简介&quot;&gt;项目简介&lt;/h2</summary>
      
    
    
    
    <category term="项目" scheme="https://youngsay.cn/categories/%E9%A1%B9%E7%9B%AE/"/>
    
    
    <category term="React" scheme="https://youngsay.cn/tags/React/"/>
    
    <category term="Tauri" scheme="https://youngsay.cn/tags/Tauri/"/>
    
  </entry>
  
  <entry>
    <title>为 Hexo 博客添加说说功能</title>
    <link href="https://youngsay.cn/2026/05/02/hexo-moments-feature/"/>
    <id>https://youngsay.cn/2026/05/02/hexo-moments-feature/</id>
    <published>2026-05-02T12:00:00.000Z</published>
    <updated>2026-08-12T16:20:03.448Z</updated>
    
    <content type="html"><![CDATA[<p>最近给博客添加了一个「说说」功能，可以发布短内容、想法、观点，类似朋友圈。这篇文章记录了设计思路、实现过程和遇到的问题。</p><h2 id="需求背景">需求背景</h2><p>博客通常用来写长文章，但很多时候只想记录一些短想法、碎碎念，不值得写一篇完整的文章。说说功能就是为了解决这个问题：</p><ul><li>发布短内容，支持文字和图片</li><li>手机端也能方便发布</li><li>在首页侧边栏显示预览</li><li>有独立的聚合页面</li></ul><h2 id="技术方案选型">技术方案选型</h2><h3 id="内容存储">内容存储</h3><p>考虑了几个方案：</p><ol><li><strong>自建后端服务</strong> - 需要服务器、数据库，维护成本高</li><li><strong>第三方评论系统</strong> - 如 Valine、Waline，但数据不在自己手里</li><li><strong>GitHub Issues</strong> - 免费、稳定、支持 Markdown、手机可通过 GitHub App 操作</li></ol><p>最终选择了 <strong>GitHub Issues</strong>，理由：</p><ul><li>零成本，不需要额外服务器</li><li>数据完全可控，就在自己的仓库里</li><li>GitHub API 完善，方便读取</li><li>手机可以通过 GitHub App 或网页发布</li></ul><h3 id="前端展示">前端展示</h3><p>使用 Hexo 的数据文件功能，在构建时生成静态页面：</p><ul><li><code>source/_data/shuoshuo.yml</code> 存储说说数据</li><li>自定义 Pug 模板渲染页面</li><li>JavaScript 实现首页侧边栏预览</li></ul><h2 id="实现细节">实现细节</h2><h3 id="1-创建独立的-GitHub-Action">1. 创建独立的 GitHub Action</h3><p>为了方便复用，我创建了一个独立的 GitHub Action 仓库：<a href="https://github.com/panghutx/hexo-moments-action">hexo-moments-action</a></p><p><strong>核心功能</strong>：</p><ul><li>调用 GitHub API 获取带指定标签的 Issues</li><li>解析 Issue 内容，支持 Markdown 图片语法</li><li>生成 Hexo 数据文件 <code>shuoshuo.yml</code></li></ul><p><strong>Action 的核心逻辑</strong>：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 从 GitHub API 获取 Issues</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">fetchIssues</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(</span><br><span class="line">    <span class="string">`https://api.github.com/repos/<span class="subst">$&#123;owner&#125;</span>/<span class="subst">$&#123;repo&#125;</span>/issues?labels=<span class="subst">$&#123;label&#125;</span>&amp;state=open&amp;sort=created&amp;direction=desc`</span>,</span><br><span class="line">    &#123; <span class="attr">headers</span>: &#123; <span class="title class_">Authorization</span>: <span class="string">`token <span class="subst">$&#123;token&#125;</span>`</span> &#125; &#125;</span><br><span class="line">  );</span><br><span class="line">  <span class="keyword">return</span> response.<span class="title function_">json</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 解析内容，提取图片</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">parseContent</span>(<span class="params">body</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> imageRegex = <span class="regexp">/!\[.*?\]\((.*?)\)/g</span>;</span><br><span class="line">  <span class="keyword">const</span> images = [...body.<span class="title function_">matchAll</span>(imageRegex)].<span class="title function_">map</span>(<span class="function"><span class="params">m</span> =&gt;</span> m[<span class="number">1</span>]);</span><br><span class="line">  <span class="keyword">const</span> content = body.<span class="title function_">replace</span>(imageRegex, <span class="string">&#x27;&#x27;</span>).<span class="title function_">trim</span>();</span><br><span class="line">  <span class="keyword">return</span> &#123; content, images &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 生成 YAML 数据文件</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">generateYaml</span>(<span class="params">issues</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> issues.<span class="title function_">map</span>(<span class="function"><span class="params">issue</span> =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">date</span>: issue.<span class="property">created_at</span>,</span><br><span class="line">    <span class="attr">content</span>: <span class="title function_">parseContent</span>(issue.<span class="property">body</span>).<span class="property">content</span>,</span><br><span class="line">    <span class="attr">tags</span>: issue.<span class="property">labels</span>.<span class="title function_">map</span>(<span class="function"><span class="params">l</span> =&gt;</span> l.<span class="property">name</span>).<span class="title function_">filter</span>(<span class="function"><span class="params">n</span> =&gt;</span> n !== label),</span><br><span class="line">    <span class="attr">key</span>: <span class="string">`moment-<span class="subst">$&#123;issue.number&#125;</span>`</span></span><br><span class="line">  &#125;));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Action 的优势</strong>：</p><ul><li>可被任何 Hexo 博客复用</li><li>支持自定义标签、输出路径等配置</li><li>输出 <code>has-changes</code> 标识，方便后续流程判断</li></ul><h3 id="2-在博客仓库中配置-Workflow">2. 在博客仓库中配置 Workflow</h3><p>创建 <code>.github/workflows/moments.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Sync</span> <span class="string">Moments</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">issues:</span></span><br><span class="line">    <span class="attr">types:</span> [<span class="string">opened</span>, <span class="string">edited</span>, <span class="string">closed</span>, <span class="string">reopened</span>, <span class="string">labeled</span>, <span class="string">unlabeled</span>]</span><br><span class="line">  <span class="attr">workflow_dispatch:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">write</span></span><br><span class="line">  <span class="attr">issues:</span> <span class="string">read</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">sync:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">github.event_name</span> <span class="string">==</span> <span class="string">&#x27;workflow_dispatch&#x27;</span> <span class="string">||</span> <span class="string">contains(github.event.issue.labels.*.name,</span> <span class="string">&#x27;moment&#x27;</span><span class="string">)</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span> <span class="string">repository</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Sync</span> <span class="string">moments</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">panghutx/hexo-moments-action@v1</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">github-token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Commit</span> <span class="string">source</span> <span class="string">changes</span></span><br><span class="line">        <span class="attr">if:</span> <span class="string">steps.sync.outputs.has-changes</span> <span class="string">==</span> <span class="string">&#x27;true&#x27;</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          git config user.name &quot;github-actions[bot]&quot;</span></span><br><span class="line"><span class="string">          git config user.email &quot;github-actions[bot]@users.noreply.github.com&quot;</span></span><br><span class="line"><span class="string">          git add source/_data/shuoshuo.yml</span></span><br><span class="line"><span class="string">          git commit -m &quot;chore: sync moments from issues [skip ci]&quot;</span></span><br><span class="line"><span class="string">          git push</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Generate</span> <span class="string">and</span> <span class="string">Deploy</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          npx hexo clean</span></span><br><span class="line"><span class="string">          npx hexo generate</span></span><br><span class="line"><span class="string">          npx hexo deploy</span></span><br></pre></td></tr></table></figure><h3 id="3-页面模板">3. 页面模板</h3><p>在 Butterfly 主题中创建了两个 Pug 模板：</p><p><strong><code>media.pug</code></strong> - 首页侧边栏预览组件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">#moments-widget</span><br><span class="line">  .moments-header</span><br><span class="line">    a(href=&#x27;/moments/&#x27;) 动态</span><br><span class="line">  .moments-content</span><br><span class="line">    each moment in site.data.shuoshuo.slice(0, 5)</span><br><span class="line">      .moment-item</span><br><span class="line">        .moment-date= date(moment.date, &#x27;MM-DD&#x27;)</span><br><span class="line">        .moment-text= moment.content</span><br></pre></td></tr></table></figure><p><strong><code>shuoshuo.pug</code></strong> - 独立页面完整列表：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">each moment in site.data.shuoshuo</span><br><span class="line">  article.shuoshuo-item</span><br><span class="line">    .meta</span><br><span class="line">      time= date(moment.date, &#x27;YYYY-MM-DD&#x27;)</span><br><span class="line">      if moment.tags</span><br><span class="line">        each tag in moment.tags</span><br><span class="line">          span.tag= tag</span><br><span class="line">    .content!= moment.content</span><br></pre></td></tr></table></figure><h3 id="4-JavaScript-增强">4. JavaScript 增强</h3><p>首页侧边栏通过 JavaScript 动态加载最新内容：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">loadMoments</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;/moments/&#x27;</span>);</span><br><span class="line">  <span class="comment">// 解析页面，提取说说数据</span></span><br><span class="line">  <span class="keyword">const</span> moments = <span class="title function_">parseMomentsFromHtml</span>(html);</span><br><span class="line">  <span class="title function_">renderMomentsWidget</span>(moments);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="遇到的问题">遇到的问题</h2><h3 id="问题一：Git-Submodule-导致主题文件丢失">问题一：Git Submodule 导致主题文件丢失</h3><p><strong>现象</strong>：本地预览正常，但 GitHub Actions 部署后页面空白，控制台报 “No layout” 错误。</p><p><strong>原因</strong>：Butterfly 主题是作为 Git Submodule 引入的，指向官方仓库。但官方仓库没有我自定义的 <code>media.pug</code> 和 <code>shuoshuo.pug</code> 模板文件。GitHub Actions checkout 时只拉取了官方版本，丢失了本地修改。</p><p><strong>解决方案</strong>：将主题从 Submodule 转换为普通目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 删除 submodule 的 .git 目录</span></span><br><span class="line"><span class="built_in">rm</span> -rf themes/butterfly/.git</span><br><span class="line"></span><br><span class="line"><span class="comment"># 从 git 索引中移除 submodule 引用</span></span><br><span class="line">git <span class="built_in">rm</span> --cached themes/butterfly</span><br><span class="line"></span><br><span class="line"><span class="comment"># 将主题文件作为普通文件添加</span></span><br><span class="line">git add themes/butterfly/</span><br><span class="line">git commit -m <span class="string">&quot;fix: convert butterfly theme from submodule to regular files&quot;</span></span><br></pre></td></tr></table></figure><h3 id="问题二：SSH-部署失败">问题二：SSH 部署失败</h3><p><strong>现象</strong>：GitHub Actions 部署到服务器时报 “Permission denied”。</p><p><strong>原因</strong>：缺少 SSH 私钥配置。</p><p><strong>解决方案</strong>：</p><ol><li>在 GitHub 仓库设置中添加 <code>SSH_PRIVATE_KEY</code> secret</li><li>在 workflow 中添加 SSH 配置步骤：</li></ol><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Setup</span> <span class="string">SSH</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    mkdir -p ~/.ssh</span></span><br><span class="line"><span class="string">    echo &quot;$&#123;&#123; secrets.SSH_PRIVATE_KEY &#125;&#125;&quot; &gt; ~/.ssh/id_rsa</span></span><br><span class="line"><span class="string">    chmod 600 ~/.ssh/id_rsa</span></span><br><span class="line"><span class="string">    ssh-keyscan -H your-server-ip &gt;&gt; ~/.ssh/known_hosts</span></span><br></pre></td></tr></table></figure><h3 id="问题三：Git-用户未配置">问题三：Git 用户未配置</h3><p><strong>现象</strong>：部署时报错 “Please tell me who you are”。</p><p><strong>解决方案</strong>：在 workflow 中配置 git 用户：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Generate</span> <span class="string">and</span> <span class="string">Deploy</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    git config --global user.name &quot;your-name&quot;</span></span><br><span class="line"><span class="string">    git config --global user.email &quot;your-email&quot;</span></span><br><span class="line"><span class="string">    npx hexo deploy</span></span><br></pre></td></tr></table></figure><h3 id="问题四：Workflow-文件放在错误的位置">问题四：Workflow 文件放在错误的位置</h3><p><strong>现象</strong>：在博客静态仓库（GitHub Pages 仓库）中添加了 <code>.github/workflows/moments.yml</code>，但 Issue 创建后没有任何反应。</p><p><strong>原因</strong>：Hexo 部署时只会将 <code>public/</code> 目录下的静态文件推送到 GitHub Pages 仓库，<code>.github/workflows</code> 目录根本不会被上传。GitHub Actions 只能在源码仓库中触发。</p><p><strong>解决方案</strong>：创建独立的源码仓库 <code>hexo-blog</code>，将 workflow 文件放在源码仓库中。最终形成了两个仓库：</p><ul><li><code>hexo-blog</code>：源码仓库，包含 workflow、主题、配置等</li><li><code>blog</code>：静态仓库，只包含生成的静态文件（GitHub Pages）</li></ul><h3 id="问题五：Workflow-只更新源码，不更新博客">问题五：Workflow 只更新源码，不更新博客</h3><p><strong>现象</strong>：Workflow 运行成功，<code>shuoshuo.yml</code> 文件也更新了，但博客页面没有变化。</p><p><strong>原因</strong>：Workflow 更新了源码仓库中的数据文件，但没有触发 Hexo 重新构建和部署。需要在同一个 workflow 中完成：同步数据 → 构建 → 部署。</p><p><strong>解决方案</strong>：在 workflow 中添加构建和部署步骤：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">steps:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Sync</span> <span class="string">moments</span></span><br><span class="line">    <span class="attr">uses:</span> <span class="string">panghutx/hexo-moments-action@v1</span></span><br><span class="line"></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Commit</span> <span class="string">source</span> <span class="string">changes</span></span><br><span class="line">    <span class="comment"># 更新源码仓库</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Generate</span> <span class="string">and</span> <span class="string">Deploy</span></span><br><span class="line">    <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      npx hexo clean</span></span><br><span class="line"><span class="string">      npx hexo generate</span></span><br><span class="line"><span class="string">      npx hexo deploy  # 部署到静态仓库</span></span><br></pre></td></tr></table></figure><p>这样整个流程就打通了：Issue 更新 → 同步数据 → 提交源码 → 构建静态文件 → 部署到 GitHub Pages。</p><h2 id="最终效果">最终效果</h2><ul><li><strong>发布方式</strong>：在 GitHub 仓库创建 Issue，添加 <code>moment</code> 标签</li><li><strong>首页预览</strong>：侧边栏显示最新的 5 条说说</li><li><strong>独立页面</strong>：<code>/moments/</code> 显示完整列表，支持标签分类</li><li><strong>自动化</strong>：Issue 创建或更新后自动构建部署</li></ul><h2 id="总结">总结</h2><p>本文介绍了如何为 Hexo 博客添加说说功能：</p><ol><li><strong>方案选择</strong>：使用 GitHub Issues 作为内容存储，零成本且数据可控</li><li><strong>核心实现</strong>：创建独立的 GitHub Action 同步 Issues 数据，生成 Hexo 数据文件</li><li><strong>前端展示</strong>：自定义 Pug 模板渲染页面，JavaScript 实现首页侧边栏预览</li><li><strong>自动化流程</strong>：Issue 更新后自动同步、构建、部署</li></ol><p><strong>相关代码仓库</strong>：</p><ul><li>同步 Action：<a href="https://github.com/panghutx/hexo-moments-action">panghutx/hexo-moments-action</a></li></ul><p>如果你也想在 Hexo 博客添加类似功能，可以直接使用 <code>panghutx/hexo-moments-action</code>，只需在源码仓库中添加 workflow 配置即可。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近给博客添加了一个「说说」功能，可以发布短内容、想法、观点，类似朋友圈。这篇文章记录了设计思路、实现过程和遇到的问题。&lt;/p&gt;
&lt;h2 id=&quot;需求背景&quot;&gt;需求背景&lt;/h2&gt;
&lt;p&gt;博客通常用来写长文章，但很多时候只想记录一些短想法、碎碎念，不值得写一篇完整的文章。说说功</summary>
      
    
    
    
    <category term="项目" scheme="https://youngsay.cn/categories/%E9%A1%B9%E7%9B%AE/"/>
    
    
    <category term="Hexo" scheme="https://youngsay.cn/tags/Hexo/"/>
    
    <category term="GitHub" scheme="https://youngsay.cn/tags/GitHub/"/>
    
  </entry>
  
  <entry>
    <title>博客评论系统迁移：从 Valine 到 Waline</title>
    <link href="https://youngsay.cn/2026/04/26/migrate-valine-to-waline/"/>
    <id>https://youngsay.cn/2026/04/26/migrate-valine-to-waline/</id>
    <published>2026-04-26T13:00:00.000Z</published>
    <updated>2026-05-06T12:22:28.627Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景">背景</h2><p>LeanCloud 官方公告将于 <strong>2027 年 1 月 12 日停止对外提供服务</strong>。作为 Valine 评论系统的数据存储后端，这意味着所有依赖 LeanCloud 的 Valine 评论数据将面临丢失风险。</p><p>我的博客之前使用 Valine + LeanCloud 的组合，必须在此之前完成迁移。经过调研，最终选择了 Waline 作为替代方案。</p><h2 id="为什么选择-Waline">为什么选择 Waline</h2><p>Waline 是 Valine 的继任者，主要优势：</p><ul><li><strong>数据迁移简单</strong>：支持从 LeanCloud 直接导入评论数据</li><li><strong>功能更丰富</strong>：支持邮件通知、微信通知、垃圾评论过滤、评论管理后台</li><li><strong>部署灵活</strong>：支持 Vercel、Railway、自建服务器等多种部署方式</li><li><strong>兼容 Valine</strong>：前端配置几乎一样，迁移成本低</li><li><strong>持续维护</strong>：活跃的开源社区，定期更新</li></ul><h2 id="部署步骤">部署步骤</h2><h3 id="1-部署-Waline-到-Vercel">1. 部署 Waline 到 Vercel</h3><ol><li>访问 <a href="https://waline.js.org/">Waline 官网</a></li><li>点击「部署到 Vercel」按钮</li><li>授权 Vercel 访问 GitHub</li><li>创建仓库并部署</li></ol><p>部署完成后会获得一个 Vercel 域名，如 <code>https://your-project.vercel.app</code>。</p><h3 id="2-创建数据库">2. 创建数据库</h3><p>Waline 需要一个数据库存储评论数据。Vercel 已集成 Neon（PostgreSQL 云数据库），可以直接在 Vercel 中创建：</p><ol><li>在 Vercel 项目中进入 <strong>Storage</strong> → <strong>Create Database</strong></li><li>选择 <strong>Neon Postgres</strong></li><li>选择区域（建议选 Singapore 或 Tokyo，延迟较低）</li><li>创建完成后会自动生成环境变量</li></ol><p>Neon 免费版提供：</p><ul><li>0.5 GB 存储</li><li>自动休眠（无访问时暂停，访问时自动唤醒）</li><li>足够个人博客使用</li></ul><h3 id="3-配置环境变量">3. 配置环境变量</h3><p>创建 Neon 数据库后，Vercel 会自动注入 <code>DATABASE_URL</code> 环境变量，无需手动配置。</p><p>如果需要手动添加，在 Vercel 项目 <strong>Settings</strong> → <strong>Environment Variables</strong> 中添加：</p><table><thead><tr><th>变量名</th><th>说明</th></tr></thead><tbody><tr><td><code>DATABASE_URL</code></td><td>Neon 连接字符串</td></tr></tbody></table><p>配置完成后 <strong>Redeploy</strong> 项目。</p><h3 id="4-绑定自定义域名（可选）">4. 绑定自定义域名（可选）</h3><ol><li>Vercel 项目 → Settings → Domains</li><li>添加你的域名，如 <code>comments.yourdomain.com</code></li><li>在域名服务商处添加 CNAME 记录指向 Vercel</li></ol><h3 id="5-修改博客配置">5. 修改博客配置</h3><p>修改 Hexo 的 Butterfly 主题配置文件 <code>_config.butterfly.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">comments:</span></span><br><span class="line">  <span class="attr">use:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">Waline</span></span><br><span class="line"></span><br><span class="line"><span class="attr">waline:</span></span><br><span class="line">  <span class="attr">serverURL:</span> <span class="string">https://comments.yourdomain.com/</span></span><br><span class="line">  <span class="attr">avatar:</span> <span class="string">retro</span></span><br><span class="line">  <span class="attr">visitor:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">option:</span></span><br><span class="line">    <span class="attr">lang:</span> <span class="string">zh-CN</span></span><br></pre></td></tr></table></figure><h2 id="数据迁移">数据迁移</h2><h3 id="1-导出-LeanCloud-数据">1. 导出 LeanCloud 数据</h3><ol><li>登录 <a href="https://console.leancloud.cn/">LeanCloud 控制台</a></li><li>选择应用 → 数据存储 → <code>_Comment</code> 表</li><li>点击「导出数据」→ 选择 JSON 格式</li></ol><h3 id="2-转换数据格式">2. 转换数据格式</h3><p>LeanCloud 导出的数据格式与 Waline 不同，需要转换。可以使用以下脚本：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> valineFile = <span class="string">&#x27;Comment.0.jsonl&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> outFile = <span class="string">&#x27;waline_import.json&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> raw = fs.<span class="title function_">readFileSync</span>(valineFile, <span class="string">&#x27;utf8&#x27;</span>).<span class="title function_">trim</span>();</span><br><span class="line"><span class="keyword">const</span> valineArr = raw.<span class="title function_">split</span>(<span class="regexp">/\r?\n/</span>).<span class="title function_">filter</span>(<span class="function"><span class="params">l</span> =&gt;</span> l.<span class="title function_">trim</span>()).<span class="title function_">map</span>(<span class="function"><span class="params">l</span> =&gt;</span> <span class="title class_">JSON</span>.<span class="title function_">parse</span>(l));</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> idMap = <span class="keyword">new</span> <span class="title class_">Map</span>();</span><br><span class="line">valineArr.<span class="title function_">forEach</span>(<span class="function">(<span class="params">v, index</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> oldId = v.<span class="property">_id</span> ?? v.<span class="property">objectId</span>;</span><br><span class="line">  <span class="keyword">if</span> (oldId) idMap.<span class="title function_">set</span>(<span class="title class_">String</span>(oldId), index + <span class="number">1</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">toWalineId</span> = (<span class="params">rawId</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (!rawId) <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">  <span class="keyword">return</span> idMap.<span class="title function_">get</span>(<span class="title class_">String</span>(rawId)) ?? <span class="literal">null</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> comments = valineArr.<span class="title function_">map</span>(<span class="function">(<span class="params">v, index</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">user_id</span>: <span class="literal">null</span>,</span><br><span class="line">  <span class="attr">comment</span>: v.<span class="property">comment</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">ip</span>: v.<span class="property">ip</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">link</span>: v.<span class="property">link</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">mail</span>: v.<span class="property">mail</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">nick</span>: v.<span class="property">nick</span> || <span class="string">&#x27;匿名&#x27;</span>,</span><br><span class="line">  <span class="attr">pid</span>: <span class="title function_">toWalineId</span>(v.<span class="property">pid</span>),</span><br><span class="line">  <span class="attr">rid</span>: <span class="title function_">toWalineId</span>(v.<span class="property">rid</span>),</span><br><span class="line">  <span class="attr">status</span>: <span class="string">&#x27;approved&#x27;</span>,</span><br><span class="line">  <span class="attr">ua</span>: v.<span class="property">ua</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">url</span>: v.<span class="property">url</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">objectId</span>: index + <span class="number">1</span>,</span><br><span class="line">  <span class="attr">insertedAt</span>: v.<span class="property">insertedAt</span>?.<span class="property">$date</span> || v.<span class="property">createdAt</span>,</span><br><span class="line">  <span class="attr">createdAt</span>: v.<span class="property">createdAt</span>,</span><br><span class="line">  <span class="attr">updatedAt</span>: v.<span class="property">updatedAt</span> || v.<span class="property">createdAt</span></span><br><span class="line">&#125;));</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> walineExport = &#123;</span><br><span class="line">  <span class="attr">__version</span>: <span class="string">&#x27;1.39.3&#x27;</span>,</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;waline&#x27;</span>,</span><br><span class="line">  <span class="attr">version</span>: <span class="number">1</span>,</span><br><span class="line">  <span class="attr">time</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>(),</span><br><span class="line">  <span class="attr">tables</span>: [<span class="string">&#x27;Comment&#x27;</span>, <span class="string">&#x27;Counter&#x27;</span>, <span class="string">&#x27;Users&#x27;</span>],</span><br><span class="line">  <span class="attr">data</span>: &#123;</span><br><span class="line">    <span class="title class_">Comment</span>: comments,</span><br><span class="line">    <span class="title class_">Counter</span>: [],</span><br><span class="line">    <span class="title class_">Users</span>: []</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">fs.<span class="title function_">writeFileSync</span>(outFile, <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(walineExport, <span class="literal">null</span>, <span class="number">2</span>));</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`转换完成: <span class="subst">$&#123;comments.length&#125;</span> 条评论`</span>);</span><br></pre></td></tr></table></figure><p>运行脚本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node convert.js</span><br></pre></td></tr></table></figure><h3 id="3-导入到-Waline">3. 导入到 Waline</h3><ol><li>访问你的 Waline 管理后台：<code>https://comments.yourdomain.com/ui</code></li><li>首次访问设置管理员账号</li><li>进入「管理」→「导入」</li><li>上传转换后的 <code>waline_import.json</code> 文件</li></ol><h2 id="配置邮件通知">配置邮件通知</h2><p>Waline 支持评论邮件通知，以 QQ 邮箱为例：</p><h3 id="1-获取授权码">1. 获取授权码</h3><ol><li>登录 QQ 邮箱网页版</li><li>设置 → 账户 → POP3/IMAP/SMTP服务</li><li>开启「IMAP/SMTP服务」</li><li>生成授权码</li></ol><h3 id="2-配置环境变量">2. 配置环境变量</h3><p>在 Vercel 中添加：</p><table><thead><tr><th>变量名</th><th>值</th></tr></thead><tbody><tr><td><code>SMTP_SERVICE</code></td><td><code>QQ</code></td></tr><tr><td><code>SMTP_USER</code></td><td>你的QQ邮箱</td></tr><tr><td><code>SMTP_PASS</code></td><td>邮箱授权码（不是密码）</td></tr><tr><td><code>SMTP_SECURE</code></td><td><code>true</code></td></tr><tr><td><code>SITE_NAME</code></td><td>博客名称</td></tr><tr><td><code>SITE_URL</code></td><td>博客地址</td></tr><tr><td><code>AUTHOR_EMAIL</code></td><td>博主邮箱（接收通知）</td></tr></tbody></table><p>配置后 Redeploy 项目即可生效。</p><h2 id="总结">总结</h2><p>从 Valine 迁移到 Waline 的完整流程：</p><ol><li>在 Vercel 部署 Waline 服务</li><li>创建数据库并配置环境变量</li><li>从 LeanCloud 导出评论数据</li><li>转换格式后导入 Waline</li><li>修改博客配置指向新的评论服务</li><li>可选：配置邮件通知和头像</li></ol><p>Waline 相比 Valine 功能更完善，社区更活跃，是目前博客评论系统的不错选择。如果你的博客也在使用 Valine + LeanCloud，建议尽早迁移，避免数据丢失。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;
&lt;p&gt;LeanCloud 官方公告将于 &lt;strong&gt;2027 年 1 月 12 日停止对外提供服务&lt;/strong&gt;。作为 Valine 评论系统的数据存储后端，这意味着所有依赖 LeanCloud 的 Valine 评论数据将面临丢失</summary>
      
    
    
    
    <category term="项目" scheme="https://youngsay.cn/categories/%E9%A1%B9%E7%9B%AE/"/>
    
    
    <category term="Hexo" scheme="https://youngsay.cn/tags/Hexo/"/>
    
    <category term="Waline" scheme="https://youngsay.cn/tags/Waline/"/>
    
  </entry>
  
  <entry>
    <title>豆瓣 RSS + Hexo：打造自动同步的书影音记录页</title>
    <link href="https://youngsay.cn/2026/04/26/douban-hexo-sync/"/>
    <id>https://youngsay.cn/2026/04/26/douban-hexo-sync/</id>
    <published>2026-04-26T06:00:00.000Z</published>
    <updated>2026-05-06T12:20:23.140Z</updated>
    
    <content type="html"><![CDATA[<p>记录读过的书、看过的电影，是很多人坚持多年的习惯。豆瓣作为国内主流的书影音平台，积累了丰富的个人数据。本文将介绍如何利用豆瓣 RSS + Python 脚本 + Hexo 数据文件，打造一个自动同步的书影音记录页面。</p><h2 id="效果展示">效果展示</h2><p>最终效果如下：页面支持电影/书籍 Tab 切换，卡片展示封面、标题、评分和评语，响应式布局适配移动端，并支持暗黑模式。</p><p><strong>功能亮点：</strong></p><ul><li>自动从豆瓣 RSS 同步标记内容</li><li>封面图片本地化，解决豆瓣防盗链问题</li><li>支持评分（1-5 星）和评语展示</li><li>响应式网格布局 + 暗黑模式适配</li><li>GitHub Actions 定时自动同步</li></ul><h2 id="需求分析">需求分析</h2><h3 id="为什么选择豆瓣-RSS">为什么选择豆瓣 RSS</h3><p>获取豆瓣数据有几种方案：</p><table><thead><tr><th>方案</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>豆瓣 API</td><td>数据结构化、功能完整</td><td>需申请 Key、有配额限制、API 已停止开放</td></tr><tr><td>爬虫</td><td>数据全面</td><td>易被封禁、维护成本高</td></tr><tr><td>RSS</td><td>开放协议、无需认证、稳定可靠</td><td>数据字段有限</td></tr></tbody></table><p>豆瓣 RSS 是最简单可靠的方案。每个用户都有公开的 RSS 地址，格式为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">https://www.douban.com/feed/people/&#123;用户ID&#125;/interests?cat=movie</span><br><span class="line">https://www.douban.com/feed/people/&#123;用户ID&#125;/interests?cat=book</span><br></pre></td></tr></table></figure><h3 id="整体架构">整体架构</h3><p>数据流程如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">豆瓣 RSS → Python 脚本解析 → YAML 数据文件 → Hexo 渲染 → 静态页面</span><br></pre></td></tr></table></figure><p>技术栈：</p><ul><li><strong>前端</strong>：Hexo 数据文件 + Butterfly 主题 + 自定义 CSS</li><li><strong>后端</strong>：Python（feedparser、BeautifulSoup、PyYAML）</li><li><strong>自动化</strong>：GitHub Actions 定时任务</li></ul><h3 id="核心问题">核心问题</h3><p>实现过程中需要解决几个问题：</p><ol><li><strong>豆瓣防盗链</strong>：外链图片无法直接显示，需要下载到本地</li><li><strong>数据去重</strong>：增量更新，避免重复同步</li><li><strong>评分解析</strong>：RSS 中评分是中文（力荐/推荐等），需映射为数字</li></ol><h2 id="前端页面开发">前端页面开发</h2><h3 id="Hexo-数据文件机制">Hexo 数据文件机制</h3><p>Hexo 支持在 <code>source/_data/</code> 目录下放置 YAML 或 JSON 文件，通过 <code>site.data</code> 在模板中访问。这种方式非常适合管理结构化数据。</p><p>创建两个数据文件：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># source/_data/movies.yml</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">title:</span> <span class="string">肖申克的救赎</span></span><br><span class="line">  <span class="attr">cover:</span> <span class="string">/images/douban/xxx.jpg</span></span><br><span class="line">  <span class="attr">date:</span> <span class="number">2025-11-25</span></span><br><span class="line">  <span class="attr">rating:</span> <span class="number">5</span></span><br><span class="line">  <span class="attr">comment:</span> <span class="string">希望是美好的事物</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># source/_data/books.yml</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">title:</span> <span class="string">被讨厌的勇气</span></span><br><span class="line">  <span class="attr">cover:</span> <span class="string">/images/douban/xxx.jpg</span></span><br><span class="line">  <span class="attr">date:</span> <span class="number">2026-04-26</span></span><br><span class="line">  <span class="attr">rating:</span> <span class="number">4</span></span><br></pre></td></tr></table></figure><p>字段说明：</p><table><thead><tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr></thead><tbody><tr><td>title</td><td>string</td><td>是</td><td>标题</td></tr><tr><td>cover</td><td>string</td><td>是</td><td>封面图片路径</td></tr><tr><td>date</td><td>string</td><td>是</td><td>标记日期</td></tr><tr><td>rating</td><td>number</td><td>否</td><td>评分 1-5</td></tr><tr><td>comment</td><td>string</td><td>否</td><td>短评</td></tr></tbody></table><h3 id="Butterfly-主题适配">Butterfly 主题适配</h3><p>Butterfly 主题通过 <code>page.type</code> 支持自定义页面类型。在 <code>page.pug</code> 中添加 media 类型：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">case page.type</span><br><span class="line">  when &#x27;media&#x27;</span><br><span class="line">    include includes/page/media.pug</span><br></pre></td></tr></table></figure><p>创建 <code>source/media/index.md</code>：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">title: 书影音</span><br><span class="line">type: media</span><br><span class="line"><span class="section">date: 2024-01-01 00:00:00</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">记录我看过的电影和读过的书籍。</span><br></pre></td></tr></table></figure><p>模板文件 <code>themes/butterfly/layout/includes/page/media.pug</code> 核心逻辑：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">// Tab 切换</span><br><span class="line">.media-tabs</span><br><span class="line">  button.media-tab.active(data-tab=&quot;movies&quot;) 电影</span><br><span class="line">  button.media-tab(data-tab=&quot;books&quot;) 书籍</span><br><span class="line"></span><br><span class="line">// 电影列表</span><br><span class="line">.media-content.movies.active</span><br><span class="line">  .media-grid</span><br><span class="line">    - const movies = site.data.movies || []</span><br><span class="line">    - const sortedMovies = movies.sort((a, b) =&gt; new Date(b.date) - new Date(a.date))</span><br><span class="line">    each item in sortedMovies</span><br><span class="line">      .media-card</span><br><span class="line">        .media-card-cover</span><br><span class="line">          img(src=url_for(item.cover) alt=item.title)</span><br><span class="line">        .media-card-info</span><br><span class="line">          h3.media-card-title= item.title</span><br><span class="line">          .media-card-rating</span><br><span class="line">            - for (let i = 0; i &lt; 5; i++)</span><br><span class="line">              span.star(class=i &lt; item.rating ? &#x27;filled&#x27; : &#x27;&#x27;) ★</span><br><span class="line">          if item.comment</span><br><span class="line">            p.media-card-comment= item.comment</span><br></pre></td></tr></table></figure><h3 id="CSS-样式设计">CSS 样式设计</h3><p>样式文件 <code>source/css/media.css</code>，在配置中引入：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># _config.butterfly.yml</span></span><br><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&lt;link</span> <span class="string">rel=&quot;stylesheet&quot;</span> <span class="string">href=&quot;/css/media.css&quot;&gt;</span></span><br></pre></td></tr></table></figure><p>关键样式：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 卡片网格布局 */</span></span><br><span class="line"><span class="selector-class">.media-grid</span> &#123;</span><br><span class="line">  <span class="attribute">display</span>: grid;</span><br><span class="line">  <span class="attribute">grid-template-columns</span>: <span class="built_in">repeat</span>(auto-fill, <span class="built_in">minmax</span>(<span class="number">200px</span>, <span class="number">1</span>fr));</span><br><span class="line">  <span class="attribute">gap</span>: <span class="number">20px</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 封面图 2:3 比例 */</span></span><br><span class="line"><span class="selector-class">.media-card-cover</span> &#123;</span><br><span class="line">  <span class="attribute">aspect-ratio</span>: <span class="number">2</span> / <span class="number">3</span>;</span><br><span class="line">  <span class="attribute">overflow</span>: hidden;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.media-card</span><span class="selector-pseudo">:hover</span> <span class="selector-class">.media-card-cover</span> <span class="selector-tag">img</span> &#123;</span><br><span class="line">  <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1.05</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 评分星星 */</span></span><br><span class="line"><span class="selector-class">.media-card-rating</span> <span class="selector-class">.star</span> &#123;</span><br><span class="line">  <span class="attribute">color</span>: <span class="number">#ddd</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.media-card-rating</span> <span class="selector-class">.star</span><span class="selector-class">.filled</span> &#123;</span><br><span class="line">  <span class="attribute">color</span>: <span class="number">#f5a623</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 暗黑模式 */</span></span><br><span class="line"><span class="selector-attr">[data-theme=<span class="string">&quot;dark&quot;</span>]</span> <span class="selector-class">.media-card</span> &#123;</span><br><span class="line">  <span class="attribute">background</span>: <span class="number">#1f1f1f</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="数据同步脚本">数据同步脚本</h2><h3 id="RSS-解析流程">RSS 解析流程</h3><p>使用 <code>feedparser</code> 解析 RSS，通过链接域名区分电影和书籍：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> feedparser</span><br><span class="line"></span><br><span class="line">RSS_URLS = &#123;</span><br><span class="line">    <span class="string">&quot;movie&quot;</span>: <span class="string">f&quot;https://www.douban.com/feed/people/<span class="subst">&#123;DOUBAN_ID&#125;</span>/interests?cat=movie&quot;</span>,</span><br><span class="line">    <span class="string">&quot;book&quot;</span>: <span class="string">f&quot;https://www.douban.com/feed/people/<span class="subst">&#123;DOUBAN_ID&#125;</span>/interests?cat=book&quot;</span>,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">feed = feedparser.parse(RSS_URLS[<span class="string">&quot;movie&quot;</span>])</span><br><span class="line"><span class="keyword">for</span> entry <span class="keyword">in</span> feed.entries:</span><br><span class="line">    link = entry.get(<span class="string">&quot;link&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> <span class="string">&quot;movie.douban.com&quot;</span> <span class="keyword">not</span> <span class="keyword">in</span> link:</span><br><span class="line">        <span class="keyword">continue</span>  <span class="comment"># 跳过非电影条目</span></span><br></pre></td></tr></table></figure><p>RSS 条目标题格式为&quot;看过肖申克的救赎&quot;或&quot;想看xxx&quot;，通过正则提取状态和标题：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> re</span><br><span class="line"></span><br><span class="line">status_match = re.<span class="keyword">match</span>(<span class="string">r&quot;^(看过|读过|在看|在读|想看|想读)(.+)$&quot;</span>, title_raw)</span><br><span class="line"><span class="keyword">if</span> status_match:</span><br><span class="line">    status = status_match.group(<span class="number">1</span>)  <span class="comment"># 看过/读过</span></span><br><span class="line">    title = status_match.group(<span class="number">2</span>)   <span class="comment"># 肖申克的救赎</span></span><br></pre></td></tr></table></figure><p>只同步&quot;看过&quot;和&quot;读过&quot;的条目，跳过&quot;想看&quot;等状态。</p><h3 id="HTML-解析与数据提取">HTML 解析与数据提取</h3><p>RSS 的 <code>description</code> 字段是 HTML 格式，包含封面、评分、评语：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> bs4 <span class="keyword">import</span> BeautifulSoup</span><br><span class="line"></span><br><span class="line">soup = BeautifulSoup(description, <span class="string">&quot;html.parser&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 提取封面图</span></span><br><span class="line">img_tag = soup.find(<span class="string">&quot;img&quot;</span>)</span><br><span class="line">cover_url = img_tag.get(<span class="string">&quot;src&quot;</span>) <span class="keyword">if</span> img_tag <span class="keyword">else</span> DEFAULT_COVER</span><br><span class="line"></span><br><span class="line"><span class="comment"># 提取评分</span></span><br><span class="line">rating_p = soup.find(<span class="string">&quot;p&quot;</span>, string=re.<span class="built_in">compile</span>(<span class="string">r&quot;推荐:&quot;</span>))</span><br><span class="line"><span class="keyword">if</span> rating_p:</span><br><span class="line">    rating_match = re.search(<span class="string">r&quot;推荐:\s*(力荐|推荐|还行|较差|很差)&quot;</span>, rating_p.get_text())</span><br><span class="line">    rating = RATING_MAP.get(rating_match.group(<span class="number">1</span>))  <span class="comment"># 映射为 1-5</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 提取评语</span></span><br><span class="line">comment_p = soup.find(<span class="string">&quot;p&quot;</span>, string=re.<span class="built_in">compile</span>(<span class="string">r&quot;备注:&quot;</span>))</span><br><span class="line"><span class="keyword">if</span> comment_p:</span><br><span class="line">    comment = re.search(<span class="string">r&quot;备注:\s*(.+)&quot;</span>, comment_p.get_text()).group(<span class="number">1</span>)</span><br></pre></td></tr></table></figure><p>评分映射表：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">RATING_MAP = &#123;</span><br><span class="line">    <span class="string">&quot;力荐&quot;</span>: <span class="number">5</span>,</span><br><span class="line">    <span class="string">&quot;推荐&quot;</span>: <span class="number">4</span>,</span><br><span class="line">    <span class="string">&quot;还行&quot;</span>: <span class="number">3</span>,</span><br><span class="line">    <span class="string">&quot;较差&quot;</span>: <span class="number">2</span>,</span><br><span class="line">    <span class="string">&quot;很差&quot;</span>: <span class="number">1</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="图片本地化">图片本地化</h3><p>豆瓣图片有防盗链，外链无法显示。解决方案是下载到本地：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> urllib.request</span><br><span class="line"><span class="keyword">import</span> hashlib</span><br><span class="line"><span class="keyword">from</span> pathlib <span class="keyword">import</span> Path</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">download_image</span>(<span class="params">url: <span class="built_in">str</span>, title: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="comment"># 使用 URL hash 作为文件名，避免重复下载</span></span><br><span class="line">    url_hash = hashlib.md5(url.encode()).hexdigest()[:<span class="number">10</span>]</span><br><span class="line">    ext = url.split(<span class="string">&#x27;.&#x27;</span>)[-<span class="number">1</span>].split(<span class="string">&#x27;?&#x27;</span>)[<span class="number">0</span>] <span class="keyword">or</span> <span class="string">&#x27;jpg&#x27;</span></span><br><span class="line">    filename = <span class="string">f&quot;<span class="subst">&#123;url_hash&#125;</span>.<span class="subst">&#123;ext&#125;</span>&quot;</span></span><br><span class="line">    local_path = Path(<span class="string">&quot;source/images/douban&quot;</span>) / filename</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> local_path.exists():</span><br><span class="line">        <span class="keyword">return</span> <span class="string">f&quot;/images/douban/<span class="subst">&#123;filename&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 设置 Referer 绕过防盗链</span></span><br><span class="line">    req = urllib.request.Request(url, headers=&#123;</span><br><span class="line">        <span class="string">&#x27;User-Agent&#x27;</span>: <span class="string">&#x27;Mozilla/5.0 ...&#x27;</span>,</span><br><span class="line">        <span class="string">&#x27;Referer&#x27;</span>: <span class="string">&#x27;https://www.douban.com/&#x27;</span></span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">with</span> urllib.request.urlopen(req, timeout=<span class="number">10</span>) <span class="keyword">as</span> response:</span><br><span class="line">        <span class="keyword">with</span> <span class="built_in">open</span>(local_path, <span class="string">&#x27;wb&#x27;</span>) <span class="keyword">as</span> f:</span><br><span class="line">            f.write(response.read())</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="string">f&quot;/images/douban/<span class="subst">&#123;filename&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><h3 id="YAML-数据管理">YAML 数据管理</h3><p>增量更新，基于标题去重：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> yaml</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">merge_data</span>(<span class="params">existing: <span class="type">List</span>[<span class="type">Dict</span>], new_items: <span class="type">List</span>[<span class="type">Dict</span>]</span>) -&gt; <span class="type">List</span>[<span class="type">Dict</span>]:</span><br><span class="line">    existing_titles = &#123;item.get(<span class="string">&quot;title&quot;</span>) <span class="keyword">for</span> item <span class="keyword">in</span> existing&#125;</span><br><span class="line">    truly_new = [item <span class="keyword">for</span> item <span class="keyword">in</span> new_items <span class="keyword">if</span> item.get(<span class="string">&quot;title&quot;</span>) <span class="keyword">not</span> <span class="keyword">in</span> existing_titles]</span><br><span class="line">    <span class="keyword">return</span> truly_new + existing  <span class="comment"># 新条目放前面</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 保存时保持可读格式</span></span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(file_path, <span class="string">&quot;w&quot;</span>, encoding=<span class="string">&quot;utf-8&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    yaml.dump(data, f, allow_unicode=<span class="literal">True</span>, default_flow_style=<span class="literal">False</span>, sort_keys=<span class="literal">False</span>)</span><br></pre></td></tr></table></figure><p>完整脚本已上传至 GitHub 仓库，依赖：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># requirements.txt</span><br><span class="line">feedparser==6.0.10</span><br><span class="line">PyYAML==6.0.1</span><br><span class="line">beautifulsoup4==4.12.2</span><br></pre></td></tr></table></figure><h2 id="自动化集成">自动化集成</h2><h3 id="GitHub-Actions-配置">GitHub Actions 配置</h3><p>创建 <code>.github/workflows/douban-sync.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Douban</span> <span class="string">Sync</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">schedule:</span></span><br><span class="line">    <span class="comment"># 每周一凌晨 3:00 (北京时间)</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">cron:</span> <span class="string">&#x27;0 19 * * 0&#x27;</span></span><br><span class="line">  <span class="attr">workflow_dispatch:</span>  <span class="comment"># 支持手动触发</span></span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">write</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">sync:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span> <span class="string">repository</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Setup</span> <span class="string">Python</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/setup-python@v5</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">python-version:</span> <span class="string">&#x27;3.x&#x27;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">pip</span> <span class="string">install</span> <span class="string">-r</span> <span class="string">tools/requirements.txt</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">sync</span> <span class="string">script</span></span><br><span class="line">        <span class="attr">id:</span> <span class="string">sync</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          output=$(python tools/douban_sync.py)</span></span><br><span class="line"><span class="string">          echo &quot;$output&quot;</span></span><br><span class="line"><span class="string">          if echo &quot;$output&quot; | grep -q &quot;HAS_NEW_CONTENT=True&quot;; then</span></span><br><span class="line"><span class="string">            echo &quot;has_new=true&quot; &gt;&gt; $GITHUB_OUTPUT</span></span><br><span class="line"><span class="string">          fi</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Commit</span> <span class="string">and</span> <span class="string">push</span> <span class="string">changes</span></span><br><span class="line">        <span class="attr">if:</span> <span class="string">steps.sync.outputs.has_new</span> <span class="string">==</span> <span class="string">&#x27;true&#x27;</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          git config user.name &quot;github-actions[bot]&quot;</span></span><br><span class="line"><span class="string">          git config user.email &quot;github-actions[bot]@users.noreply.github.com&quot;</span></span><br><span class="line"><span class="string">          git add source/_data/movies.yml source/_data/books.yml</span></span><br><span class="line"><span class="string">          git commit -m &quot;chore: sync douban data [skip ci]&quot;</span></span><br><span class="line"><span class="string">          git push</span></span><br></pre></td></tr></table></figure><p>关键点：</p><ul><li><code>cron: '0 19 * * 0'</code> 表示每周日 UTC 19:00（北京时间周一凌晨 3 点）</li><li>脚本输出 <code>HAS_NEW_CONTENT=True</code> 时才提交，避免空提交</li><li><code>[skip ci]</code> 提交信息跳过 CI 触发，防止循环</li></ul><h3 id="本地调试">本地调试</h3><p>手动运行脚本验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /path/to/hexo</span><br><span class="line">python tools/douban_sync.py</span><br></pre></td></tr></table></figure><p>输出示例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">2026-04-26 13:45:21 - INFO - 开始同步 movie...</span><br><span class="line">2026-04-26 13:45:21 - INFO - 解析到 2 个有效条目</span><br><span class="line">2026-04-26 13:45:21 - INFO - 无新内容，跳过保存</span><br><span class="line">2026-04-26 13:45:21 - INFO - 开始同步 book...</span><br><span class="line">2026-04-26 13:45:23 - INFO - 新增条目: 被讨厌的勇气</span><br><span class="line">2026-04-26 13:45:23 - INFO - 同步完成，新增 1 条记录</span><br><span class="line">HAS_NEW_CONTENT=True</span><br></pre></td></tr></table></figure><h2 id="总结">总结</h2><p>本文介绍了如何利用豆瓣 RSS + Python + Hexo 打造自动同步的书影音记录页：</p><ul><li><strong>前端</strong>：Hexo 数据文件机制 + Butterfly 主题自定义页面 + CSS 卡片布局</li><li><strong>后端</strong>：Python 解析 RSS、提取数据、下载图片、管理 YAML</li><li><strong>自动化</strong>：GitHub Actions 定时同步，无需手动维护</li></ul><p><strong>可扩展方向</strong>：</p><ul><li>添加音乐、游戏分类</li><li>接入 Goodreads、IMDb 等平台</li><li>增加年度统计、评分分布图表</li><li>瀑布流布局、筛选排序功能</li></ul><hr><p><strong>相关链接</strong>：</p><ul><li>博客地址：<a href="https://youngsay.cn/media/">https://youngsay.cn/media/</a></li><li>完整代码：<a href="https://github.com/panghutx/douban-sync">GitHub 仓库 <code>tools/douban_sync.py</code></a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;记录读过的书、看过的电影，是很多人坚持多年的习惯。豆瓣作为国内主流的书影音平台，积累了丰富的个人数据。本文将介绍如何利用豆瓣 RSS + Python 脚本 + Hexo 数据文件，打造一个自动同步的书影音记录页面。&lt;/p&gt;
&lt;h2 id=&quot;效果展示&quot;&gt;效果展示&lt;/h2&gt;
</summary>
      
    
    
    
    <category term="项目" scheme="https://youngsay.cn/categories/%E9%A1%B9%E7%9B%AE/"/>
    
    
    <category term="Hexo" scheme="https://youngsay.cn/tags/Hexo/"/>
    
    <category term="Python" scheme="https://youngsay.cn/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>近期碎碎念</title>
    <link href="https://youngsay.cn/2026/03/22/20260322/"/>
    <id>https://youngsay.cn/2026/03/22/20260322/</id>
    <published>2026-03-22T09:39:15.000Z</published>
    <updated>2026-05-06T12:18:25.739Z</updated>
    
    <content type="html"><![CDATA[<p>过完年到现在也有一个月了，又开始了新的一年。</p><p>这一个月也没啥特别的，和往常一样，和往年一样。</p><p>总想有点新鲜感，总想有点进步，总觉得新的一年就要有新气象，不能墨守成规，毫无长进。可一工作起来，一天又一天地过去，早出晚归，似乎没有自己的时间。好在周末可以稍作休息，有了反思和自责的时间，写下这篇文字，告诫自己要有长进，不要再这样下去。可到了工作日，又开始日复一日，两点一线，把周末的反思完全抛在脑后。</p><h2 id="上班的心态">上班的心态</h2><p>关于上班，可以有两种想法：<strong>一种是上班就是用时间换金钱，出卖自己的时间为企业劳动</strong>。这种想法可以完全把上班和生活分开，上班的时间是企业的，下班的时间才属于我。在互联网这种高强度的环境下，持有这种想法会更痛苦。因为工作时间很长，一天12小时在公司，如果这12小时完全不属于自己，再去掉睡眠和通勤时间，属于自己的时间寥寥。</p><p>另一种想法是<strong>上班不是出卖自己的时间为企业劳动，而是为自己工作，是在用时间在积累自己的资产，并且有随时换家公司服务的能力</strong>。只有这种想法，才不至于陷入缺少自己时间的困境。在工作中的每一个事项，我的每一个产出，我学到的每一项技能，都是在为自己积累资产。所以工作中要转变观念，不只于完成任务，而是完成这项任务，我得到了什么，我有没有在积累自己的资产。</p><h2 id="幻想与现实">幻想与现实</h2><p><strong>不想上班的原因在于感受的是具体的痛苦，同时想象着未知的快乐</strong>。不想早出晚归、两点一线，不想在办公楼里坐上一天，忙着干不完的工作，承受着完不成的kpi。想要躺在床上，无所事事，看着阳光洒在床边，享受惬意的时光。想要旅居生活，走遍世界各地，体验不同的风土人情。</p><p>当下你有工作，感受到的是被约束的痛苦，但其他的路径、生活方式也没有想象的那么快乐。旅居生活在社交媒体上看起来全是落日、咖啡和自由，虽然我没有经历过，但背后一定有痛苦。</p><p>孤独。接近了人群，也远离了人群。可以走遍世界各地，认识不同的人，但也只是“浅尝辄止”，不会深入接触就要分别，短暂相遇后变各奔东西。遇到的人遍地都是，说得着的千里难寻。</p><p>选择。普通人选择一个工作后，日常的大多数事都是靠惯性来完成的。一个项目启动初期就有了deadline，你不必选择也无权选择合适完成。一周工作5天，一天12小时，你无权选择工作4天或更少的时间。少了选择的机会，也少了抉择的精力损耗。旅居生活今天住哪，怎么坐公交，哪里安全，每换一个新环境，都需要花费很大的力气去选择。</p><p>总之，上班是确定的痛苦，不上班是不确定的痛苦。</p><h2 id="长期主义">长期主义</h2><p>如果以上都说服不了自己，还是不想上班，还是想拥有不一样的生活，那么就要坚信“好事还在后头呢”。现在是在为未来而努力，当下的工作是为了未来的不工作。你只看到了别人打鱼，却没看到织网。打鱼只需要一天，但织网呢？你看到的他人的光鲜亮丽，他人的幸福生活，也全是过去长期的积累后才获得的。除了富二代们，很少人能生来就衣食无忧，生来就过着幸福、惬意的生活。</p><p>坚持长期主义，就不能计较当下的得失。今天哪怕遇到天大的困难，多年后来看也许都微不足道，眼前再重要的事，多年后来看也许都不重要。所以不必为工作殚精竭虑，不必因没完成的任务而寝食难安，“天空飘来五个字，那都不是事”！</p><p>坚持长期主义，就要过好当下。“上班让人感到最可怕的地方是，ta居然让我因为盼着退休而期待衰老，让我完全不珍惜剩余人生里最年轻的每一天”，看到这句话很感同身受，有时总想着工作日赶快过去，一天又一天，一周又一周，流逝着自己的生命。人们往往高估了一天能做的事，却极大地低估了一年能做的事。过好当下的每一天，每天进步1%，一年后的能力便是现在的37.8倍。</p><h2 id="我要做什么">我要做什么</h2><p>说到就要做到，洋洋洒洒写了一千字不能最后还是无济于事，“晚上想了千条路，早上醒来走原路”便是我最生动的写照。从本周开始，我要写周报记录每天的进步，哪怕很小很小，哪怕只写5行代码，只记1个单词，也值得记录。在平常的每一天中，需要做点什么来打破无聊。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;过完年到现在也有一个月了，又开始了新的一年。&lt;/p&gt;
&lt;p&gt;这一个月也没啥特别的，和往常一样，和往年一样。&lt;/p&gt;
&lt;p&gt;总想有点新鲜感，总想有点进步，总觉得新的一年就要有新气象，不能墨守成规，毫无长进。可一工作起来，一天又一天地过去，早出晚归，似乎没有自己的时间。好在周末</summary>
      
    
    
    
    <category term="随笔" scheme="https://youngsay.cn/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://youngsay.cn/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>养一只OpenClaw小龙虾</title>
    <link href="https://youngsay.cn/2026/03/07/openclaw/"/>
    <id>https://youngsay.cn/2026/03/07/openclaw/</id>
    <published>2026-03-07T08:45:15.000Z</published>
    <updated>2026-05-06T12:18:25.630Z</updated>
    
    <content type="html"><![CDATA[<p>最近OpenClaw大火，打开各大自媒体，全是养只小龙虾🦞，腾讯现场装机排满了长队，网上甚至也有了付费安装的业务。</p><p>在openclaw在圈内小火的时候，带动了mac mini的销量，而我似乎没有养龙虾🦞的需求（穷），就没有凑热闹。最近又火爆互联网，刚好有闲置的服务器，也想来体验一下。</p><p><img src="https://offer.youngsay.cn/blog/file-20260307154216193.png" alt=""></p><h2 id="什么是OpenClaw">什么是OpenClaw</h2><p>官网是这样介绍的，OpenClaw——适用于任何操作系统的 AI 智能体 Gateway 网关，支持 WhatsApp、Telegram、Discord、iMessage 等。也就是说能将聊天软件与Agent连接，作为个人的AI助理，让ta帮你阅读新闻、回复邮件。这些功能Claude Code都能完成，只不过OpenClaw有了可视化UI、连接聊天App，操作更便捷，上手更容易，而且可以24小时为你工作。</p><h2 id="安装到服务器">安装到服务器</h2><h3 id="安装">安装</h3><p>各大模型提供商都支持在OpenClaw中使用，不少厂商也都推出了开发者的coding plan订阅计划，大家可以按需选择，这里不再多说。</p><ul><li>执行该命令，会帮你自动安装需要的环境，如node.js 22+，Git</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://openclaw.ai/install.sh | bash</span><br></pre></td></tr></table></figure><ul><li>根据提示完成openClaw配置</li></ul><ol><li>基本配置</li></ol><table><thead><tr><th>提示信息</th><th>配置</th></tr></thead><tbody><tr><td>I understand this is personal-by-default and shared/multi-user use requires lock-down. Continue?</td><td>选择 “Yes”</td></tr><tr><td>Onboarding mode</td><td>选择 “QuickStart”</td></tr></tbody></table><ol start="2"><li>模型配置</li></ol><table><thead><tr><th></th><th></th></tr></thead><tbody><tr><td>提示信息</td><td>配置</td></tr><tr><td>Model/auth provider</td><td>按需选择，我使用的是火山引擎，这里选择 “Volcano Engine”。<br><br><img src="https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/c5d4b31423cd491181a379c48ffbbd63~tplv-goo7wpa0wc-image.image" alt="Image"></td></tr><tr><td>How do you want to provide this API key?</td><td>选择 “Paste API key now”</td></tr><tr><td>Enter Volcano Engine API key</td><td>设置 api key</td></tr><tr><td>Default model</td><td>选择 Coding Plan 支持的模型（volcengine-plan），本文设置为 ”volcengine-plan/ark-code-latest“<br><br><img src="https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/3ff3825a5f4043599ff191d44d7ed742~tplv-goo7wpa0wc-image.image" alt="Image"><br></td></tr></tbody></table><ol start="3"><li>功能配置</li></ol><table><thead><tr><th></th><th></th></tr></thead><tbody><tr><td>提示信息</td><td>配置</td></tr><tr><td>Select channel (QuickStart)</td><td>选择 “Skip for now”，后续可以配置。</td></tr><tr><td>Configure skills now? (recommended)</td><td>选择 “No”，后续可以配置。</td></tr><tr><td>Enable hooks?</td><td>按空格键选中选项，按回车键进入下一步。</td></tr><tr><td>How do you want to hatch your bot?</td><td>选择 “Hatch in TUI”。</td></tr></tbody></table><h3 id="绑定域名">绑定域名</h3><p>绑定域名后可以外网访问webUI，方便操作。</p><p>vim /etc/nginx/sites-available/oepnclaw</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">    listen 443 ssl;</span><br><span class="line">    server_name www.example.cn; #输入自己的域名</span><br><span class="line"></span><br><span class="line">    location / &#123;</span><br><span class="line">        proxy_pass http://127.0.0.1:18789;</span><br><span class="line"></span><br><span class="line">        proxy_http_version 1.1;</span><br><span class="line">        proxy_set_header Upgrade $http_upgrade;</span><br><span class="line">        proxy_set_header Connection &quot;upgrade&quot;;</span><br><span class="line">        proxy_set_header Host $host;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>激活你的网站配置文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo ln -s /etc/nginx/sites-available/oepnclaw /etc/nginx/sites-enabled/</span><br><span class="line">sudo nginx -t</span><br><span class="line">sudo systemctl reload nginx</span><br></pre></td></tr></table></figure><p>在nginx中添加ssl：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo certbot --nginx -d example.com -d www.example.com #输入自己的域名</span><br><span class="line"># 检查续期：</span><br><span class="line">sudo certbot renew --dry-run</span><br></pre></td></tr></table></figure><h3 id="修改配置文件">修改配置文件</h3><p>vim /root/.openclaw/openclaw.json</p><p>找到配置文件中gataway部分，将mode修改为mode，bind修改为lan<br>remote部分url写入自己的域名，如wss://example.com</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"> &quot;gateway&quot;: &#123;</span><br><span class="line">   &quot;port&quot;: 18789,</span><br><span class="line">   &quot;mode&quot;: &quot;remote&quot;,</span><br><span class="line">   &quot;bind&quot;: &quot;lan&quot;,</span><br><span class="line">   &quot;controlUi&quot;: &#123;</span><br><span class="line">     &quot;allowedOrigins&quot;: [</span><br><span class="line">       &quot;http://localhost:18789&quot;,</span><br><span class="line">       &quot;http://127.0.0.1:18789&quot;</span><br><span class="line">     ]</span><br><span class="line">   &#125;,</span><br><span class="line">   &quot;auth&quot;: &#123;</span><br><span class="line">     &quot;mode&quot;: &quot;token&quot;,</span><br><span class="line">     &quot;token&quot;: &quot;xxx&quot;</span><br><span class="line">   &#125;,</span><br><span class="line">   &quot;tailscale&quot;: &#123;</span><br><span class="line">     &quot;mode&quot;: &quot;off&quot;,</span><br><span class="line">     &quot;resetOnExit&quot;: false</span><br><span class="line">   &#125;,</span><br><span class="line">   &quot;remote&quot;: &#123;</span><br><span class="line">     &quot;url&quot;: &quot;wss://example.com&quot;,</span><br><span class="line">     &quot;token&quot;: &quot;xxx&quot;</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="启动oepnclaw">启动oepnclaw</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openclaw gateway --allow-unconfigured</span><br></pre></td></tr></table></figure><p>启动成功即可在外网访问openclaw，地址：<a href="https://example.com?token=xxx%EF%BC%8Cxxx%E4%B8%BA%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E4%B8%AD%E7%9A%84token">https://example.com?token=xxx，xxx为配置文件中的token</a></p><h3 id="常见问题">常见问题</h3><p>Q：提示pairing required怎么回事<br>A：设备需要网关主机的配对批准。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">openclaw devices list  </span><br><span class="line">openclaw devices approve &lt;requestId&gt;</span><br></pre></td></tr></table></figure><p><img src="https://offer.youngsay.cn/blog/file-20260307154216189.png" alt=""><br><img src="https://offer.youngsay.cn/blog/file-20260307154216183.png" alt=""></p><h3 id="接入聊天软件">接入聊天软件</h3><p>OpenClaw支持WhatsApp、Telegram等各种软件，详细方法可以参考官方文档。<a href="https://docs.openclaw.ai/zh-CN/channels">接入聊天渠道</a>我选择的是接入飞书机器人。</p><p><img src="https://offer.youngsay.cn/blog/file-20260307154216172.jpg" alt=""></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近OpenClaw大火，打开各大自媒体，全是养只小龙虾🦞，腾讯现场装机排满了长队，网上甚至也有了付费安装的业务。&lt;/p&gt;
&lt;p&gt;在openclaw在圈内小火的时候，带动了mac mini的销量，而我似乎没有养龙虾🦞的需求（穷），就没有凑热闹。最近又火爆互联网，刚好有闲</summary>
      
    
    
    
    <category term="AI" scheme="https://youngsay.cn/categories/AI/"/>
    
    
    <category term="OpenClaw" scheme="https://youngsay.cn/tags/OpenClaw/"/>
    
  </entry>
  
  <entry>
    <title>如何将Hexo部署至服务器</title>
    <link href="https://youngsay.cn/2026/01/02/redeploy-hexo/"/>
    <id>https://youngsay.cn/2026/01/02/redeploy-hexo/</id>
    <published>2026-01-02T13:44:15.000Z</published>
    <updated>2026-05-06T12:18:25.658Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言">前言</h2><p>2022年腾讯云搞活动买了五年的服务器，才300多。hexo一直部署在服务器上，比在github或vercel上要快不少。最近想用服务器部署一个项目，用到了node18，发现服务器系统还是centos7，在2024年中旬就不再维护了。服务器上除了博客和大学做的一些<s>垃圾</s>也没什么重要的东西，所以今天直接重装了系统，导致博客打不开，晚上又重新部署了下博客。</p><p>上一次部署的时候就没有留痕，这次部署也遇到些问题，所以打算写一篇博客记录一下。</p><hr><h2 id="一、-环境准备：安装核心依赖">一、 环境准备：安装核心依赖</h2><p>首先，我们需要安装 Node.js、Git、Nginx 以及用于自动化配置 HTTPS 的 Certbot。以 Ubuntu/Debian 系系统为例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># 添加 Node.js 18 运行源</span><br><span class="line">curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -</span><br><span class="line"></span><br><span class="line"># 更新并安装依赖</span><br><span class="line">sudo apt update</span><br><span class="line">sudo apt install -y nodejs git nginx certbot python3-certbot-nginx</span><br></pre></td></tr></table></figure><blockquote><p>为什么用 Certbot？</p><p>以往配置 HTTPS 需要手动生成 CSR、上传验证文件、修改 Nginx 配置，流程极其繁琐。现在只需一行命令，Certbot 就能自动完成证书申请和 Nginx 配置，简直是运维神器。</p></blockquote><hr><h2 id="二、-配置-Nginx：让站点可访问">二、 配置 Nginx：让站点可访问</h2><p>我们需要为博客创建一个 Nginx 配置文件。</p><ol><li>创建配置文件：<code>sudo vim /etc/nginx/sites-available/yourdomain</code></li><li><strong>写入基础配置：</strong></li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">    listen 80;</span><br><span class="line">    server_name example.com www.example.com; # 替换为你的域名</span><br><span class="line"></span><br><span class="line">    root /var/www/hexo; # 博客文件存放目录</span><br><span class="line">    index index.html;</span><br><span class="line"></span><br><span class="line">    location / &#123;</span><br><span class="line">        try_files $uri $uri/ /index.html;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    access_log /var/log/nginx/hexo.access.log;</span><br><span class="line">    error_log  /var/log/nginx/hexo.error.log;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li><strong>激活配置：</strong></li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># 创建软链接以激活站点</span><br><span class="line">sudo ln -s /etc/nginx/sites-available/yourdomain /etc/nginx/sites-enabled/</span><br><span class="line"></span><br><span class="line"># 测试配置并重启 Nginx</span><br><span class="line">sudo nginx -t</span><br><span class="line">sudo systemctl reload nginx</span><br></pre></td></tr></table></figure><blockquote><p><strong>小技巧：</strong> 使用 <code>sites-available</code> 和 <code>sites-enabled</code> 的模式是为了方便管理。如果你想临时下线某个站点，只需删除 <code>sites-enabled</code> 里的快捷方式（软链接）即可，源文件依然保留。</p></blockquote><h2 id="三、-开启-HTTPS：安全加固">三、 开启 HTTPS：安全加固</h2><p>在域名解析生效后，执行以下操作，Certbot 会自动修改 Nginx 配置并加入 SSL 支持：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sudo certbot --nginx -d example.com -d www.example.com</span><br><span class="line"></span><br><span class="line"># 测试自动续期（模拟执行）</span><br><span class="line">sudo certbot renew --dry-run</span><br></pre></td></tr></table></figure><h2 id="四、-自动化部署：Git-Hooks-方案">四、 自动化部署：Git Hooks 方案</h2><p>为了实现本地执行 <code>hexo d</code> 就能自动更新服务器文件，我们需要在服务器上配置一个 Git 仓库作为“中转站”。</p><h3 id="1-安全起见：创建专用-git-用户">1. 安全起见：创建专用 git 用户</h3><p>为了避免直接使用 root 账户带来的安全隐患，我们创建一个名为 <code>git</code> 的用户。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo useradd -m git</span><br><span class="line">sudo passwd git # 设置密码</span><br></pre></td></tr></table></figure><h3 id="2-配置-SSH-免密登录（可选但推荐）">2. 配置 SSH 免密登录（可选但推荐）</h3><p>在本地终端（如 Git Bash）生成秘钥对并将其公钥上传至服务器，后续部署无需反复输入密码。</p><ul><li><strong>本地操作</strong>：在本地计算机上打开Git Bush执行命令: <code>ssh-keygen -t rsa</code>，生成密钥对。运行完后可在<code>C:\Users\用户名.ssh</code>文件下找到<code>id_rsa</code>和<code>id_rsa.pub</code>文件，前者为私钥文件，后者为公钥文件</li><li><strong>服务器操作：</strong><br>在服务器终端先执行<code>makdir -p /home/git/.ssh</code>，后执行<code>cd /home/git/.ssh</code>以及<code>nano authorized_keys</code>，输入本地id_rsa.pub文件内容。粘入公钥内容后按：Ctrl+O （写出 write-out），回车确认文件名（默认显示 authorized_keys），然后按 Ctrl+X 退出 nano。</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">su - git</span><br><span class="line">makdir -p /home/git/.ssh</span><br><span class="line">cd /home/git/.ssh</span><br><span class="line">nano authorized_keys # 粘贴本地公钥</span><br><span class="line">chmod 700 ~/.ssh</span><br><span class="line">chmod 600 ~/.ssh/authorized_keys</span><br></pre></td></tr></table></figure><h3 id="3-创建-Git-裸库与钩子">3. 创建 Git 裸库与钩子</h3><p><strong>裸库（Bare Repository）</strong> 不包含工作区，仅用于存储版本历史，是实现自动部署的核心。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cd /home/git</span><br><span class="line">git init --bare blog.git</span><br><span class="line">chown -R git:git blog.git</span><br></pre></td></tr></table></figure><p>接着，编辑<strong>钩子文件（post-receive）</strong>，当有新代码 push 进来时，自动将其检出到 Web 目录：</p><ol><li><strong>新建文件：</strong> <code>nano /home/git/blog.git/hooks/post-receive</code></li><li><strong>写入以下脚本：</strong></li></ol> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">#!/bin/bash</span><br><span class="line"># --work-tree 对应 Nginx 配置中的 root 目录</span><br><span class="line"># --git-dir 对应 Git 仓库目录</span><br><span class="line">git --work-tree=/var/www/hexo --git-dir=/home/git/blog.git checkout -f</span><br></pre></td></tr></table></figure><ol start="3"><li><strong>赋予执行权限并调整目录所有权：</strong></li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">chmod +x /home/git/blog.git/hooks/post-receive</span><br><span class="line"># 确保 git 用户对 Web 目录有写入权限</span><br><span class="line">sudo chown -R git:git /var/www/hexo</span><br></pre></td></tr></table></figure><h2 id="五、-本地配置：一键发布">五、 本地配置：一键发布</h2><p>最后一步，修改本地 Hexo 根目录下的 <code>_config.yml</code> 文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">deploy:</span><br><span class="line">  type: git</span><br><span class="line">  repository: git@你的服务器IP:/home/git/blog.git</span><br><span class="line">  branch: master</span><br></pre></td></tr></table></figure><p><strong>发布流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">hexo clean</span><br><span class="line">hexo g</span><br><span class="line">hexo d</span><br></pre></td></tr></table></figure><p>至此可以在浏览器中输入域名来访问博客站点了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;
&lt;p&gt;2022年腾讯云搞活动买了五年的服务器，才300多。hexo一直部署在服务器上，比在github或vercel上要快不少。最近想用服务器部署一个项目，用到了node18，发现服务器系统还是centos7，在2024年中旬就不再维护了。</summary>
      
    
    
    
    <category term="项目" scheme="https://youngsay.cn/categories/%E9%A1%B9%E7%9B%AE/"/>
    
    
    <category term="Hexo" scheme="https://youngsay.cn/tags/Hexo/"/>
    
  </entry>
  
  <entry>
    <title>2025年终总结</title>
    <link href="https://youngsay.cn/2025/12/14/2025/"/>
    <id>https://youngsay.cn/2025/12/14/2025/</id>
    <published>2025-12-14T08:45:15.000Z</published>
    <updated>2026-05-06T12:18:25.593Z</updated>
    
    <content type="html"><![CDATA[<p>匆匆，又过了一年，今年是我的本命年，上月刚过了本命年生日，也没有特别的仪式，和往常一样，和往年一样。</p><p>这一年，是我毕业后的第二年，也是工作的第二年，与大学生活渐行渐远，上学时不觉得多么幸福，失去后才懂得珍惜，<strong>我活在幸福里，而我却不知道</strong>。当然，人也会美化过去，包括过去的痛苦。这一年，我换了家公司，继续从事测试开发的工作，算上实习的时间，已经是一名练习时长两年半的测试开发工程师了。工作两年多的时间，一定是有一些改变的，对工作技能的熟悉，balabala，不仅仅是班味更重、身体更差…上学时想找到工作，工作后不想上班，想起朋友和我说的一句话，“生活像一枚钟表，每天都是从12转到12，只要有电就这样一直走下去。”早出晚归，两点一线，我想着也是很多人不想上班的原因，不是讨厌自己的工作，而是缺少对时间的掌控，不能灵活支配自己的时间。<strong>对时间的掌控力，是影响幸福感的关键因素</strong>。此外，社交媒体是情绪放大器，打开社交媒体，有人在游山玩水，有人用心生活，怎么大家都不用上班，更加加剧了不想上班的情绪。</p><p>这一年，我整本读完的有《德米安》《小王子》 《悲喜同源》《软技能：代码之外的生存指南》《金钱心理学》，阅读的目的，一是为了消遣娱乐，一是希望能学到点什么，无论出于哪种目的，我都没做好。90%的书都没能读完，有的内容不感兴趣，有的觉得没什么新观点，比如最近看的《少有人走的路》，感觉书中讲的都是我知道的，重要的是没有亲身实践，做不到知行合一。10%的书可能读完了，但没能有效吸收，为了读而读，像是有人催促一般，迫不及待快点读完，读的过程没有过多思考，读后也没有总结整理，更没有实践。<strong>得到的是信息，而不是知识</strong>。这个时代最不缺的就是信息，每天被大量信息裹挟，“垃圾进，垃圾出”，除了浪费了时间，什么都没得到。</p><p>这一年，我的居住条件有了些改善，从次卧搬到了主卧独卫，这是今年做的比较满意的事。佛教讲“境随心转”，但我还没有那般修行，早上醒来，看到阳光打到床上，心情都会变好一些，一个较好的居住环境对我影响还挺大的。这一年，我的物欲需求趋于稳定，随着生活水平的提升，物欲需求仍像从前那样，这也是我比较欣慰的。真正了解自己的需求，而不是创造需求。明年，我要适当增加对体验的支出，<strong>相比物质消费，体验消费能带来更持久的幸福感</strong>，多年后回想起那次旅行，和朋友谈起那次晚餐，久久回甘。这一年，最爱我的姥姥走了，遗憾没能多陪陪姥姥。看到一句话感到很暖，“死亡，就是从有子女的地方，搬到了有父母的地方“，姥姥一定是想家了。转眼间我都工作了，父母年纪也越来越大了，小时候觉得无所不能的他们渐渐开始需要我拿定注意，但其实我没有做好承担责任的准备。</p><p>就写到这。我不善于规划，一天，一周，一年，就这样稍纵即逝，我有时会反思，心血来潮制定计划，但执行不了几天又和往常一样，就像现在，我写着年终总结，可能会立下几个flag，到明年年终来看，一个也没能实现。一次又一次尝试改变，一次又一次以失败告终，<strong>晚上想了千条路，早上醒来走原路</strong>。我会抱怨工作太忙，没有时间思考，没有时间改变。但一个又一个宁静的深夜，一个又一个空闲的假期，换来的不是自我反思，不是自我改变，而是逃避直面自己，用娱乐填补空虚。我知道要自律，要运动，要学习…但我做不到知行合一。在困难面前我会退缩，责任面前我会逃避，我喜欢做快乐、容易、简单的事。但可能也没那么遭，只要承认自己是个普通人。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;匆匆，又过了一年，今年是我的本命年，上月刚过了本命年生日，也没有特别的仪式，和往常一样，和往年一样。&lt;/p&gt;
&lt;p&gt;这一年，是我毕业后的第二年，也是工作的第二年，与大学生活渐行渐远，上学时不觉得多么幸福，失去后才懂得珍惜，&lt;strong&gt;我活在幸福里，而我却不知道&lt;/str</summary>
      
    
    
    
    <category term="随笔" scheme="https://youngsay.cn/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="年终总结" scheme="https://youngsay.cn/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>2023年终总结</title>
    <link href="https://youngsay.cn/2024/01/01/2023/"/>
    <id>https://youngsay.cn/2024/01/01/2023/</id>
    <published>2024-01-01T03:03:15.000Z</published>
    <updated>2026-05-06T12:18:25.667Z</updated>
    
    <content type="html"><![CDATA[<p>一年又过去了，这是博客的第三个年终总结。</p><p>回看了前两年的年终总结，都是那样普通平常，而且前两年赶上了疫情，要么延期开学，要么封校，要么提前放假。与博友们的丰富多彩的生活相比，我就像白活了一样。</p><p>今年与往年有些不同，有了新的身份——实习生，活跃地点也由家和学校转移到了公司、出租屋。</p><p>1-4月，开启人生第一份实习，新鲜又陌生，期待又恐惧。</p><p>5月，担心挂科重回学校，4门必修课，大半学期没听课，最后考的还不错。所以说要活在当下，不要透支烦恼，你所想的种种麻烦大都不会发生，即便发生也都会顺利解决。</p><p>8-10月，秋招。秋招并不顺利，我想这是心态的问题，心态差，做什么都无济于事了。每天只是麻木的做着测评、笔试、面试，事后没有复盘、反思。看似很努力，其实只将一件事重复了一次又一次。</p><p>9月，大四开学，学校组织去外地实训，度过快乐、难忘的两周。一起去海边、聚餐、熬夜看鬼片，这才是大学生活。</p><p>11月，秋招不顺利，二次实习。</p><p>2024年了，虚岁24岁了，我不敢想。</p><p>还记得小学英语课上一个小插曲，老师问大家里的梦想是什么，我说想做policeman，老师听成了postman（其实postman还挺符合我的，现在听到postman，脑子里只有那个调试接口的软件），到现在还印象深刻。当时为什么说policeman，我好像没有过当警察的梦想，可能有过却被我忘了，可能是刚好想起这个单词。</p><p>上高中之后，我的目标渐渐清晰，我想做一个程序员，当时对码农概念还比较单一，只是想做出属于自己的网站，也不懂什么前端、后端，网上找到一些便宜甚至免费的虚拟主机，再找到一些现成的源码，上传上去，就能访问了。当时搭过各种博客（wordpress、emlog、typecho…）、影视网站、论坛（Flarunm、Discuz），甚至还有刷钻的网站。捣鼓这些玩意，就浪费了一天又一天的时间，一无所获却倾尽所有。</p><p>再后来才接触到前端，看到自己写的东西能展示在页面上，乐在其中。上了大学，又接触到了后端。从只会写一些简单的页面，到前后端交互，能写一些逻辑，不知道当时自己有多快乐。</p><p>到了大三、大四，有了就业的压力，好像已经忘了最初的目标。不再沉浸在编程的快乐，而是苦于八股文、算法、找工作…想想初学编程的时候，只是单纯喜欢，没有想过什么钱多钱少，什么大厂小厂，一个小功能实现了，我都能乐一天了。现在呢，想要更好的工作，想要更高的工资，永远不知足。</p><p>与过去的自己相比，我已经成长了。从搬源码，到写一些简单的静态页面，到能够前后端交互，虽然这些并不难，但对比以前的自己，已经进步了。从想做一个码农，到成为了一个码农，我的目标已经实现了。<strong>如果我是当初的我，我一定会非常高兴。即使我是现在的我，也没理由不高兴，对吗？</strong></p><p>2024，保持热爱，找寻自我。不再羡慕他人的精彩，为自己的成长喝彩。人生，才刚刚开始。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;一年又过去了，这是博客的第三个年终总结。&lt;/p&gt;
&lt;p&gt;回看了前两年的年终总结，都是那样普通平常，而且前两年赶上了疫情，要么延期开学，要么封校，要么提前放假。与博友们的丰富多彩的生活相比，我就像白活了一样。&lt;/p&gt;
&lt;p&gt;今年与往年有些不同，有了新的身份——实习生，活跃地点</summary>
      
    
    
    
    <category term="随笔" scheme="https://youngsay.cn/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="年终总结" scheme="https://youngsay.cn/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>2022年终总结</title>
    <link href="https://youngsay.cn/2023/01/21/2022/"/>
    <id>https://youngsay.cn/2023/01/21/2022/</id>
    <published>2023-01-21T07:15:15.000Z</published>
    <updated>2026-05-06T12:18:25.702Z</updated>
    
    <content type="html"><![CDATA[<p>时间过得真快啊，今天就是大年三十了，一年又这样过去了。</p><p>没什么可写的，但又想写点什么，毕竟好久没写记录自己的日常了，连”个站商店“也把我网站删除了。与其说是年终总结，不如说是流水账。</p><blockquote><p>《个站商店》给你发了单独的邮件，</p><p>hi 胖虎同学 ：</p><p>很抱歉，你的网站《胖虎同学》未能通过『个站商店』的审核，主要原因是内容非原创，多是技术类或者转载类，这类网站一般比较少人看。等你整改后，欢迎再来哦！</p><p>———— 祝生活愉快！ 邮件来自《个站商店》 2023-01-18 12:51:59</p></blockquote><p>今年也和往年一样，疫情，开学，放假……</p><p>2月28日，开学，开学没几天，疫情严重，在校上网课。5月25日，请假离校，提前过上暑假生活……</p><p>9月5号开始返校，已经是大三的老家伙了，因家里疫情被延期返校，13日回校。开学没几天，疫情严重，在校上网课。11月26日，请假离校，提前过上寒假生活……</p><p>除了开学放假外，今年有两件事可以说说。一件好事，一件坏事。</p><p>一件好事就是找到了实习。12月，我开始准备找实习的事，很幸运在12月30日拿到了实习offer。</p><p>1月5号，去公司报到，开始实习生活。1月21也就是今天，回家过年。这月只工作了11天，而这一周人都走的差不多了，基本上就是摸鱼，所以只工作了5天。</p><p>在这段时间里，感觉挺快乐，是我之前没有体会到的新生活。但也遇到些问题。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;时间过得真快啊，今天就是大年三十了，一年又这样过去了。&lt;/p&gt;
&lt;p&gt;没什么可写的，但又想写点什么，毕竟好久没写记录自己的日常了，连”个站商店“也把我网站删除了。与其说是年终总结，不如说是流水账。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;《个站商店》给你发了单独的邮件，&lt;/</summary>
      
    
    
    
    <category term="随笔" scheme="https://youngsay.cn/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="年终总结" scheme="https://youngsay.cn/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>MySQL数据库：事务</title>
    <link href="https://youngsay.cn/2022/11/29/MySQL-transaction/"/>
    <id>https://youngsay.cn/2022/11/29/MySQL-transaction/</id>
    <published>2022-11-29T05:04:41.000Z</published>
    <updated>2026-05-06T12:18:25.669Z</updated>
    
    <content type="html"><![CDATA[<h3 id="事务的概念">事务的概念</h3><p><strong>事务</strong>是逻辑上的一组操作，能够将各SQL语句打包执行，<strong>要么全都执行，要么全都不执行</strong>。</p><p>想象我们在操作数据库时，数据库突然挂了，或者多个线程写入数据库时覆盖了彼此的更改……一系列操作都会导致数据库数据的不一致性，基于这些问题，数据库有了事务这一机制。</p><p>拿转账举例，A想要向B转账500元，该操作分为两步：</p><ul><li>A的账户减少500元</li><li>B的账户增加500元</li></ul><p>事务会将这两步打包在一起执行，要么全都执行，要么全都不执行。不会出现A的账户少了500，而B账户没有增加500的情况。</p><h3 id="如何使用事务">如何使用事务</h3><p>①开启事务</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">start</span> transaction;</span><br></pre></td></tr></table></figure><p>②执行多条SQL语句</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- A账户减少500</span></span><br><span class="line"><span class="keyword">update</span> accout <span class="keyword">set</span> money<span class="operator">=</span>money<span class="number">-500</span> <span class="keyword">where</span> name <span class="operator">=</span> <span class="string">&#x27;A&#x27;</span>;</span><br><span class="line"><span class="comment">-- B账户增加500</span></span><br><span class="line"><span class="keyword">update</span> accout <span class="keyword">set</span> money<span class="operator">=</span>money<span class="operator">+</span><span class="number">500</span> <span class="keyword">where</span> name <span class="operator">=</span> <span class="string">&#x27;B&#x27;</span>;</span><br></pre></td></tr></table></figure><p>③回滚或提交：rollback/commit;</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">commit</span>;</span><br></pre></td></tr></table></figure><p>说明：rollback即是全部失败，commit即是全部成功。</p><h3 id="事务的特性">事务的特性</h3><p>关系型数据库（例如：<code>MySQL</code>、<code>SQL Server</code>、<code>Oracle</code> 等）事务都有四大特性：</p><p>原子性</p><p><strong>原子性</strong>：事务为执行的最小单元，不可再划分，要么全都执行，要么全都不执行。</p><p><strong>一致性</strong>：事务执行前后的数据要保持一致，结果不能出现异常。拿转账举例，转账前后两个账户的总额是不变的。</p><p><strong>持久性</strong>：事务带来的影响是持久的，不会因为数据库故障而产生任何影响。</p><p><strong>隔离性</strong>：并发访问数据库时，各事务之间的数据库是独立的，一个用户的事务不会被其他事务所干扰。</p><p>只有保证事务的原子性、持久性和隔离性，才能保证事务的一致性，所以说原子性、持久性和隔离性是事务的手段，一致性是目的。</p><h3 id="并发事务带来的问题">并发事务带来的问题</h3><p><strong>脏读</strong>：一个事务正在访问数据库并对数据进行修改，而该修改并未提交到数据库，另一个进行此时访问数据库读取数据。而这里访问的数据只是一个中间的数据，不一定是最终的结果，这里读到的数据就是一个“脏数据”，基于“脏数据”做出的操作可能是错误的。</p><p><strong>不可重复读</strong>：在一个事务内多次读取同一数据。在多次读取数据之间，另一个事务访问并修改了该数据，那么再次读取该数据时，发现与之前读取到的数据不一样。在一个事务内两次读取到的数据不一样，称为不可重复读。</p><p><strong>幻读</strong>:一个事务(T1)读取了几行数据，此时另一个事务(T2)插入了一些数据。事务T1再次读取该数据发现多了一些原本不存在的数据，这就是幻读。幻读是不可重复读的特殊情况，不可重复读强调的是数据的更改，幻读强调的是数据的增加。</p><h3 id="事务隔离级别">事务隔离级别</h3><p>SQL标准定义了四个隔离级别，隔离级别越高，并行化就越低。</p><ul><li>read-uncommitted：读取未提交。最低的隔离级别。允许读取未提交的数据，可能导致脏读、不可重复读、幻读。</li><li>read-committed：读取已提交。不能读取未提交的数据，即事务在修改数据时另一个事务不能执行读操作。可以防止脏读，不能避免不可重复读、幻读。</li><li>repeatable-read：可重复读。事务在读取数据时，其他事务不能修改正在读取的数据。因此对同一数据的多次修改结果都是一致的，可以防止脏读、不可重复读，不能避免幻读。</li><li>serializable：串行化。最高的隔离级别。各事务串行执行，相互之间不存在干扰，可以防止脏读、不可重复读和幻读。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h3 id=&quot;事务的概念&quot;&gt;事务的概念&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;事务&lt;/strong&gt;是逻辑上的一组操作，能够将各SQL语句打包执行，&lt;strong&gt;要么全都执行，要么全都不执行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;想象我们在操作数据库时，数据库突然挂了，或者多个线程写入</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="MySQL" scheme="https://youngsay.cn/tags/MySQL/"/>
    
  </entry>
  
  <entry>
    <title>MySQL数据库：索引</title>
    <link href="https://youngsay.cn/2022/11/28/MySQL-index/"/>
    <id>https://youngsay.cn/2022/11/28/MySQL-index/</id>
    <published>2022-11-28T13:04:41.000Z</published>
    <updated>2026-05-06T12:18:25.588Z</updated>
    
    <content type="html"><![CDATA[<p>索引是一种用于快速查找和检索数据的数据结构，类似于图书的目录，通过索引可以更快地找到对应的数据。</p><h3 id="索引的优缺点">索引的优缺点</h3><p>优点：</p><ul><li>提高了查找的效率</li><li>通过创建唯一性索引，确保了每一行数据的唯一性。</li></ul><p>缺点：</p><ul><li>索引使用物理文件存储，带来了空间的消耗。</li><li>创建和维护索引需要耗费时间。当数据发生更改时，索引也会动态修改，降低SQL执行效率。</li></ul><h3 id="索引SQL">索引SQL</h3><h4 id="查看索引">查看索引</h4><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> 索引名 <span class="keyword">from</span> 表名(列名);</span><br></pre></td></tr></table></figure><p><img src="https://cos.youngsay.cn/blog/202211282118307.png" alt="image-20221128161811672"></p><h4 id="创建索引">创建索引</h4><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">create</span> index 索引名 <span class="keyword">on</span> 表名(列名);</span><br></pre></td></tr></table></figure><p><img src="https://cos.youngsay.cn/blog/202211282118347.png" alt="image-20221128162120786"></p><h4 id="删除索引">删除索引</h4><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">drop</span> index 索引名 <span class="keyword">on</span> 表名</span><br></pre></td></tr></table></figure><p><img src="https://cos.youngsay.cn/blog/202211282118794.png" alt="image-20221128162440340"></p><h3 id="索引底层的数据结构">索引底层的数据结构</h3><p>索引是一种查找更快的数据结果。</p><p>数组和链表的查找都需要遍历，最先淘汰。</p><p>二叉搜索树最坏情况下会变成单支树，查找的时间复杂度变为O(N)，淘汰。</p><p>AVL树和红黑树使得左右子树高度相对平衡，但数据量太大时，树的高度过高，比较次数过多，即磁盘IO过多，淘汰。</p><p>哈希表查找的时间复杂度是O(1)，是不是可以作为索引的数据结构了呢？NO！哈希表不支持顺序和范围查找，而SQL要经常进行排序和</p><p>范围查询，淘汰。</p><p><strong>目前大多数数据库采用B树或B+树作为索引结构</strong>，在MySQL中，MyISAM 引擎和 InnoDB 引擎都是使用 B+树 作为索引结构。</p><p>B树称为多路平衡查找树，B+树是在B树基础上的变现，二者都是多叉平衡树。</p><h4 id="B树">B树</h4><p><img src="https://cos.youngsay.cn/blog/202211282118366.png" alt="image-20221128203706890"></p><h4 id="B-树">B+树</h4><p><img src="https://cos.youngsay.cn/blog/202211282118609.png" alt="image-20221128203611266"></p><h4 id="区别和联系">区别和联系</h4><ul><li><p>B树N个值，划分为N+1个结点；B+树N个值，划分为N个结点。</p></li><li><p>B树叶子结点相互独立;B+树叶子结点是链式结构，指向相邻结点。</p></li><li><p>B树每个结点既存放key，又存放data；B+树只有叶子结点既存放key，又存放data，其他结点只存key。</p></li><li><p>B树的查找过程是对每个关键字二分查找，可能没有到叶子结点就检索到了；B+树更加稳定，每次查找都是从根节点开始，到叶子结点结束。</p></li></ul><h4 id="索引为什么要用B-树">索引为什么要用B+树</h4><ul><li>每次都是从根节点到叶子结点，IO次数都差不多，查询效率稳定。</li><li>磁盘IO请求数少，查询速度快。</li><li>叶子结点采用链式存储结构，方便数据范围查询。</li><li>非叶子结点只存储key，占用空间小，甚至可以缓存到内存中。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;索引是一种用于快速查找和检索数据的数据结构，类似于图书的目录，通过索引可以更快地找到对应的数据。&lt;/p&gt;
&lt;h3 id=&quot;索引的优缺点&quot;&gt;索引的优缺点&lt;/h3&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提高了查找的效率&lt;/li&gt;
&lt;li&gt;通过创建唯一性索引，确保了每一行数据</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="MySQL" scheme="https://youngsay.cn/tags/MySQL/"/>
    
  </entry>
  
  <entry>
    <title>socket套接字网络编程</title>
    <link href="https://youngsay.cn/2022/11/22/Java-Socket/"/>
    <id>https://youngsay.cn/2022/11/22/Java-Socket/</id>
    <published>2022-11-22T06:14:41.000Z</published>
    <updated>2026-05-06T12:18:25.626Z</updated>
    
    <content type="html"><![CDATA[<p>网络编程使计算机不再是一个孤立的个体，而是一个互联的整体，通过网络编程，可实现主机各进程间的数据传输。网络编程的核心就是套接字socket。socket是由系统提供用于网络通信的技术，是基于TCP/IP协议的网络通信的基本操作单元。</p><p>socket主要分为三类：流套接字、数据报套接字、原始套接字。我们这里主要介绍前两种：</p><p><strong>流套接字</strong>：使用传输层TCP协议，提供面向连接、可靠的数据传输，面向字节流，有接收缓冲区和发送缓冲区，传输数据大小无限制。</p><p><strong>数据报套接字</strong>：使用传输层UDP协议，提供无连接、不可靠传输，面向数据报，只有接收缓冲区，传输大小受限，一次最多传输64K。</p><p>下面我们来看下Java中是如何使用socket来进行网络编程的。</p><h3 id="数据报套接字">数据报套接字</h3><p>Java数据报套接字编程有两个核心的类，<code>DatagramSocket</code>和<code>DatagramPacket</code>。</p><h4 id="两个核心类">两个核心类</h4><p><code>DatagramSocket</code>用于创建一个数据报套接字，用于发送和接收数据报。主要的构造方法如下：</p><table><thead><tr><th>方法名</th><th>说明</th></tr></thead><tbody><tr><td>DatagramSocket()</td><td>创建一个UDP数据报套接字的Socket，绑定到本机任意一个随机端口 （一般用于客户端）</td></tr><tr><td>DatagramSocket(int port)</td><td>创建一个UDP数据报套接字的Socket，绑定到本机指定的端口（一般用 于服务端）</td></tr></tbody></table><p>常用方法如下：</p><table><thead><tr><th>方法名</th><th>说明</th></tr></thead><tbody><tr><td>void receive(DatagramPacket p)</td><td>从此套接字接收数据报（如果没有接收到数据报，会阻塞等待）</td></tr><tr><td>void send(DatagramPacket p)</td><td>从此套接字发送数据报包（不会阻塞等待，直接发送）</td></tr><tr><td>void close()</td><td>关闭此数据报套接字</td></tr></tbody></table><p>方法内的参数类型，就是我们要介绍的另一个核心类：<code>DatagramPacket</code>，该类表示数据报包，用于进程间的发送和接收。主要构造方法如下：</p><table><thead><tr><th>方法名</th><th>说明</th></tr></thead><tbody><tr><td>DatagramPacket(byte[] buf, int length)</td><td>构造一个数据报包，<strong>用来接收</strong>，接收的数据保存在字节数组buf中，接收指定长度length</td></tr><tr><td>DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)</td><td>构造一个数据报包，<strong>用来发送</strong>，发送的数据为字节数组buf，从offset到长度length，指明目标主机地址address，即IP和端口号</td></tr></tbody></table><p>常用方法如下：</p><table><thead><tr><th>方法名</th><th>说明</th></tr></thead><tbody><tr><td>InetAddress getAddress()</td><td>获取该数据报发送或接收数据报的计算机的IP地址。</td></tr><tr><td>getSocketAddress()</td><td>获取该数据包发送到或正在从其发送的远程主机的SocketAddress（通常为IP地址+端口号）。</td></tr><tr><td>int getPort()</td><td>获取该数据报发送或接收数据报的端口号</td></tr><tr><td>byte[] getData()</td><td>获取数据报中的数据</td></tr><tr><td>int getLength()</td><td>返回要发送的数据的长度或接收到的数据的长度。</td></tr></tbody></table><h4 id="服务器端">服务器端</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UdpServer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="type">DatagramSocket</span> <span class="variable">socket</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">UdpServer</span><span class="params">(<span class="type">int</span> port)</span> <span class="keyword">throws</span> SocketException &#123;</span><br><span class="line">        <span class="comment">//创建一个服务器端的数据报套接字，通常指明端口</span></span><br><span class="line">        socket = <span class="keyword">new</span> <span class="title class_">DatagramSocket</span>(port);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">start</span><span class="params">()</span>&#123;</span><br><span class="line">System.out.println(<span class="string">&quot;服务器已连接&quot;</span>);</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">                <span class="comment">//1.接收请求</span></span><br><span class="line">                <span class="comment">//创建数据报包，用来接收客户端发来的请求</span></span><br><span class="line">                <span class="type">DatagramPacket</span> <span class="variable">reqPacket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DatagramPacket</span>(<span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">1024</span>],<span class="number">1024</span>);</span><br><span class="line">                <span class="comment">//接收数据报，没有数据传来时会阻塞等待</span></span><br><span class="line">                socket.receive(reqPacket);</span><br><span class="line">                <span class="comment">//★从接收的数据报中解析数据，构造成请求字符串</span></span><br><span class="line">                <span class="type">String</span> <span class="variable">req</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(reqPacket.getData(),<span class="number">0</span>,reqPacket.getLength(),<span class="string">&quot;utf8&quot;</span>);</span><br><span class="line">                <span class="comment">//2.构造响应</span></span><br><span class="line">                <span class="comment">//这里没有做处理，发来什么就返回什么。</span></span><br><span class="line">                <span class="type">String</span> <span class="variable">resp</span> <span class="operator">=</span> process(req);</span><br><span class="line">                <span class="comment">//创建数据报包，用来发送响应</span></span><br><span class="line">                <span class="comment">//将响应字符串转换成字节数组，指明目的主机地址</span></span><br><span class="line">                <span class="type">DatagramPacket</span> <span class="variable">respPacket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DatagramPacket</span>(resp.getBytes(),resp.getBytes().length,reqPacket.getSocketAddress());</span><br><span class="line">                <span class="comment">//3.发送响应</span></span><br><span class="line">                socket.send(respPacket);</span><br><span class="line">                <span class="comment">//4.打印日志</span></span><br><span class="line">                System.out.printf(<span class="string">&quot;[%s:%d] req: %s, resp: %s\n&quot;</span>,</span><br><span class="line">                        reqPacket.getAddress().toString(), reqPacket.getPort(), req, resp);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//构造响应</span></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">process</span><span class="params">(String req)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> req;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> SocketException &#123;</span><br><span class="line">        <span class="type">UdpServer</span> <span class="variable">server</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UdpServer</span>(<span class="number">9090</span>);</span><br><span class="line">        server.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码中★处要尤为注意：</p><p>构造请求字符串的第三个参数：请求字符串长度，是接收到的数据报的长度，reqPacket.getLength()。一定不要写成reqPacket.getData().length()，这个是字节数组的长度，该代码为1024。</p><h4 id="客户端">客户端</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UdpClient</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> port; <span class="comment">//服务器端口号</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String ip; <span class="comment">//服务器ip地址</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">DatagramSocket</span> <span class="variable">socket</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">UdpClient</span><span class="params">(String ip,<span class="type">int</span> port)</span> <span class="keyword">throws</span> SocketException &#123;</span><br><span class="line">        <span class="comment">//创建一个客户端的数据报套接字，通常不指明端口</span></span><br><span class="line">        <span class="built_in">this</span>.socket = <span class="keyword">new</span> <span class="title class_">DatagramSocket</span>();</span><br><span class="line">        <span class="built_in">this</span>.ip = ip;</span><br><span class="line">        <span class="built_in">this</span>.port = port;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">start</span><span class="params">()</span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">                <span class="comment">//0.输入请求</span></span><br><span class="line">                System.out.println(<span class="string">&quot;请输入请求：&quot;</span>);</span><br><span class="line">                <span class="type">Scanner</span> <span class="variable">scanner</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Scanner</span>(System.in);</span><br><span class="line">                <span class="type">String</span> <span class="variable">req</span> <span class="operator">=</span> scanner.nextLine();</span><br><span class="line">                <span class="comment">//1.构造请求</span></span><br><span class="line">                <span class="comment">//创建一个数据报包，用来发送请求★</span></span><br><span class="line">                <span class="type">DatagramPacket</span> <span class="variable">reqPacket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DatagramPacket</span>(req.getBytes(),req.getBytes().length,InetAddress.getByName(ip),port);</span><br><span class="line">                <span class="comment">//2.发送请求</span></span><br><span class="line">                socket.send(reqPacket);</span><br><span class="line">                <span class="comment">//创建接收数据包，用来接收服务器发来的响应</span></span><br><span class="line">                <span class="type">DatagramPacket</span> <span class="variable">respPacket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DatagramPacket</span>(<span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">1024</span>],<span class="number">1024</span>);</span><br><span class="line">                <span class="comment">//3.接收响应</span></span><br><span class="line">                socket.receive(respPacket);</span><br><span class="line">                <span class="type">String</span> <span class="variable">resp</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(respPacket.getData(),<span class="number">0</span>,respPacket.getLength());</span><br><span class="line">                System.out.println(<span class="string">&quot;resp：&quot;</span>+resp);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            e.printStackTrace();</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="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> SocketException &#123;</span><br><span class="line">        <span class="comment">//参数内为服务器的ip和端口号</span></span><br><span class="line">        <span class="type">UdpClient</span> <span class="variable">client</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UdpClient</span>(<span class="string">&quot;127.0.0.1&quot;</span>,<span class="number">9090</span>);</span><br><span class="line">        client.start();</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>代码中★处：</p><p>构造发送数据包时要传入目的主机的地址，InetAddress.getByName(ip),port分别为主机的ip地址和端口号，除了这样，还可以通过创建socketAddress类来指定主机地址。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">SocketAddress</span> <span class="variable">socketAddress</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">InetSocketAddress</span>(ip,port);</span><br><span class="line"><span class="type">DatagramPacket</span> <span class="variable">reqPacket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DatagramPacket</span>(req.getBytes(),req.getBytes().length,socketAddress);</span><br></pre></td></tr></table></figure><h3 id="流套接字">流套接字</h3><p>Java流套接字编程也有两个核心的类，<code>ServerSocket</code>和<code>Socket</code>。</p><h4 id="两个核心类-2">两个核心类</h4><p>ServerSockert通常用于创建一个服务器端的流套接字，常用构造方法如下：</p><table><thead><tr><th>方法名</th><th>说明</th></tr></thead><tbody><tr><td>ServerSocket(int port)</td><td>创建一个服务端流套接字Socket，并绑定到指定端口</td></tr></tbody></table><p>常用方法如下：</p><table><thead><tr><th>方法名</th><th>说明</th></tr></thead><tbody><tr><td>Socket accept()</td><td>监听要连接此客户端的套接字，有客户端连接后，返回一个服务端Socket对象，并基于该Socket建立与客户端的连接，否则阻塞等待</td></tr><tr><td>void close()</td><td>关闭此套接字</td></tr></tbody></table><p>Socket通常用于常见一个客户端的流套接字，常用构造方法如下：</p><table><thead><tr><th>方法名</th><th>说明</th></tr></thead><tbody><tr><td>Socket(String host, int port)</td><td>创建一个客户端流套接字Socket，并与对应IP的主机上，对应端口的进程建立连接</td></tr></tbody></table><p>常用方法如下：</p><table><thead><tr><th>方法名</th><th>说明</th></tr></thead><tbody><tr><td>InetAddress getInetAddress()</td><td>返回套接字所连接的地址</td></tr><tr><td>InputStream getInputStream()</td><td>返回此套接字的输入流</td></tr><tr><td>OutputStream getOutputStream()</td><td>返回此套接字的输出流</td></tr></tbody></table><h4 id="服务器端-2">服务器端</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TcpServer</span> &#123;</span><br><span class="line">    <span class="type">ServerSocket</span> <span class="variable">server</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">TcpServer</span><span class="params">(<span class="type">int</span> port)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        <span class="comment">//创建一个服务器套接字，指定服务器端口号</span></span><br><span class="line">        server = <span class="keyword">new</span> <span class="title class_">ServerSocket</span>(port);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">start</span><span class="params">()</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">            <span class="comment">//监听客户端并建立连接</span></span><br><span class="line">            <span class="type">Socket</span> <span class="variable">client</span> <span class="operator">=</span> server.accept();</span><br><span class="line">            processConnection(client);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">processConnection</span><span class="params">(Socket client)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        System.out.printf(<span class="string">&quot;[%s,%d]客户端建立连接\n&quot;</span>,client.getInetAddress(),client.getPort());</span><br><span class="line">        <span class="comment">//创建输入输出流，输入流用来接收客户端传来的数据，输出流用来将数据发给客户端</span></span><br><span class="line">        <span class="keyword">try</span>(<span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> client.getInputStream())&#123;</span><br><span class="line">            <span class="keyword">try</span>(<span class="type">OutputStream</span> <span class="variable">outputStream</span> <span class="operator">=</span> client.getOutputStream())&#123;</span><br><span class="line">                <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">                    <span class="type">Scanner</span> <span class="variable">scanner</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Scanner</span>(inputStream);</span><br><span class="line">                    <span class="keyword">if</span>(!scanner.hasNext())&#123;</span><br><span class="line">                        System.out.printf(<span class="string">&quot;[%s,%d]客户端断开连接&quot;</span>,client.getInetAddress(),client.getPort());</span><br><span class="line">                        <span class="keyword">break</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="comment">//1.接收请求</span></span><br><span class="line">                    <span class="type">String</span> <span class="variable">req</span> <span class="operator">=</span> scanner.nextLine();</span><br><span class="line">                    <span class="comment">//2.构造响应</span></span><br><span class="line">                    <span class="type">String</span> <span class="variable">resp</span> <span class="operator">=</span> process(req);</span><br><span class="line">                    <span class="comment">//3.返回响应</span></span><br><span class="line">                    <span class="type">PrintWriter</span> <span class="variable">printWriter</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PrintWriter</span>(outputStream);</span><br><span class="line">                    <span class="comment">//★调用println方法，不能是print和write</span></span><br><span class="line">                    printWriter.println(resp);</span><br><span class="line">                    printWriter.flush();</span><br><span class="line">                    System.out.printf(<span class="string">&quot;req:%s,resp:%s\n&quot;</span>,req,resp);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">//★关闭连接</span></span><br><span class="line">            client.close();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">process</span><span class="params">(String req)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> req;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        <span class="type">TcpServer</span> <span class="variable">server</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TcpServer</span>(<span class="number">9090</span>);</span><br><span class="line">        server.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>★处要尤为注意：</p><ol><li><p>调用的是println方法，不是print也不是write。println方法会为写入的数据后面添加上换行符，而print和write不会。客户端接收数据时是以换行符为结束来获取数据。如果找不到换行符，将一直不能接收响应，造成阻塞。同理，客户端发送请求时也要使用println方法，否则服务器也会接收不到请求而一直阻塞。</p></li><li><p>大家可能会想，为什么这里的client要关闭连接，而server和之前的数据报套接字都不需要关闭连接。关闭连接的前提是不再使用了，对于UDP和ServerSocket，它们是贯穿程序始终的，而这里的client，也就是Socket，每个连接都有一个Socket，断开连接也就不再需要这个Socket了，需要进行释放。</p></li></ol><h4 id="客户端-2">客户端</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TcpClient</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> port;</span><br><span class="line">    <span class="keyword">private</span> String ip;</span><br><span class="line">    <span class="type">Socket</span> <span class="variable">client</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">TcpClient</span><span class="params">(String ip,<span class="type">int</span> port)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        <span class="built_in">this</span>.port = port;</span><br><span class="line">        <span class="built_in">this</span>.ip = ip;</span><br><span class="line">        <span class="comment">//★创建客户端套接字，并将其连接到指定IP地址的指定端口号。</span></span><br><span class="line">        <span class="built_in">this</span>.client = <span class="keyword">new</span> <span class="title class_">Socket</span>(ip,port);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">start</span><span class="params">()</span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span>(<span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> client.getInputStream()) &#123;</span><br><span class="line">            <span class="keyword">try</span>(<span class="type">OutputStream</span> <span class="variable">outputStream</span> <span class="operator">=</span> client.getOutputStream())&#123;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">                    <span class="comment">//1.构造请求</span></span><br><span class="line">                    <span class="type">Scanner</span> <span class="variable">scanner</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Scanner</span>(System.in);</span><br><span class="line">                    <span class="type">String</span> <span class="variable">req</span> <span class="operator">=</span> scanner.next();</span><br><span class="line">                    <span class="comment">//2.发送请求</span></span><br><span class="line">                    <span class="type">PrintWriter</span> <span class="variable">printWriter</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PrintWriter</span>(outputStream);</span><br><span class="line">                    <span class="comment">//★调用println方法，不能是print和write</span></span><br><span class="line">                    printWriter.println(req);</span><br><span class="line">                    printWriter.flush();</span><br><span class="line">                    <span class="comment">//3.接收响应</span></span><br><span class="line">                    <span class="type">Scanner</span> <span class="variable">respScan</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Scanner</span>(inputStream);</span><br><span class="line">                    <span class="type">String</span> <span class="variable">resp</span> <span class="operator">=</span> respScan.next();</span><br><span class="line">                    System.out.println(<span class="string">&quot;resp:&quot;</span>+resp);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            e.printStackTrace();</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="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        <span class="type">TcpClient</span> <span class="variable">client</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TcpClient</span>(<span class="string">&quot;127.0.0.1&quot;</span>,<span class="number">9090</span>);</span><br><span class="line">        client.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="问题">问题</h4><p>流套接字的客户端服务器通信还存在一个问题，只能满足一个客户端的通信。其他客户端尝试与服务器连接时，会发生阻塞。观察服务器代码中下面这段代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">start</span><span class="params">()</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">        <span class="comment">//监听客户端并建立连接</span></span><br><span class="line">        <span class="type">Socket</span> <span class="variable">client</span> <span class="operator">=</span> server.accept();</span><br><span class="line">        processConnection(client);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>客户端1与服务器建立连接后，服务器线程执行processConnection代码中的操作，代码中存在while循环，不断地接收客户端1发来的请求，构造响应，返回响应……此时客户端2想要与服务器通信，而服务器线程正在processConnection的while循环中等待客户端1的请求，导致服务器不能与客户端2建立连接。</p><p>TCP每次都需要建立连接，只能支持一对一，也就是端到端的通信。就像打电话一样，同一时间只能接听一个电话，当正在通话时，别的电话是打不进来的。而UDP是无连接的，因此支持一对一、一对多、多对多交互通信。所以我们在数据报套接字时没有出现该问题。</p><p>解决办法也很简单，因为processConnection中的循环未结束导致服务器不能执行accept操作，所以单独创建一个新的线程来完成processConnection操作即可。我们可以用多线程或线程池来解决。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">start</span><span class="params">()</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">        <span class="comment">//建立连接</span></span><br><span class="line">        <span class="type">Socket</span> <span class="variable">client</span> <span class="operator">=</span> server.accept();</span><br><span class="line">        <span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(()-&gt;&#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                processConnection(client);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        t.start();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>其他代码不变，只需改动start方法内的代码，为processConnection操作创建新的线程。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;网络编程使计算机不再是一个孤立的个体，而是一个互联的整体，通过网络编程，可实现主机各进程间的数据传输。网络编程的核心就是套接字socket。socket是由系统提供用于网络通信的技术，是基于TCP/IP协议的网络通信的基本操作单元。&lt;/p&gt;
&lt;p&gt;socket主要分为三类：</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="socket" scheme="https://youngsay.cn/tags/socket/"/>
    
  </entry>
  
  <entry>
    <title>Java文件和IO流</title>
    <link href="https://youngsay.cn/2022/11/17/Java-IO/"/>
    <id>https://youngsay.cn/2022/11/17/Java-IO/</id>
    <published>2022-11-17T12:14:41.000Z</published>
    <updated>2026-05-06T12:18:25.639Z</updated>
    
    <content type="html"><![CDATA[<h3 id="文件基础操作">文件基础操作</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="comment">//创建一个hello的目录</span></span><br><span class="line">    <span class="type">File</span> <span class="variable">dir</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">File</span>(<span class="string">&quot;hello&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span>(!dir.exists())&#123;</span><br><span class="line">        dir.mkdirs();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//在hello目录下创建一个hello.txt文件</span></span><br><span class="line">    <span class="type">File</span> <span class="variable">file</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">File</span>(dir+<span class="string">&quot;/hello.txt&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span>(!file.exists())&#123;</span><br><span class="line">        file.createNewFile();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//得到文件名</span></span><br><span class="line">    System.out.println(file.getName());</span><br><span class="line">    <span class="comment">//得到文件的绝对路径</span></span><br><span class="line">    System.out.println(file.getAbsolutePath());</span><br><span class="line">    <span class="comment">//得到文件的规范路径，当路径不规范时，会进行优化(去掉.)</span></span><br><span class="line">    System.out.println(file.getCanonicalPath());</span><br><span class="line">    <span class="comment">//得到父目录名</span></span><br><span class="line">    System.out.println(file.getParent());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>具体详见：<a href="https://docs.oracle.com/javase/8/docs/api/">File (Java Platform SE 8 ) (oracle.com)</a></p><h3 id="IO流">IO流</h3><p>IO即输入和输出（Input、Output），输入就是将数据读入到内存，输出就是将数据输出到外部存储（如文件、数据库）。数据的输入输出就像水流一样，因此成为IO流。</p><p>IO流的处理方式可分为两种：字节流和字符流。Java中针对IO流的处理有很多类，但最基本的只有4个抽象类。<code>InputStream</code>、<code>OutputStream</code>、<code>Reader</code>、<code>Writer</code>，前两个是对字节流的输入输出，后两个是对字符流的输入输出。下面我们拿普通文件的读写举例，介绍它们的基本操作。</p><h4 id="字节流">字节流</h4><h5 id="InputStream">InputStream</h5><p>用于对文件进行读操作，构造方法内可以是一个文件类型的参数，也可以是一个文件路径(字符串)的参数。</p><p>常用方法：</p><p><code>read()</code> ：一次读取一字节，返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。返回 <code>-1</code> ，表示读到文件的末尾，读取结束。</p><p><code>read(byte b[ ])</code> : 一次读取多个字节，将输入流字节存储到数组b，返回结果为读取字节长度。返回-1，表示读到文件的末尾，读取结束。</p><p><code>read(byte b[], int off, int len)</code> ：在<code>read(byte b[ ])</code> 方法的基础上增加了 <code>off</code> 参数（偏移量）和 <code>len</code> 参数（要读取的最大字节数）。</p><p><code>close()</code> ：关闭输入流释放相关的系统资源。</p><ul><li>一次读取一字节</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">readOne</span><span class="params">(File file,FileInputStream fileInputStream)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        fileInputStream = <span class="keyword">new</span> <span class="title class_">FileInputStream</span>(file);</span><br><span class="line">        <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">read</span> <span class="operator">=</span> fileInputStream.read();</span><br><span class="line">            <span class="keyword">if</span>(read==-<span class="number">1</span>)&#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            System.out.print((<span class="type">char</span>)read);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        fileInputStream.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>一次读取多个字节</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">readByte</span><span class="params">(File file,FileInputStream inputStream)</span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        inputStream = <span class="keyword">new</span> <span class="title class_">FileInputStream</span>(file);</span><br><span class="line">        <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">            <span class="type">byte</span>[] bytes = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">1024</span>];</span><br><span class="line">            <span class="type">int</span> <span class="variable">read</span> <span class="operator">=</span> inputStream.read(bytes);</span><br><span class="line">            <span class="keyword">if</span>(read==-<span class="number">1</span>)&#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; read; i++) &#123;</span><br><span class="line">                System.out.print((<span class="type">char</span>)bytes[i]);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (FileNotFoundException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125;<span class="keyword">finally</span>&#123;</span><br><span class="line">        inputStream.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="OutputStream">OutputStream</h5><p>用于对文件进行写操作，构造方法内可以是一个文件类型的参数，也可以是一个文件路径(字符串)的参数，同时可以指定第二个参数表示是否追加写入(是否清空之前的内容)，true表示追加写入，默认为false。</p><p>在写操作时数据暂时放在缓存区，只有缓存区满后才会真正写入到设备，所以别忘了调用<code>fulsh()</code>，防止数据没有写入设备。</p><p>常用方法：</p><p><code>write(int b)</code> ：将特定字节写入输出流。</p><p><code>write(byte b[ ])</code> : 将数组<code>b</code> 写入到输出流，等价于 <code>write(b, 0, b.length)</code> 。</p><p><code>write(byte[] b, int off, int len)</code> : 在<code>write(byte b[ ])</code> 方法的基础上增加了 <code>off</code> 参数（偏移量）和 <code>len</code> 参数（要读取的最大字节数）。</p><p><code>flush()</code> ：刷新此输出流并强制写出所有缓冲的输出字节。</p><p><code>close()</code> ：关闭输出流释放相关的系统资源。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">writeOne</span><span class="params">(File file, FileOutputStream outputStream)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        outputStream = <span class="keyword">new</span> <span class="title class_">FileOutputStream</span>(file);</span><br><span class="line">        outputStream.write(<span class="number">103</span>);</span><br><span class="line">        outputStream.write(<span class="number">104</span>);</span><br><span class="line">        outputStream.flush();</span><br><span class="line">        </span><br><span class="line">    &#125; <span class="keyword">catch</span> (FileNotFoundException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">        outputStream.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="字符流">字符流</h4><p>字节是电脑中存储信息的基本单位，那为什么还需要字符流呢？</p><p>当存储信息中有中文时，以字节流来读取会出现乱码。字节流更适用于音频、图片等类型文件，当文件中涉及字符类型时，使用字符流更好。字符流默认编码是<code>unicode</code>。</p><p>常见的字符编码：<code>utf8</code> :英文占 1 字节，中文占 3 字节，<code>unicode</code>：任何字符都占 2 个字节，<code>gbk</code>：英文占 1 字节，中文占 2 字节。</p><h5 id="Reader">Reader</h5><p>常用方法：</p><p><code>read()</code> : 从输入流读取一个字符。</p><p><code>read(char[] cbuf)</code> : 从输入流中读取一些字符，并将它们存储到字符数组 <code>cbuf</code>中，等价于 <code>read(cbuf, 0, cbuf.length)</code> 。</p><p><code>read(char[] cbuf, int off, int len)</code> ：在<code>read(char[] cbuf)</code> 方法的基础上增加了 <code>off</code> 参数（偏移量）和 <code>len</code> 参数（要读取的最大字节数）。</p><p><code>close()</code> : 关闭输入流并释放相关的系统资源。</p><ul><li>一次读取一个字符</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">readOne</span><span class="params">(File file,Reader reader)</span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        reader = <span class="keyword">new</span> <span class="title class_">FileReader</span>(file);</span><br><span class="line">        <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">read</span> <span class="operator">=</span> reader.read();</span><br><span class="line">            <span class="keyword">if</span>(read==-<span class="number">1</span>)&#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            System.out.print((<span class="type">char</span>)read);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (FileNotFoundException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125;<span class="keyword">finally</span>&#123;</span><br><span class="line">        reader.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>一次读取多个字符</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">readCharacter</span><span class="params">(File file,Reader reader)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        reader = <span class="keyword">new</span> <span class="title class_">FileReader</span>(file);</span><br><span class="line">        <span class="type">char</span>[] buffer = <span class="keyword">new</span> <span class="title class_">char</span>[<span class="number">1024</span>];</span><br><span class="line">        <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">read</span> <span class="operator">=</span> reader.read(buffer);</span><br><span class="line">            <span class="keyword">if</span> (read==-<span class="number">1</span>)&#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="type">String</span> <span class="variable">s</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(buffer,<span class="number">0</span>,read);</span><br><span class="line">            System.out.println(s);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">catch</span> (FileNotFoundException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">        reader.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="Writer">Writer</h5><p>常用方法：</p><p><code>write(int c)</code> : 写入单个字符。</p><p><code>write(char[] cbuf)</code> ：写入字符数组 <code>cbuf</code>，等价于<code>write(cbuf, 0, cbuf.length)</code>。</p><p><code>write(char[] cbuf, int off, int len)</code> ：在<code>write(char[] cbuf)</code> 方法的基础上增加了 <code>off</code> 参数（偏移量）和 <code>len</code> 参数（要读取的最大字节数）。</p><p><code>write(String str)</code> ：写入字符串，等价于 <code>write(str, 0, str.length())</code> 。</p><p><code>write(String str, int off, int len)</code> ：在<code>write(String str)</code> 方法的基础上增加了 <code>off</code> 参数（偏移量）和 <code>len</code> 参数（要读取的最大字节数）。</p><p><code>append(CharSequence csq)</code> ：将指定的字符序列附加到指定的 <code>Writer</code> 对象并返回该 <code>Writer</code> 对象。</p><p><code>append(char c)</code> ：将指定的字符附加到指定的 <code>Writer</code> 对象并返回该 <code>Writer</code> 对象。</p><p><code>flush()</code> ：刷新此输出流并强制写出所有缓冲的输出字符。</p><p><code>close()</code>:关闭输出流释放相关的系统资源</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">writeOne</span><span class="params">(File file,Writer writer)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        writer = <span class="keyword">new</span> <span class="title class_">FileWriter</span>(file);</span><br><span class="line">        writer.write(<span class="string">&quot;我是panghutx&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">        e.printStackTrace();</span><br><span class="line">    &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">        writer.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h3 id=&quot;文件基础操作&quot;&gt;文件基础操作&lt;/h3&gt;
&lt;figure class=&quot;highlight java&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;li</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="IO" scheme="https://youngsay.cn/tags/IO/"/>
    
  </entry>
  
  <entry>
    <title>多线程实例：线程池</title>
    <link href="https://youngsay.cn/2022/11/14/Java-ThreadPool/"/>
    <id>https://youngsay.cn/2022/11/14/Java-ThreadPool/</id>
    <published>2022-11-14T09:04:41.000Z</published>
    <updated>2026-05-06T12:18:25.699Z</updated>
    
    <content type="html"><![CDATA[<p>顾名思义，线程池就是存放线程的池子。直接用线程不就好了吗，为什么还需要线程池呢？</p><p>相比进程，线程已经轻量很多了，创建和销毁线程消耗的资源会少很多，但终究还是有消耗的。为了进一步降低消耗，我们需要用到线程池。需要用到线程时不用申请，直接从线程池中取，用完之后也不用销毁，再放进线程池。</p><p>标准库中的线程池（简化版本)：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="type">ExecutorService</span> <span class="variable">pool</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</span><br><span class="line">    pool.submit(<span class="keyword">new</span> <span class="title class_">Runnable</span>() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;hello&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>模拟实现一个简化版本的线程池：</p><ol><li>描述任务：直接用Runnable接口即可</li><li>组织任务：用到阻塞队列</li><li>描述工作线程：创建Worker类继承Thread，重写run方法，从阻塞队列中取任务，然后执行任务</li><li>组织工作线程：实例化工作线程，加入到线程池，启动线程</li><li>提交任务：往线程池中添加任务（具体是将任务添加到阻塞队列）</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ThreatPool</span> &#123;</span><br><span class="line">    <span class="comment">//描述一个任务</span></span><br><span class="line">    <span class="comment">//组织一个任务</span></span><br><span class="line">    BlockingDeque&lt;Runnable&gt; queue = <span class="keyword">new</span> <span class="title class_">LinkedBlockingDeque</span>&lt;&gt;();</span><br><span class="line">    <span class="comment">//描述一个线程</span></span><br><span class="line">    <span class="comment">//线程是用来执行任务的，而任务都存放在队列中</span></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Worker</span> <span class="keyword">extends</span> <span class="title class_">Thread</span>&#123;</span><br><span class="line">        <span class="comment">//通过构造方法能够拿到外面的queue</span></span><br><span class="line">        <span class="keyword">private</span> BlockingDeque&lt;Runnable&gt; queue = <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">public</span> <span class="title function_">Worker</span><span class="params">(BlockingDeque&lt;Runnable&gt; queue)</span> &#123;</span><br><span class="line">            <span class="built_in">this</span>.queue = queue;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="comment">//从queue中取任务，然后执行任务</span></span><br><span class="line">            <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="type">Runnable</span> <span class="variable">runnable</span> <span class="operator">=</span> queue.take();</span><br><span class="line">                    runnable.run();</span><br><span class="line">                &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                    e.printStackTrace();</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><span class="line">    <span class="comment">//组织线程,创建线程</span></span><br><span class="line">    List&lt;Worker&gt; workers = <span class="keyword">new</span> <span class="title class_">LinkedList</span>&lt;&gt;();</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">ThreatPool</span><span class="params">(<span class="type">int</span> n)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; n; i++) &#123;</span><br><span class="line">            <span class="comment">//创建线程，存放到线程池,执行线程</span></span><br><span class="line">            <span class="type">Worker</span> <span class="variable">worker</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Worker</span>(queue);</span><br><span class="line">            workers.add(worker);</span><br><span class="line">            worker.start();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//提交任务到线程</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sumbit</span><span class="params">(Runnable runnable)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        queue.put(runnable);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="type">ThreatPool</span> <span class="variable">pool</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ThreatPool</span>(<span class="number">10</span>);<span class="comment">//线程数10</span></span><br><span class="line">        <span class="comment">//执行1000个任务</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++) &#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">workId</span> <span class="operator">=</span> i;</span><br><span class="line">            pool.sumbit(<span class="keyword">new</span> <span class="title class_">Runnable</span>() &#123;</span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">                    System.out.println(<span class="string">&quot;任务:&quot;</span>+workId+Thread.currentThread().getName());</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><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;顾名思义，线程池就是存放线程的池子。直接用线程不就好了吗，为什么还需要线程池呢？&lt;/p&gt;
&lt;p&gt;相比进程，线程已经轻量很多了，创建和销毁线程消耗的资源会少很多，但终究还是有消耗的。为了进一步降低消耗，我们需要用到线程池。需要用到线程时不用申请，直接从线程池中取，用完之后也不</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="多线程" scheme="https://youngsay.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>多线程实例：定时器</title>
    <link href="https://youngsay.cn/2022/11/14/Java-Timer/"/>
    <id>https://youngsay.cn/2022/11/14/Java-Timer/</id>
    <published>2022-11-14T07:04:41.000Z</published>
    <updated>2026-05-06T12:18:25.556Z</updated>
    
    <content type="html"><![CDATA[<p>定时器是实际开发中常用的组件，例如文章的定时发布，双11的准点抢购活动等。</p><p>下面我们来看一下Java标准库中的定时器。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="type">Timer</span> <span class="variable">timer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Timer</span>();</span><br><span class="line">    timer.schedule(<span class="keyword">new</span> <span class="title class_">TimerTask</span>() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;hello&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;, <span class="number">3000</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该定时器会在3秒之后输出“hello”。创建一个定时器需要用到Timer类中的核心方法schedule，该方法内有两个参数，一个表示要执行的任务，一个表示任务在多长时间后执行。</p><p>认识了标准库中的定时器后，我们可以自己来模拟实现一个定时器。</p><p>首先，描述一个任务。创建一个MyTask类，类中有两个属性：一个是执行的任务，一个是任务执行时间。这两个属性类似于标准库schedule方法内的两个参数。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyTask</span> <span class="keyword">implements</span> <span class="title class_">Comparable</span>&lt;MyTask&gt;&#123;</span><br><span class="line">    <span class="comment">//具体要干啥</span></span><br><span class="line">    <span class="keyword">private</span> Runnable runnable;</span><br><span class="line">    <span class="comment">//啥时候开始干</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> time;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">MyTask</span><span class="params">(Runnable runnable,<span class="type">long</span> dalay)</span>&#123;</span><br><span class="line">        <span class="built_in">this</span>.runnable = runnable;</span><br><span class="line">        <span class="built_in">this</span>.time=System.currentTimeMillis()+dalay;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span>&#123;</span><br><span class="line">        runnable.run();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">long</span> <span class="title function_">getTime</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> time;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">compareTo</span><span class="params">(MyTask o)</span> &#123;</span><br><span class="line">        <span class="comment">//时间小的排在前面</span></span><br><span class="line">        <span class="keyword">return</span> (<span class="type">int</span>) (<span class="built_in">this</span>.time-o.time);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来，组织一个任务类。如何组织任务类呢，我们这里用到<strong>优先级阻塞队列</strong>。每个任务的执行时间（指的是在多长时间后执行）不同，根据时间大小来排序，进而优先执行队头任务，因此需要优先级队列。</p><p>最后，我们还需要一个线程不断的去扫描到了时间的任务，然后执行这个任务。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyTimer</span>&#123;</span><br><span class="line">    <span class="comment">//组织一个任务</span></span><br><span class="line">    <span class="keyword">private</span> PriorityBlockingQueue&lt;MyTask&gt; queue = <span class="keyword">new</span> <span class="title class_">PriorityBlockingQueue</span>&lt;&gt;();</span><br><span class="line">    <span class="keyword">private</span> <span class="type">Object</span> <span class="variable">locker</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">schedule</span><span class="params">(Runnable runnable,<span class="type">long</span> delay)</span>&#123;</span><br><span class="line">        <span class="type">MyTask</span> <span class="variable">myTask</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MyTask</span>(runnable,delay);</span><br><span class="line">        queue.put(myTask);</span><br><span class="line">        <span class="keyword">synchronized</span> (locker)&#123;</span><br><span class="line">            locker.notify();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//执行到时间的任务</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">MyTimer</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="type">Thread</span> <span class="variable">t1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(()-&gt;&#123;</span><br><span class="line">            <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">//取队首元素</span></span><br><span class="line">                    <span class="type">MyTask</span> <span class="variable">task</span> <span class="operator">=</span> queue.take();</span><br><span class="line">                    <span class="type">long</span> time=System.currentTimeMillis();</span><br><span class="line">                    <span class="keyword">if</span>(time&lt;task.getTime())&#123;</span><br><span class="line">                        <span class="comment">//时间还没到</span></span><br><span class="line">                        queue.put(task);</span><br><span class="line">                        <span class="keyword">synchronized</span> (locker)&#123;</span><br><span class="line">                            locker.wait(task.getTime()-time);</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">                        <span class="comment">//到时间了，执行这个任务</span></span><br><span class="line">                        task.run();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                    e.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        t1.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>模拟一个定时器，总共分三步。第一步，<s>把冰箱打开</s>描述一个任务，即要执行的任务和任务多长时间后开始执行。第二步，组织一个任务，这里用到了优先级阻塞队列。第三步，利用一个线程扫描任务，执行到时间的任务。</p><p>下面有两个问题需要注意。</p><ol><li>任务类要放进优先级阻塞队列中，优先级阻塞队列根据时间先后进行排序。因此我们的任务类要实现<code>Comparable&lt;MyTask&gt;</code>接口，然后重写比较规则。</li><li>线程扫描任务，会从队头取元素，判断是否到时间了，如果没到，再放回队列。接着继续取元素……如果不加限制，它一直不停的扫描队首元素, 看看是否能执行这个任务，这样会大量消耗CPU。因此我们利用wait来使这个线程等待，时间到后再唤醒。此外当新加入一个任务后我们也需要用notify来唤醒扫描线程，因为可能该任务的时间更小，优先级更高，所以需要重新扫描任务队列。（这也就决定了必须用wait，而不能用sleep，因为sleep不能中途唤醒）</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;定时器是实际开发中常用的组件，例如文章的定时发布，双11的准点抢购活动等。&lt;/p&gt;
&lt;p&gt;下面我们来看一下Java标准库中的定时器。&lt;/p&gt;
&lt;figure class=&quot;highlight java&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="多线程" scheme="https://youngsay.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>多线程实例：阻塞队列</title>
    <link href="https://youngsay.cn/2022/11/14/Java-BlockingQueue/"/>
    <id>https://youngsay.cn/2022/11/14/Java-BlockingQueue/</id>
    <published>2022-11-14T04:04:41.000Z</published>
    <updated>2026-05-06T12:18:25.671Z</updated>
    
    <content type="html"><![CDATA[<p>阻塞队列是一种具有阻塞功能的队列，满足队列“先进先出”的特点，是一种线性安全的数据结构。当队列为空时，执行出队操作会进行阻塞，直到队列中有元素为止；当队列已经满了，执行入堆操作会进行阻塞，知道队列有空间为止。</p><p>阻塞队列的一个典型应用常见就是“生产者消费者”模型。毫无疑问，该模型有两个主体：生产者和消费者。生产者线程负责生产产品，将生产好的产品放进阻塞队列。消费者线程负责消费产品，直接从阻塞队列取产品。</p><p>生产者消费者模型具有解耦、平衡速度差异的特点。</p><p>解耦：生产者无需关注是谁在消费产品，消费了多少产品，只需关注生产操作即可；消费者无需关注是谁在生产产品，生产了多少产品，只需关注消费操作即可。</p><p>平衡速度差异：设想11.11，大量用户向服务器发送了大量的支付请求，一次性将这些请求交给服务器处理，服务器可能会hole不住，因此将这些请求都放进阻塞队列，消费者线程从阻塞队列一个一个地来处理请求就好了。这样就起到了“削峰填谷”的作用，平衡了生产者和消费者之间的速度差异。</p><h3 id="标准库中的阻塞队列：">标准库中的阻塞队列：</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">    BlockingDeque&lt;String&gt; queue = <span class="keyword">new</span> <span class="title class_">LinkedBlockingDeque</span>&lt;&gt;();</span><br><span class="line">    queue.put(<span class="string">&quot;hello&quot;</span>);</span><br><span class="line">    <span class="type">String</span> <span class="variable">s</span> <span class="operator">=</span> queue.take();</span><br><span class="line">    System.out.println(s);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.</li><li>put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.</li><li>BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性</li></ul><h3 id="模拟实现阻塞队列：">模拟实现阻塞队列：</h3><ul><li>循环队列+阻塞等待</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyQueue</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span>[] elem = <span class="keyword">new</span> <span class="title class_">int</span>[<span class="number">10</span>];</span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> head;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> tail;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> size;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">final</span> <span class="type">Object</span> <span class="variable">locker</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">put</span><span class="params">(<span class="type">int</span> val)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (locker)&#123;</span><br><span class="line">            <span class="comment">//队列满了，阻塞等待</span></span><br><span class="line">            <span class="keyword">if</span>(size==elem.length)&#123;</span><br><span class="line">                locker.wait();</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//一轮循环，让尾指针指向数组下标为0的位置</span></span><br><span class="line">            <span class="keyword">if</span>(tail==elem.length)&#123;</span><br><span class="line">                tail=<span class="number">0</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            elem[tail++]=val;</span><br><span class="line">            size++;</span><br><span class="line">            <span class="comment">//唤醒消费者线程的堵塞等待</span></span><br><span class="line">            locker.notify();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">take</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (locker)&#123;</span><br><span class="line">            <span class="comment">//队列空了，阻塞等待</span></span><br><span class="line">            <span class="keyword">if</span>(size==<span class="number">0</span>)&#123;</span><br><span class="line">                locker.wait();</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span>(head==elem.length)&#123;</span><br><span class="line">                head=<span class="number">0</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="type">int</span> <span class="variable">ret</span> <span class="operator">=</span> elem[head];</span><br><span class="line">            head++;</span><br><span class="line">            size--;</span><br><span class="line">            <span class="comment">//唤醒生产者线程的堵塞等待</span></span><br><span class="line">            locker.notify();</span><br><span class="line">            <span class="keyword">return</span> ret;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"><span class="comment">//测试代码</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="type">MyQueue</span> <span class="variable">queue</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MyQueue</span>();</span><br><span class="line">        <span class="comment">//生产者线程</span></span><br><span class="line">        <span class="type">Thread</span> <span class="variable">t1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(()-&gt;&#123;</span><br><span class="line">            <span class="type">int</span> i=<span class="number">0</span>;</span><br><span class="line">            <span class="keyword">while</span> (<span class="literal">true</span>)&#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    queue.put(i);</span><br><span class="line">                    System.out.println(<span class="string">&quot;生产了：&quot;</span>+i);</span><br><span class="line">                    i++;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                    e.printStackTrace();</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="comment">//消费者线程</span></span><br><span class="line">        <span class="type">Thread</span> <span class="variable">t2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(()-&gt;&#123;</span><br><span class="line">            <span class="keyword">while</span>(<span class="literal">true</span>)&#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="type">int</span> <span class="variable">take</span> <span class="operator">=</span> queue.take();</span><br><span class="line">                    System.out.println(<span class="string">&quot;消费了：&quot;</span>+take);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                    e.printStackTrace();</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><span class="line">        t1.start();</span><br><span class="line">        t2.start();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;阻塞队列是一种具有阻塞功能的队列，满足队列“先进先出”的特点，是一种线性安全的数据结构。当队列为空时，执行出队操作会进行阻塞，直到队列中有元素为止；当队列已经满了，执行入堆操作会进行阻塞，知道队列有空间为止。&lt;/p&gt;
&lt;p&gt;阻塞队列的一个典型应用常见就是“生产者消费者”模型</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="多线程" scheme="https://youngsay.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Java线程的创建及状态</title>
    <link href="https://youngsay.cn/2022/10/12/Java-Thread/"/>
    <id>https://youngsay.cn/2022/10/12/Java-Thread/</id>
    <published>2022-10-12T13:04:41.000Z</published>
    <updated>2026-05-06T12:18:25.685Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是线程">什么是线程</h2><p>在计算机中，一个任务就是一个进程。打开任务管理器，我们可以看见多个进程。如浏览器，QQ。线程是进程的再划分，是进程的一部分。一个进程可以只有一个线程（主线程），也可以有多个线程。拿浏览器举例，打开浏览器的多个网页，一个网页在播放音乐，一个网页在播放视频，一个网页在浏览博客。每个网页有不同的分工，这些浏览器的每个子任务就是一个线程。</p><p>线程是更轻量的进程，创建一个线程的消耗要低于创建一个进程，且同一进程的不同线程之间可以共享资源，效率更高。通过多线程可以更好地实现并发编程。</p><p><strong>进程vs线程</strong></p><ul><li>进程包含线程，进程是线程的一部分，一个进程中至少包含一个线程。</li><li>进程与进程之间不能共享资源，同一个进程的线程之间可以共享资源。</li><li>进程是系统分配资源的最小单位，线程是系统调度执行的最小单位。</li><li>多进程稳定性高于多线程，一个进程的崩溃不会影响其他线程；多线程下一个线程的崩溃可能影响该进程。</li></ul><h2 id="创建线程">创建线程</h2><p>一个线程至少包含一个进程，<code>main()</code>就是一个线程，我们还可以在里面创建多个线程。</p><p><strong>1.继承<code>Thread</code>类,重写<code>run()</code>方法</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MyThread</span>();</span><br><span class="line">        t.start(); <span class="comment">// 启动新线程</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyThread</span> <span class="keyword">extends</span> <span class="title class_">Thread</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;start new thread!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>2.创建<code>Thread</code>实例，实现<code>Runnable</code>接口</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(<span class="keyword">new</span> <span class="title class_">MyRunnable</span>());</span><br><span class="line">        t.start(); <span class="comment">// 启动新线程</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyRunnable</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;start new thread!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>3.其他变形</strong></p><ul><li>匿名内部类创建Thread子类对象</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Thread</span> <span class="variable">t1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>() &#123;</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;使用匿名类创建 Thread 子类对象&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>匿名内部类创建Runnable子类对象</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Thread</span> <span class="variable">t2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(<span class="keyword">new</span> <span class="title class_">Runnable</span>() &#123;</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;使用匿名类创建 Runnable 子类对象&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ul><li>lambda表达式创建Thread子类对象</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Thread</span> <span class="variable">t4</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;使用匿名类创建 Thread 子类对象&quot;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="Thread方法"><code>Thread</code>方法</h2><p><strong>常见构造方法</strong></p><table><thead><tr><th>构造方法</th></tr></thead><tbody><tr><td><code>Thread()</code>  分配一个新的 <code>Thread</code>对象。</td></tr><tr><td><code>Thread(Runnable target)</code>  分配一个新的 <code>Thread</code>对象。</td></tr><tr><td><code>Thread(Runnable target, String name)</code>  分配一个新的 <code>Thread</code>对象，name为新线程的名字。</td></tr><tr><td><code>Thread(String name)</code>  分配一个新的 <code>Thread</code>对象，name为新线程的名字。</td></tr></tbody></table><p><strong>启动一个线程.start()</strong></p><p>Thread对象对创建后，并没有启动线程，执行该指令后才会真正创建一个线程。即内核中创建了一个内存控制块PCB。</p><p><strong>中断一个线程</strong></p><p>方法1：自定义变量作为标志位</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//自定义变量flag作为标志位</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">volatile</span> <span class="type">boolean</span> <span class="variable">flag</span> <span class="operator">=</span> <span class="literal">false</span>; </span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">    <span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>()&#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">while</span>(!flag)&#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;线程正在运行&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">    t.start();</span><br><span class="line">    Thread.sleep(<span class="number">1000</span>); <span class="comment">//使线程进入休眠状态</span></span><br><span class="line">    flag = <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>方法2：<code>interrupted()</code>方法中断线程</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ThreadDemo3</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>()&#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">                <span class="keyword">while</span>(!Thread.currentThread().isInterrupted())&#123;</span><br><span class="line">                    System.out.println(<span class="string">&quot;线程运行中&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">        t.start();</span><br><span class="line">        Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">        t.interrupt();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Thread.currentThread().isInterrupted()</code>获取当前线程是否被中断；<code>interrupted()</code>向线程发出中断请求。</p><p>Thread收到通知方式有两种：①当线程因wait/sleep/join等方法处于堵塞状态时，以 <code>InterruptedException </code>异常的形式通知。②<code>isInterrupted()</code>标志位置为true.</p><p><strong>等待一个线程.join()</strong></p><p>执行该指令，会使该线程执行结束后再执行下一线程，即将并发编程变成了串行。比如用某软件下载一部电影，可以边下载边观看，当执行该指令后，就要等下载完后再看了~</p><p><strong>休眠一个线程sleep()</strong></p><p>让线程进入休眠状态，进入休眠状态的指令，他的属性<code>isAlive()</code>为Timed Waiting。线程状态会稍后介绍到。</p><h2 id="线程状态">线程状态</h2><p>注意，这里我们说的是java线程的六种状态。利用<code>isAlive</code>属性可以观察线程的状态，线程有以下6种状态：</p><ul><li>New：新创建的线程，但未执行<code>start()</code>，线程没有存活；</li><li>Runnable：运行中的线程，正在执行<code>run()</code>方法的Java代码；</li><li>Blocked：运行中的线程，因为某些操作被阻塞而挂起；</li><li>Waiting：运行中的线程，因为某些操作在等待中，如执行<code>wait()</code>方法；</li><li>Timed Waiting：运行中的线程，因为执行<code>sleep()</code>方法正在计时等待；</li><li>Terminated：线程结束，线程没有存活。</li></ul><h2 id="线程状态转换">线程状态转换</h2><p>线程被创建出来，处于New状态。调用start方法后，处于Runnable状态。当线程获取不到锁时，处于Blocked状态，直到获取到锁后进入Runnable状态。运行的线程调用wait或join方法后会进入Waiting状态，直到唤醒通知后进入Runnable状态。调用带有超时参数的方法后会进入Timed Waiting状态，直到时间到后进入Runnable状态。运行的线程执行完run方法或出现异常后进入Terminated状态。</p><p>与操作系统中的进程状态不同，java没有就绪态。操作系统中，一个进程调用start后并没有直接运行，而是进入就绪态，当获取到cpu时间片后进入运行态。我们可以认为在java中，就绪态和运行态同属于Runnable。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;什么是线程&quot;&gt;什么是线程&lt;/h2&gt;
&lt;p&gt;在计算机中，一个任务就是一个进程。打开任务管理器，我们可以看见多个进程。如浏览器，QQ。线程是进程的再划分，是进程的一部分。一个进程可以只有一个线程（主线程），也可以有多个线程。拿浏览器举例，打开浏览器的多个网页，一个网页</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="多线程" scheme="https://youngsay.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>线程安全及解决方案</title>
    <link href="https://youngsay.cn/2022/09/26/Thread-safe/"/>
    <id>https://youngsay.cn/2022/09/26/Thread-safe/</id>
    <published>2022-09-26T09:40:41.000Z</published>
    <updated>2026-05-06T12:18:25.648Z</updated>
    
    <content type="html"><![CDATA[<p>安全，已经成为非常重要的社会话题。所谓“安全第一”，“安全无小事”（手动滑稽），同样，多线程中，线程安全也是非常重要的话题。那么是什么原因造成了线程不安全，又如何解决线程不安全呢？</p><h3 id="造成线程不安全的原因">造成线程不安全的原因</h3><ol><li><p>线程的调度</p><p>各线程之间是抢占式执行的，线程的执行顺序是随机的，因此可能会产生各种问题。现在最流行什么？做核酸！如果做核酸是抢占式的，做核酸没有一个顺序，做核酸顺序完全靠运气，这能安全吗！</p></li><li><p>多线程同时修改同一变量</p><p>如果是多线程同时读取同一变量，不涉及修改的操作，是线程安全的。但如果多线程同时修改同一变量，这能不乱吗？想当年家里买了一台电脑，我姐姐要用电脑玩QQ炫舞，我要用电脑玩穿越火线，你争我抢，打的是不可开交。</p></li><li><p>操作指令不是原子的</p><p>例如一条加法指令，其实要执行三条指令，load、add、save，先将内存中的变量加载到寄存器，在寄存器中完成加法操作，再将结果写会内存中。假设线程1完成了load、add操作，线程2完成了load、add、save操作，当线程1再去执行save操作，便将线程2的操作覆盖了，线程2说：线程1真是一个猪队友。</p></li><li><p>内存可见性</p><p>线程1循环进行读操作，线程2看心情进行修改操作。我们知道，读操作是将内存变量加载到寄存器，然后读取寄存器。而线程1循环加载内存中的值到寄存器，线程2又迟迟不修改，线程1说：你当我傻吗？于是线程1干脆去读取寄存器了，这就是编译器的优化。拿做核酸举例，最初是刮嗓子，但日复一日，现在有些已经是刮舌头了，这难道是疫情的优化？大部分情况下是安全的，但也可能会翻车。线程1循环读操作，线程2突然心情不好，很快啊，修改了内存中的值。而线程1却绕过了内存，从寄存器读值，这能安全吗！</p></li></ol><h3 id="解决方案">解决方案</h3><p>针对原因1，线程的调度就是抢占式执行的，我们无能为力。针对原因2，我们可以通过调整代码结构，使不同线程操作不同变量，但我这菜鸡技术，既然能写出一个bug，就能写出无数个bug，一顿调整猛如虎，结果bug乘以5.</p><p>所以我们解决方案寄托在原因3、4上。</p><p>Java中提供了<code>synchronized</code>关键字来对操作上锁。拿上面的加法操作举例，为加法这一方法加上synchronized后，就为该操作上锁了。想象要在卫生间完成加法操作，（不敢想象），小县城来的孩子线程1来执行加法操作，发现卫生间没人，他进来后把门锁上，进行加法操作。又一个小县城的孩子线程2来到卫生间前，也想要做加法操作，发现门锁了，只好等到线程1出来后才能进去，他是等也得等，不等也得等，这样就保证了操作的原子性。</p><p>Java还提供了<code>volatile</code>关键字来解决内存可见性问题，synchronized也可以解决该问题，只不过开销更大。为代码加上volatile后，就能禁止编译器进行优化，线程1无论做多少次循环读操作，都要耐心地从内存中读值，不能直接去读取寄存器中的值。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;安全，已经成为非常重要的社会话题。所谓“安全第一”，“安全无小事”（手动滑稽），同样，多线程中，线程安全也是非常重要的话题。那么是什么原因造成了线程不安全，又如何解决线程不安全呢？&lt;/p&gt;
&lt;h3 id=&quot;造成线程不安全的原因&quot;&gt;造成线程不安全的原因&lt;/h3&gt;
&lt;ol&gt;
&lt;</summary>
      
    
    
    
    <category term="笔记" scheme="https://youngsay.cn/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="多线程" scheme="https://youngsay.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
</feed>
