<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Albert World</title>
  <icon>https://www.gravatar.com/avatar/8ccc6291d5bfd34e7aaf601c4ebec7e1</icon>
  
  <link href="/blog/atom.xml" rel="self"/>
  
  <link href="http://AlbertGithubHome.github.io/blog/"/>
  <updated>2024-12-28T16:59:10.317Z</updated>
  <id>http://AlbertGithubHome.github.io/blog/</id>
  
  <author>
    <name>Albert Shi</name>
    <email>347070901@qq.com</email>
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>GoAdmin框架实现的后台应用遇到的鉴权跳转问题</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/12/29/GoAdmin%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%90%8E%E5%8F%B0%E5%BA%94%E7%94%A8%E9%81%87%E5%88%B0%E7%9A%84%E9%89%B4%E6%9D%83%E8%B7%B3%E8%BD%AC%E9%97%AE%E9%A2%98/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/12/29/GoAdmin框架实现的后台应用遇到的鉴权跳转问题/</id>
    <published>2024-12-28T16:58:05.000Z</published>
    <updated>2024-12-28T16:59:10.317Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>GoAdmin是一个基于 <code>golang</code> 面向生产的数据可视化管理平台搭建框架，可以让你使用简短的代码在极短时间内搭建起一个管理后台，虽然我也会去修改和新增一些功能页面，但是它给我的感觉总像一个黑盒子，大概是我对Web开发的知识了解甚少，总是照葫芦画瓢的完成一些工作，最近在鉴权和会话过期的处理上遇到了一点点问题，直接GET访问页面在会话过期时会自动跳转到登录页面，而点击按钮提交表单时如果已经会话过期了却没有任何反应，我觉得是少设置了一些东西，绕了一大圈才算勉强解决，但这应该不是最优的方案，先记录一下，后续找到好的解决方案再优化。</p><a id="more"></a><h1 id="问题的发现与解决"><a href="#问题的发现与解决" class="headerlink" title="问题的发现与解决"></a>问题的发现与解决</h1><p>使用GoAdmin搭建的后台网站，如果长时间停留在一个页面，点击菜单中的其他页面时会自动跳转到登录页面，但是点击页面表单上的提交按钮却没有反应，这种情况会让人迷惑，不知道刚刚这个操作到底有没有执行成功，要解决这个问题就先查代码吧？实际上时候发现，这时点击提交按钮时报了一个js错误，只有在浏览器的控制台才能看到，起初没发现也是走了一些弯路。</p><p>一开始我认为两种情况一个表现正常、一个表现不正常是因为GET和POST的缘故，所以我对比了两种请求处理从注册路由开始的逻辑代码，尽最大可能保持一样，但没有解决。</p><p>注册路由的方式，<a href="https://github.com/GoAdminGroup/go-admin/blob/3d6ab2c7e1c2c6a8e3df89d8794f660158121864/plugins/admin/router.go#L17" target="_blank" rel="noopener">GoAdmin开源项目</a>中有相关代码</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">authRoute.POST(<span class="string">"/menu/edit"</span>, admin.guardian.MenuEdit, admin.handler.EditMenu).Name(<span class="string">"menu_edit"</span>)</span><br><span class="line">authRoute.POST(<span class="string">"/menu/order"</span>, admin.handler.MenuOrder).Name(<span class="string">"menu_order"</span>)</span><br><span class="line">authRoute.GET(<span class="string">"/menu"</span>, admin.handler.ShowMenu).Name(<span class="string">"menu"</span>)</span><br><span class="line">authRoute.GET(<span class="string">"/menu/edit/show"</span>, admin.handler.ShowEditMenu).Name(<span class="string">"menu_edit_show"</span>)</span><br></pre></td></tr></table></figure><p>我简单的理解就是，告诉框架当访问到前面这个路径的时候，依次调用后面的处理函数就行了，当然这也是我这两天跟踪代码时领取的，最后的 <code>Name</code> 函数是为这个路由命令，而我的应用不管是GET路由还是POST路由都使用了 <code>auth.Middleware(admin.Conn)</code> 这个鉴权函数，按道理说肯定都进行了鉴权处理，不应该有差异的，最开始有一些包装函数逻辑顺序的差异，我调整成一样的依旧不生效。</p><p>我使用断点，单步调试查看GET和POST的区别，但是没有发现差异，两者都返回了302状态码表示重定向，但是GET成功跳转了，而POST没有变化</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Middleware is the default auth middleware of plugins.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Middleware</span><span class="params">(conn db.Connection)</span> <span class="title">context</span>.<span class="title">Handler</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> DefaultInvoker(conn).Middleware()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// DefaultInvoker return a default Invoker.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">DefaultInvoker</span><span class="params">(conn db.Connection)</span> *<span class="title">Invoker</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;Invoker&#123;</span><br><span class="line">        prefix: config.Prefix(),</span><br><span class="line">        authFailCallback: <span class="function"><span class="keyword">func</span><span class="params">(ctx *context.Context)</span></span> &#123;</span><br><span class="line">            <span class="keyword">if</span> ctx.Request.URL.Path == config.Url(config.GetLoginUrl()) &#123;</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> ctx.Request.URL.Path == config.Url(<span class="string">"/logout"</span>) &#123;</span><br><span class="line">                ctx.Write(<span class="number">302</span>, <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>&#123;</span><br><span class="line">                    <span class="string">"Location"</span>: config.Url(config.GetLoginUrl()),</span><br><span class="line">                &#125;, <span class="string">``</span>)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">            param := <span class="string">""</span></span><br><span class="line">            <span class="keyword">if</span> ref := ctx.Referer(); ref != <span class="string">""</span> &#123;</span><br><span class="line">                param = <span class="string">"?ref="</span> + url.QueryEscape(ref)</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            u := config.Url(config.GetLoginUrl() + param)</span><br><span class="line">            _, err := ctx.Request.Cookie(DefaultCookieKey)</span><br><span class="line">            referer := ctx.Referer()</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (ctx.Headers(constant.PjaxHeader) == <span class="string">""</span> &amp;&amp; ctx.Method() != <span class="string">"GET"</span>) ||</span><br><span class="line">                err != <span class="literal">nil</span> ||</span><br><span class="line">                referer == <span class="string">""</span> &#123;</span><br><span class="line">                ctx.Write(<span class="number">302</span>, <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>&#123;</span><br><span class="line">                    <span class="string">"Location"</span>: u,</span><br><span class="line">                &#125;, <span class="string">``</span>)</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                msg := language.Get(<span class="string">"login overdue, please login again"</span>)</span><br><span class="line">                ctx.HTML(http.StatusOK, <span class="string">`&lt;script&gt;</span></span><br><span class="line"><span class="string">    if (typeof(swal) === "function") &#123;</span></span><br><span class="line"><span class="string">        swal(&#123;</span></span><br><span class="line"><span class="string">            type: "info",</span></span><br><span class="line"><span class="string">            title: "`</span>+language.Get(<span class="string">"login info"</span>)+<span class="string">`",</span></span><br><span class="line"><span class="string">            text: "`</span>+msg+<span class="string">`",</span></span><br><span class="line"><span class="string">            showCancelButton: false,</span></span><br><span class="line"><span class="string">            confirmButtonColor: "#3c8dbc",</span></span><br><span class="line"><span class="string">            confirmButtonText: '`</span>+language.Get(<span class="string">"got it"</span>)+<span class="string">`',</span></span><br><span class="line"><span class="string">        &#125;)</span></span><br><span class="line"><span class="string">        setTimeout(function()&#123; location.href = "`</span>+u+<span class="string">`"; &#125;, 3000);</span></span><br><span class="line"><span class="string">    &#125; else &#123;</span></span><br><span class="line"><span class="string">        alert("`</span>+msg+<span class="string">`")</span></span><br><span class="line"><span class="string">        location.href = "`</span>+u+<span class="string">`"</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">        &lt;/script&gt;`</span>)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        permissionDenyCallback: <span class="function"><span class="keyword">func</span><span class="params">(rawCtx *context.Context)</span></span> &#123;</span><br><span class="line">            <span class="keyword">if</span> rawCtx.Headers(constant.PjaxHeader) == <span class="string">""</span> &amp;&amp; rawCtx.Method() != <span class="string">"GET"</span> &#123;</span><br><span class="line">                rawCtx.JSON(http.StatusForbidden, <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">interface</span>&#123;&#125;&#123;</span><br><span class="line">                    <span class="string">"code"</span>: http.StatusForbidden,</span><br><span class="line">                    <span class="string">"msg"</span>:  language.Get(errors.PermissionDenied),</span><br><span class="line">                &#125;)</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                page.SetPageContent(rawCtx, Auth(rawCtx), <span class="function"><span class="keyword">func</span><span class="params">(ctx <span class="keyword">interface</span>&#123;&#125;)</span> <span class="params">(types.Panel, error)</span></span> &#123;</span><br><span class="line">                    <span class="keyword">return</span> template2.WarningPanel(rawCtx, errors.PermissionDenied, template2.NoPermission403Page), <span class="literal">nil</span></span><br><span class="line">                &#125;, conn)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        conn: conn,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>两种方式函数最终都走到了下面这个分支，结果却不相同</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (ctx.Headers(constant.PjaxHeader) == <span class="string">""</span> &amp;&amp; ctx.Method() != <span class="string">"GET"</span>) ||</span><br><span class="line">    err != <span class="literal">nil</span> ||</span><br><span class="line">    referer == <span class="string">""</span> &#123;</span><br><span class="line">    ctx.Write(<span class="number">302</span>, <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>&#123;</span><br><span class="line">        <span class="string">"Location"</span>: u,</span><br><span class="line">    &#125;, <span class="string">``</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>没办法，这时我只能打开浏览器按下F12开始调试，渐渐发现了一些端倪，无论是点击菜单还是点击提交按钮，都收到了302也进行了跳转，但是点提交按钮这种情况收到302后虽然跳转成功了，但是页面没有切换，赶紧问了一下ChatGPT，他告诉我很可能是Ajax的锅。</p><p>Ajax究竟是个啥，虽然之前听过，但是我却说不上来，继续搜索吧。</p><p>AJAX（Asynchronous JavaScript and XML）是一种用于创建快速动态网页的技术。通过使用 AJAX，网页能够与服务器进行异步交互，而无需重新加载整个页面。AJAX 允许在后台与服务器进行通信，并使用 JavaScript 更新网页的部分内容。</p><p>使用 AJAX 的优势很多，比如：</p><ul><li>提高用户体验：AJAX 允许在后台加载数据，从而使网页更流畅，用户不需要等待整个页面重新加载。</li><li>减少服务器负载：只请求所需的数据，而不是重新加载整个页面，降低了数据传输量。</li><li>更快的响应时间：通过部分更新页面内容，用户能够更快地看到更新结果。</li></ul><p>常见的 AJAX 应用场景如下：</p><ul><li>表单提交：在用户提交表单时，AJAX 可以异步提交数据，避免页面刷新。</li><li>动态内容加载：在用户滚动或点击时加载更多内容，例如无限滚动。</li><li>实时数据更新：如聊天应用、股票价格更新等实时信息。</li></ul><p>这里的表单提交我确实使用了AJAX，这就导致它没有正确处理302状态码，我当时是这么想的，也是朝着这个方向努力的</p><p>找到页面上中设置Ajax回调函数的逻辑</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">panel.EnableAjaxData(types.AjaxData&#123;</span><br><span class="line">    SuccessTitle: <span class="string">"success"</span>,</span><br><span class="line">    DisableJump:  <span class="literal">true</span>,</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">aform := components.Form().</span><br><span class="line">    SetId(<span class="string">"cmd-form"</span>).</span><br><span class="line">    SetUrl(<span class="string">"/admin/cmd"</span>).</span><br><span class="line">    SetAjax(panel.AjaxSuccessJS, panel.AjaxErrorJS)</span><br></pre></td></tr></table></figure><p>我开始拿 <code>panel.AjaxErrorJS</code> 开刀，我分析了前面的代码，以为这个字段是空的，准备替换成如下代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span>(<span class="params">data</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (data.status === <span class="number">302</span>) &#123;</span><br><span class="line">            <span class="comment">// 处理重定向</span></span><br><span class="line">            <span class="keyword">const</span> redirectUrl = data.headers.get(<span class="string">'Location'</span>);</span><br><span class="line">            <span class="keyword">if</span> (redirectUrl) &#123;</span><br><span class="line">                <span class="built_in">window</span>.location.href = redirectUrl;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            consle.log(data.responseText);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很遗憾不太好用，有时好使有时不好使，并且页面一直在上方显示着加载中的进度条，没办法继续问ChatGPT吧，有一项相关提醒：</p><blockquote><p>错误处理逻辑：确保在 error 函数中正确处理请求失败的情况。如果没有适当地处理错误，进度条可能会一直显示。</p></blockquote><p>所以我又看了一下控制台，发现错误提醒，告诉我没有函数名：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Uncaught <span class="built_in">SyntaxError</span>: <span class="built_in">Function</span> statements <span class="built_in">require</span> a <span class="function"><span class="keyword">function</span> <span class="title">name</span></span></span><br></pre></td></tr></table></figure><p>所以我改成了下面这样</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">alala</span>(<span class="params">data</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (data.status === <span class="number">302</span>) &#123;</span><br><span class="line">...</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但这次一点也不好使了，我查看页面源码一看，原来变成了下面这样，能好使才怪了</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> (<span class="params">data</span>) </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">alala</span>(<span class="params">data</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (data.status === <span class="number">302</span>) &#123;</span><br><span class="line">    ...</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>进一步分析我发现 <code>panel.AjaxSuccessJS</code> 和 <code>panel.AjaxErrorJS</code> 都不是空的，不能简单覆盖，需要做一步调整，这两个变量的初始化代码在 <code>EnableAjaxData</code> 函数</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre></td><td class="code"><pre><span class="line">func (f *FormPanel) EnableAjaxData(data AjaxData) *FormPanel &#123;</span><br><span class="line">    f.Ajax = <span class="literal">true</span></span><br><span class="line">    <span class="keyword">if</span> f.AjaxSuccessJS == template.JS(<span class="string">""</span>) &#123;</span><br><span class="line">        successMsg := modules.AorB(data.SuccessTitle != <span class="string">""</span>, <span class="string">`"`</span>+data.SuccessTitle+<span class="string">`"`</span>, <span class="string">"data.msg"</span>)</span><br><span class="line">        errorMsg := modules.AorB(data.ErrorTitle != <span class="string">""</span>, <span class="string">`"`</span>+data.ErrorTitle+<span class="string">`"`</span>, <span class="string">"data.msg"</span>)</span><br><span class="line">        jump := modules.AorB(data.SuccessJumpURL != <span class="string">""</span>, <span class="string">`"`</span>+data.SuccessJumpURL+<span class="string">`"`</span>, <span class="string">"data.data.url"</span>)</span><br><span class="line">        text := modules.AorB(data.SuccessText != <span class="string">""</span>, <span class="string">`text:"`</span>+data.SuccessText+<span class="string">`",`</span>, <span class="string">""</span>)</span><br><span class="line">        wrongText := modules.AorB(data.ErrorText != <span class="string">""</span>, <span class="string">`text:"`</span>+data.ErrorText+<span class="string">`",`</span>, <span class="string">"text:data.msg,"</span>)</span><br><span class="line">        jumpURL := <span class="string">""</span></span><br><span class="line">        <span class="keyword">if</span> !data.DisableJump &#123;</span><br><span class="line">            <span class="keyword">if</span> data.JumpInNewTab != <span class="string">""</span> &#123;</span><br><span class="line">                jumpURL = <span class="string">`listenerForAddNavTab(`</span> + jump + <span class="string">`, "`</span> + data.JumpInNewTab + <span class="string">`");`</span></span><br><span class="line">            &#125;</span><br><span class="line">            jumpURL += <span class="string">`$.pjax(&#123;url: `</span> + jump + <span class="string">`, container: '#pjax-container'&#125;);`</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            jumpURL = <span class="string">`</span></span><br><span class="line"><span class="string">        if (data.data &amp;&amp; data.data.token !== "") &#123;</span></span><br><span class="line"><span class="string">            $("input[name='__go_admin_t_']").val(data.data.token)</span></span><br><span class="line"><span class="string">        &#125;`</span></span><br><span class="line">        &#125;</span><br><span class="line">        f.AjaxSuccessJS = template.JS(<span class="string">`</span></span><br><span class="line"><span class="string">    if (typeof (data) === "string") &#123;</span></span><br><span class="line"><span class="string">        data = JSON.parse(data);</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">    if (data.code === 200) &#123;</span></span><br><span class="line"><span class="string">        swal(&#123;</span></span><br><span class="line"><span class="string">            type: "success",</span></span><br><span class="line"><span class="string">            title: `</span> + successMsg + <span class="string">`,</span></span><br><span class="line"><span class="string">            `</span> + text + <span class="string">`</span></span><br><span class="line"><span class="string">            showCancelButton: false,</span></span><br><span class="line"><span class="string">            confirmButtonColor: "#3c8dbc",</span></span><br><span class="line"><span class="string">            confirmButtonText: '`</span> + language.Get(<span class="string">"got it"</span>) + <span class="string">`',</span></span><br><span class="line"><span class="string">        &#125;, function() &#123;</span></span><br><span class="line"><span class="string">            $(".modal-backdrop.fade.in").remove();</span></span><br><span class="line"><span class="string">            `</span> + jumpURL + <span class="string">`</span></span><br><span class="line"><span class="string">            `</span> + data.SuccessJS + <span class="string">`</span></span><br><span class="line"><span class="string">        &#125;);</span></span><br><span class="line"><span class="string">    &#125; else &#123;</span></span><br><span class="line"><span class="string">        if (data.data &amp;&amp; data.data.token !== "") &#123;</span></span><br><span class="line"><span class="string">            $("input[name='__go_admin_t_']").val(data.data.token);</span></span><br><span class="line"><span class="string">        &#125;</span></span><br><span class="line"><span class="string">        swal(&#123;</span></span><br><span class="line"><span class="string">            type: "error",</span></span><br><span class="line"><span class="string">            title: `</span> + errorMsg + <span class="string">`,</span></span><br><span class="line"><span class="string">            `</span> + wrongText + <span class="string">`</span></span><br><span class="line"><span class="string">            showCancelButton: false,</span></span><br><span class="line"><span class="string">            confirmButtonColor: "#3c8dbc",</span></span><br><span class="line"><span class="string">            confirmButtonText: '`</span> + language.Get(<span class="string">"got it"</span>) + <span class="string">`',</span></span><br><span class="line"><span class="string">        &#125;)</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">`</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> f.AjaxErrorJS == template.JS(<span class="string">""</span>) &#123;</span><br><span class="line">        errorMsg := modules.AorB(data.ErrorTitle != <span class="string">""</span>, <span class="string">`"`</span>+data.ErrorTitle+<span class="string">`"`</span>, <span class="string">"data.responseJSON.msg"</span>)</span><br><span class="line">        error2Msg := modules.AorB(data.ErrorTitle != <span class="string">""</span>, <span class="string">`"`</span>+data.ErrorTitle+<span class="string">`"`</span>, <span class="string">"'"</span>+language.Get(<span class="string">"error"</span>)+<span class="string">"'"</span>)</span><br><span class="line">        wrongText := modules.AorB(data.ErrorText != <span class="string">""</span>, <span class="string">`text:"`</span>+data.ErrorText+<span class="string">`",`</span>, <span class="string">"text:data.msg,"</span>)</span><br><span class="line">        f.AjaxErrorJS = template.JS(<span class="string">`</span></span><br><span class="line"><span class="string">    if (data.responseText !== "") &#123;</span></span><br><span class="line"><span class="string">        if (data.responseJSON.data &amp;&amp; data.responseJSON.data.token !== "") &#123;</span></span><br><span class="line"><span class="string">            $("input[name='__go_admin_t_']").val(data.responseJSON.data.token)</span></span><br><span class="line"><span class="string">        &#125;</span></span><br><span class="line"><span class="string">        swal(&#123;</span></span><br><span class="line"><span class="string">            type: "error",</span></span><br><span class="line"><span class="string">            title: `</span> + errorMsg + <span class="string">`,</span></span><br><span class="line"><span class="string">            `</span> + wrongText + <span class="string">`</span></span><br><span class="line"><span class="string">            showCancelButton: false,</span></span><br><span class="line"><span class="string">            confirmButtonColor: "#3c8dbc",</span></span><br><span class="line"><span class="string">            confirmButtonText: '`</span> + language.Get(<span class="string">"got it"</span>) + <span class="string">`',</span></span><br><span class="line"><span class="string">        &#125;)</span></span><br><span class="line"><span class="string">    &#125; else &#123;</span></span><br><span class="line"><span class="string">        swal(&#123;</span></span><br><span class="line"><span class="string">            type: "error",</span></span><br><span class="line"><span class="string">            title: `</span> + error2Msg + <span class="string">`,</span></span><br><span class="line"><span class="string">            `</span> + wrongText + <span class="string">`</span></span><br><span class="line"><span class="string">            showCancelButton: false,</span></span><br><span class="line"><span class="string">            confirmButtonColor: "#3c8dbc",</span></span><br><span class="line"><span class="string">            confirmButtonText: '`</span> + language.Get(<span class="string">"got it"</span>) + <span class="string">`',</span></span><br><span class="line"><span class="string">        &#125;)</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">`</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> f</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>仿照着它的逻辑，在设置Ajax回调函数时我直接写function之中的内容，反正最后function和大括号它会自动生成，改完<code>AjaxErrorJS</code>后满心欢喜的测试，心想马上就解决了，但是发现居然没有什么效果，打开控制台一开又报错了</p><figure class="highlight plain"><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">VM4407:2 Uncaught SyntaxError: Unexpected token &apos;&lt;&apos;, &quot;</span><br><span class="line">    &lt;!DOCTYPE &quot;... is not valid JSON</span><br><span class="line">    at JSON.parse (&lt;anonymous&gt;)</span><br><span class="line">    at Object.success (eval at globalEval (all.min.8425540791.js:1:2618), &lt;anonymous&gt;:14:13)</span><br><span class="line">    at j (all.min.8425540791.js:1:27005)</span><br><span class="line">    at Object.fireWith [as resolveWith] (all.min.8425540791.js:1:27818)</span><br><span class="line">    at x (all.min.8425540791.js:1:75367)</span><br><span class="line">    at XMLHttpRequest.&lt;anonymous&gt; (all.min.8425540791.js:1:78879)</span><br></pre></td></tr></table></figure><p>什么意思呢？分析过后其实是调用Ajax回调函数 <code>AjaxSuccessJS</code> 时，执行 <code>data = JSON.parse(data);</code> 这一句报了错，这里有点奇怪，起初以为302会调用到error的回调函数，现在一看跑到success函数中去了</p><p>大概分析原因如下：</p><p>302 是一个重定向状态码，表示所请求的资源临时位于不同的 URI。通常，响应中会包含一个 Location 头，指示新位置。当 AJAX 请求收到 302 状态码时，浏览器会处理重定向，通常会自动跟随新的 URL。</p><p>如果重定向的目标是一个 HTML 页面（如登录页面），浏览器会将该页面的内容返回给 AJAX 请求的 success 回调，而不是将其处理为错误。换句话说，AJAX 请求会成功接收到重定向目标的 HTML 内容，因为 HTTP 状态码 302 仍被视为“成功”请求的一部分（尽管它表示需要重定向）。</p><p>知道了这些就明白怎么去解决问题了，既然返回的是登录界面的HTML不是json数据，那么我就先判断，如果是登录界面的时候跳转就可以了，为了提升体验，可以加一个弹窗提示和延迟跳转</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (data.includes(<span class="string">"!DOCTYPE html"</span>) &amp;&amp; data.includes(<span class="string">"登录"</span>)) &#123;</span><br><span class="line">    swal(&#123;</span><br><span class="line">        title: <span class="string">"会话已过期"</span>,</span><br><span class="line">        text: <span class="string">"请重新登录，即将跳转到登录页面……"</span>,</span><br><span class="line">        icon: <span class="string">"warning"</span>,</span><br><span class="line">        button: <span class="literal">false</span>,</span><br><span class="line">        timer: <span class="number">2000</span></span><br><span class="line">    &#125;);</span><br><span class="line">    setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="built_in">window</span>.location.href = <span class="string">"/login"</span>;</span><br><span class="line">    &#125;, <span class="number">2000</span>);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    data = <span class="built_in">JSON</span>.parse(data);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>关于这个问题我又问了一下ChatGPT，得到的答案是这样的：</p><blockquote><p>为什么浏览器已经收到了跳转而来的登录界面的内容，却没有跳转，而是继续保持在过期的页面呢</p></blockquote><p>当浏览器发出 AJAX 请求并收到 302 重定向时，通常的行为是浏览器会自动向新的 URL 发起请求。但是，如果在 AJAX 请求的上下文中出现以下情况，可能导致浏览器没有自动跳转，而是显示重定向目标页面的内容，可能的原因如下：</p><ol><li><p><strong>AJAX 请求的响应处理</strong>：</p><ul><li>当 AJAX 请求收到 302 响应时，浏览器会自动请求新的 URL，但这个行为通常不影响当前页面的 URL。相反，它只是在 AJAX 请求的上下文中更新了响应内容。</li><li>如果服务器返回的 302 重定向是指向一个 HTML 页面（如登录页面），并且这个 HTML 内容被返回到 AJAX 的 <code>success</code> 回调中，那么浏览器会将这个内容显示在当前页面，而不会自动进行 URL 更改。</li></ul></li><li><p><strong>AJAX 的特性</strong>：</p><ul><li>AJAX 的目的就是异步加载数据，而不影响页面的整体加载。因此，即使 AJAX 请求收到重定向，浏览器的行为是将新的内容注入到当前页面中，而不是直接改变当前页面的 URL。</li></ul></li><li><p><strong>浏览器行为</strong>：</p><ul><li>在某些情况下（尤其是在使用 JavaScript 进行页面更新时），浏览器可能不会进行页面跳转，因为页面的 JavaScript 处理了新的 HTML 内容。开发者可以在 AJAX 请求的 <code>success</code> 回调中自定义如何处理这种情况。</li></ul></li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>GoAdmin是一个用 <code>golang</code> 编写的前后端一体的面向生产的数据可视化管理平台搭建框架</li><li>AJAX是一种用于创建快速动态网页的技术，能够实现与服务器进行异步交互，更新网页的部分内容</li><li><code>swal</code> 是 SweetAlert 库的一个函数，用于创建漂亮的弹出框（模态窗口）以显示消息、警告、提示等</li><li>SweetAlert有1和2两个版本，SweetAlert 2 的返回值支持 Promise，可以使用 <code>then()</code> 方法</li><li>当使用Ajax发送请求时入股遇到重定向返回给ajax回调函数success的数据是新页面的HTML内容</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/144718042" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>我饮人间二两酒，一饮无奈一饮空！我欠人间二两墨，一笔相思，一笔错！也许人生就是这样，不品人间酒，不知其中醉，不陷世间情，怎知其中泪。永远不用幻想着另一条路是美好的，那只是我们内心美化后的结果，经历过的才是真实~</p><p>2024-12-26 22:07:26</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;GoAdmin是一个基于 &lt;code&gt;golang&lt;/code&gt; 面向生产的数据可视化管理平台搭建框架，可以让你使用简短的代码在极短时间内搭建起一个管理后台，虽然我也会去修改和新增一些功能页面，但是它给我的感觉总像一个黑盒子，大概是我对Web开发的知识了解甚少，总是照葫芦画瓢的完成一些工作，最近在鉴权和会话过期的处理上遇到了一点点问题，直接GET访问页面在会话过期时会自动跳转到登录页面，而点击按钮提交表单时如果已经会话过期了却没有任何反应，我觉得是少设置了一些东西，绕了一大圈才算勉强解决，但这应该不是最优的方案，先记录一下，后续找到好的解决方案再优化。&lt;/p&gt;
    
    </summary>
    
      <category term="Web" scheme="http://AlbertGithubHome.github.io/blog/categories/Web/"/>
    
    
      <category term="Go" scheme="http://AlbertGithubHome.github.io/blog/tags/Go/"/>
    
      <category term="GoAdmin" scheme="http://AlbertGithubHome.github.io/blog/tags/GoAdmin/"/>
    
      <category term="Ajax" scheme="http://AlbertGithubHome.github.io/blog/tags/Ajax/"/>
    
      <category term="swal" scheme="http://AlbertGithubHome.github.io/blog/tags/swal/"/>
    
      <category term="callback" scheme="http://AlbertGithubHome.github.io/blog/tags/callback/"/>
    
      <category term="302重定向" scheme="http://AlbertGithubHome.github.io/blog/tags/302%E9%87%8D%E5%AE%9A%E5%90%91/"/>
    
  </entry>
  
  <entry>
    <title>Windows下使用adb实现在模拟器中ping</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/12/29/Windows%E4%B8%8B%E4%BD%BF%E7%94%A8adb%E5%AE%9E%E7%8E%B0%E5%9C%A8%E6%A8%A1%E6%8B%9F%E5%99%A8%E4%B8%ADping/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/12/29/Windows下使用adb实现在模拟器中ping/</id>
    <published>2024-12-28T16:42:07.000Z</published>
    <updated>2024-12-28T16:43:22.659Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>有时在模拟器中测试应用不像在Windows这种开发环境中那么方便，毕竟Windows或者Linux下的工具五花八门，可以满足各种测试需求，比如应用在模拟器中无法登录时，想测试模拟器的网络情况，如果是在Windows或者Linux下各种网络工具任君挑选，但是在模拟器中运行个命令行下的ping命令的都费劲，但方法还是有的，我就找个不那么费劲的方法吧，记录在此方便查阅。</p><a id="more"></a><h1 id="安装adb"><a href="#安装adb" class="headerlink" title="安装adb"></a>安装adb</h1><p><code>adb</code>（Android Debug Bridge）是一个命令行工具，用于与 Android 设备或模拟器进行交互。它是 <code>Android SDK</code> 的一部分，广泛用于开发、调试和管理 Android 应用程序和设备，如果我们仅是简单使用，不必安装完整的 Android SDK，直接下载Google提供的adb工具包，下载后解压即可使用。</p><ul><li>Windows版本：<a href="https://dl.google.com/android/repository/platform-tools-latest-windows.zip" target="_blank" rel="noopener">https://dl.google.com/android/repository/platform-tools-latest-windows.zip</a></li><li>Linux版本：<a href="https://dl.google.com/android/repository/platform-tools-latest-linux.zip" target="_blank" rel="noopener">https://dl.google.com/android/repository/platform-tools-latest-linux.zip</a></li><li>Mac版本：<a href="https://dl.google.com/android/repository/platform-tools-latest-darwin.zip" target="_blank" rel="noopener">https://dl.google.com/android/repository/platform-tools-latest-darwin.zip</a></li></ul><p>Windows中下载解压后需要将加压后的文件夹添加到<code>Path</code>环境变量，按键windows+i打开设置界面搜索“环境变量”回车。</p><p>在选项 <code>环境变量</code> -&gt; <code>系统变量</code> -&gt; <code>Path</code> 输入解压后端的文件夹，比如 <code>D:\platform-tools\</code> 确定后保存</p><p>打开CMD命令行工具输入 <code></code>验证，显示如下类似信息证明工具已经就位</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">C:\Users\Administrator&gt;adb --version</span><br><span class="line">Android Debug Bridge version 1.0.41</span><br><span class="line">Version 35.0.2-12147458</span><br><span class="line">Installed as D:\ProgramNoBlank\platform-tools\adb.exe</span><br><span class="line">Running on Windows 10.0.18363</span><br></pre></td></tr></table></figure><h1 id="执行adb命令"><a href="#执行adb命令" class="headerlink" title="执行adb命令"></a>执行adb命令</h1><p>有了adb就可以和连接了电脑的Android设备通信，Android模拟器也可以看成是安装设备，首先要找到模拟器设备id</p><h2 id="查找模拟器设备"><a href="#查找模拟器设备" class="headerlink" title="查找模拟器设备"></a>查找模拟器设备</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">C:\Users\Administrator&gt;adb devices</span><br><span class="line">List of devices attached</span><br><span class="line">C:\Users\Administrator&gt;adb devices</span><br><span class="line">List of devices attached</span><br><span class="line">emulator-5554   device</span><br></pre></td></tr></table></figure><h2 id="链接模拟器命令行"><a href="#链接模拟器命令行" class="headerlink" title="链接模拟器命令行"></a>链接模拟器命令行</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">C:\Users\Administrator&gt;adb -s emulator-5554 shell</span><br><span class="line">OP564B:/ $</span><br></pre></td></tr></table></figure><h2 id="执行ping命令"><a href="#执行ping命令" class="headerlink" title="执行ping命令"></a>执行ping命令</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">OP564B:/ $ ping 008ct.top</span><br><span class="line">PING 008ct.top (198.18.0.152) 56(84) bytes of data.</span><br><span class="line">64 bytes from 198.18.0.152: icmp_seq=1 ttl=64 time=0.636 ms</span><br><span class="line">64 bytes from 198.18.0.152: icmp_seq=2 ttl=64 time=1.13 ms</span><br><span class="line">64 bytes from 198.18.0.152: icmp_seq=3 ttl=64 time=1.39 ms</span><br><span class="line">64 bytes from 198.18.0.152: icmp_seq=4 ttl=64 time=1.57 ms</span><br><span class="line">64 bytes from 198.18.0.152: icmp_seq=5 ttl=64 time=1.51 ms</span><br><span class="line">^C</span><br><span class="line">--- 008ct.top ping statistics ---</span><br><span class="line">5 packets transmitted, 5 received, 0% packet loss, time 4009ms</span><br><span class="line">rtt min/avg/max/mdev = 0.636/1.251/1.579/0.344 ms</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li><code>adb</code> 是一个命令行工具，属于 <code>Android SDK</code> 的一部分，用于与 Android 设备或模拟器进行交互</li><li>借助 <code>adb</code> 工具可以实现在宿主机上调试和处理Android设备或模拟器上的应用</li><li>查询机器连接的Android设备列表命令 <code>adb devices</code></li><li>连接Android模拟器中的命令行工具的命令 <code>adb -s emulator-5554 shell</code></li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143605659" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>曾国藩、左宗棠、洪秀全分别相差1岁，命运却迥然不同，左宗棠第一次见曾国藩的评价居然是“才具稍欠开展”，命运戏人啊</p><p>2024-11-12 17:52:57</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;有时在模拟器中测试应用不像在Windows这种开发环境中那么方便，毕竟Windows或者Linux下的工具五花八门，可以满足各种测试需求，比如应用在模拟器中无法登录时，想测试模拟器的网络情况，如果是在Windows或者Linux下各种网络工具任君挑选，但是在模拟器中运行个命令行下的ping命令的都费劲，但方法还是有的，我就找个不那么费劲的方法吧，记录在此方便查阅。&lt;/p&gt;
    
    </summary>
    
      <category term="windows" scheme="http://AlbertGithubHome.github.io/blog/categories/windows/"/>
    
    
      <category term="ping" scheme="http://AlbertGithubHome.github.io/blog/tags/ping/"/>
    
      <category term="windows" scheme="http://AlbertGithubHome.github.io/blog/tags/windows/"/>
    
      <category term="adb" scheme="http://AlbertGithubHome.github.io/blog/tags/adb/"/>
    
      <category term="Android" scheme="http://AlbertGithubHome.github.io/blog/tags/Android/"/>
    
      <category term="模拟器" scheme="http://AlbertGithubHome.github.io/blog/tags/%E6%A8%A1%E6%8B%9F%E5%99%A8/"/>
    
  </entry>
  
  <entry>
    <title>简单整理下protobuf中常用的几个工具函数</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/12/25/%E7%AE%80%E5%8D%95%E6%95%B4%E7%90%86%E4%B8%8Bprotobuf%E4%B8%AD%E5%B8%B8%E7%94%A8%E7%9A%84%E5%87%A0%E4%B8%AA%E5%B7%A5%E5%85%B7%E5%87%BD%E6%95%B0/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/12/25/简单整理下protobuf中常用的几个工具函数/</id>
    <published>2024-12-25T15:30:00.000Z</published>
    <updated>2024-12-28T16:57:28.182Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>游戏项目中使用protobuf作为双端协议通讯的方式，算是一种中规中矩的方法了，偶尔会需要查看序列化后的消息占用空间大小，一直都是用序列化后的字符串来查看，即使临时测试一个结构占用空间也要序列化后才能调用 <code>.size()</code> 函数，最近偶然间发现有个 <code>ByteSizeLong()</code> 可直接用于BP结构上，这倒是简单了一点点，所以把常用的几个函数列举总结一下，免的自己时间太长忘记了。</p><a id="more"></a><h1 id="使用示例"><a href="#使用示例" class="headerlink" title="使用示例"></a>使用示例</h1><h2 id="定义message结构"><a href="#定义message结构" class="headerlink" title="定义message结构"></a>定义message结构</h2><p>在调用函数之前先定义一个结构文件吧</p><figure class="highlight plain"><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">syntax = &quot;proto3&quot;;</span><br><span class="line">package ProtoMsg;</span><br><span class="line">option go_package = &quot;./;ProtoMsg&quot;;</span><br><span class="line"></span><br><span class="line">message Address &#123;                  // 地址结构</span><br><span class="line">    string city = 1;</span><br><span class="line">    string street = 2;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">message Person &#123;</span><br><span class="line">    int32 age = 1;                 // 年龄</span><br><span class="line">    string name = 2;               // 姓名</span><br><span class="line">    bool student = 3;              // 学生</span><br><span class="line">    Address address = 4;           // 地址</span><br><span class="line">    map&lt;string, string&gt; tags = 5;  // 标签信息</span><br><span class="line">    repeated int64 lucky_nums = 6; // 幸运数字</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="结构体赋值"><a href="#结构体赋值" class="headerlink" title="结构体赋值"></a>结构体赋值</h2><p>接着我们使用C++语言给这个结构赋值，然后尝试在此基础上调用不同的函数</p><figure class="highlight cpp"><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">ProtoMsg::Person person;</span><br><span class="line">person.set_age(<span class="number">18</span>);</span><br><span class="line">person.set_name(<span class="string">"albert"</span>);</span><br><span class="line">person.set_student(<span class="literal">true</span>);</span><br><span class="line">person.mutable_address()-&gt;set_city(<span class="string">"beijing"</span>);</span><br><span class="line">person.mutable_address()-&gt;set_street(<span class="string">"soho"</span>);</span><br><span class="line">(*person.mutable_tags())[<span class="string">"hobby"</span>] = <span class="string">"reading"</span>;</span><br><span class="line">(*person.mutable_tags())[<span class="string">"family"</span>] = <span class="string">"warm"</span>;</span><br><span class="line">person.add_lucky_nums(<span class="number">1</span>);</span><br><span class="line">person.add_lucky_nums(<span class="number">9</span>);</span><br></pre></td></tr></table></figure><h2 id="ByteSizeLong"><a href="#ByteSizeLong" class="headerlink" title="ByteSizeLong()"></a>ByteSizeLong()</h2><p>这是一个获取protobuf结构大小的函数，起初我找到的是 <code>ByteSize()</code>，但是这个函数返回值是int，现在已经不建议使用了，所以之后请直接使用 <code>ByteSizeLong()</code> 来获取结构大小，就像下面这样：</p><figure class="highlight cpp"><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="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">"show person size ["</span> &lt;&lt; person.ByteSizeLong() &lt;&lt; <span class="string">"] and address size ["</span> &lt;&lt; person.address().ByteSizeLong() &lt;&lt; <span class="string">"]."</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出结果</span></span><br><span class="line"><span class="comment">// show person size [67] and address size [15].</span></span><br></pre></td></tr></table></figure><h2 id="GetTypeName"><a href="#GetTypeName" class="headerlink" title="GetTypeName()"></a>GetTypeName()</h2><p>这个函数可以获取结构的名字，有一点点元数据的意思，在输出日志时可以使内容更清晰：</p><figure class="highlight cpp"><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="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">"show person type name ["</span> &lt;&lt; person.GetTypeName() &lt;&lt; <span class="string">"] and address type name ["</span> &lt;&lt; person.address().GetTypeName() &lt;&lt; <span class="string">"]."</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出结果</span></span><br><span class="line"><span class="comment">// show person type name [ProtoMsg.Person] and address type name [ProtoMsg.Address].</span></span><br></pre></td></tr></table></figure><h2 id="DebugString"><a href="#DebugString" class="headerlink" title="DebugString()"></a>DebugString()</h2><p>这个函数看名字就知道和调试信息有关，它可以打印整个结构的数据：</p><figure class="highlight cpp"><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="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">"show person debug string ["</span> &lt;&lt; person.DebugString() &lt;&lt; <span class="string">"]."</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出结果</span></span><br><span class="line"><span class="comment">// show person debug string [age: 18</span></span><br><span class="line"><span class="comment">// name: "albert"</span></span><br><span class="line"><span class="comment">// student: true</span></span><br><span class="line"><span class="comment">// address &#123;</span></span><br><span class="line"><span class="comment">//   city: "beijing"</span></span><br><span class="line"><span class="comment">//   street: "soho"</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br><span class="line"><span class="comment">// lucky_nums: 1</span></span><br><span class="line"><span class="comment">// lucky_nums: 9</span></span><br><span class="line"><span class="comment">// tags &#123;</span></span><br><span class="line"><span class="comment">//   key: "family"</span></span><br><span class="line"><span class="comment">//   value: "warm"</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br><span class="line"><span class="comment">// tags &#123;</span></span><br><span class="line"><span class="comment">//   key: "hobby"</span></span><br><span class="line"><span class="comment">//   value: "reading"</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br><span class="line"><span class="comment">// ].</span></span><br></pre></td></tr></table></figure><h2 id="ShortDebugString"><a href="#ShortDebugString" class="headerlink" title="ShortDebugString()"></a>ShortDebugString()</h2><p>我们发现 <code>DebugString()</code> 输出的内容换行较多，虽然看起来更清晰，但是不便于存入日志按行查找，要想存储日志文件中，<code>ShortDebugString()</code> 函数是更好的选择:</p><figure class="highlight cpp"><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="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">"show person short debug string ["</span> &lt;&lt; person.ShortDebugString() &lt;&lt; <span class="string">"]."</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出结果</span></span><br><span class="line"><span class="comment">// show person short debug string [age: 18 name: "albert" student: true address &#123; city: "beijing" street: "soho" &#125; lucky_nums: 1 lucky_nums: 9 tags &#123; key: "family" value: "warm" &#125; tags &#123; key: "hobby" value: "reading" &#125;].</span></span><br></pre></td></tr></table></figure><h2 id="CopyFrom-const-MessageLite-amp-other"><a href="#CopyFrom-const-MessageLite-amp-other" class="headerlink" title="CopyFrom(const MessageLite&amp; other)"></a>CopyFrom(const MessageLite&amp; other)</h2><p>这个结构用于结构的拷贝，比如你已经有了一个地址结构对象，再创建新的 <code>Person</code> 时就不必对address字段一一赋值，而是可以直接使用CopyFrom函数完成：</p><figure class="highlight cpp"><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">    ProtoMsg::Address addr;</span><br><span class="line">    addr.set_city(<span class="string">"shanghai"</span>);</span><br><span class="line">    addr.set_street(<span class="string">"nanjinglu"</span>);</span><br><span class="line">    person.mutable_address()-&gt;CopyFrom(addr);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">std</span>::count &lt;&lt; <span class="string">"show person short debug string ["</span> &lt;&lt; person.ShortDebugString() &lt;&lt; <span class="string">"]."</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出结果</span></span><br><span class="line"><span class="comment">// show person short debug string [age: 18 name: "albert" student: true address &#123; city: "shanghai" street: "nanjinglu" &#125; lucky_nums: 1 lucky_nums: 9 tags &#123; key: "family" value: "warm" &#125; tags &#123; key: "hobby" value: "reading" &#125;].</span></span><br></pre></td></tr></table></figure><h2 id="Clear"><a href="#Clear" class="headerlink" title="Clear()"></a>Clear()</h2><p>清空当前消息的所有字段，重置为默认状态，这个我倒是不太常用，但是测试时偶尔会用到：</p><figure class="highlight cpp"><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">    person.Clear();</span><br><span class="line"></span><br><span class="line">    <span class="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">"show person size ["</span> &lt;&lt; person.ByteSizeLong() &lt;&lt; <span class="string">"] and short debug string ["</span> &lt;&lt; person.ShortDebugString() &lt;&lt; <span class="string">"]."</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出结果</span></span><br><span class="line"><span class="comment">// show person size [0] and short debug string [].</span></span><br></pre></td></tr></table></figure><h1 id="Ohters"><a href="#Ohters" class="headerlink" title="Ohters"></a>Ohters</h1><p>其他还有一些函数属于使用protobuf的常见函数，我就不再举例了，简单列举下函数作用：</p><ul><li><code>MergeFrom(const MessageLite&amp; other)</code> 合并另一个消息的内容到当前消息中（可能会覆盖现有字段）</li><li><code>SerializeToString(std::string* output)</code> 将消息序列化为字符串格式，并将结果存储在提供的 std::string 对象中</li><li><code>ParseFromString(const std::string&amp; data)</code> 从给定的字符串解析出消息数据</li><li><code>std::string SerializeAsString() const</code> 将消息序列化为字符串格式并返回</li><li><code>bool SerializeToArray(void* data, int size) const</code> 直接将消息序列化到用户提供的缓冲区中</li><li><code>google::protobuf::MessageLite::ParseFromArray(const void *data, int size)</code> 将给定的二进制数据解析为 Protocol Buffers 消息对象</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li><code>ByteSizeLong()</code> 可以用来获取一个protobuf结构的大小</li><li><code>GetTypeName()</code> 可以用来返回一个结构的名字</li><li><code>ShortDebugString()</code> 可以用返回一个结构的数据内容</li><li><code>protobuf</code> 这种经典的开源库挖一挖总会有惊喜</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/144611011" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>慢就是快，每个人都有不同的解读，曾经有那么两次我好像都能顿悟了，可思绪闪现了那么一刻就消失了；稳稳地慢是幸运的，最近总是听人讲到控制回撤这个词，尽管他喊得声嘶力竭，起初我却不以为意，但经历的多了以后开始理解他所说的价值，才理解慢慢变富的含义，既然是概率游戏，只要能保证胜率在51%，那么最后一定会赢的，我们要做的只是按照自己的节奏不断地重复，不断地重复就好~</p><p>2024-12-25 13:38:19</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;游戏项目中使用protobuf作为双端协议通讯的方式，算是一种中规中矩的方法了，偶尔会需要查看序列化后的消息占用空间大小，一直都是用序列化后的字符串来查看，即使临时测试一个结构占用空间也要序列化后才能调用 &lt;code&gt;.size()&lt;/code&gt; 函数，最近偶然间发现有个 &lt;code&gt;ByteSizeLong()&lt;/code&gt; 可直接用于BP结构上，这倒是简单了一点点，所以把常用的几个函数列举总结一下，免的自己时间太长忘记了。&lt;/p&gt;
    
    </summary>
    
      <category term="C/C++" scheme="http://AlbertGithubHome.github.io/blog/categories/C-C/"/>
    
    
      <category term="C++ protobuf ByteSizeLong GetTypeName DebugString" scheme="http://AlbertGithubHome.github.io/blog/tags/C-protobuf-ByteSizeLong-GetTypeName-DebugString/"/>
    
  </entry>
  
  <entry>
    <title>使用mysqlx操作MySQL8.0数据库时遭遇CDK Error: Connection refused (generic:111)错误</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/12/21/%E4%BD%BF%E7%94%A8mysqlx%E6%93%8D%E4%BD%9CMySQL8-0%E6%95%B0%E6%8D%AE%E5%BA%93%E6%97%B6%E9%81%AD%E9%81%87CDK-Error-Connection-refused-generic-111-%E9%94%99%E8%AF%AF/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/12/21/使用mysqlx操作MySQL8-0数据库时遭遇CDK-Error-Connection-refused-generic-111-错误/</id>
    <published>2024-12-20T16:00:00.000Z</published>
    <updated>2024-12-28T16:55:16.343Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>游戏服务器一个配置存储服务，使用 <code>mysql-connector-c++ 8.0</code> 连接 <code>MySQL 8.0</code> 数据库，使用 <code>MySQL X Protocol</code>， 它是 MySQL 8.0 引入的一种新的通信协议，它是为了支持现代应用程序开发需求而设计的。除了关系型数据存储外，MySQL X Protocol 可以方便地处理文档存储，允许存储和检索 JSON 等文档类型的数据，使其更适合现代的 NoSQL 风格的开发模式。该协议支持异步操作，提高了性能，允许更高效的并发处理，特别是在处理大量请求时。支持更丰富的数据操作，包括 CRUD 操作、事务处理、SQL 语句执行，并且可以处理复杂的查询，例如对文档存储的查询。</p><a id="more"></a><p><code>mysqlx</code> 是 MySQL 提供的用于与 MySQL 服务器进行交互的 C++ 客户端库，基于 MySQL X Protocol 开发。它提供了一组 C++ API，使开发人员可以在 C++ 程序中使用 MySQL 服务器的功能。而我就是用这套API是遇到了问题，本来这套功能在本地测试时很顺畅，没有发现问题，但是放到线上环境时就发现无法存储数据，调试线上环境还是有一些难度的。</p><p>因为线上程序编译为release版本，几乎没有可以调试的符号，起初还怀疑是release版本有问题，但是本地编译成release版本依旧正常，所以我就迅速用一个debug版本的so库替换了线上的release版本库，结果废了半天劲就找到了 <code>CDK Error: Connection refused (generic:111)</code> 这个错误，对于这个问题的处理还查了很多解决办法，但回过头来看，这个提示已经很明显了，就是不让连接呗，应该第一时间想到连接权限问题的，但是这有点马后炮，关于这个问题还是走了不少弯路，不过也学到了很多，总结一下做个备忘录。</p><h1 id="问题现场"><a href="#问题现场" class="headerlink" title="问题现场"></a>问题现场</h1><p>查找object时的示例代码:</p><figure class="highlight cpp"><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">mysqlx::Session sess = sqlClient-&gt;getSession();</span><br><span class="line">mysqlx::DocResult result = sess</span><br><span class="line">    .getDefaultSchema()</span><br><span class="line">    .getCollection(collectionName)</span><br><span class="line">    .find(searchPattern)</span><br><span class="line">    .execute();</span><br><span class="line"></span><br><span class="line">mysqlx::DbDoc doc;</span><br><span class="line"><span class="keyword">while</span> (doc = result.fetchOne())</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>添加新object时的实例代码：</p><figure class="highlight cpp"><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">mysqlx::Session sess = sqlClient-&gt;getSession();</span><br><span class="line">mysqlx::Result result = sess</span><br><span class="line">    .getDefaultSchema()</span><br><span class="line">    .createCollection(collectionName, <span class="literal">true</span>)</span><br><span class="line">    .add(jsonString)</span><br><span class="line">    .execute();</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> result.getAffectedItemsCount() &gt; <span class="number">0</span>;</span><br></pre></td></tr></table></figure><p>报错的函数主要集中在上面两段逻辑，实际上通过debug库断点，已经能定位到 <code>mysqlx::Session sess = sqlClient-&gt;getSession();</code> 这一句就开始报异常了，异常类型 <code>catch (const mysqlx::Error&amp; err) { ... }</code>，打印信息也就是标题中看到的 CDK Error: Connection refused (generic:111)。</p><p>这里的Collection相当于关系数据库中的一个表，实际上可以通过关系数据库导出的方式，将Collection导出或者导出，仅导出表结构的命令</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">mysqldump -u username -p --no-data source_database table_name &gt; table_structure.sql</span><br></pre></td></tr></table></figure><p>导出的 table_structure.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><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="comment">-- MySQL dump 10.13  Distrib 8.0.24, for Linux (x86_64)</span></span><br><span class="line"><span class="comment">--</span></span><br><span class="line"><span class="comment">-- Host: localhost    Database: source_database</span></span><br><span class="line"><span class="comment">-- ------------------------------------------------------</span></span><br><span class="line"><span class="comment">-- Server version       8.0.24</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */</span>;</span><br><span class="line"><span class="comment">/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */</span>;</span><br><span class="line"><span class="comment">/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */</span>;</span><br><span class="line"><span class="comment">/*!50503 SET NAMES utf8mb4 */</span>;</span><br><span class="line"><span class="comment">/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */</span>;</span><br><span class="line"><span class="comment">/*!40103 SET TIME_ZONE='+00:00' */</span>;</span><br><span class="line"><span class="comment">/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */</span>;</span><br><span class="line"><span class="comment">/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */</span>;</span><br><span class="line"><span class="comment">/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */</span>;</span><br><span class="line"><span class="comment">/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">--</span></span><br><span class="line"><span class="comment">-- Table structure for table `GlobalConfig`</span></span><br><span class="line"><span class="comment">--</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">DROP</span> <span class="keyword">TABLE</span> <span class="keyword">IF</span> <span class="keyword">EXISTS</span> <span class="string">`GlobalConfig`</span>;</span><br><span class="line"><span class="comment">/*!40101 SET @saved_cs_client     = @@character_set_client */</span>;</span><br><span class="line"><span class="comment">/*!50503 SET character_set_client = utf8mb4 */</span>;</span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`GlobalConfig`</span> (</span><br><span class="line">  <span class="string">`doc`</span> <span class="keyword">json</span> <span class="keyword">DEFAULT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`_id`</span> varbinary(<span class="number">32</span>) <span class="keyword">GENERATED</span> <span class="keyword">ALWAYS</span> <span class="keyword">AS</span> (json_unquote(json_extract(<span class="string">`doc`</span>,_utf8mb4<span class="string">'$._id'</span>))) <span class="keyword">STORED</span> <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`_json_schema`</span> <span class="keyword">json</span> <span class="keyword">GENERATED</span> <span class="keyword">ALWAYS</span> <span class="keyword">AS</span> (_utf8mb4<span class="string">'&#123;"type":"object"&#125;'</span>) <span class="keyword">VIRTUAL</span>,</span><br><span class="line">  PRIMARY <span class="keyword">KEY</span> (<span class="string">`_id`</span>),</span><br><span class="line">  <span class="keyword">CONSTRAINT</span> <span class="string">`$val_strict_64BAB193F9193856B3219B21CF70823072B92EB1`</span> <span class="keyword">CHECK</span> (json_schema_valid(<span class="string">`_json_schema`</span>,<span class="string">`doc`</span>)) <span class="comment">/*!80016 NOT ENFORCED */</span></span><br><span class="line">) <span class="keyword">ENGINE</span>=<span class="keyword">InnoDB</span> <span class="keyword">DEFAULT</span> <span class="keyword">CHARSET</span>=utf8mb4 <span class="keyword">COLLATE</span>=utf8mb4_0900_ai_ci;</span><br><span class="line"><span class="comment">/*!40101 SET character_set_client = @saved_cs_client */</span>;</span><br><span class="line"><span class="comment">/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*!40101 SET SQL_MODE=@OLD_SQL_MODE */</span>;</span><br><span class="line"><span class="comment">/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */</span>;</span><br><span class="line"><span class="comment">/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */</span>;</span><br><span class="line"><span class="comment">/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */</span>;</span><br><span class="line"><span class="comment">/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */</span>;</span><br><span class="line"><span class="comment">/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */</span>;</span><br><span class="line"><span class="comment">/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- Dump completed on 2024-12-19 23:03:52</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">mysql -u username -p target_database &lt; table_name.sql</span><br></pre></td></tr></table></figure><h1 id="解决问题"><a href="#解决问题" class="headerlink" title="解决问题"></a>解决问题</h1><p>有经验之后可以从提示 <code>CDK Error: Connection refused (generic:111)</code> 推断出是连接权限问题，但要准确找到问题点还需要逐步排查</p><ol><li>MySQL绑定的IP地址是多少？允许哪些主机访问</li><li>MySQL监听的端口是多少？允许通过哪些端口访问</li><li>MySQL中定义了哪些用户？哪些用户可以访问</li><li>MySQL中定义的用户绑定了哪些主机？已存在的用户可以通过哪些主机访问</li><li>MySQL中绑定了主机的用户有哪些权限？可以访问的用户是否有指定的操作权限，比如增加、删除等</li></ol><p>这么一看还真是挺麻烦的，不过不用担心，当处理的次数多了自然而然就理解了，先看前两个问题，MySQL绑定的IP和端口吧</p><h2 id="MySQL绑定IP和端口"><a href="#MySQL绑定IP和端口" class="headerlink" title="MySQL绑定IP和端口"></a>MySQL绑定IP和端口</h2><p>他们配置在文件 <code>/etc/mysql/mysql.conf.d/mysqld.cnf</code> 中，这里面文件名很相似，我的环境是在Ubuntu20.04，版本 目录结构如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># tree /etc/mysql</span></span><br><span class="line">/etc/mysql</span><br><span class="line">├── conf.d</span><br><span class="line">│   ├── mysql.cnf</span><br><span class="line">│   └── mysqldump.cnf</span><br><span class="line">├── debian.cnf</span><br><span class="line">├── debian-start</span><br><span class="line">├── my.cnf -&gt; /etc/alternatives/my.cnf</span><br><span class="line">├── my.cnf.fallback</span><br><span class="line">├── mysql.cnf</span><br><span class="line">└── mysql.conf.d</span><br><span class="line">    ├── mysql.cnf</span><br><span class="line">    └── mysqld.cnf</span><br></pre></td></tr></table></figure><p>部分文件内容</p><figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">[mysqld]</span><br><span class="line">#</span><br><span class="line"># * Basic Settings</span><br><span class="line">#</span><br><span class="line">user            = mysql</span><br><span class="line"># pid-file      = /var/run/mysqld/mysqld.pid</span><br><span class="line"># socket        = /var/run/mysqld/mysqld.sock</span><br><span class="line">port            = 3306</span><br><span class="line"># datadir       = /var/lib/mysql</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"># If MySQL is running as a replication slave, this should be</span><br><span class="line"># changed. Ref https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_tmpdir</span><br><span class="line"># tmpdir                = /tmp</span><br><span class="line">#</span><br><span class="line"># Instead of skip-networking the default is now to listen only on</span><br><span class="line"># localhost which is more compatible and is not less secure.</span><br><span class="line">bind-address            = 127.0.0.1</span><br><span class="line">mysqlx-bind-address     = 127.0.0.1</span><br></pre></td></tr></table></figure><p>无论是传统的mysql协议还是新的mysqlx协议，都默认允许本机回环地址 <code>127.0.0.1</code> 连接，mysql协议默认端口3306，mysqlx默认端口33060，如果想修改在文件 <code>mysqld.cnf</code> 修改后重启MySQL服务 <code>sudo systemctl restart mysql</code> 既可以了，可以查询MySQL变量看看目前生效的配置值。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; SHOW VARIABLES LIKE <span class="string">'%bind%'</span>;</span><br><span class="line">+---------------------+-----------+</span><br><span class="line">| Variable_name       | Value     |</span><br><span class="line">+---------------------+-----------+</span><br><span class="line">| bind_address        | 127.0.0.1 |</span><br><span class="line">| mysqlx_bind_address | 127.0.0.1 |</span><br><span class="line">+---------------------+-----------+</span><br><span class="line">2 rows <span class="keyword">in</span> <span class="built_in">set</span> (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; SHOW VARIABLES LIKE <span class="string">'%port%'</span>;</span><br><span class="line">+--------------------------+-------+</span><br><span class="line">| Variable_name            | Value |</span><br><span class="line">+--------------------------+-------+</span><br><span class="line">| mysqlx_port              | 33060 |</span><br><span class="line">| port                     | 3306  |</span><br><span class="line">+--------------------------+-------+</span><br></pre></td></tr></table></figure><p>对于 MySQL 服务绑定地址 <code>0.0.0.0</code>、<code>192.168.1.100</code>、<code>127.0.0.1</code> 和 <code>localhost</code> 有一些差异，其中192.168.1.100为本机内网地址，对比结果整理如下：</p><table><thead><tr><th><strong>对比维度</strong></th><th><strong>0.0.0.0</strong></th><th><strong>192.168.1.100</strong></th><th><strong>127.0.0.1</strong></th><th><strong>localhost</strong></th></tr></thead><tbody><tr><td><strong>含义</strong></td><td>监听所有网络接口（本机、内网、外网）。</td><td>监听特定的本机网络接口，仅允许特定 IP 连接。</td><td>仅监听本机回环接口，仅限本机访问。</td><td>通常解析为 Unix Socket 或 <code>127.0.0.1</code>。</td></tr><tr><td><strong>访问范围</strong></td><td>所有来源（包括本地和远程）。</td><td>仅允许通过指定的网络接口访问（如内网 IP）。</td><td>仅限本机访问，无法从外部访问。</td><td>仅限本机访问，无法从外部访问。</td></tr><tr><td><strong>使用协议</strong></td><td>强制使用 TCP/IP 协议。</td><td>强制使用 TCP/IP 协议。</td><td>强制使用 TCP/IP 协议。</td><td>优先使用 Unix Socket，无法使用时退回到 TCP/IP 协议。</td></tr><tr><td><strong>性能</strong></td><td>可能因监听所有接口稍有性能影响。</td><td>仅监听单个接口，性能略高于 <code>0.0.0.0</code>。</td><td>受限于网络栈处理效率。</td><td>使用 Unix Socket 时性能最高；TCP/IP 时与 <code>127.0.0.1</code> 相同。</td></tr><tr><td><strong>安全性</strong></td><td>最不安全，需防火墙限制来源以防外网暴露。</td><td>较为安全，仅允许指定接口连接，适合内网使用。</td><td>高安全性，仅限本机访问，不暴露给外部网络。</td><td>高安全性，仅限本机访问，Unix Socket 更加安全。</td></tr><tr><td><strong>适用场景</strong></td><td>内网/外网连接，需严格配置防火墙规则。</td><td>局域网访问，仅需特定网络接口的连接。</td><td>单机应用或本地调试环境，仅限本机访问。</td><td>本地高频操作，优先使用更高效的 Unix Socket。</td></tr><tr><td><strong>配置方法</strong></td><td><code>bind-address = 0.0.0.0</code></td><td><code>bind-address = 192.168.1.100</code></td><td><code>bind-address = 127.0.0.1</code></td><td><code>bind-address = 127.0.0.1</code>（间接实现）。</td></tr><tr><td><strong>访问示例</strong></td><td>所有主机（如 <code>mysql -h &lt;任意IP&gt;</code>）。</td><td>指定内网 IP（如 <code>mysql -h 192.168.1.100</code>）。</td><td>本机访问（如 <code>mysql -h 127.0.0.1</code>）。</td><td>本机访问（如 <code>mysql -h localhost</code>）。</td></tr></tbody></table><h2 id="MySQL用户和绑定的主机"><a href="#MySQL用户和绑定的主机" class="headerlink" title="MySQL用户和绑定的主机"></a>MySQL用户和绑定的主机</h2><p>在MySQL中不仅定义了用户和对应权限，还规定了用户绑定的主机，也就是说权限不仅仅分配给一个用户的，而是分配给通过指定机器访问的用户的，单单这么说对于没有操作的人来说可能有些懵，我们可以实际操作一下：</p><p>通过 <code>SELECT DISTINCT CONCAT(&#39;User: &#39;&#39;&#39;,user,&#39;&#39;&#39;@&#39;&#39;&#39;,host,&#39;&#39;&#39;;&#39;) AS query FROM mysql.user;</code> 可以查询用户</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; SELECT DISTINCT CONCAT(<span class="string">'User: '</span><span class="string">''</span>,user,<span class="string">''</span><span class="string">'@'</span><span class="string">''</span>,host,<span class="string">''</span><span class="string">';'</span>) AS query FROM mysql.user;</span><br><span class="line">+---------------------------------------+</span><br><span class="line">| query                                 |</span><br><span class="line">+---------------------------------------+</span><br><span class="line">| User: <span class="string">'webview'</span>@<span class="string">'%'</span>;                  |</span><br><span class="line">| User: <span class="string">'comm'</span>@<span class="string">'192.168.1.100'</span>;         |</span><br><span class="line">| User: <span class="string">'comm'</span>@<span class="string">'localhost'</span>;             |</span><br><span class="line">| User: <span class="string">'debian-sys-maint'</span>@<span class="string">'localhost'</span>; |</span><br><span class="line">| User: <span class="string">'admin'</span>@<span class="string">'localhost'</span>;            |</span><br><span class="line">| User: <span class="string">'mysql.infoschema'</span>@<span class="string">'localhost'</span>; |</span><br><span class="line">| User: <span class="string">'mysql.session'</span>@<span class="string">'localhost'</span>;    |</span><br><span class="line">| User: <span class="string">'mysql.sys'</span>@<span class="string">'localhost'</span>;        |</span><br><span class="line">| User: <span class="string">'root'</span>@<span class="string">'localhost'</span>;             |</span><br><span class="line">| User: <span class="string">'zone0001'</span>@<span class="string">'localhost'</span>;         |</span><br><span class="line">+---------------------------------------+</span><br><span class="line">11 rows <span class="keyword">in</span> <span class="built_in">set</span> (0.00 sec)</span><br></pre></td></tr></table></figure><p>用户名加上后面的主机名才是权限授予的主体，可以看到comm这个用户名可以通过localhost本机访问，也可以通过 192.168.1.100 这台机器访问，如果还需要通过 192.168.1.200，那么还需要创建一个 <code>&#39;comm&#39;@&#39;192.168.1.200&#39;</code> 的用户并授予权限才可以。</p><p>如果想要一个用户可以在任何主机上登录访问MySQL，主机部分用 <code>%</code> 代替，就比如 <code>&#39;webview&#39;@&#39;%&#39;</code> 这个用户，不过不建议这样做，安全性明显降低。</p><h2 id="MySQL授予和回收权限"><a href="#MySQL授予和回收权限" class="headerlink" title="MySQL授予和回收权限"></a>MySQL授予和回收权限</h2><p>查询权限可以通过 <code>show grants for 用户</code> 查询，测试如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><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">mysql&gt; show grants <span class="keyword">for</span> <span class="string">'comm'</span>@<span class="string">'localhost'</span>;</span><br><span class="line">+----------------------------------------------------------+</span><br><span class="line">| Grants <span class="keyword">for</span> comm@localhost                                |</span><br><span class="line">+----------------------------------------------------------+</span><br><span class="line">| GRANT USAGE ON *.* TO `comm`@`localhost`                 |</span><br><span class="line">| GRANT ALL PRIVILEGES ON `mycomm`.* TO `comm`@`localhost` |</span><br><span class="line">+----------------------------------------------------------+</span><br><span class="line">2 rows <span class="keyword">in</span> <span class="built_in">set</span> (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; show grants <span class="keyword">for</span> <span class="string">'root'</span>@<span class="string">'localhost'</span>;</span><br><span class="line">+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</span><br><span class="line">| Grants <span class="keyword">for</span> root@localhost                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |</span><br><span class="line">+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</span><br><span class="line">| GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION                                                                                                                                                                                                                                                                                                                                                                                     |</span><br><span class="line">| GRANT APPLICATION_PASSWORD_ADMIN,AUDIT_ABORT_EXEMPT,AUDIT_ADMIN,AUTHENTICATION_POLICY_ADMIN,BACKUP_ADMIN,BINLOG_ADMIN,BINLOG_ENCRYPTION_ADMIN,CLONE_ADMIN,CONNECTION_ADMIN,ENCRYPTION_KEY_ADMIN,FIREWALL_EXEMPT,FLUSH_OPTIMIZER_COSTS,FLUSH_STATUS,FLUSH_TABLES,FLUSH_USER_RESOURCES,GROUP_REPLICATION_ADMIN,GROUP_REPLICATION_STREAM,INNODB_REDO_LOG_ARCHIVE,INNODB_REDO_LOG_ENABLE,PASSWORDLESS_USER_ADMIN,PERSIST_RO_VARIABLES_ADMIN,REPLICATION_APPLIER,REPLICATION_SLAVE_ADMIN,RESOURCE_GROUP_ADMIN,RESOURCE_GROUP_USER,ROLE_ADMIN,SENSITIVE_VARIABLES_OBSERVER,SERVICE_CONNECTION_ADMIN,SESSION_VARIABLES_ADMIN,SET_USER_ID,SHOW_ROUTINE,SYSTEM_USER,SYSTEM_VARIABLES_ADMIN,TABLE_ENCRYPTION_ADMIN,TELEMETRY_LOG_ADMIN,XA_RECOVER_ADMIN ON *.* TO `root`@`localhost` WITH GRANT OPTION |</span><br><span class="line">| GRANT PROXY ON ``@`` TO `root`@`localhost` WITH GRANT OPTION                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |</span><br><span class="line">+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</span><br><span class="line">3 rows <span class="keyword">in</span> <span class="built_in">set</span> (0.00 sec)</span><br></pre></td></tr></table></figure><p>创建用户和授予权限的方法</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">CREATE USER <span class="string">'comm'</span>@<span class="string">'192.168.1.200'</span> IDENTIFIED BY <span class="string">'mypassword'</span>;</span><br><span class="line">GRANT ALL PRIVILEGES ON *.* TO <span class="string">'comm'</span>@<span class="string">'192.168.1.200'</span>;</span><br><span class="line">FLUSH PRIVILEGES;</span><br></pre></td></tr></table></figure><p>这样就创建了一个 <code>&#39;comm&#39;@&#39;192.168.1.200&#39;</code> 用户，并授予用户 comm 在 192.168.1.200 主机上对所有数据库和表的所有权限，如果允许用户将自己拥有的权限授予其他用户，可以加上 <code>WITH GRANT OPTION</code> 语句，完整命令如下：</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">GRANT ALL PRIVILEGES ON *.* TO <span class="string">'comm'</span>@<span class="string">'192.168.1.200'</span> WITH GRANT OPTION;</span><br></pre></td></tr></table></figure><p>回收权限的的命令如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">REVOKE ALL PRIVILEGES, GRANT OPTION FROM <span class="string">'comm'</span>@<span class="string">'192.168.1.200'</span>;</span><br><span class="line">FLUSH PRIVILEGES;</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li><code>CDK Error: Connection refused (generic:111)</code> 表示目标数据库拒绝连接，查询MySQL配置和权限分配是否正确</li><li>导出MySQL指定表结构 <code>mysqldump -u username -p --no-data source_database table_name &gt; table_structure.sql</code></li><li>查询MySQL用户 <code>SELECT DISTINCT CONCAT(&#39;User: &#39;&#39;&#39;,user,&#39;&#39;&#39;@&#39;&#39;&#39;,host,&#39;&#39;&#39;;&#39;) AS query FROM mysql.user;</code></li><li>查询指定用户的权限 <code>show grants for &#39;root&#39;@&#39;localhost&#39;;</code></li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/144537785" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>程序是很单纯很单纯的，哪里能有什么坏心思，如果不能按照预期的情况表现，99.99%的情况下是使用方法不对，程序单纯到只会按照指令形式，如果表现不好，一定是指令不好~</p><p>2024-12-20 19:50:50</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;游戏服务器一个配置存储服务，使用 &lt;code&gt;mysql-connector-c++ 8.0&lt;/code&gt; 连接 &lt;code&gt;MySQL 8.0&lt;/code&gt; 数据库，使用 &lt;code&gt;MySQL X Protocol&lt;/code&gt;， 它是 MySQL 8.0 引入的一种新的通信协议，它是为了支持现代应用程序开发需求而设计的。除了关系型数据存储外，MySQL X Protocol 可以方便地处理文档存储，允许存储和检索 JSON 等文档类型的数据，使其更适合现代的 NoSQL 风格的开发模式。该协议支持异步操作，提高了性能，允许更高效的并发处理，特别是在处理大量请求时。支持更丰富的数据操作，包括 CRUD 操作、事务处理、SQL 语句执行，并且可以处理复杂的查询，例如对文档存储的查询。&lt;/p&gt;
    
    </summary>
    
      <category term="MySQL" scheme="http://AlbertGithubHome.github.io/blog/categories/MySQL/"/>
    
    
      <category term="C++" scheme="http://AlbertGithubHome.github.io/blog/tags/C/"/>
    
      <category term="MySQL" scheme="http://AlbertGithubHome.github.io/blog/tags/MySQL/"/>
    
      <category term="User" scheme="http://AlbertGithubHome.github.io/blog/tags/User/"/>
    
      <category term="Grants" scheme="http://AlbertGithubHome.github.io/blog/tags/Grants/"/>
    
      <category term="Host" scheme="http://AlbertGithubHome.github.io/blog/tags/Host/"/>
    
  </entry>
  
  <entry>
    <title>Redis查询占用空间最大的10个key</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/12/17/Redis%E6%9F%A5%E8%AF%A2%E5%8D%A0%E7%94%A8%E7%A9%BA%E9%97%B4%E6%9C%80%E5%A4%A7%E7%9A%8410%E4%B8%AAkey/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/12/17/Redis查询占用空间最大的10个key/</id>
    <published>2024-12-17T15:45:00.000Z</published>
    <updated>2024-12-28T16:53:15.269Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近电脑性能告急，所有磁盘变红无空间，C盘更是剩下0字节，内存<code>90%+</code>，CPU占用<code>90%+</code>，使用spacesniffer查询哪些文件可以删除，优先被我干掉的是休眠功能自动生成的 hiberfil.sys 文件，然后这个内存分页文件 pagefile.sys 我没动，本来我的内存就不够用，删除了情况会更糟糕，然后映入眼帘是的 <code>dump.rdb</code> 文件，这个redis的持久化文件怎么这么大，足足有6个G，不仅占用了我6个G的C盘，还占用了我6个G的内存，不会是上次测试存储的大数据没删除吧，赶紧去瞅瞅。</p><h1 id="查找Redis大key"><a href="#查找Redis大key" class="headerlink" title="查找Redis大key"></a>查找Redis大key</h1><p>查找的思路很简单粗暴，利用redis的API，将所有的key的大小查找出来，然后从大到小排序，找出最大的10个key。</p><p>需要注意的是，我这是本地的测试环境，为了方便所以使用了 <code>keys</code> 这个令人闻风丧胆的命令来阻塞获取了所有的key，如果是在生产环境中可千万不要这么写，使用 <code>scan</code> 是一个很好的替代方案，查询代码如下：</p><figure class="highlight py"><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">#!/usr/bin/python3.6</span></span><br><span class="line"><span class="comment"># -*- coding: utf-8 -*-</span></span><br><span class="line"><span class="comment"># 查询redis中占用空间最大的10个key</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> redis</span><br><span class="line"></span><br><span class="line"><span class="comment"># game redis</span></span><br><span class="line">redis_pool = redis.ConnectionPool(host=<span class="string">'127.0.0.1'</span>, password=<span class="string">'myword'</span>, port=<span class="number">6379</span>)</span><br><span class="line">redis_server = redis.Redis(connection_pool=redis_pool)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取所有键</span></span><br><span class="line">keys = redis_server.keys(<span class="string">'*'</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取每个键的内存使用情况</span></span><br><span class="line">key_sizes = []</span><br><span class="line"><span class="keyword">for</span> key <span class="keyword">in</span> keys:</span><br><span class="line">    size = redis_server.memory_usage(key)</span><br><span class="line">    key_sizes.append((key, size))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 按内存使用大小排序，取前10个</span></span><br><span class="line">top_keys = sorted(key_sizes, key=<span class="keyword">lambda</span> x: x[<span class="number">1</span>], reverse=<span class="keyword">True</span>)[:<span class="number">10</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出结果</span></span><br><span class="line"><span class="keyword">for</span> key, size <span class="keyword">in</span> top_keys:</span><br><span class="line">    print(<span class="string">f"Key: <span class="subst">&#123;key&#125;</span>, Size: <span class="subst">&#123;size&#125;</span> bytes"</span>)</span><br><span class="line">    <span class="comment">#redis_server.delete(key)</span></span><br></pre></td></tr></table></figure><p>果然不出所料，内存占用前几名的key都是之前我做测试时使用的超大key，赶紧使用 <code>redis_server.delete(key)</code> 函数删除掉，因为当时测试使用了统一的AAA前缀，为了提高后续查找效率，我把获取key的代码改成了 <code>keys = redis_server.keys(&#39;AAA*&#39;)</code>，这样查找前10个key速度就快很多了</p><h1 id="使用Scan命令优化"><a href="#使用Scan命令优化" class="headerlink" title="使用Scan命令优化"></a>使用Scan命令优化</h1><p>前面提到 <code>keys</code> 命令不要在生产环境使用，如果要遍历key最好使用 <code>scan</code> 来代替，所以优化后的代码如下：</p><figure class="highlight py"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> redis</span><br><span class="line"></span><br><span class="line"><span class="comment"># 连接 Redis</span></span><br><span class="line">redis_pool = redis.ConnectionPool(host=<span class="string">'127.0.0.1'</span>, password=<span class="string">'myword'</span>, port=<span class="number">6379</span>)</span><br><span class="line">redis_server = redis.Redis(connection_pool=redis_pool)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 存储键和它们的内存使用情况</span></span><br><span class="line">key_sizes = []</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用 SCAN 命令遍历所有键</span></span><br><span class="line">cursor = <span class="number">0</span></span><br><span class="line"><span class="keyword">while</span> <span class="keyword">True</span>:</span><br><span class="line">    cursor, keys = redis_server.scan(cursor, count=<span class="number">1000</span>)  <span class="comment"># 每次扫描最多返回 1000 个键</span></span><br><span class="line">    <span class="keyword">for</span> key <span class="keyword">in</span> keys:</span><br><span class="line">        size = redis_server.memory_usage(key)</span><br><span class="line">        key_sizes.append((key, size))</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 如果游标返回为 0，说明遍历完所有键</span></span><br><span class="line">    <span class="keyword">if</span> cursor == <span class="number">0</span>:</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 按内存使用大小排序，取前10个</span></span><br><span class="line">top_keys = sorted(key_sizes, key=<span class="keyword">lambda</span> x: x[<span class="number">1</span>], reverse=<span class="keyword">True</span>)[:<span class="number">10</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出结果</span></span><br><span class="line"><span class="keyword">for</span> key, size <span class="keyword">in</span> top_keys:</span><br><span class="line">    print(<span class="string">f"Key: <span class="subst">&#123;key&#125;</span>, Size: <span class="subst">&#123;size&#125;</span> bytes"</span>)</span><br><span class="line">    <span class="comment"># 如果需要删除键，可以在这里调用 redis_server.delete(key)</span></span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>Windows电脑磁盘不足时，如果不使用休眠功能，可以把C盘根目录的 hiberfil.sys 文件删掉</li><li>获取所有可以的命令是 <code>keys</code>，但这是个阻塞命令，不要轻易在生产环境下使用，必要时使用 <code>scan</code> 命令代替</li><li>使用 <code>redis_server.memory_usage(key)</code> 命令可以获取key占用内存大小，删除key使用 <code>redis_server.delete(key)</code></li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/144471100" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>体系决定交易盈亏，规则决定交易生死，格局决定交易高度。真正的亏钱，源于贪婪，常常出现在看到别人盈利而担心自己赚不到这一波的时候~</p><p>如果一直谨小慎微并不会亏多少，而当人因为一些外部原因获取超额盈利却把它作为自身能力时，也就逐步拉开了亏损的大幕</p><p>2024-12-17 15:45:32</p></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;最近电脑性能告急，所有磁盘变红无空间，C盘更是剩下0字节，内存&lt;code&gt;90%+&lt;/code&gt;，CPU占用&lt;code&gt;90%+&lt;/code
      
    
    </summary>
    
      <category term="Redis" scheme="http://AlbertGithubHome.github.io/blog/categories/Redis/"/>
    
    
      <category term="redis" scheme="http://AlbertGithubHome.github.io/blog/tags/redis/"/>
    
      <category term="Windows" scheme="http://AlbertGithubHome.github.io/blog/tags/Windows/"/>
    
      <category term="hiberfil.sys" scheme="http://AlbertGithubHome.github.io/blog/tags/hiberfil-sys/"/>
    
      <category term="keys" scheme="http://AlbertGithubHome.github.io/blog/tags/keys/"/>
    
      <category term="scan" scheme="http://AlbertGithubHome.github.io/blog/tags/scan/"/>
    
  </entry>
  
  <entry>
    <title>linux内存相关命令的尝试</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/12/14/linux%E5%86%85%E5%AD%98%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4%E7%9A%84%E5%B0%9D%E8%AF%95/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/12/14/linux内存相关命令的尝试/</id>
    <published>2024-12-14T15:00:00.000Z</published>
    <updated>2024-12-28T16:51:28.413Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>菜就多练，昨天看了一篇有关剖析 RocksDB 内存超限问题的文章，里面用了多种内存分析工具，看到那些貌似熟悉的名字，却一时也想不起怎么来用，当然像 free 和 top 这种命令还是比较熟的，但是像 vmstat 和 pidstat 这些就比较模糊了，翻了翻之前的总结也简单的描述过使用方法，但是对于这种已经忘记的实时也是不可回避的，菜就多练，今天就再练一练。</p><a id="more"></a><p>俗话说熟能生巧，有时候见的多了自然而然的就能发现规律了，比如 <code>stat</code> 这个词可是Linux命令中常见的面孔，它是单词 <code>statistics</code> 的缩写，也就统计的意思，所以就会出现类似 <code>iostat</code>、<code>vmstat</code>、<code>netstat</code>、<code>pidstat</code>、<code>mpstat</code> 等命令，相似的还有各种 ps 和 各种 top 相关的命令，今天此文因内存而起，我们就先看看内存相关的 <code>free</code>、<code>vmstat</code>、<code>pidstat</code>、<code>sar</code> 这几个命令。</p><h1 id="free"><a href="#free" class="headerlink" title="free"></a>free</h1><p>free 命令是我最常用的内存命令，通常我使用时会加上 <code>-h</code> 参数更易读一些，命令虽简单，但是想完全理解却不易，并且不同的系统对于各列的参数实现还不完全相同，而我作为实用派，不愿想个学究一样去深挖各个参数的含义，还是以实用主义为主，明白各参数的基本含义即可</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></pre></td><td class="code"><pre><span class="line">$ free -h</span><br><span class="line">              total        used        free      shared  buff/cache   available</span><br><span class="line">Mem:          7.3Gi       5.8Gi       377Mi        40Mi       1.1Gi       1.2Gi</span><br><span class="line">Swap:         9.0Gi       5.2Gi       3.7Gi</span><br></pre></td></tr></table></figure><p><code>free -h</code> 的输出显示了系统内存和交换空间的使用情况，在我看来total≈used+available，buff/cache的空间可能一部分属于used，又有一部分属于available，free应该总是小于available的，大体就是这些规则，精确的公式我是给不出的，对于常用的判断够用了：</p><hr><h2 id="Mem-部分的解释"><a href="#Mem-部分的解释" class="headerlink" title="Mem 部分的解释"></a>Mem 部分的解释</h2><ul><li><strong>total</strong>: 总的物理内存容量（7.3Gi）。</li><li><strong>used</strong>: 已被使用的物理内存，包括应用程序和系统缓存（5.8Gi）。</li><li><strong>free</strong>: 尚未分配的空闲物理内存（377Mi）。</li><li><strong>shared</strong>: 共享内存，主要用于 <code>tmpfs</code> 和其他共享内存用途（40Mi）。</li><li><strong>buff/cache</strong>: 用于缓冲区（buffer）和缓存（cache）的内存（1.1Gi）。<ul><li>缓冲区用于临时存储写入磁盘的数据。</li><li>缓存用于存储从磁盘读取的数据，以便更快地访问。</li></ul></li><li><strong>available</strong>: 可供应用程序使用的内存（1.2Gi）。<ul><li>这个值是对系统内存的综合估算，它考虑了缓存中可以释放的部分。</li></ul></li></ul><hr><h2 id="Swap-部分的解释"><a href="#Swap-部分的解释" class="headerlink" title="Swap 部分的解释"></a>Swap 部分的解释</h2><ul><li><strong>total</strong>: 系统的交换空间总大小（9.0Gi）。</li><li><strong>used</strong>: 已被使用的交换空间（5.2Gi）。</li><li><strong>free</strong>: 尚未使用的交换空间（3.7Gi）。</li></ul><h1 id="vmstat"><a href="#vmstat" class="headerlink" title="vmstat"></a>vmstat</h1><p>vmstat 是 Linux 和类 Unix 系统上的一个命令，用于实时监控系统性能，包括 CPU、内存、分页、磁盘 I/O 和上下文切换等信息。它非常适合用于分析系统瓶颈或性能问题。</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></pre></td><td class="code"><pre><span class="line">$ vmstat 2 5 -S M</span><br><span class="line">procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----</span><br><span class="line"> r  b   swpd   free   buff  cache   si   so    bi    bo   <span class="keyword">in</span>   cs us sy id wa st</span><br><span class="line"> 0  0   5351    526    152   1062    0    0    11    21    2    2  3  1 96  0  0</span><br><span class="line"> 0  0   5351    526    152   1062    0    0     0     0 14509 35013  2  1 97  0  0</span><br><span class="line"> 0  0   5351    525    152   1063    0    0     0    86 14671 35064  2  1 97  0  0</span><br><span class="line"> 0  0   5351    525    152   1062    0    0     0    68 14692 35164  2  1 97  0  0</span><br><span class="line"> 2  0   5351    525    152   1063    0    0     0   122 14504 35142  2  1 96  0  0</span><br></pre></td></tr></table></figure><p>这个命令表示每2秒刷新一次，共刷新显示5次，单位用M，接下来以第一行数字为例，解释数据含义：</p><hr><h2 id="Procs-进程"><a href="#Procs-进程" class="headerlink" title="Procs (进程)"></a>Procs (进程)</h2><ul><li><strong><code>r</code></strong>: 正在运行队列中等待 CPU 的进程数。<code>0</code> 表示当前没有需要 CPU 的进程在排队。</li><li><strong><code>b</code></strong>: 处于等待 I/O 的阻塞进程数。<code>0</code> 表示没有进程因等待 I/O 阻塞。</li></ul><h2 id="Memory-内存"><a href="#Memory-内存" class="headerlink" title="Memory (内存)"></a>Memory (内存)</h2><ul><li><strong><code>swpd</code></strong>: 使用的交换空间大小（单位：MB）。<code>5351</code> 表示已使用 5351 MB 的交换空间。</li><li><strong><code>free</code></strong>: 空闲内存大小（单位：MB）。<code>526</code> 表示当前系统有 526 MB 的空闲内存。</li><li><strong><code>buff</code></strong>: 用于缓冲区的内存大小（单位：MB）。<code>152</code> 表示 152 MB 的内存被用作缓冲区。</li><li><strong><code>cache</code></strong>: 用于缓存的内存大小（单位：MB）。<code>1062</code> 表示 1062 MB 的内存被用作文件缓存。</li></ul><h2 id="Swap-交换"><a href="#Swap-交换" class="headerlink" title="Swap (交换)"></a>Swap (交换)</h2><ul><li><strong><code>si</code></strong>: 从磁盘交换到内存的数据速率（单位：MB/s）。<code>0</code> 表示当前没有从交换分区加载到内存的数据。</li><li><strong><code>so</code></strong>: 从内存交换到磁盘的数据速率（单位：MB/s）。<code>0</code> 表示当前没有将内存数据写入交换分区。</li></ul><h2 id="IO-磁盘-I-O"><a href="#IO-磁盘-I-O" class="headerlink" title="IO (磁盘 I/O)"></a>IO (磁盘 I/O)</h2><ul><li><strong><code>bi</code></strong>: 每秒从块设备（磁盘等）读取的数据量（单位：块）。<code>11</code> 表示每秒读取了 11 块数据。</li><li><strong><code>bo</code></strong>: 每秒写入块设备的数据量（单位：块）。<code>21</code> 表示每秒写入了 21 块数据。</li></ul><h2 id="System-系统"><a href="#System-系统" class="headerlink" title="System (系统)"></a>System (系统)</h2><ul><li><strong><code>in</code></strong>: 每秒中断数（硬件和软件中断）。 <code>2</code> 表示每秒发生了 2 次中断。</li><li><strong><code>cs</code></strong>: 每秒上下文切换次数。 <code>2</code> 表示每秒进行了 2 次上下文切换。</li></ul><h2 id="CPU-处理器"><a href="#CPU-处理器" class="headerlink" title="CPU (处理器)"></a>CPU (处理器)</h2><ul><li><strong><code>us</code></strong>: 用户态进程占用 CPU 时间的百分比。 <code>3</code> 表示 CPU 时间的 3% 用于运行用户进程。</li><li><strong><code>sy</code></strong>: 内核态进程占用 CPU 时间的百分比。 <code>1</code> 表示 CPU 时间的 1% 用于运行内核进程。</li><li><strong><code>id</code></strong>: CPU 空闲时间的百分比。 <code>96</code> 表示 CPU 时间的 96% 是空闲的。</li><li><strong><code>wa</code></strong>: CPU 等待 I/O 完成的时间百分比。 <code>0</code> 表示 CPU 没有时间在等待 I/O 操作完成。</li><li><strong><code>st</code></strong>: 被虚拟机（如 KVM）偷走的时间百分比。 <code>0</code> 表示没有 CPU 时间被虚拟机消耗。</li></ul><p>这个命令可以用来监控系统性能瓶颈：</p><ul><li>如果 r 的值过高，说明 CPU 短缺。</li><li>如果 si 和 so 的值大，说明系统内存不足，频繁使用交换空间。</li><li>如果 wa 的值高，说明 I/O 操作是瓶颈。</li></ul><h1 id="pidstat"><a href="#pidstat" class="headerlink" title="pidstat"></a>pidstat</h1><p>pidstat 是 Linux 系统中 sysstat 工具集的一部分，用于监控和报告单个进程的性能指标。它可以提供有关 CPU、内存、I/O、线程等的详细信息，帮助分析系统性能瓶颈。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$ pidstat -p 242285 -r 2 5</span><br><span class="line">Linux 5.4.0-200-generic (ThinkPad-X390)     2024年12月14日  _x86_64_        (8 CPU)</span><br><span class="line"></span><br><span class="line">15时10分47秒   UID       PID  minflt/s  majflt/s     VSZ     RSS   %MEM  Command</span><br><span class="line">15时10分49秒  1000    242285      0.00      0.00  302184   79776   1.05  Game10010601</span><br><span class="line">15时10分51秒  1000    242285      0.00      0.00  302184   79776   1.05  Game10010601</span><br><span class="line">15时10分53秒  1000    242285      0.00      0.00  302184   79776   1.05  Game10010601</span><br><span class="line">15时10分55秒  1000    242285      0.00      0.00  302184   79776   1.05  Game10010601</span><br><span class="line">15时10分57秒  1000    242285      0.00      0.00  302184   79776   1.05  Game10010601</span><br><span class="line">Average:     1000    242285      0.00      0.00  302184   79776   1.05  Game10010601</span><br></pre></td></tr></table></figure><p>这个命令监控242285这个进程内存情况，每2秒刷新一次，共刷新显示5次，单位用M，接下来以第一行数字为例，解释数据含义：</p><h2 id="标题行解释"><a href="#标题行解释" class="headerlink" title="标题行解释"></a>标题行解释</h2><ul><li><strong><code>Linux 5.4.0-200-generic</code></strong>：运行的内核版本。</li><li><strong><code>ThinkPad-X390</code></strong>：主机名。</li><li><strong><code>2024年12月14日</code></strong>：当前日期。</li><li><strong><code>_x86_64_</code></strong>：处理器架构。</li><li><strong><code>(8 CPU)</code></strong>：系统中有 8 个 CPU 核心。</li></ul><h2 id="数据列解释"><a href="#数据列解释" class="headerlink" title="数据列解释"></a>数据列解释</h2><ul><li><strong><code>时间</code></strong>：采样的时间戳。</li><li><strong><code>UID</code></strong>：用户 ID，显示运行进程的用户（<code>1000</code> 表示非 root 用户）。</li><li><strong><code>PID</code></strong>：进程 ID，这里是 <code>242285</code>。</li><li><strong><code>minflt/s</code></strong>：每秒次要缺页错误数（<strong>无需从磁盘加载内存页</strong>）。</li><li><strong><code>majflt/s</code></strong>：每秒主要缺页错误数（<strong>需要从磁盘加载内存页</strong>）。</li><li><strong><code>VSZ</code></strong>：虚拟内存大小（以 KB 为单位）。这里是 <code>302184 KB</code>，即约 <code>302 MB</code>。</li><li><strong><code>RSS</code></strong>：驻留内存大小（以 KB 为单位），即实际使用的物理内存。这里是 <code>79776 KB</code>，即约 <code>78 MB</code>。</li><li><strong><code>%MEM</code></strong>：进程使用的物理内存百分比。这里是 <code>1.05%</code>。</li><li><strong><code>Command</code></strong>：进程的名称或命令，这里是 <code>Game10010601</code>。</li></ul><p>这些行展示了每次采样的统计数据。这里看到该进程的 <code>minflt/s</code> 和 <code>majflt/s</code> 均为 <code>0.00</code>，表示没有发生缺页错误，内存访问性能良好。虚拟内存大小 (<code>VSZ</code>) 和驻留内存大小 (<code>RSS</code>) 都是恒定的，说明该进程的内存使用稳定。</p><h1 id="sar"><a href="#sar" class="headerlink" title="sar"></a>sar</h1><p>sar（System Activity Report）命令是一个用于收集、报告和保存系统活动信息的工具，通常在 Linux 和 Unix 系统中使用。它是 sysstat 软件包的一部分，提供了对系统性能的详细监控和分析。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$ sar -r 2 5</span><br><span class="line">Linux 5.4.0-200-generic (ThinkPad-X390)     2024年12月14日  _x86_64_        (8 CPU)</span><br><span class="line"></span><br><span class="line">15时26分06秒 kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty</span><br><span class="line">15时26分08秒    455724   1363580   5547036     72.86    160416    859272  23892108    140.53   4759984   1552228       428</span><br><span class="line">15时26分10秒    455684   1363544   5547052     72.86    160416    859280  23886860    140.50   4760760   1552232       436</span><br><span class="line">15时26分12秒    455456   1363320   5547276     72.87    160416    859284  23890572    140.53   4760420   1552236       476</span><br><span class="line">15时26分14秒    455456   1363324   5547272     72.87    160416    859288  23890572    140.53   4760476   1552240       480</span><br><span class="line">15时26分16秒    454700   1362572   5548028     72.88    160420    859284  23873744    140.43   4762228   1552244       480</span><br><span class="line">Average:       455404   1363268   5547333     72.87    160417    859282  23886771    140.50   4760774   1552236       460</span><br></pre></td></tr></table></figure><p>这个命令表示每2秒刷新一次内存信息，共刷新显示5次，接下来以第一行数字为例，解释数据含义：</p><h2 id="字段含义解释"><a href="#字段含义解释" class="headerlink" title="字段含义解释"></a>字段含义解释</h2><ul><li><strong>时间戳</strong>（如 <code>15时26分08秒</code>）：表示采集数据的时间。</li><li><strong><code>kbmemfree</code></strong>：可用内存（以 KB 为单位）。表示系统中尚未被使用的内存量。</li><li><strong><code>kbavail</code></strong>：可用内存（即使在缓存和缓冲区中也可用的内存，以 KB 为单位）。这个值通常比 <code>kbmemfree</code> 更准确，因为它考虑了系统可以回收的缓存和缓冲区内存。</li><li><strong><code>kbmemused</code></strong>：已使用内存（以 KB 为单位）。计算公式为：<code>总内存 - 可用内存</code>。</li><li><strong><code>%memused</code></strong>：已使用内存的百分比。计算公式为：<code>(kbmemused / 总内存) * 100</code>。</li><li><strong><code>kbbuffers</code></strong>：用于文件系统缓存的内存（以 KB 为单位）。这是 Linux 内核用于存储未写入硬盘的数据的缓存。</li><li><strong><code>kbcached</code></strong>：用于缓存的内存（以 KB 为单位）。这些是存储在内存中的文件数据，Linux 会尽量使用这部分内存来加快读取速度。</li><li><strong><code>kbcommit</code></strong>：提交内存（以 KB 为单位）。表示可以分配给进程的内存总量，达到该值时会导致内存不足。</li><li><strong><code>%commit</code></strong>：提交内存的百分比。表示已提交内存与可用内存的比例。</li><li><strong><code>kbactive</code></strong>：活动内存（以 KB 为单位）。表示最近被访问的内存页。</li><li><strong><code>kbinact</code></strong>：非活动内存（以 KB 为单位）。表示不再被访问的内存页。</li><li><strong><code>kbdirty</code></strong>：脏页内存（以 KB 为单位）。表示已被修改但尚未写入硬盘的内存。</li></ul><h2 id="示例分析"><a href="#示例分析" class="headerlink" title="示例分析"></a>示例分析</h2><ul><li><code>kbmemfree</code> 在 455724 KB 左右，表示系统还有相当数量的可用内存，说实话也不算多了，才400多M。</li><li><code>kbmemused</code> 的值为 5547036 KB，表示大约 5.5 GB 的内存已被使用，且 <code>%memused</code> 为 72.86%，表明内存使用率较高。</li><li><code>kbbuffers</code> 和 <code>kbcached</code> 的值相对较小，这可能表示系统正在利用可用内存缓存数据，从而优化性能。</li><li><code>kbcommit</code> 和 <code>%commit</code> 的值显示系统的内存承诺也在一个相对健康的范围。</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li><code>free -h</code> 最常用的内存查看工具，可以方便快捷知晓内存总体情况，查看总量与可用内存值</li><li><code>vmstat 2 5 -S M</code> 可用于监控内存，分析系统瓶颈或性能问题，r 的值过高说明 CPU 短缺。si 和 so 的值大，说明系统内存不足，频繁使用交换空间</li><li><code>pidstat -p 242285 -r 2 5</code> 可提供单个进程的内存使用信息，分析缺页情况</li><li><code>sar -r 2 5</code> 可分析内存总体情况，查看活动内存、非活动内存、脏页内存等信息</li><li>因统计口径的不同，可能会遇到两个命令查出来的值不同，这些不必强求，熟悉各个命令的特点，遇到问题多方比较就可以了</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/144139353" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>我熬的不是夜，而是短暂的自由。因为我知道，一旦睡着了，再次醒来时我便不再属于我自已~</p><p>2024-12-14 16:04:22</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;菜就多练，昨天看了一篇有关剖析 RocksDB 内存超限问题的文章，里面用了多种内存分析工具，看到那些貌似熟悉的名字，却一时也想不起怎么来用，当然像 free 和 top 这种命令还是比较熟的，但是像 vmstat 和 pidstat 这些就比较模糊了，翻了翻之前的总结也简单的描述过使用方法，但是对于这种已经忘记的实时也是不可回避的，菜就多练，今天就再练一练。&lt;/p&gt;
    
    </summary>
    
      <category term="linux" scheme="http://AlbertGithubHome.github.io/blog/categories/linux/"/>
    
    
      <category term="linux" scheme="http://AlbertGithubHome.github.io/blog/tags/linux/"/>
    
      <category term="运维" scheme="http://AlbertGithubHome.github.io/blog/tags/%E8%BF%90%E7%BB%B4/"/>
    
      <category term="服务器" scheme="http://AlbertGithubHome.github.io/blog/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
      <category term="free" scheme="http://AlbertGithubHome.github.io/blog/tags/free/"/>
    
      <category term="内存查询" scheme="http://AlbertGithubHome.github.io/blog/tags/%E5%86%85%E5%AD%98%E6%9F%A5%E8%AF%A2/"/>
    
  </entry>
  
  <entry>
    <title>日常应用开发遇到的小问题二三则</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/11/29/%E6%97%A5%E5%B8%B8%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E9%81%87%E5%88%B0%E7%9A%84%E5%B0%8F%E9%97%AE%E9%A2%98%E4%BA%8C%E4%B8%89%E5%88%99/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/11/29/日常应用开发遇到的小问题二三则/</id>
    <published>2024-11-29T12:21:48.000Z</published>
    <updated>2024-12-28T16:49:42.494Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>这两天的工作又相对杂乱一些，处理一下A事情，又要搞一搞B事情，需要盯着C事情，还要尝试一下D事情，所以这篇总结没有什么主线，主要目的是记录一下最近解决的问题，先不展开讨论，当类似的问题积累一些再展开描述，我就先记录一下流水账了。</p><a id="more"></a><h1 id="Redis问题"><a href="#Redis问题" class="headerlink" title="Redis问题"></a>Redis问题</h1><p>本来是一个常规清理数据，<a href="https://blog.csdn.net/albertsh/article/details/143218353" target="_blank" rel="noopener">Redis回收内存碎片</a>的操作，但是因redis-server版本问题被迫切换解决方案</p><h2 id="启用碎片自动回收失败"><a href="#启用碎片自动回收失败" class="headerlink" title="启用碎片自动回收失败"></a>启用碎片自动回收失败</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; config <span class="built_in">set</span> activedefrag yes</span><br><span class="line">(error) ERR Active defragmentation cannot be enabled: it requires a Redis server compiled with</span><br><span class="line">a modified Jemalloc like the one shipped by default with the Redis <span class="built_in">source</span> distribution</span><br></pre></td></tr></table></figure><p>开启主动碎片整理机制失败，提示错误大概意思是说，当前的 Redis 服务器未使用支持<strong>主动碎片整理（Active Defragmentation）</strong>的 Jemalloc 分配器，因为Redis 的主动碎片整理功能依赖于 Jemalloc 分配器，如果 Redis 是使用其他分配器（如 libc malloc）编译的，或者当前的 Jemalloc 缺乏必要的功能，就会导致该功能无法启用。</p><p>可以通过Redis命令 <code>redis-cli INFO memory | grep allocator</code> 来查询，如果显示 <code>mem_allocator:jemalloc</code>，则表示启用了 Jemalloc。如果显示 <code>mem_allocator:libc</code>，则说明当前未使用 Jemalloc，需要重新安装或编译 Redis。</p><p>我运行一看当然是 <code>mem_allocator:libc</code> 了，看来没办法进行碎片整理了，幸好是个slave节点，所以我干脆保存数据后重启一下吧。</p><h2 id="启动Redis未脱离终端"><a href="#启动Redis未脱离终端" class="headerlink" title="启动Redis未脱离终端"></a>启动Redis未脱离终端</h2><p>首先关闭redis服务</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">shutdown SAVE</span><br></pre></td></tr></table></figure><p>这个过程会进行同步存储，所以会给你一种卡死了的状态，因为我这里有90G数据，存储大概用了10多分钟，Redis服务成功关闭</p><p>然后按配置文件启动Redis</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">/usr/<span class="built_in">local</span>/bin/redis-server ./redis.conf</span><br></pre></td></tr></table></figure><p>启动完终端就停在这个了，难道我起了个前台程序？我记得之前都是这么启动的啊，总不能为了脱离终端我还要使用 <code>nohup</code> 和 <code>&amp;</code> 来配合吧，有点low啊，主要是日志是不是就放到nohup.out文件里了，找找有没有配置吧，打开redis.conf文件发现<code>daemonize no</code>，我的天啊，这个redis-server为什么这么与众不同，守护进程模式居然是关着的，虽然说把配置文件改成 <code>daemonize yes</code> 再启动就好了，但是我好奇的是之前是怎么启动的。</p><p>我决定不修改配置文件了，保持原来的样子，直接在启动时指定 <code>--daemonize yes</code> 好了，这样可以达到目的，在Linux系统中使用Redis时，命令行参数和配置文件参数（也称为Redis配置）具有不同的优先级。如果同一个配置选项在配置文件和命令行参数中被设置，那么命令行参数将覆盖配置文件中的设置。</p><h1 id="Vercel问题"><a href="#Vercel问题" class="headerlink" title="Vercel问题"></a>Vercel问题</h1><p>之前简单聊过将Nextjs框架编写的网站部署到Vecel有天然的适应性，因为Vercel背后的团队就是开发出Nextjs框架的那群人，部署到Vercel以后绑定一个域名就可以使用了，不需要自己安装运行环境，不需要配置DN缓存，不需要配置SSL证书，真是方便极了，但免费的账户是有资源限制的，特别是要团队开发的话需要购买付费版本。</p><h2 id="未在Vecel团队的人提交无法触发自动部署"><a href="#未在Vecel团队的人提交无法触发自动部署" class="headerlink" title="未在Vecel团队的人提交无法触发自动部署"></a>未在Vecel团队的人提交无法触发自动部署</h2><p>用过Vercel就会发现，当通过github导入项目之后，以后每次推送到github后都能自动触发Vercel的网站发布功能，但是未添加到Vercel团队的账号提交时无法触发自动部署，提示</p><blockquote><p>Vercel - No GitHub account was found matching the commit author email address</p></blockquote><p>一种办法就是把所有可能提交的账号都添加到Vercel团队，但是这是要花钱的，每添加一个成员每月$20，还有就是找已经在团队的成员再提交一次，触发自动部署就行了，这个成本也很低，也就是仓库记录不那么干净了而已</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">git commit --allow-empty -m<span class="string">"trigger redeployment"</span></span><br></pre></td></tr></table></figure><h2 id="更新package-json后部署Vercel时报错"><a href="#更新package-json后部署Vercel时报错" class="headerlink" title="更新package.json后部署Vercel时报错"></a>更新package.json后部署Vercel时报错</h2><p>因为我使用 <code>npm install</code> 初始化完项目后，启动时有两个警告</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">Module not found: Can&apos;t resolve &apos;bufferutil&apos; in &apos;E:\WorkSpace\nodeweb\qxweb\node_modules\ws\lib&apos;</span><br><span class="line"></span><br><span class="line">Import trace for requested module:</span><br><span class="line">./node_modules/ws/lib/buffer-util.js</span><br><span class="line">./node_modules/ws/lib/websocket.js</span><br><span class="line">./node_modules/ws/index.js</span><br><span class="line">./node_modules/@supabase/realtime-js/dist/main/RealtimeClient.js</span><br><span class="line">./node_modules/@supabase/realtime-js/dist/main/index.js</span><br><span class="line">./node_modules/@supabase/supabase-js/dist/main/index.js</span><br><span class="line">./store/SupabaseStore.tsx</span><br><span class="line"></span><br><span class="line">./node_modules/ws/lib/validation.js</span><br><span class="line">Module not found: Can&apos;t resolve &apos;utf-8-validate&apos; in &apos;E:\WorkSpace\nodeweb\qxweb\node_modules\ws\lib&apos;</span><br><span class="line"></span><br><span class="line">Import trace for requested module:</span><br><span class="line">./node_modules/ws/lib/validation.js</span><br><span class="line">./node_modules/ws/lib/websocket.js</span><br><span class="line">./node_modules/ws/index.js</span><br><span class="line">./node_modules/@supabase/realtime-js/dist/main/RealtimeClient.js</span><br><span class="line">./node_modules/@supabase/realtime-js/dist/main/index.js</span><br><span class="line">./node_modules/@supabase/supabase-js/dist/main/index.js</span><br><span class="line">./store/SupabaseStore.tsx</span><br></pre></td></tr></table></figure><p>所以我就手动安装了这两个库 <code>npm isntall bufferutil utf-8-validate</code>，这就导致我的 <code>package.json</code> 文件更新内容里增加了这两个库的引用版本，提交代码部署Vercel时报错</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Running build <span class="keyword">in</span> Washington, D.C., USA (East) – iad1</span><br><span class="line">Cloning github.com/2338-AI/quantum-solutions-web (Branch: main, Commit: 96249d1)</span><br><span class="line">Previous build cache not available</span><br><span class="line">Cloning completed: 2.607s</span><br><span class="line">Running <span class="string">"vercel build"</span></span><br><span class="line">Vercel CLI 39.1.1</span><br><span class="line">Detected `pnpm-lock.yaml` version 9 generated by pnpm@9.x</span><br><span class="line">Installing dependencies...</span><br><span class="line"> ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with <span class="string">"frozen-lockfile"</span> because pnpm-lock.yaml is not up to date with &lt;ROOT&gt;/package.json</span><br><span class="line">Note that <span class="keyword">in</span> CI environments this setting is <span class="literal">true</span> by default. If you still need to run install <span class="keyword">in</span> such cases, use <span class="string">"pnpm install --no-frozen-lockfile"</span></span><br><span class="line">    Failure reason:</span><br><span class="line">    specifiers <span class="keyword">in</span> the lockfile (&#123;<span class="string">"@douyinfe/semi-ui"</span>:<span class="string">"^2.44.0"</span>,<span class="string">"@react-spring/web"</span>:<span class="string">"^9.7.5"</span>,<span class="string">"@supabase/supabase-js"</span>:<span class="string">"^2.38.1"</span>,<span class="string">"accept-language"</span>:<span class="string">"^3.0.18"</span>,<span class="string">"classes-names"</span>:<span class="string">"^1.0.0"</span>,<span class="string">"emailjs-com"</span>:<span class="string">"^3.2.0"</span>,<span class="string">"i18next"</span>:<span class="string">"^23.5.1"</span>,<span class="string">"i18next-browser-languagedetector"</span>:<span class="string">"^7.1.0"</span>,<span class="string">"i18next-resources-to-backend"</span>:<span class="string">"^1.1.4"</span>,<span class="string">"is-mobile"</span>:<span class="string">"^5.0.0"</span>,<span class="string">"next"</span>:<span class="string">"13.5.4"</span>,<span class="string">"react"</span>:<span class="string">"^18"</span>,<span class="string">"react-dom"</span>:<span class="string">"^18"</span>,<span class="string">"react-google-recaptcha"</span>:<span class="string">"^3.1.0"</span>,<span class="string">"react-i18next"</span>:<span class="string">"^13.3.0"</span>,<span class="string">"swiper"</span>:<span class="string">"^11.1.14"</span>,<span class="string">"tailwind-merge"</span>:<span class="string">"^2.5.4"</span>,<span class="string">"@types/node"</span>:<span class="string">"^20"</span>,<span class="string">"@types/react"</span>:<span class="string">"^18"</span>,<span class="string">"@types/react-dom"</span>:<span class="string">"^18"</span>,<span class="string">"@types/react-google-recaptcha"</span>:<span class="string">"^2.1.7"</span>,<span class="string">"@typescript-eslint/eslint-plugin"</span>:<span class="string">"^6.3.0"</span>,<span class="string">"@typescript-eslint/parser"</span>:<span class="string">"^6.3.0"</span>,<span class="string">"autoprefixer"</span>:<span class="string">"^10"</span>,<span class="string">"eslint"</span>:<span class="string">"^8"</span>,<span class="string">"eslint-config-airbnb"</span>:<span class="string">"^19.0.4"</span>,<span class="string">"eslint-config-airbnb-typescript"</span>:<span class="string">"^17.1.0"</span>,<span class="string">"eslint-config-next"</span>:<span class="string">"13.5.4"</span>,<span class="string">"postcss"</span>:<span class="string">"^8"</span>,<span class="string">"postcss-pxtorem"</span>:<span class="string">"^6.1.0"</span>,<span class="string">"prettier"</span>:<span class="string">"^3.3.3"</span>,<span class="string">"prettier-plugin-tailwindcss"</span>:<span class="string">"^0.6.8"</span>,<span class="string">"tailwindcss"</span>:<span class="string">"^3"</span>,<span class="string">"typescript"</span>:<span class="string">"^5"</span>&#125;) don<span class="string">'t match specs in package.json (&#123;"@types/node":"^20","@types/react":"^18","@types/react-dom":"^18","@types/react-google-recaptcha":"^2.1.7","@typescript-eslint/eslint-plugin":"^6.3.0","@typescript-eslint/parser":"^6.3.0","autoprefixer":"^10","eslint":"^8","eslint-config-airbnb":"^19.0.4","eslint-config-airbnb-typescript":"^17.1.0","eslint-config-next":"13.5.4","postcss":"^8","postcss-pxtorem":"^6.1.0","prettier":"^3.3.3","prettier-plugin-tailwindcss":"^0.6.8","tailwindcss":"^3","typescript":"^5","@douyinfe/semi-ui":"^2.44.0","@react-spring/web":"^9.7.5","@supabase/supabase-js":"^2.38.1","accept-language":"^3.0.18","bufferutil":"^4.0.8","classes-names":"^1.0.0","emailjs-com":"^3.2.0","i18next":"^23.5.1","i18next-browser-languagedetector":"^7.1.0","i18next-resources-to-backend":"^1.1.4","is-mobile":"^5.0.0","next":"13.5.4","react":"^18","react-dom":"^18","react-google-recaptcha":"^3.1.0","react-i18next":"^13.3.0","swiper":"^11.1.14","tailwind-merge":"^2.5.4","utf-8-validate":"^6.0.5"&#125;)</span></span><br><span class="line"><span class="string">Error: Command "pnpm install" exited with 1</span></span><br></pre></td></tr></table></figure><p>这个错误提示说明 Vercel 在部署过程中使用 <code>pnpm</code> 安装依赖时，遇到了 <code>pnpm-lock.yaml</code> 和 <code>package.json</code> 文件中的依赖不匹配问题。具体来说，<code>pnpm-lock.yaml</code> 文件中的依赖版本与 <code>package.json</code> 中声明的版本范围不一致，导致 <code>pnpm</code> 无法继续安装。</p><p>看到这时我才意识到我的项目里有个 <code>pnpm-lock.yaml</code>，而我使用的 <code>npm</code> 安装，本地有个 <code>package-lock.json</code> 文件，这样一看是我工具用错了呀，我说这个项目怎么没提交 <code>package-lock.json</code> 文件呢</p><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">rm -rf .node_modules package-lock.json</span><br><span class="line">pnpm install</span><br></pre></td></tr></table></figure><p>输出如下</p><figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">PS E:\WorkSpace\nodeweb\qxweb&gt; pnpm install</span><br><span class="line"></span><br><span class="line">   ╭──────────────────────────────────────────────────────────────────╮</span><br><span class="line">   │                                                                  │</span><br><span class="line">   │                Update available! 9.5.0 → 9.14.2.                 │</span><br><span class="line">   │   Changelog: https://github.com/pnpm/pnpm/releases/tag/v9.14.2   │</span><br><span class="line">   │                Run &quot;pnpm add -g pnpm&quot; to update.                 │</span><br><span class="line">   │                                                                  │</span><br><span class="line">   │         Follow @pnpmjs for updates: https://x.com/pnpmjs         │</span><br><span class="line">   │                                                                  │</span><br><span class="line">   ╰──────────────────────────────────────────────────────────────────╯</span><br><span class="line"></span><br><span class="line">Lockfile is up to date, resolution step is skipped</span><br><span class="line">Packages: +423</span><br><span class="line">++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++</span><br><span class="line"> WARN  GET https://registry.npmmirror.com/next/-/next-13.5.4.tgz error (ECONNRESET). Will retry in 10 seconds. 2 retries left.</span><br><span class="line">Downloading @next/swc-win32-x64-msvc@13.5.4: 36.94 MB/36.94 MB, done</span><br><span class="line">Downloading next@13.5.4: 16.80 MB/16.80 MB, done</span><br><span class="line">Downloading typescript@5.2.2: 7.23 MB/7.23 MB, done</span><br><span class="line">Progress: resolved 423, reused 184, downloaded 239, added 423, done</span><br><span class="line">node_modules/.pnpm/es5-ext@0.10.62/node_modules/es5-ext: Running postinstall script, done in 43.1s</span><br><span class="line">node_modules/.pnpm/utf-8-validate@5.0.10/node_modules/utf-8-validate: Running install script, done in 52.3s</span><br><span class="line">node_modules/.pnpm/bufferutil@4.0.8/node_modules/bufferutil: Running install script, done in 52.3s</span><br><span class="line"></span><br><span class="line">dependencies:</span><br><span class="line">+ @douyinfe/semi-ui 2.44.0</span><br><span class="line">+ @react-spring/web 9.7.5</span><br><span class="line">+ @supabase/supabase-js 2.38.1</span><br><span class="line">+ accept-language 3.0.18</span><br><span class="line">+ classes-names 1.0.0</span><br><span class="line">+ emailjs-com 3.2.0</span><br><span class="line">+ i18next 23.5.1</span><br><span class="line">+ i18next-browser-languagedetector 7.1.0</span><br><span class="line">+ i18next-resources-to-backend 1.1.4</span><br><span class="line">+ is-mobile 5.0.0</span><br><span class="line">+ next 13.5.4</span><br><span class="line">+ react 18.2.0</span><br><span class="line">+ react-dom 18.2.0</span><br><span class="line">+ react-google-recaptcha 3.1.0</span><br><span class="line">+ react-i18next 13.3.0</span><br><span class="line">+ swiper 11.1.14</span><br><span class="line">+ tailwind-merge 2.5.4</span><br><span class="line"></span><br><span class="line">devDependencies:</span><br><span class="line">+ @types/node 20.8.6</span><br><span class="line">+ @types/react 18.2.28</span><br><span class="line">+ @types/react-dom 18.2.13</span><br><span class="line">+ @types/react-google-recaptcha 2.1.7</span><br><span class="line">+ postcss-pxtorem 6.1.0</span><br><span class="line">+ prettier 3.3.3</span><br><span class="line">+ prettier-plugin-tailwindcss 0.6.8</span><br><span class="line">+ tailwindcss 3.3.3</span><br><span class="line">+ typescript 5.2.2</span><br><span class="line"></span><br><span class="line">Done in 21m 2.3s</span><br></pre></td></tr></table></figure><p>这次再启动项目 <code>pnpm run dev</code> 就不报缺失模块的警告了</p><p>简单说下 <code>npm</code> 和 <code>pnpm</code> 的关系，<code>npm</code> 是 Node.js 的默认包管理工具，用于安装、管理和共享 JavaScript 包。安装 Node.js 时会自带 npm，不需要额外安装。<code>pnpm</code> 是一种兼容 npm 和 yarn 的包管理工具。它与 npm 类似，也用来管理 JavaScript 包，但通过优化磁盘使用和依赖解析性能解决了 npm 的一些问题。</p><p>再简单说下 <code>package.json</code>、<code>package-lock.json</code> 和 <code>pnpm-lock.yaml</code> 的关系</p><ul><li><code>package.json</code> 描述项目的基本信息（如项目名称、版本号），声明项目的依赖项、脚本命令和配置，是项目中最重要的依赖描述文件，是开发者直接编辑的文件，但不包含依赖的具体版本信息</li><li><code>package-lock.json</code> 是 npm 包管理工具生成的锁定文件，记录依赖的具体版本号和结构，确保不同环境中安装的依赖版本一致。不需要手动编辑，由 npm 自动生成和管理。</li><li><code>pnpm-lock.yaml</code> 是 pnpm 包管理工具生成的锁定文件，用于精确记录依赖的版本号和安装结构。确保团队或 CI/CD 环境在安装依赖时，所有人使用的依赖版本完全一致。不需要手动编辑，由 pnpm 自动生成和管理。</li></ul><p>可以看到<code>package-lock.json</code> 和 <code>pnpm-lock.yaml</code>是互斥的，平时 <code>npm</code> 和 <code>pnpm</code> 选择一个就行，别混着用。</p><h1 id="Android问题"><a href="#Android问题" class="headerlink" title="Android问题"></a>Android问题</h1><p>Apple推送选择APNs就好了，而Android推送一直就是老大难，因为FCM对谷歌框架的依赖，国内基本是不可用的，所以在之前的蛮荒年代，真是八仙过海各显其能，相互拉起、定时检测等等搞得手机卡顿的不行，后来各家厂商基本都实现了自己的通道，比如华为、小米、OPPO、Vivo等等，所以找个聚合的SDK也是能办到的，但是国外还是主要依赖FCM。</p><p>但随着Android版本的提升，推送的要求越来越严，一旦应用被强制杀死，FCM消息虽然能达到手机，但是不允许拉起应用，导致消息无法触及到用户，这也是目前国外环境要实现推送所面临的难点。</p><p>要想能收到FCM的推送消息，就得保证应用不被杀死，可能得方向有提高优先级、定时检测重启、杀死后立即重启、采用其他的推送通道等等，但是效果都不理想，之前好用的方式升级版本或者切换一个手机厂商就不太好使了，没有什么通用的解决办法。</p><p>后来发现，真对国内手机只要给应用开启自启动权限，那么即使被强杀后也能收到FCM通知，或许这是一个可以努力的方向，但这个权限不是通用的逻辑，每个手机厂商都有自己的白名单，大厂APP出生就在白名单里，而我们自己开发的APP需要经过复杂的引导操作才能加入其中，虽然很难，但终归是第一条可行的路。</p><h2 id="主动请求通知权限"><a href="#主动请求通知权限" class="headerlink" title="主动请求通知权限"></a>主动请求通知权限</h2><p>在 Android 13 (API 33) 及以上版本，应用需要 <strong>主动请求通知权限</strong>，以便能够发送通知。对于之前的版本，应用只需要在 <code>AndroidManifest.xml</code> 中声明 <code>POST_NOTIFICATIONS</code> 权限即可。然而，从 Android 13 开始，仅在声明权限的基础上，还需要在运行时申请此权限。</p><p><strong>步骤1：修改 AndroidManifest.xml</strong></p><p>首先，在 <code>AndroidManifest.xml</code> 文件中声明权限：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">"android.permission.POST_NOTIFICATIONS"</span> /&gt;</span></span><br></pre></td></tr></table></figure></p><p><strong>步骤2：在应用中请求通知权限</strong></p><p>从 Android 13 开始，你需要在运行时请求通知权限。你可以使用 <code>NotificationManagerCompat</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.os.Build;</span><br><span class="line"><span class="keyword">import</span> android.widget.Toast;</span><br><span class="line"><span class="keyword">import</span> androidx.core.app.NotificationManagerCompat;</span><br><span class="line"><span class="keyword">import</span> androidx.appcompat.app.AppCompatActivity;</span><br><span class="line"><span class="keyword">import</span> android.os.Bundle;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MainActivity</span> <span class="keyword">extends</span> <span class="title">AppCompatActivity</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 检查 Android 13 及以上版本是否需要请求通知权限</span></span><br><span class="line">        <span class="keyword">if</span> (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.TIRAMISU) &#123;</span><br><span class="line">            NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(<span class="keyword">this</span>);</span><br><span class="line">            <span class="comment">// 检查通知权限是否已被授权</span></span><br><span class="line">            <span class="keyword">if</span> (!notificationManagerCompat.areNotificationsEnabled()) &#123;</span><br><span class="line">                <span class="comment">// 如果没有授权，可以弹出提示或者引导用户</span></span><br><span class="line">                requestNotificationPermission();</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">                Toast.makeText(<span class="keyword">this</span>, <span class="string">"通知权限已授权"</span>, Toast.LENGTH_SHORT).show();</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">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">requestNotificationPermission</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 你可以在这里引导用户到设置页面手动授权</span></span><br><span class="line">        Toast.makeText(<span class="keyword">this</span>, <span class="string">"请在设置中授权通知权限"</span>, Toast.LENGTH_LONG).show();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>步骤3：引导用户授权</strong></p><p>如果用户没有授权通知权限，你通常需要提供一个方法引导他们到设置页面，在该页面中手动授权。因为从 Android 13 开始，权限只能通过系统设置进行手动授权，而不是通过应用内弹窗。</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">goToSettings</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    Intent intent = <span class="keyword">new</span> Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)</span><br><span class="line">            .putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());</span><br><span class="line">    startActivity(intent);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="网络状态变化的监听不能使用静态注册"><a href="#网络状态变化的监听不能使用静态注册" class="headerlink" title="网络状态变化的监听不能使用静态注册"></a>网络状态变化的监听不能使用静态注册</h2><p>从 <strong>Android 7.0 (API 24)</strong> 开始，<strong>网络状态变化的监听</strong>不能使用静态注册 (<code>&lt;receiver&gt;</code> 标签在 <code>AndroidManifest.xml</code> 中注册)，必须通过动态注册来实现。这是出于优化电池和性能的考虑，Android 不再允许应用通过静态注册的方式来监听系统广播（如网络变化、屏幕开关等），尤其是对敏感的系统事件。</p><p><strong>静态注册 (AndroidManifest.xml)</strong></p><p>   静态注册会在 <code>AndroidManifest.xml</code> 中声明广播接收器，并且会在整个应用运行期间自动接收系统广播。对于网络状态变化（如 <code>CONNECTIVITY_ACTION</code>）的静态注册从 Android 7.0 开始被限制。</p><p>   <strong>AndroidManifest.xml 中的静态注册示例</strong>（在 Android 7.0 之前是有效的）：<br>   <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">receiver</span> <span class="attr">android:name</span>=<span class="string">".NetworkChangeReceiver"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">intent-filter</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.net.conn.CONNECTIVITY_CHANGE"</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">intent-filter</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">receiver</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>   这种方式在 Android 7.0 及以后会被限制，无法接收到网络状态变化的广播。</p><p><strong>动态注册 (代码中注册)</strong></p><p>   动态注册意味着在应用运行时，通过 <code>Context.registerReceiver()</code> 来注册接收器，这样可以选择性地在需要时注册，并在不需要时取消注册。这样做的好处是，可以更灵活地控制接收器的生命周期，避免不必要的电池消耗。</p><p>   <strong>动态注册的示例</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MainActivity</span> <span class="keyword">extends</span> <span class="title">AppCompatActivity</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> NetworkReceiver networkReceiver;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line">        setContentView(R.layout.activity_main);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 初始化网络广播接收器</span></span><br><span class="line">        networkReceiver = <span class="keyword">new</span> NetworkReceiver();</span><br><span class="line">        IntentFilter filter = <span class="keyword">new</span> IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 动态注册接收器</span></span><br><span class="line">        registerReceiver(networkReceiver, filter);</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="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onDestroy</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">super</span>.onDestroy();</span><br><span class="line">        <span class="comment">// 注销接收器</span></span><br><span class="line">        unregisterReceiver(networkReceiver);</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="class"><span class="keyword">class</span> <span class="title">NetworkReceiver</span> <span class="keyword">extends</span> <span class="title">BroadcastReceiver</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onReceive</span><span class="params">(Context context, Intent intent)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 获取网络连接状态</span></span><br><span class="line">        ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);</span><br><span class="line">        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (activeNetwork != <span class="keyword">null</span> &amp;&amp; activeNetwork.isConnected()) &#123;</span><br><span class="line">            Log.d(<span class="string">"NetworkReceiver"</span>, <span class="string">"网络已连接"</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            Log.d(<span class="string">"NetworkReceiver"</span>, <span class="string">"网络未连接"</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>   在这个示例中，使用 <code>registerReceiver()</code> 动态注册了一个监听网络状态变化的广播接收器 <code>NetworkReceiver</code>，并在 <code>onCreate()</code> 中进行注册，在 <code>onDestroy()</code> 中进行注销，以避免内存泄漏。</p><h2 id="各种Service介绍和对比"><a href="#各种Service介绍和对比" class="headerlink" title="各种Service介绍和对比"></a>各种Service介绍和对比</h2><p>在 Android 中，<code>Service</code> 是一种后台组件，用于在应用中执行长时间运行的任务。根据服务的用途和生命周期，Android 提供了不同类型的服务。以下是 Android 中常见的 <code>Service</code> 类型和 <code>BroadcastReceiver</code> 的介绍及对比：</p><p><strong>普通 Service (Normal Service)</strong> 没有前台 UI 组件。通常用于执行后台任务，不与用户交互，可以在应用的任何地方启动，并在后台运行，直到它完成工作或被显式停止，生命周期为 <code>onCreate()</code> → <code>onStartCommand()</code> → <code>onDestroy()</code>，适用执行短时间的后台任务，如数据同步、文件下载等场景。</p><p><strong>前台服务 (Foreground Service)</strong> 在运行时必须显示一个持续的通知（通知栏），因此它会在用户和系统资源管理中被认为是一个“重要”的服务，不容易被系统杀死。生命周期为 <code>onCreate()</code> → <code>onStartCommand()</code> → <code>onDestroy()</code> (服务必须调用 <code>startForeground()</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></pre></td><td class="code"><pre><span class="line">Notification notification = <span class="keyword">new</span> NotificationCompat.Builder(<span class="keyword">this</span>, CHANNEL_ID)</span><br><span class="line">    .setContentTitle(<span class="string">"My Foreground Service"</span>)</span><br><span class="line">    .setContentText(<span class="string">"Service is running in the foreground"</span>)</span><br><span class="line">    .setSmallIcon(R.drawable.ic_notification)</span><br><span class="line">    .build();</span><br><span class="line">startForeground(<span class="number">1</span>, notification);</span><br></pre></td></tr></table></figure><p><strong>JobService</strong>  是一种特殊类型的服务，它用于执行计划任务（即 <code>JobScheduler</code>），在特定条件下（如网络连接、充电等）启动。<code>JobService</code> 适合用于执行延迟任务或条件任务，如周期性数据同步、定时上传等，与 <code>JobScheduler</code> 配合使用，可以在设备的空闲时间或在设备满足某些条件时执行任务。生命周期为 <code>onCreate()</code> → <code>onStartJob()</code> → <code>onStopJob()</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyJobService</span> <span class="keyword">extends</span> <span class="title">JobService</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">onStartJob</span><span class="params">(JobParameters params)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 执行任务</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>; <span class="comment">// 返回 true 表示任务正在进行中</span></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="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">onStopJob</span><span class="params">(JobParameters params)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 停止任务</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>; <span class="comment">// 返回 true 表示任务被取消</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>BroadcastReceiver</strong>  是 Android 中用于接收广播的组件。它通过监听系统广播或自定义广播来响应事件（如网络状态变化、电池电量变化等）。<code>BroadcastReceiver</code> 不会启动一个独立的线程，它只会在接收到广播时执行相应的代码。<code>onReceive()</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">NetworkChangeReceiver</span> <span class="keyword">extends</span> <span class="title">BroadcastReceiver</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onReceive</span><span class="params">(Context context, Intent intent)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) &#123;</span><br><span class="line">            <span class="comment">// 处理网络变化</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><table><thead><tr><th><strong>服务类型</strong></th><th><strong>生命周期</strong></th><th><strong>适用场景</strong></th><th><strong>优缺点</strong></th></tr></thead><tbody><tr><td>普通 <code>Service</code></td><td><code>onCreate()</code> → <code>onStartCommand()</code> → <code>onDestroy()</code></td><td>短时间的后台任务</td><td>简单易用，但容易被系统杀死，适用于短时间的任务</td></tr><tr><td>前台 <code>ForegroundService</code></td><td><code>onCreate()</code> → <code>onStartCommand()</code> → <code>onDestroy()</code></td><td>长时间运行的任务，如播放音乐、导航</td><td>不容易被杀死，但需要显示通知，可能影响用户体验</td></tr><tr><td><code>JobService</code></td><td><code>onCreate()</code> → <code>onStartJob()</code> → <code>onStopJob()</code></td><td>执行定期任务、延迟任务，或者需要满足某些条件的任务</td><td>适用于需要在特定条件下执行的任务，延迟执行，但不能精确控制执行时机</td></tr><tr><td><code>BroadcastReceiver</code></td><td><code>onReceive()</code></td><td>响应广播事件，如网络状态变化、电池状态变化等</td><td>适合处理广播事件，不能执行长时间操作，执行时间受限制</td></tr></tbody></table><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>Redis如果未使用Jemalloc无法开启主动的碎片回收，通过 <code>redis-cli INFO memory | grep allocator</code> 可查询内存分配器</li><li>启动Redis的方法 <code>/usr/local/bin/redis-server ./redis.conf --daemonize yes</code></li><li>管理node模块时可以选择 <code>nmp</code> 或者 <code>pnmp</code>，不要混用，总的来说后者更优秀一点</li><li>管理Nextjs项目时最好吧 <code>package.json</code> 和 <code>package-lock.json</code>(pnpm-lock.yaml)都上传，便于安装出相同的运行环境</li><li>触发Vercel重新部署的命令 <code>git commit --allow-empty -m&quot;trigger redeployment&quot;</code></li><li>Android的服务有很多种，普通服务Service、前台服务ForegroundService，定时服务JobService等等</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143925720" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>在自己的世界里独善其身，在别人的世界里顺其自然~</p><p>2024-11-29 15:50:46</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;这两天的工作又相对杂乱一些，处理一下A事情，又要搞一搞B事情，需要盯着C事情，还要尝试一下D事情，所以这篇总结没有什么主线，主要目的是记录一下最近解决的问题，先不展开讨论，当类似的问题积累一些再展开描述，我就先记录一下流水账了。&lt;/p&gt;
    
    </summary>
    
      <category term="Android" scheme="http://AlbertGithubHome.github.io/blog/categories/Android/"/>
    
    
      <category term="Redis" scheme="http://AlbertGithubHome.github.io/blog/tags/Redis/"/>
    
      <category term="Android" scheme="http://AlbertGithubHome.github.io/blog/tags/Android/"/>
    
      <category term="Vercel" scheme="http://AlbertGithubHome.github.io/blog/tags/Vercel/"/>
    
      <category term="pnpm" scheme="http://AlbertGithubHome.github.io/blog/tags/pnpm/"/>
    
      <category term="shutdown" scheme="http://AlbertGithubHome.github.io/blog/tags/shutdown/"/>
    
  </entry>
  
  <entry>
    <title>git克隆报错时The project you were looking for could not be found...</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/11/21/git%E5%85%8B%E9%9A%86%E6%8A%A5%E9%94%99%E6%97%B6The-project-you-were-looking-for-could-not-be-found/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/11/21/git克隆报错时The-project-you-were-looking-for-could-not-be-found/</id>
    <published>2024-11-20T17:45:00.000Z</published>
    <updated>2024-12-28T16:47:38.789Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>Ps D:>git clone <a href="http://gitlab.yyy.com/project/xxx.git" target="_blank" rel="noopener">http://gitlab.yyy.com/project/xxx.git</a><br>Cloning into ‘xxx’..<br>remote: The project you were looking for could not be found or you don’t have permission to view it.<br>fatal: repository ‘<a href="http://gitlab.yyy.com/project/xxx.git/&#39;not" target="_blank" rel="noopener">http://gitlab.yyy.com/project/xxx.git/&#39;not</a> found</p></blockquote><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>问题如上，使用 <code>git clone</code> 命令克隆一个仓库的时候，提示我项目找不到或者没有权限，一开始我还以为路径拼错了，核对了两遍确认仓库地址正确的，访问仓库的权限肯定也有的，我使用账号和密码可以成功登录gitlab网站并且看到这个项目，网上查询方案大概原因是电脑的git自动保存了其他的用户名密码信息，与当前项目的用户名密码与之前的发生冲突，处理掉这种情况就行了。</p><a id="more"></a><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><p>清理或者避开这种冲突的情况：</p><h2 id="临时方案"><a href="#临时方案" class="headerlink" title="临时方案"></a>临时方案</h2><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">git <span class="built_in">clone</span> http://username:password@gitlab.yyy.com/project/xxx.git</span><br></pre></td></tr></table></figure><h2 id="永久方案"><a href="#永久方案" class="headerlink" title="永久方案"></a>永久方案</h2><p>清除本地保存git账户，下次再进行git clone操作时，在弹出窗口中输入用户名密码</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">git config --system --<span class="built_in">unset</span>  credential.helper</span><br></pre></td></tr></table></figure><p>如果是 Linux 或 Mac系统改为下面的命令</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">git config --global --<span class="built_in">unset</span> credential.helper</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>临时用户克隆仓库的命令 <code>git clone http://username:password@gitlab.yyy.com/project/xxx.git</code></li><li>清理本地git账户的命令 <code>git config --system --unset  credential.helper</code></li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143864424" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>国王们以世俗的权柄和虚名让你下跪，只有你可以让自己站起来，满嘴的仁义道德，不过是国王统治下的壁垒，几千年根深蒂固~</p><p>2024-11-20 21:15:33</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;Ps D:&gt;git clone &lt;a href=&quot;http://gitlab.yyy.com/project/xxx.git&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://gitlab.yyy.com/project/xxx.git&lt;/a&gt;&lt;br&gt;Cloning into ‘xxx’..&lt;br&gt;remote: The project you were looking for could not be found or you don’t have permission to view it.&lt;br&gt;fatal: repository ‘&lt;a href=&quot;http://gitlab.yyy.com/project/xxx.git/&amp;#39;not&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://gitlab.yyy.com/project/xxx.git/&amp;#39;not&lt;/a&gt; found&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;问题如上，使用 &lt;code&gt;git clone&lt;/code&gt; 命令克隆一个仓库的时候，提示我项目找不到或者没有权限，一开始我还以为路径拼错了，核对了两遍确认仓库地址正确的，访问仓库的权限肯定也有的，我使用账号和密码可以成功登录gitlab网站并且看到这个项目，网上查询方案大概原因是电脑的git自动保存了其他的用户名密码信息，与当前项目的用户名密码与之前的发生冲突，处理掉这种情况就行了。&lt;/p&gt;
    
    </summary>
    
      <category term="Git" scheme="http://AlbertGithubHome.github.io/blog/categories/Git/"/>
    
    
      <category term="git" scheme="http://AlbertGithubHome.github.io/blog/tags/git/"/>
    
      <category term="gitlab" scheme="http://AlbertGithubHome.github.io/blog/tags/gitlab/"/>
    
      <category term="clone" scheme="http://AlbertGithubHome.github.io/blog/tags/clone/"/>
    
      <category term="鉴权" scheme="http://AlbertGithubHome.github.io/blog/tags/%E9%89%B4%E6%9D%83/"/>
    
      <category term="not-be-found" scheme="http://AlbertGithubHome.github.io/blog/tags/not-be-found/"/>
    
      <category term="credential" scheme="http://AlbertGithubHome.github.io/blog/tags/credential/"/>
    
  </entry>
  
  <entry>
    <title>会打字就能建网站的广告词是否夸大其词呢</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/11/18/%E4%BC%9A%E6%89%93%E5%AD%97%E5%B0%B1%E8%83%BD%E5%BB%BA%E7%BD%91%E7%AB%99%E7%9A%84%E5%B9%BF%E5%91%8A%E8%AF%8D%E6%98%AF%E5%90%A6%E5%A4%B8%E5%A4%A7%E5%85%B6%E8%AF%8D%E5%91%A2/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/11/18/会打字就能建网站的广告词是否夸大其词呢/</id>
    <published>2024-11-18T15:30:00.000Z</published>
    <updated>2024-12-28T16:45:08.380Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>首先声明这不是一篇广告文，只是我最近用了一款建站的产品，在使用过程中也遇到了一些问题，所以总结一下避免以后继续踩坑，其中“会打字就能建网站”的广告词还是挺吸引人的，给人一种“有手就行”的既视感。</p><a id="more"></a><p>这款产品的名字叫 <a href="https://wanwang.aliyun.com/webdesign/sumei" target="_blank" rel="noopener">“云·速成美站”</a>，是一种典型的SaaS（软件即服务）产品，也是一款自助型网站建设产品。</p><p>这广告词听听就好了，我一开始并没有拿到这么明确的产品规格，放到我手里的是一个完成了ICP备案的域名和一台ECS实例，目的是访问域名返回一个简约的企业网站，所以我一开始的想法是为了显示网站我干脆在ECS搭建一个算了，放个静态页面或者用Python自动生成个简易的首页，再把域名解析到ECS的服务器就完活了，后来觉得態简陋了，那干脆安装个WordPress，正好前段时间一直在忙这些事情，也算是有一些经验了，搭建起来应该会很快的。</p><h1 id="发现ECS实例的秘密"><a href="#发现ECS实例的秘密" class="headerlink" title="发现ECS实例的秘密"></a>发现ECS实例的秘密</h1><p>后来渐渐发现不对劲，这台ECS的操作系统是 <code>Alibaba Cloud Linux 3.2104 LTS 64位 等保2.0三级版</code>，而镜像是<code>云·原生建站-容器版</code>(m-2zedbkbwq1gwd41nysyv)，并且没有操作密码也没绑定密钥，这个ECS看起来不简单呀，应该是一个包含了建站产品的镜像，根本不是一个简简单单的的逻辑器，对于这点账号所有者并不清楚，只告诉我买了一个建站套餐，好吧，我继续翻一翻。</p><p>终于通过订单，在<code>云市场</code> -&gt; <code>已购买服务</code> 中发现了 <code>云·原生建站-容器版</code>，同时还有一个 <code>云·速成美站</code>，果然是一个预制的建站产品，然后就找找教程吧，发现了这个<a href="https://netmarket.oss-cn-hangzhou.aliyuncs.com/7e417a8495784cb999fd175c394673de.pdf" target="_blank" rel="noopener">云·原生建站-docker 版产品激活流程</a></p><p>教程中提到直接浏览器访问“ECS公网IP”，然后设置管理员手机号，然后绑定网站即可。一些相关的访问链接入下：</p><ul><li><a href="https://market.aliyun.com/xinxuan/webdesign" target="_blank" rel="noopener">阿里云建站</a></li><li><a href="https://market.aliyun.com/products/57342011/cmjj00059550.html;jsessionid=0FA9F1CBC724E0E4E70FB5B663B13CED?" target="_blank" rel="noopener">云·原生建站-容器版</a></li><li><a href="https://market.aliyun.com/products/56598032/cmgj027604.html#sku=yuncode21604000017" target="_blank" rel="noopener">云·速成美站</a></li><li><a href="https://netmarket.oss-cn-hangzhou.aliyuncs.com/e1f744673dac48c4a0debed7f4f381f4.pdf" target="_blank" rel="noopener">致云·速成美站用户的一封信</a></li></ul><p>因为教程让直接访问ECS公网IP，浏览器默认会使用80端口，如果可以进行才做，说明这个ECS实例本身就带了一个web服务才对，再回想刚才实例的镜像名字应该就是定制的，包含一个Web服务应该很容易，正当我以为按教程操作就万事大吉时，得到的却是“地址无法访问”的提示。</p><p>如果是一个仅仅会打字的人到这里估计已经懵圈了，而我还是有点基础的，去查询了防火墙发现80端口没有放开，所以手动开启了80端口的入站规则，成功设置了管理员手机号，但是却无法理解绑定网站是怎么一个操作，也找不到可以绑定的网站产品，更无法理解这个ECS <code>云·原生建站-容器版</code>和 <code>云·速成美站</code> 究竟是什么关系。</p><h1 id="网站设计"><a href="#网站设计" class="headerlink" title="网站设计"></a>网站设计</h1><p>后来还是在订单了找到 <code>云·速成美站</code>，通过“免登”方式进入了后台，俨然一副WP的画面，只是作为企业网站建站工具，他更加的具象化了，什么公司名啊、ICP备案号啊、公司电话啊、公司Logo等等，只要你在后台设置了，就可以在网站上直接显示，并且有上千套模板一键替换。</p><p>这个感觉就是像是你买完产品，他自动给你注册了一个WordPress账号，然后给你预制了一些企业官网的模板一样，我简单填写了信息，绑定了之前备过案的域名，因为域名也在阿里云上申请的，所以一键绑定会非常方便，否则就要自己去添加解析记录了。</p><p>绑定验证需要一点点时间，显示15分钟，但是我有一个子域名大概8小时才验证通过，验证完成之后就可以开启 HTTPS，可以用免费的，并且会自动续期，对于不爱折腾的小白来说还是非常方便的。</p><h1 id="再次处理ECS实例"><a href="#再次处理ECS实例" class="headerlink" title="再次处理ECS实例"></a>再次处理ECS实例</h1><p>这时我的网站可以通过域名访问了，通过ping命令发现网站并不是我们之前提到的ECS的公网IP，那么要ECS还有什么用呢？这是一直困扰着我的问题，这时我再来到绑定页面发现感刚刚建站的那个产品出现了，激动的我赶紧点击绑定按钮，得到的却是“该ECS初始化失败，请重置ECS镜像后重试”的报错，提了工单，第二天得到反馈让我把实例的操作系统重新安装一下，当然在替换的过程中因为选错了镜像还把之前的预制系统给删没了，后来好不容易在镜像市场中找到 <code>云·原生建站-容器版</code> 又替换了回来。</p><p>重启安装好镜像的ECS，绑定时依旧报原来的问题，但是放了大概1个小时之后，再次尝试绑定就成功了，然后又迷迷糊糊的绑定了一次域名，开启了HTTPS，这次在Ping域名的时候，返回的就是ECS的公网IP，通过反复翻阅文档，然后与客服交流，终于弄懂了一些之前的疑问。</p><h1 id="相关疑问"><a href="#相关疑问" class="headerlink" title="相关疑问"></a>相关疑问</h1><blockquote><p>购买了云·速成美站还需要购买什么吗？</p><blockquote><p>云·速成美站已集成了服务器，建设过程中也无需任何额外费用。网站上线，仅需拥有一个域名。</p></blockquote><p>如何进入产品后台？</p><blockquote><p>登录阿里云“管理控制台—云市场—已购买的服务—免登”，进入产品后台，开始网站搭建。</p></blockquote><p>域名已经在工信部备案，使用你们的云·速成美站，是否还需要重新备案呢？</p><blockquote><p>如果域名已在阿里云备案通过，且备案未被注销，无需重新备案。如果你的域名非阿里云备案，需要将备案信息接入到阿里云</p></blockquote><p>既然云·速成美站已集成了服务器，那我们文中提到的ECS服务器又有什么用呢？</p><blockquote><p>这是给开发人员准备的，可以更加精细化定制和修改，独立部署ECS可以提高百度搜索排名，还可以通过IP进行访问，网站是部署在提供的ECS内，而不是跟别人公用的</p></blockquote></blockquote><h1 id="正确的操作步骤"><a href="#正确的操作步骤" class="headerlink" title="正确的操作步骤"></a>正确的操作步骤</h1><p>通过后视镜来看这个建站过程走了很多弯路，正确的步骤如下：</p><ol><li>购买备案建站套餐</li><li>在<code>云市场</code> -&gt; <code>已购买服务</code> 中找到 <code>云·速成美站</code>，点击免登按钮进入网站设计后台，填写基本信息后发布</li><li>访问ECS的安全规则，开启80端口的入站规则</li><li>浏览器访问ECS公网IP，按照教程绑定网站和ECS</li><li>进入网站后台绑定域名，开启HTTPS</li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>IaaS (Infrastructure as a Service) - 基础设施即服务，提供最基础的计算资源，包括服务器、存储、网络和虚拟化环境等</li><li>PaaS (Platform as a Service) - 平台即服务，提供一个完整的开发和部署环境，允许用户专注于应用程序的开发，而不需要管理底层基础设施</li><li>SaaS (Software as a Service) - 软件即服务，是一种提供应用程序的服务模式，用户可以直接通过网络访问和使用应用软件，无需自己安装或维护任何硬件或软件</li><li>现在的建站真是太方便了，比我那时候专门做首页设计要快速多了，之前我还只是创建静态页面</li><li>云·速成美站已集成了服务器，建设过程中也无需任何额外费用。网站上线，仅需拥有一个域名</li><li>虽说这种产品建站很方便，但是却没有达到 “会打字就能建网站” 的这个口号</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143720785" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>很多人生的选择不是深思熟虑就能选对的，还有一些运气在里面的，甚至是运气占了主要因素，总是觉得没选的那条路是精彩非凡的坦途，内心的美化罢了~</p><p>2024-11-18 17:28:28</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;首先声明这不是一篇广告文，只是我最近用了一款建站的产品，在使用过程中也遇到了一些问题，所以总结一下避免以后继续踩坑，其中“会打字就能建网站”的广告词还是挺吸引人的，给人一种“有手就行”的既视感。&lt;/p&gt;
    
    </summary>
    
      <category term="Web" scheme="http://AlbertGithubHome.github.io/blog/categories/Web/"/>
    
    
      <category term="Saas" scheme="http://AlbertGithubHome.github.io/blog/tags/Saas/"/>
    
      <category term="建站" scheme="http://AlbertGithubHome.github.io/blog/tags/%E5%BB%BA%E7%AB%99/"/>
    
      <category term="ICP备案" scheme="http://AlbertGithubHome.github.io/blog/tags/ICP%E5%A4%87%E6%A1%88/"/>
    
      <category term="HTTPS" scheme="http://AlbertGithubHome.github.io/blog/tags/HTTPS/"/>
    
      <category term="ECS" scheme="http://AlbertGithubHome.github.io/blog/tags/ECS/"/>
    
  </entry>
  
  <entry>
    <title>Docker启动gitlab后22端口被占用如何解决</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/11/08/Docker%E5%90%AF%E5%8A%A8gitlab%E5%90%8E22%E7%AB%AF%E5%8F%A3%E8%A2%AB%E5%8D%A0%E7%94%A8%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/11/08/Docker启动gitlab后22端口被占用如何解决/</id>
    <published>2024-11-07T16:30:00.000Z</published>
    <updated>2024-12-28T16:41:08.044Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>这两天一直在研究Docker启动gitlab的问题，邮件问题暂时告一段落，当真正开始使用gitlab克隆的时候发现，之前22端口被占用无法启动，我就用2022端口做了一个映射，但是在gitab界面上提供的克隆地址类似这样 <code>git@gitlab.gameup.com:games/xxx.git</code>，丝毫没有端口 2022 的影子，执行 <code>git clone git@gitlab.gameup.com:games/xxx.git</code> 果然不可下载，所以搜索了一圈解决方案总结在此，方便日后查找。</p><h1 id="Docker启动参数"><a href="#Docker启动参数" class="headerlink" title="Docker启动参数"></a>Docker启动参数</h1><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">docker run --detach \</span><br><span class="line">  --hostname gitlab.gameup.com \</span><br><span class="line">  --publish 443:443 --publish 80:80 --publish 2022:22 \</span><br><span class="line">  --name gitlab \</span><br><span class="line">  --restart always \</span><br><span class="line">  --volume /<span class="built_in">export</span>/docker/gitlab/config:/etc/gitlab \</span><br><span class="line">  --volume /<span class="built_in">export</span>/docker/gitlab/logs:/var/<span class="built_in">log</span>/gitlab \</span><br><span class="line">  --volume /<span class="built_in">export</span>/docker/gitlab/data:/var/opt/gitlab \</span><br><span class="line">  registry.cn-hangzhou.aliyuncs.com/z5z/gitlab-ce:latest</span><br></pre></td></tr></table></figure><p>这个在之前的<a href="https://blog.csdn.net/albertsh/article/details/143493367" target="_blank" rel="noopener">文章</a>中提到过，因为 80 和 443端口正常可用，所有采用HTTP方式的下载 <code>git clone http://gitlab.gameup.com/games/xxx.git</code> 是正常的，而 SSH下载的这种方式，因为修改了默认端口，导致提供的链接无法下载，即使我像这样 <code>git clone git@gitlab.gameup.com:2022/games/xxx.git</code> 加上了端口也无法正常克隆。</p><p>用一种比较笨的方法就是，关闭SSH服务或者将宿主机的SSH服务改个端口，但这是一台远程服务器，一般都通过SSH协议访问，关闭不太现实，修改端口的话倒是可行，可是之前这台机器上运行着多个任务，这意味着很多服务脚本都得修改，所以也不是很好的办法，不能因为加了个新服务就把原来的老脚本统统改一遍吧，尝试多次终于找到一个可以接收的方法。</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><ol><li><p>进入gitlab容器 <code>docker exec -it gitlab</code></p></li><li><p>修改配置文件 <code>/etc/gitlab/gitlab.rb</code> 中的内容</p> <figure class="highlight rb"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab_rails[<span class="string">'gitlab_shell_ssh_port'</span>] = <span class="number">2022</span></span><br></pre></td></tr></table></figure><p> 注意这个设置只是修改了 GitLab 的显示信息，不会更改 GitLab 容器内的 SSH 服务端口</p></li><li><p>执行 <code>gitlab-ctl reconfigure</code> 即可</p><p> 修改执行之后，再次查看gitlab下载地址发现改成了 <code>ssh://git@gitlab.gameup.com:2022/games/xxx.git</code>，不仅加了端口号，还在前面加上了 <code>ssh://</code>，怪不得我自己加端口号不能能用呢，关于这一点我也查到了相关解释：</p></li></ol><blockquote><p>GitLab 提供的 SSH 克隆地址（如 git@gitlab.gameup.com:2022/games/xxx.git）是一种简化格式，它没有指定协议前缀 ssh:// 和端口号。这是因为默认情况下，Git 使用 SSH 协议连接，并默认通过端口 22 来克隆和操作仓库。因此，这种简化格式省略了 ssh:// 前缀和端口号</p><p>对于标准端口（22），简化格式的地址是有效的，因为 Git 客户端会默认使用 SSH 协议并连接到 22 端口,但是，当你的 SSH 端口不同于 22（如使用 2022 时），Git 默认的简化格式就不适用了，需要显式指定端口号或配置 SSH，也就是写成 ssh://git@gitlab.gameup.com:2022/games/xxx.git 的形式</p></blockquote><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>使用docker启动gitlab遭遇22端口占用时可以选择其他端口映射 <code>--publish 2022:22</code></li><li>修改映射端口后为了让下载地址显示正确，需要修改gitlab配置文件 <code>gitlab_rails[&#39;gitlab_shell_ssh_port&#39;] = 2022</code></li><li><code>git@gitlab.gameup.com:games/xxx.git</code> 是一种ssh协议的简化形式，完整的地址应该为 <code>ssh://git@gitlab.gameup.com:22/games/xxx.git</code></li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143577720" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>不要再抑郁下去了孩子，你要像一个，神经病一样活泼开朗，不要太在乎别人的目光，因为没人会注意你。</p><p>2024-11-7 19:21:48</p></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;这两天一直在研究Docker启动gitlab的问题，邮件问题暂时告一段落，当真正开始使用gitlab克隆的时候发现，之前22端口被占用无法启
      
    
    </summary>
    
      <category term="Docker" scheme="http://AlbertGithubHome.github.io/blog/categories/Docker/"/>
    
    
      <category term="ssh" scheme="http://AlbertGithubHome.github.io/blog/tags/ssh/"/>
    
      <category term="docker" scheme="http://AlbertGithubHome.github.io/blog/tags/docker/"/>
    
      <category term="gitlab" scheme="http://AlbertGithubHome.github.io/blog/tags/gitlab/"/>
    
      <category term="端口22" scheme="http://AlbertGithubHome.github.io/blog/tags/%E7%AB%AF%E5%8F%A322/"/>
    
      <category term="gitlab-ctl" scheme="http://AlbertGithubHome.github.io/blog/tags/gitlab-ctl/"/>
    
      <category term="reconfigure" scheme="http://AlbertGithubHome.github.io/blog/tags/reconfigure/"/>
    
  </entry>
  
  <entry>
    <title>使用 Let’s Encrypt 获取免费SSL证书</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/11/06/%E4%BD%BF%E7%94%A8-Let%E2%80%99s-Encrypt-%E8%8E%B7%E5%8F%96%E5%85%8D%E8%B4%B9SSL%E8%AF%81%E4%B9%A6/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/11/06/使用-Let’s-Encrypt-获取免费SSL证书/</id>
    <published>2024-11-06T15:45:00.000Z</published>
    <updated>2024-12-28T16:39:02.380Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>之前不太了解SSL免费证书，只研究过一点自签名SSL证书的<a href="https://blog.csdn.net/shihengzhen101/article/details/134724042" target="_blank" rel="noopener">知识</a>，前几年的12306就是用的这种证书，访问网站时要求你下载安装并信任根证书，后来在腾讯云上薅了一个免费一年的SSL证书，过期后也就没有再更新，在我印象中SSL证书整的很贵，一般的不涉及隐私新的的交互也不需要非得 <code>HTTPS</code>。</p><p>从今年我开始接触到了更多的SSL证书知识，原来腾讯云上的SSL证书过期后还可以免费申请，AWS上也有可以自动续期的免费SSL证书，甚至还有 <code>Let’s Encrypt</code>、<code>ZeroSSL</code>、<code>SSL For Free</code>、<code>Cloudflare</code>、<code>BuyPass</code> 等众多可以提供免费证书的机构，本文我们来试试 <code>Let’s Encrypt</code> 怎么用。</p><a id="more"></a><p><strong>Let’s Encrypt</strong> 是一个提供 <strong>免费 SSL/TLS 证书</strong> 的认证机构（CA），旨在通过简化证书申请和安装过程来普及 HTTPS 加密，提升互联网安全性。它由 <strong>Internet Security Research Group (ISRG)</strong> 运营，并在全球范围内免费提供数字证书，用于加密网站流量，防止数据在传输过程中被窃取或篡改。</p><p>Let’s Encrypt 是一个非常受欢迎且实用的工具，能够帮助网站管理员免费获得 SSL/TLS 证书并轻松安装，尤其适用于那些希望以低成本保障网站安全的小型网站和个人项目。然而，对于需要更高级别验证（如 EV 证书）的大型网站或企业来说，Let’s Encrypt 可能不完全符合需求。</p><h1 id="申请需求"><a href="#申请需求" class="headerlink" title="申请需求"></a>申请需求</h1><p>我的机器操作系统是 <code>CentOS 7</code>，因为本地还占用了80端口提供web服务，故不能使用<strong>HTTP-01</strong>验证方式，使用证书的是另一台机器，所以这里只需要申请获得证书文件即可，不需要自动部署和更新。</p><p>要在 CentOS 7 上使用 Let’s Encrypt 并通过 DNS 验证申请 SSL 证书，而不依赖 80 端口，可以选择 Certbot 的 <strong>DNS 验证模式</strong>。这种方法适用于可以访问域名的 DNS 管理，并手动添加验证记录。完成验证后，您可以手动将生成的证书文件复制到目标机器上进行安装。</p><h1 id="详细步骤："><a href="#详细步骤：" class="headerlink" title="详细步骤："></a>详细步骤：</h1><h2 id="安装-Certbot"><a href="#安装-Certbot" class="headerlink" title="安装 Certbot"></a>安装 Certbot</h2><p>首先，在 CentOS 7 上安装 Certbot：</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">sudo yum install epel-release -y</span><br><span class="line">sudo yum install certbot -y</span><br></pre></td></tr></table></figure><h2 id="使用-DNS-验证生成证书"><a href="#使用-DNS-验证生成证书" class="headerlink" title="使用 DNS 验证生成证书"></a>使用 DNS 验证生成证书</h2><ol><li><p>使用以下命令启动 DNS 验证模式：</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">sudo certbot certonly --manual --preferred-challenges dns -d api.moon.com</span><br></pre></td></tr></table></figure><ul><li><code>--manual</code>：表示手动输入 DNS 记录</li><li><code>--preferred-challenges dns</code>：指定使用 DNS 验证方式</li><li><code>-d api.moon.com</code>：需要申请证书的域名</li></ul></li><li><p>Certbot 会提示创建一个特定的 DNS TXT 记录来验证域名。您会看到类似以下的提示信息：</p><figure class="highlight plain"><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">Please deploy a DNS TXT record under the name</span><br><span class="line">_acme-challenge.api.moon.com with the following value:</span><br><span class="line"></span><br><span class="line">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</span><br><span class="line"></span><br><span class="line">Before continuing, verify the record is deployed.</span><br></pre></td></tr></table></figure></li><li><p>登录自己的 DNS 提供商控制台，在 <code>api.moon.com</code> 的 DNS 配置中添加一个 TXT 记录：</p><ul><li><strong>名称</strong>：<code>_acme-challenge.api.moon.com</code></li><li><strong>类型</strong>：TXT</li><li><strong>值</strong>：Certbot 提供的值（如上所示的随机字符串）</li></ul></li><li><p>等待几分钟，确保新添加的 DNS 记录已经传播。您可以使用以下命令确认 DNS 记录是否生效：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nslookup -<span class="built_in">type</span>=TXT _acme-challenge.api.moon.com</span><br></pre></td></tr></table></figure></li><li><p>一旦 DNS 记录生效并验证成功，返回到 Certbot 命令窗口并按下回车键继续。Certbot 会自动完成验证并生成 SSL 证书文件。</p></li></ol><h2 id="找到生成的证书文件"><a href="#找到生成的证书文件" class="headerlink" title="找到生成的证书文件"></a>找到生成的证书文件</h2><p>验证成功后，Certbot 会将证书文件存放在 <code>/etc/letsencrypt/live/api.moon.com/</code> 目录下，包括以下文件：</p><ul><li><strong><code>fullchain.pem</code></strong>：完整的证书链</li><li><strong><code>privkey.pem</code></strong>：私钥文件</li></ul><h2 id="将证书文件复制到目标服务器"><a href="#将证书文件复制到目标服务器" class="headerlink" title="将证书文件复制到目标服务器"></a>将证书文件复制到目标服务器</h2><p>使用 <code>scp</code> 或其他工具将生成的证书文件复制到目标服务器，比如：</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">scp /etc/letsencrypt/live/api.moon.com/fullchain.pem user@target_server:/path/to/nginx/ssl/</span><br><span class="line">scp /etc/letsencrypt/live/api.moon.com/privkey.pem user@target_server:/path/to/nginx/ssl/</span><br></pre></td></tr></table></figure><h2 id="在目标服务器上配置-Nginx-使用证书"><a href="#在目标服务器上配置-Nginx-使用证书" class="headerlink" title="在目标服务器上配置 Nginx 使用证书"></a>在目标服务器上配置 Nginx 使用证书</h2><p>在目标服务器上，编辑 Nginx 配置文件以使用新的证书：</p><figure class="highlight nginx"><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="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">443</span> ssl;</span><br><span class="line">    <span class="attribute">server_name</span> api.moon.com;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">ssl_certificate</span> /path/to/nginx/ssl/fullchain.pem;</span><br><span class="line">    <span class="attribute">ssl_certificate_key</span> /path/to/nginx/ssl/privkey.pem;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 其他 Nginx 配置</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>保存文件并重新加载 Nginx 配置：</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">sudo nginx -s reload</span><br></pre></td></tr></table></figure><h1 id="注意更新证书"><a href="#注意更新证书" class="headerlink" title="注意更新证书"></a>注意更新证书</h1><p>Let’s Encrypt 证书的有效期为 90 天。之后，可以在原服务器上再次申请新的证书，然后将新的证书文件传输到目标服务器进行更新。</p><h1 id="本地操作实例"><a href="#本地操作实例" class="headerlink" title="本地操作实例"></a>本地操作实例</h1><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><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">[root@VM-0-3-centos ssls]<span class="comment"># sudo certbot certonly --manual --preferred-challenges dns -d 008ct.top</span></span><br><span class="line">Saving debug <span class="built_in">log</span> to /var/<span class="built_in">log</span>/letsencrypt/letsencrypt.log</span><br><span class="line">Plugins selected: Authenticator manual, Installer None</span><br><span class="line">Starting new HTTPS connection (1): acme-v02.api.letsencrypt.org</span><br><span class="line">Requesting a certificate <span class="keyword">for</span> 008ct.top</span><br><span class="line">Performing the following challenges:</span><br><span class="line">dns-01 challenge <span class="keyword">for</span> 008ct.top</span><br><span class="line"></span><br><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">Please deploy a DNS TXT record under the name</span><br><span class="line">_acme-challenge.008ct.top with the following value:</span><br><span class="line"></span><br><span class="line">UYYkUEGZRuWX-2OfS2PcVwll7nmJurxjfqKhkRSxO9k</span><br><span class="line"></span><br><span class="line">Before continuing, verify the record is deployed.</span><br><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">Press Enter to Continue</span><br><span class="line">Waiting <span class="keyword">for</span> verification...</span><br><span class="line">Cleaning up challenges</span><br><span class="line">Subscribe to the EFF mailing list (email: 347070901@qq.com).</span><br><span class="line">Starting new HTTPS connection (1): supporters.eff.org</span><br><span class="line"></span><br><span class="line">IMPORTANT NOTES:</span><br><span class="line"> - Congratulations! Your certificate and chain have been saved at:</span><br><span class="line">   /etc/letsencrypt/live/008ct.top/fullchain.pem</span><br><span class="line">   Your key file has been saved at:</span><br><span class="line">   /etc/letsencrypt/live/008ct.top/privkey.pem</span><br><span class="line">   Your certificate will expire on 2025-02-04. To obtain a new or</span><br><span class="line">   tweaked version of this certificate <span class="keyword">in</span> the future, simply run</span><br><span class="line">   certbot again. To non-interactively renew *all* of your</span><br><span class="line">   certificates, run <span class="string">"certbot renew"</span></span><br><span class="line"> - If you like Certbot, please consider supporting our work by:</span><br><span class="line"></span><br><span class="line">   Donating to ISRG / Let<span class="string">'s Encrypt:   https://letsencrypt.org/donate</span></span><br><span class="line"><span class="string">   Donating to EFF:                    https://eff.org/donate-le</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">[root@VM-0-3-centos ssls]#</span></span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li><code>Let’s Encrypt</code> 是一个提供免费 SSL/TLS 证书的认证机构（CA），旨在通过简化证书申请和安装过程来普及 HTTPS 加密，提升互联网安全性</li><li>同类型的机构还有 <code>ZeroSSL</code>、<code>SSL For Free</code>、<code>Cloudflare</code>、<code>BuyPass</code> 等等，腾讯云和AWS也有针对自己产品的SSL免费证书</li><li>使用<code>Let’s Encrypt</code>申请SSL证书非常简单，可以选择 <code>DNS-01</code> 验证或者 <code>HTTP-01</code> 验证</li><li>如果证书用于本机的 <code>Nginx</code> 或者 <code>Apache</code>，还可以设置自动更新，这样就免除了服务有可能中断的麻烦</li><li>生成证书的命令 <code>sudo certbot certonly --manual --preferred-challenges dns -d api.xxx.com</code></li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143524034" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>无人扶我青云志，我自踏雪至山巅。单靠自己很难，单靠自己成功更难~</p><p>2024-11-6 19:21:14</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;之前不太了解SSL免费证书，只研究过一点自签名SSL证书的&lt;a href=&quot;https://blog.csdn.net/shihengzhen101/article/details/134724042&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;知识&lt;/a&gt;，前几年的12306就是用的这种证书，访问网站时要求你下载安装并信任根证书，后来在腾讯云上薅了一个免费一年的SSL证书，过期后也就没有再更新，在我印象中SSL证书整的很贵，一般的不涉及隐私新的的交互也不需要非得 &lt;code&gt;HTTPS&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;从今年我开始接触到了更多的SSL证书知识，原来腾讯云上的SSL证书过期后还可以免费申请，AWS上也有可以自动续期的免费SSL证书，甚至还有 &lt;code&gt;Let’s Encrypt&lt;/code&gt;、&lt;code&gt;ZeroSSL&lt;/code&gt;、&lt;code&gt;SSL For Free&lt;/code&gt;、&lt;code&gt;Cloudflare&lt;/code&gt;、&lt;code&gt;BuyPass&lt;/code&gt; 等众多可以提供免费证书的机构，本文我们来试试 &lt;code&gt;Let’s Encrypt&lt;/code&gt; 怎么用。&lt;/p&gt;
    
    </summary>
    
      <category term="Web" scheme="http://AlbertGithubHome.github.io/blog/categories/Web/"/>
    
    
      <category term="Nginx" scheme="http://AlbertGithubHome.github.io/blog/tags/Nginx/"/>
    
      <category term="Let’s-Encrypt" scheme="http://AlbertGithubHome.github.io/blog/tags/Let%E2%80%99s-Encrypt/"/>
    
      <category term="SSL证书" scheme="http://AlbertGithubHome.github.io/blog/tags/SSL%E8%AF%81%E4%B9%A6/"/>
    
      <category term="免费" scheme="http://AlbertGithubHome.github.io/blog/tags/%E5%85%8D%E8%B4%B9/"/>
    
      <category term="CentOS7" scheme="http://AlbertGithubHome.github.io/blog/tags/CentOS7/"/>
    
  </entry>
  
  <entry>
    <title>若Git子模块的远端地址发生了变化本地应该怎么调整</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/11/06/%E8%8B%A5Git%E5%AD%90%E6%A8%A1%E5%9D%97%E7%9A%84%E8%BF%9C%E7%AB%AF%E5%9C%B0%E5%9D%80%E5%8F%91%E7%94%9F%E4%BA%86%E5%8F%98%E5%8C%96%E6%9C%AC%E5%9C%B0%E5%BA%94%E8%AF%A5%E6%80%8E%E4%B9%88%E8%B0%83%E6%95%B4/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/11/06/若Git子模块的远端地址发生了变化本地应该怎么调整/</id>
    <published>2024-11-05T16:15:00.000Z</published>
    <updated>2024-12-28T16:36:37.179Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>这个问题复杂在既有Git又有子模块，本身Git的门槛就稍微高一点，再加上子模块的运用，一旦出现这种远端地址发生修改的情况会让人有些懵，不知道怎么处理，通常会乱改一通，有时候好使有时候不好使，并不清楚其中的缘由，退一步讲，如果是一个单一的Git库，如果远端地址发生了变化，使用 <code>git remote set-url origin git@new_xxx</code> 命令就可以就行修改，但是这个命令在子模块情况下行不通，为了说明子模块影响哪些配置文件，接下来会简单罗列下git子模块相关命令。</p><a id="more"></a><h1 id="git-submodule-相关命令"><a href="#git-submodule-相关命令" class="headerlink" title="git submodule 相关命令"></a>git submodule 相关命令</h1><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">git submodule add git@github.com:zzz/xxx.git xxx</span><br></pre></td></tr></table></figure><p>执行之后会 clone 该子模块对应的远程项目文件到本地父项目目录下的同名文件夹中（./xxx/），父项目下也会多一个叫 <code>.gitmodules</code> 的文件，其内容大致如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[submodule <span class="string">"xxx"</span>]</span><br><span class="line">    path = xxx</span><br><span class="line">    url = git@github.com:zzz/xxx.git</span><br></pre></td></tr></table></figure><p>同时父项目下的 .git 目录中也会新增 <code>modules/xxx/</code> 目录，里面的内容对应子模块仓库中原有的 .git 目录中的文件，此时虽然子模块目录下的 .git 依然存在，但是已经从一个文件夹变成了文件，内容如下：</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">gitdir: ../.git/modules/xxx</span><br></pre></td></tr></table></figure><p>即指向了父项目的 <code>.git/modules/xxx</code> 目录，如果运行 <code>git config --list</code> 查看项目的配置选项，会发现多了类似下面两行的内容：</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">submodule.xxx.url=git@github.com:zzz/xxx.git</span><br><span class="line">submodule.xxx.active=<span class="literal">true</span></span><br></pre></td></tr></table></figure><p>如果修改 <code>submodule.xxx.url</code> 的值，则会覆盖 <code>.gitmodules</code> 文件中对应的 url 值；</p><p>这其实是主（父）工程 <code>.git/config</code> 文件中的内容，而在主工程的 <code>.git/modules/xxx/config</code> 文件中会有下面这些内容：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[remote <span class="string">"origin"</span>]</span><br><span class="line">    url = git@github.com:zzz/xxx.git</span><br><span class="line">    fetch = +refs/heads/*:refs/remotes/origin/*</span><br></pre></td></tr></table></figure><p>总结来说子模块的远端地址出现在三个文件中 <code>.gitmodules</code>、<code>.git/config</code> 和 <code>.git/modules/xxx/config</code> 记住这些地方，我们后续会用到，这就是让人迷惑的地方。</p><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">git submodule status</span><br></pre></td></tr></table></figure><p>结果格式为:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">b01bf0c72235aba2e92e5c5f5173dd4cae9b374c xxx (heads/master)</span><br></pre></td></tr></table></figure><p>如果将父项目推送到远程仓库（如gitlab），在网页浏览该项目时子模块所在的目录会多一个类似 @b01bf0c 的后缀，即上面查看子模块命令输出内容的 hash 值的前面部分，点击这个目录会跳转到这个子模块对应的仓库地址。</p><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">git submodule deinit</span><br></pre></td></tr></table></figure><p>再次查看时输出会是这样的：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-b01bf0c72235aba2e92e5c5f5173dd4cae9b374c xxx</span><br></pre></td></tr></table></figure><p>在取消初始化子模块后，子模块目录会变为空目录，避免该子模块的文件继续占用空间。</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><p>聊了这么多，总要给出一个解决方案：</p><p><strong> 修改主工程下 <code>.gitmodules</code> 文件内的远端地址 <code>url = git@newdomain.com:zzz/xxx.git</code>，然后执行 <code>git submodule sync</code> 命令即可完成 </strong></p><p>这样操作之后 <code>.git/config</code> 和 <code>.git/modules/xxx/config</code> 文件中的远端的地址会一起被修改掉。</p><p><em>多说一点：</em></p><p>在执行 <code>git submodule init</code> 或 <code>git submodule sync</code> 命令时，Git 会将 <code>.gitmodules</code> 中的配置同步到 <code>.git/config</code> 中，以确保 <code>.git/config</code> 的信息是最新的。</p><p>而在执行 <code>git submodule update</code> 进行更新时，Git 主要使用 <code>.git/config</code> 中的子模块配置来操作子模块。</p><p>当进入子模块目录操作git时，Git使用的是 <code>.git/modules/xxx/config</code> 中的远端地址来进行更新和推送</p><h1 id="怎么保存子模块的版本呢"><a href="#怎么保存子模块的版本呢" class="headerlink" title="怎么保存子模块的版本呢"></a>怎么保存子模块的版本呢</h1><p>了解了子模块的机制，我们知道主工程只是保存了一个子模块工程的版本号，但是前面看了这么多配置文件，里面只有子模块的本地路径和远端地址，没有看到版本号啊？接下来我们找一下：</p><p>首先查看最新的提交commit，得到 <code>5c997695ed383ea52879a17f0ef6944bf99d374f</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ git <span class="built_in">log</span> -1</span><br><span class="line">commit 5c997695ed383ea52879a17f0ef6944bf99d374f (HEAD -&gt; dev, origin/dev)</span><br><span class="line">Author: demo &lt;demo@gameup.com&gt;</span><br><span class="line">Date:   Mon Nov 4 11:42:25 2024 +0800</span><br><span class="line"></span><br><span class="line">    update cmake file</span><br></pre></td></tr></table></figure><p>利用 <code>cat-file -p</code> 命令查看指定 <code>commit id</code>的仓库状态</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ git cat-file -p 5c997695ed383</span><br><span class="line">tree 8499d7cb5c4811918bfc1341bb869d8bb38c40ef</span><br><span class="line">parent b287b7783e1d44a6149de132142b88bf92b95bb3</span><br><span class="line">author demo &lt;demo@gameup.com&gt; 1730691745 +0800</span><br><span class="line">committer demo &lt;demo@gameup.com&gt; 1730691745 +0800</span><br><span class="line"></span><br><span class="line">update cmake file</span><br></pre></td></tr></table></figure><p>从结果中可以看出提交人的信息，父提交id <code>b287b7783e1d44a6149de132142b88bf92b95bb3</code>，以及一棵树tree <code>8499d7cb5c4811918bfc1341bb869d8bb38c40ef</code>，怎么理解这棵树呢？其实可以类比文件系统中的文件夹，每一个commit id实际上对应着项目文件夹的版本，其中包含子文件夹版本和各个文件的版本，那么tree <code>8499d7cb5c4811918bfc1341bb869d8bb38c40ef</code> 可以认为是项目文件夹内所有内容的Hash</p><p>接下来再利用 <code>cat-file -p</code> 命令来看看这棵tree <code>8499d7cb5c4811918bfc1341bb869d8bb38c40ef</code></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></pre></td><td class="code"><pre><span class="line">$ git cat-file -p 8499d7cb5c4811918bfc1341bb869d8bb38c40ef</span><br><span class="line">100644 blob 344bc6e4d42e8fbae98a8d808b177b6773f88d31    .gitattributes</span><br><span class="line">100644 blob c634b5c2da75f0eed128c701530afa5b2b799c69    .gitignore</span><br><span class="line">100644 blob ba0ab2fd274f219c0e007177ad861f77a57581b0    .gitmodules</span><br><span class="line">040000 tree 90317b8c9672803678037d9f006ea6818d90210b    .vscode</span><br><span class="line">040000 tree 0706cb16e4c162b194680972ab7605654a1541a1    cmake</span><br><span class="line">160000 commit cec7534dcb171d38d46ba47217694261ad4c7b15  xxx</span><br><span class="line">040000 tree 2c693865de8c02cd87f28fe85ba5e57a73617029    src</span><br></pre></td></tr></table></figure><p>这里边有3类内容，分别是 <code>blob</code>、<code>tree</code>、<code>commit</code>，blob就是具体的文件，tree就是代表一个文件夹，而 <code>xxx</code> 的类型是一个commit，从上文我们知道 <code>xxx</code> 是包含了另一个项目的文件夹，如果是一个单纯的文件夹这里的类型应该是tree，但是因为是子模块，所以类型是记录另一个项目commit id，这样我们就找到了主工程引用子模块的版本存储的位置了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>添加子模块的命令 <code>git submodule add git@github.com:zzz/xxx.git xxx</code></li><li>初始子模块 <code>git submodule init</code>，查看子模块 <code>git submodule status</code></li><li>注销子模块 <code>git submodule deinit xxx</code>，注销后文件夹清空，但是配置文件需要手动删除</li><li>子模块远端地址发生变化，手动修改 <code>.gitmodules</code> 文件中地址，然后执行 <code>git submodule sync</code> 再提交修改</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143493367" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>放下助人情节，尊重他人命运，可怜之人必有可恨之处，子非鱼，安知鱼之经历与内心~</p><p>2024-11-5 19:49:42</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;这个问题复杂在既有Git又有子模块，本身Git的门槛就稍微高一点，再加上子模块的运用，一旦出现这种远端地址发生修改的情况会让人有些懵，不知道怎么处理，通常会乱改一通，有时候好使有时候不好使，并不清楚其中的缘由，退一步讲，如果是一个单一的Git库，如果远端地址发生了变化，使用 &lt;code&gt;git remote set-url origin git@new_xxx&lt;/code&gt; 命令就可以就行修改，但是这个命令在子模块情况下行不通，为了说明子模块影响哪些配置文件，接下来会简单罗列下git子模块相关命令。&lt;/p&gt;
    
    </summary>
    
      <category term="Git" scheme="http://AlbertGithubHome.github.io/blog/categories/Git/"/>
    
    
      <category term="git" scheme="http://AlbertGithubHome.github.io/blog/tags/git/"/>
    
      <category term="commit" scheme="http://AlbertGithubHome.github.io/blog/tags/commit/"/>
    
      <category term="submodule" scheme="http://AlbertGithubHome.github.io/blog/tags/submodule/"/>
    
      <category term="cat-file" scheme="http://AlbertGithubHome.github.io/blog/tags/cat-file/"/>
    
      <category term="remote-url" scheme="http://AlbertGithubHome.github.io/blog/tags/remote-url/"/>
    
  </entry>
  
  <entry>
    <title>Gitlab新建用户无法收到邮件的问题解决办法</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/11/05/Gitlab%E6%96%B0%E5%BB%BA%E7%94%A8%E6%88%B7%E6%97%A0%E6%B3%95%E6%94%B6%E5%88%B0%E9%82%AE%E4%BB%B6%E7%9A%84%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/11/05/Gitlab新建用户无法收到邮件的问题解决办法/</id>
    <published>2024-11-04T17:00:00.000Z</published>
    <updated>2024-12-28T16:34:23.555Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>这个问题网上解决的帖子很多，根本原因就是配置错误，修改好配置就可以了，我也没有其他的更加新奇的解决办法，之所以再总结一遍只是为了自己统一查找方便，同时把一些差异点记录一下，因为版本和运行环境的不同，可能跟网上的解决方案有一点点差异，先写答案再聊不同吧。</p><a id="more"></a><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><ol><li><p>开启企业微信邮箱（QQ邮箱、163邮箱亦可）的POP3/SMTP服务，获取授权码</p></li><li><p>修改gitlab的配置文件 <code>/etc/gitlab/gitlab.rb</code> 如下</p></li></ol><figure class="highlight rb"><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">gitlab_rails[<span class="string">'smtp_enable'</span>] = <span class="literal">true</span></span><br><span class="line">gitlab_rails[<span class="string">'smtp_address'</span>] = <span class="string">"smtp.exmail.qq.com"</span></span><br><span class="line">gitlab_rails[<span class="string">'smtp_port'</span>] = <span class="number">465</span></span><br><span class="line">gitlab_rails[<span class="string">'smtp_user_name'</span>] = <span class="string">"gitlab@gameup.com"</span></span><br><span class="line">gitlab_rails[<span class="string">'smtp_password'</span>] = <span class="string">"授权码"</span></span><br><span class="line">gitlab_rails[<span class="string">'smtp_domain'</span>] = <span class="string">"exmail.qq.com"</span></span><br><span class="line">gitlab_rails[<span class="string">'smtp_authentication'</span>] = <span class="string">"login"</span></span><br><span class="line">gitlab_rails[<span class="string">'smtp_enable_starttls_auto'</span>] = <span class="literal">false</span></span><br><span class="line">gitlab_rails[<span class="string">'smtp_tls'</span>] = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line">gitlab_rails[<span class="string">'gitlab_email_enabled'</span>] = <span class="literal">true</span></span><br><span class="line">gitlab_rails[<span class="string">'gitlab_email_from'</span>] = <span class="string">'gitlab@gameup.com'</span></span><br><span class="line">gitlab_rails[<span class="string">'gitlab_email_display_name'</span>] = <span class="string">'Gitlab'</span></span><br></pre></td></tr></table></figure><ol><li><p>重新加载配置生效 <code>gitlab-ctl reconfigure</code>，等待执行完成</p></li><li><p>利用gitlab测试配置是否成功</p><ul><li>执行 gitlab-rails console进入控制台，我的机器性能较差，多等一会看到命令提示符</li><li>输入后面面的命令 <code>Notify.test_email(&#39;收件人邮箱&#39;, &#39;邮件标题&#39;, &#39;正文&#39;).deliver_now</code> 发送一封测试邮件</li><li>若成功过则收到测试邮件，失败则会在控制台显示错误消息，根据错误信息检查和修改配置即可</li></ul></li></ol><h1 id="遇到的错误"><a href="#遇到的错误" class="headerlink" title="遇到的错误"></a>遇到的错误</h1><p>我修改配置的旅程也不是一帆风顺的，期间也遇到的了几个错误，都是在 <code>Notify.test_email</code> 测试之后发现有问题，逐步修改的</p><h2 id="域名解析问题"><a href="#域名解析问题" class="headerlink" title="域名解析问题"></a>域名解析问题</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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></pre></td><td class="code"><pre><span class="line">Notify.test_email(<span class="string">'shz@gamegu.com'</span>, <span class="string">'Test Email'</span>, <span class="string">'This is a test email from GitLab'</span>).deliver_now</span><br><span class="line">--------------------------------------------------------------------------------</span><br><span class="line"> Ruby:         ruby 3.2.5 (2024-07-26 revision 31d0f1a2e7) [x86_64-linux]</span><br><span class="line"> GitLab:       17.5.1 (e8dca573167) FOSS</span><br><span class="line"> GitLab Shell: 14.39.0</span><br><span class="line"> PostgreSQL:   14.11</span><br><span class="line">------------------------------------------------------------[ booted <span class="keyword">in</span> 61.07s ]</span><br><span class="line">Loading production environment (Rails 7.0.8.4)</span><br><span class="line">Delivered mail 67248a5c847e9_6f32fe445660@gitlab.gamegu.com.mail (20023.7ms)</span><br><span class="line">/opt/gitlab/embedded/lib/ruby/3.2.0/socket.rb:231:<span class="keyword">in</span> getaddrinfo<span class="string">': getaddrinfo: Temporary failure in name resolution (SocketError)</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/3.2.0/socket.rb:231:in foreach'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/3.2.0/socket.rb:635:<span class="keyword">in</span> tcp<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:643:in tcp_socket'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:656:<span class="keyword">in</span> do_start<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:611:in start'</span></span><br><span class="line">        from /opt/gitlab/embedded/service/gitlab-rails/config/initializers/mail_starttls_patch.rb:53:<span class="keyword">in</span> start_smtp_session<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/network/delivery_methods/smtp.rb:100:in deliver!'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/message.rb:2145:<span class="keyword">in</span> do_delivery<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/message.rb:253:in block in deliver'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/actionmailer-7.0.8.4/lib/action_mailer/base.rb:588:<span class="keyword">in</span> block <span class="keyword">in</span> deliver_mail<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/notifications.rb:206:in block in instrument'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/notifications/instrumenter.rb:24:<span class="keyword">in</span> instrument<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/notifications.rb:206:in instrument'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/actionmailer-7.0.8.4/lib/action_mailer/base.rb:586:<span class="keyword">in</span> deliver_mail<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/message.rb:253:in deliver'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/actionmailer-7.0.8.4/lib/action_mailer/message_delivery.rb:119:<span class="keyword">in</span> block <span class="keyword">in</span> deliver_now<span class="string">'</span></span><br><span class="line"><span class="string">        ... 15 levels...</span></span><br><span class="line"><span class="string">irb(main):002:0&gt;</span></span><br></pre></td></tr></table></figure><h3 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h3><p>修改 <code>/etc/resolv.conf</code> 来指定自己服务器的DNS，通用的可以使用 <code>nameserver 8.8.8.8</code></p><h2 id="邮箱认证问题"><a href="#邮箱认证问题" class="headerlink" title="邮箱认证问题"></a>邮箱认证问题</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">/opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:1076:<span class="keyword">in</span> check_response<span class="string">': 501 mail from address must be same as authorization user (Net::SMTPSyntaxError)</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:1044:in getok'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:945:<span class="keyword">in</span> mailfrom<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:767:in send_message'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/network/delivery_methods/smtp_connection.rb:53:<span class="keyword">in</span> deliver!<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/network/delivery_methods/smtp.rb:101:in block in deliver!'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:612:<span class="keyword">in</span> start<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/service/gitlab-rails/config/initializers/mail_starttls_patch.rb:53:in start_smtp_session'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/network/delivery_methods/smtp.rb:100:<span class="keyword">in</span> deliver!<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/message.rb:2145:in do_delivery'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/message.rb:253:<span class="keyword">in</span> block <span class="keyword">in</span> deliver<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/actionmailer-7.0.8.4/lib/action_mailer/base.rb:588:in block in deliver_mail'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/notifications.rb:206:<span class="keyword">in</span> block <span class="keyword">in</span> instrument<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/notifications/instrumenter.rb:24:in instrument'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/notifications.rb:206:<span class="keyword">in</span> instrument<span class="string">'</span></span><br><span class="line"><span class="string">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/actionmailer-7.0.8.4/lib/action_mailer/base.rb:586:in deliver_mail'</span></span><br><span class="line">        from /opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/mail-2.8.1/lib/mail/message.rb:253:<span class="keyword">in</span> deliver<span class="string">'</span></span><br><span class="line"><span class="string">        ... 16 levels...</span></span><br></pre></td></tr></table></figure><h3 id="解决方案-1"><a href="#解决方案-1" class="headerlink" title="解决方案"></a>解决方案</h3><p><code>smtp</code> 配置中应该填写正确的邮箱和授权码，同时 <code>/etc/gitlab/gitlab.rb</code> 中设置 <code>gitlab_rails[&#39;gitlab_email_from&#39;]</code> 与 <code>gitlab_rails[&#39;smtp_user_name&#39;]</code> 一致</p><h2 id="端口配置互斥"><a href="#端口配置互斥" class="headerlink" title="端口配置互斥"></a>端口配置互斥</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab_rails[<span class="string">'smtp_tls'</span>] and gitlab_rails[<span class="string">'smtp_enable_starttls_auto'</span>] are mutually exclusive. Set one of them to <span class="literal">false</span>. SMTP providers usually use port 465 <span class="keyword">for</span> TLS and port 587 <span class="keyword">for</span> STARTTLS.</span><br></pre></td></tr></table></figure><h3 id="解决方案-2"><a href="#解决方案-2" class="headerlink" title="解决方案"></a>解决方案</h3><p>这个错误表明在 GitLab 的配置文件中同时启用了 <code>smtp_tls</code> 和 <code>smtp_enable_starttls_auto</code> 选项，但它们是互斥的，只能选择其中一个设置为 true，如果使用 TLS（通常是端口 465）仅将 <code>smtp_tls</code> 选项设置为 true，如果使用 STARTTLS（通常是端口 587）只能将 <code>smtp_enable_starttls_auto</code> 设置为true。</p><p>关于gitlab的参数，有一个<code>external_url</code> 表示gitlab的访问域名，可以配置为 <code>external_url &#39;http://gitlab.gameup.com&#39;</code>，而这个域名也可以在定义容器时通过 <code>--hostname</code> 指定</p><h1 id="运行环境"><a href="#运行环境" class="headerlink" title="运行环境"></a>运行环境</h1><p>我的Gitlab不是在服务器上直接安装的，而是通过Docker安装的gitlab镜像，服务器是CentOS7系统，gitlab组件相关版本如下：</p><blockquote><p>Components</p><p>GitLab v17.5.1<br>GitLab Shell 14.39.0<br>GitLab Workhorse v17.5.1<br>GitLab API v4<br>GitLab KAS 17.5.1<br>Ruby 3.2.5p208<br>Rails 7.0.8.4<br>PostgreSQL (main) 14.11<br>PostgreSQL (ci) 14.11<br>Redis 7.0.15</p></blockquote><p>正因为我是通过Dokcer安装的，所以上面的很多命令需要到容器中执行，也就是得先执行 <code>docker exec -it gitlab /bin/bash</code></p><p>而通过Docker安装gitlab的命令也列举一下：</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">docker run --detach \</span><br><span class="line">  --hostname gitlab.gameup.com \</span><br><span class="line">  --publish 443:443 --publish 80:80 --publish 22:22 \</span><br><span class="line">  --name gitlab \</span><br><span class="line">  --restart always \</span><br><span class="line">  --volume /<span class="built_in">export</span>/docker/gitlab/config:/etc/gitlab \</span><br><span class="line">  --volume /<span class="built_in">export</span>/docker/gitlab/logs:/var/<span class="built_in">log</span>/gitlab \</span><br><span class="line">  --volume /<span class="built_in">export</span>/docker/gitlab/data:/var/opt/gitlab \</span><br><span class="line">  registry.cn-hangzhou.aliyuncs.com/z5z/gitlab-ce:latest</span><br></pre></td></tr></table></figure><p>不要问我这个镜像名字为什么这样古怪，那么因为DockerHub镜像在当前的网络环境下实在难以下载，不得不借助Github的Actions和阿里云的镜像完成中转，可以利用这个<a href="https://github.com/AlbertGithubHome/carry-docker-image-mirror" target="_blank" rel="noopener">开源项目</a> 进行中转</p><h1 id="docker安装"><a href="#docker安装" class="headerlink" title="docker安装"></a>docker安装</h1><p>上面说了镜像难装，实际上现在连Docker的安装也需要费点劲，我曾经在AWS上安装过一些Docker镜像，那丝滑的程度真的跟教科书写的一模一样，但是在我目前所处的网络注定要一波三折了，CentOS7的系统可以按照下面的步骤操作了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><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="comment"># 备份源</span></span><br><span class="line">mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak</span><br><span class="line"><span class="comment"># yum使用阿里云</span></span><br><span class="line">curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo</span><br><span class="line">sudo yum makecache</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装必要的一些系统工具</span></span><br><span class="line">sudo yum install -y wget yum-utils device-mapper-persistent-data lvm2</span><br><span class="line"><span class="comment"># 添加软件源信息</span></span><br><span class="line">sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo</span><br><span class="line"><span class="comment"># 修改源</span></span><br><span class="line">sudo sed -i <span class="string">'s+download.docker.com+mirrors.aliyun.com/docker-ce+'</span> /etc/yum.repos.d/docker-ce.repo</span><br><span class="line"></span><br><span class="line"><span class="comment"># 更新并安装Docker-CE</span></span><br><span class="line">sudo yum makecache fast</span><br><span class="line">sudo yum -y install docker-ce</span><br><span class="line"><span class="comment"># 开启Docker服务</span></span><br><span class="line">sudo systemctl start docker</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>gitlab的配置文件为 <code>/etc/gitlab/gitlab.rb</code>， 发邮件需要配置 <code>smtp</code> 和 <code>email</code> 参数</li><li><code>smtp_tls</code> 和 <code>smtp_enable_starttls_auto</code> 选项是互斥的，只能选择其中一个设置为 true</li><li>进入到docker镜像内部的命令为 <code>docker exec -it gitlab /bin/bash</code></li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143436661" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>他时若遂凌云志，敢笑黄巢不丈夫。伟人可以被人们敬仰，也可以被用来作为登天的阶梯，借他人之成就，抒发自己之情怀~</p><p>2024-11-4 20:34:11</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;这个问题网上解决的帖子很多，根本原因就是配置错误，修改好配置就可以了，我也没有其他的更加新奇的解决办法，之所以再总结一遍只是为了自己统一查找方便，同时把一些差异点记录一下，因为版本和运行环境的不同，可能跟网上的解决方案有一点点差异，先写答案再聊不同吧。&lt;/p&gt;
    
    </summary>
    
      <category term="Git" scheme="http://AlbertGithubHome.github.io/blog/categories/Git/"/>
    
    
      <category term="docker" scheme="http://AlbertGithubHome.github.io/blog/tags/docker/"/>
    
      <category term="gitlab" scheme="http://AlbertGithubHome.github.io/blog/tags/gitlab/"/>
    
      <category term="email" scheme="http://AlbertGithubHome.github.io/blog/tags/email/"/>
    
      <category term="smtp" scheme="http://AlbertGithubHome.github.io/blog/tags/smtp/"/>
    
      <category term="阿里云" scheme="http://AlbertGithubHome.github.io/blog/tags/%E9%98%BF%E9%87%8C%E4%BA%91/"/>
    
  </entry>
  
  <entry>
    <title>整个互联网发展de云里雾里</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/11/01/%E6%95%B4%E4%B8%AA%E4%BA%92%E8%81%94%E7%BD%91%E5%8F%91%E5%B1%95de%E4%BA%91%E9%87%8C%E9%9B%BE%E9%87%8C/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/11/01/整个互联网发展de云里雾里/</id>
    <published>2024-11-01T15:45:00.000Z</published>
    <updated>2024-12-28T16:32:12.836Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>本来被腾讯云的配置调整折腾两天了，一个实例在不同的界面有不同的价格，调整配置前后我都弄不清到底便宜了多少，有些区域因老架构缺货不得不选择低配的新架构，结果费用反而上涨了，比如本来想把<code>S2 32核 128G内存</code>的机器降低的到 <code>24核96G内存</code>，但S2高内存实例已经下架了，只能使用选择S5，好家伙！新的 S5.6XLARGE96 比目前用的 S2.8XLARGE128 还贵，配置降了1/3，价格反而上去了，真是“降本”降了个寂寞。</p><a id="more"></a><h1 id="各种云层出不穷"><a href="#各种云层出不穷" class="headerlink" title="各种云层出不穷"></a>各种云层出不穷</h1><p>平时主要就用阿里云和腾讯云，很早之前用过一阵儿华为云，今天在搜腾讯云的解决方案时弹出个京东云的广告，还挺新鲜的，像天翼云、联通云这些虽然没用过，不过广告看的不少，但京东云这个确实是第一次听说，好奇心促使我搜了一下发现中国互联网，甚至物联网已经被“云”统治了。</p><h2 id="国内云"><a href="#国内云" class="headerlink" title="国内云"></a>国内云</h2><p>目前中国的云服务器市场发展迅速，主要的云服务器提供商包括一些大型互联网企业和专注于云服务的公司。以下是国内主要的云服务器提供商列表：</p><ol><li><p><strong>阿里云</strong>：中国市场占有率最高的云服务商，提供全面的云计算服务，包括弹性计算、存储、大数据、人工智能等。</p></li><li><p><strong>腾讯云</strong>：腾讯旗下的云计算平台，覆盖广泛的互联网场景，适合游戏、社交、视频、电商等领域。</p></li><li><p><strong>华为云</strong>：华为推出的云服务平台，擅长于5G、人工智能、物联网和企业级解决方案，广泛应用于政企和大型制造业客户。</p></li><li><p><strong>京东云</strong>：京东旗下的云服务品牌，专注于电商、物流和供应链解决方案，同时提供大数据和人工智能服务。</p></li><li><p><strong>百度智能云</strong>：百度的云服务品牌，以人工智能和大数据分析见长，适用于智能客服、自动驾驶、深度学习等AI场景。</p></li><li><p><strong>金山云</strong>：金山软件旗下的云计算品牌，广泛服务于互联网、游戏、视频等行业，提供计算、存储和网络加速服务。</p></li><li><p><strong>中国电信天翼云</strong>：中国电信的云服务品牌，具有较强的政企客户基础，在基础设施和专属线路服务上具有优势。</p></li><li><p><strong>中国联通沃云</strong>：中国联通的云计算品牌，提供云计算、边缘计算、物联网解决方案，在通信行业有较多应用。</p></li><li><p><strong>中国移动云（移动云）</strong>：中国移动的云计算平台，结合5G和物联网服务，在智能城市、物联网等场景有较多应用。</p></li><li><p><strong>UCloud</strong>：主要面向中小企业的独立云服务商，提供计算、存储和安全服务，较多应用于移动互联网、游戏和教育等行业。</p></li><li><p><strong>青云QingCloud</strong>：以IaaS起家，逐步向PaaS和SaaS拓展，专注于企业级客户，提供多云管理平台和私有云方案。</p></li><li><p><strong>七牛云</strong>：起初以对象存储和CDN见长，现在扩展至人工智能、数据处理和多媒体领域。</p></li><li><p><strong>灵雀云（Alauda）</strong>：专注于容器云和Kubernetes，主要面向企业提供云原生解决方案。</p></li><li><p><strong>浪潮云</strong>：浪潮集团的云计算品牌，主要提供政企客户和行业解决方案，尤其在政务和企业数字化转型方面有较多案例。</p></li><li><p><strong>用友云</strong>：专注于企业管理软件的云服务商，提供财务、供应链、人力资源等企业服务云。</p></li><li><p><strong>富通云腾</strong>：主要提供私有云、混合云服务，专注于金融、教育、医疗等行业的云解决方案。</p></li><li><p><strong>紫光云</strong>：紫光集团旗下云服务平台，面向智慧城市、工业互联网等领域，擅长政企数字化解决方案。</p></li><li><p><strong>晓通云</strong>：专注于为中小企业和政企客户提供公有云和私有云服务，涉及政务、教育和医疗等领域。</p></li></ol><p>还好还好，只有6个公司没听过，都是一些技术公司，感觉是把自己没用完的服务器放云上了。</p><h2 id="国际云"><a href="#国际云" class="headerlink" title="国际云"></a>国际云</h2><p>以下是国际上主要的云服务商，提供的服务范围广泛，涵盖计算、存储、网络、安全、人工智能、大数据等各个领域：</p><ol><li><p><strong>Amazon Web Services (AWS)</strong>：亚马逊旗下的云服务平台，也是全球市场占有率最高的云提供商之一。AWS 提供广泛的服务，包括计算、存储、数据库、机器学习等，应用于从小型初创公司到大型企业的各种业务需求。</p></li><li><p><strong>Microsoft Azure</strong>：微软的云计算平台，提供包括计算、存储、网络和开发工具在内的广泛服务。Azure 对于 Windows 和企业应用有强大支持，适合与微软产品（如 Office 365、Dynamics 365）集成，企业级用户众多。</p></li><li><p><strong>Google Cloud Platform (GCP)</strong>：谷歌的云服务平台，以数据分析、机器学习和人工智能服务见长，提供计算、存储、数据库、机器学习和物联网服务，适合大数据和 AI 场景。</p></li><li><p><strong>IBM Cloud</strong>：IBM 的云服务平台，提供混合云和 AI 解决方案，擅长于企业级应用、金融和政府领域，特别在混合云和多云架构上具有优势。</p></li><li><p><strong>Oracle Cloud</strong>：Oracle 的云平台，主要面向数据库、ERP系统和企业应用，广泛应用于金融、零售、制造等领域，提供强大的数据库即服务 (DBaaS) 和企业 SaaS 应用。</p></li><li><p><strong>Alibaba Cloud (阿里云)</strong>：阿里巴巴集团旗下的云计算品牌，也是全球市场的主要云服务商之一。阿里云在电子商务和零售领域有显著优势，提供全球化的服务和解决方案。</p></li><li><p><strong>Salesforce (Salesforce Cloud)</strong>：专注于客户关系管理 (CRM) 的云服务商，主要提供 SaaS 层服务，包括营销、销售、服务等云，广泛应用于各类企业的客户管理和数字营销。</p></li><li><p><strong>Tencent Cloud (腾讯云)</strong>：腾讯旗下的云计算品牌，广泛应用于互联网、游戏、社交媒体等行业，在亚洲市场，尤其是中国市场有较大影响力。</p></li><li><p><strong>SAP Cloud Platform</strong>：SAP 提供的云服务平台，擅长企业应用和ERP管理系统，面向制造业、零售业、金融业等企业级客户。</p></li><li><p><strong>DigitalOcean</strong>：专注于开发者友好的云计算平台，提供简单易用的虚拟机和 Kubernetes 服务，广泛应用于中小企业和初创公司的云服务需求。</p></li><li><p><strong>Rackspace Technology</strong>：提供多云和混合云服务，广泛应用于中小型企业，特别是在托管和技术支持上具有丰富的经验。</p></li><li><p><strong>OVHcloud</strong>：欧洲的云计算公司，以物理服务器、虚拟服务器、存储和托管服务为主，提供面向欧洲客户的本地化服务。</p></li><li><p><strong>Huawei Cloud (华为云)</strong>：华为的云服务平台，除了在中国市场有重要影响力，还在全球范围内提供基础设施和企业级云解决方案。</p></li><li><p><strong>Linode</strong>：主打开发者友好型的云计算服务，专注于提供简单、高效的云服务器和存储服务，适合中小企业和个人开发者。</p></li><li><p><strong>Vultr</strong>：提供全球范围内的云计算服务，以高性价比和分布广泛的数据中心而闻名，特别适合小型项目和个人开发者。</p></li><li><p><strong>NTT Communications Cloud</strong>：日本电信巨头 NTT 提供的云服务，主要面向企业级客户，广泛应用于日本及亚洲市场。</p></li><li><p><strong>Heroku (Salesforce旗下)</strong>：专注于开发者的 PaaS 平台，提供应用托管和部署服务，支持多种编程语言，广泛应用于开发和测试环境。</p></li></ol><p>一看一个不吱声，前面的几个国际大企业还有耳闻，自己家的云也有接触，其他的大多数就不太了解了。</p><h1 id="使用体验"><a href="#使用体验" class="headerlink" title="使用体验"></a>使用体验</h1><p>关于体验方面，因为很多功能我还没有用到，所以现在只是简单的谈一谈，总的来说几家大的云服务商，功能很全面，比如腾讯云、阿里云、AWS、GCP等，从其中一家迁移到另一家时总可以找到对应的服务，名字可能不一样，但东西都是类似的。</p><p>比如我们常说的云服务器，在阿里云叫 <code>ECS（Elastic Compute Service)</code>，在腾讯云叫 <code>CVM（Cloud Virtual Machine）</code>，在AWS叫 <code>EC2（Elastic Compute Cloud）</code>，在GCP叫 <code>GCE（Google Compute Engine）</code>，在Azure上叫 <code>Azure VM（Azure Virtual Machines）</code>，名字千变万化，但内容万变不离其宗。</p><p>其他的服务诸如CDN、OSS、静态IP、RDS等也都是类似的情况，所以你在切换云服务时基本不用担心找不到平替的服务，只是用起来的步骤可能会有些差异，稍微适应一些就好。</p><p>其中有个点对我影响挺大的，那就是各家运营商对于服务器的的监控，也就是CPU、内存、硬盘等硬件的使用曲线，体验了这么多家的云服务我觉得做的最好的是腾讯云。</p><p>如果非得要排个序的话大概是这样： 腾讯云 &gt; GCP &gt; AWS &gt;= 阿里云，为什么这么排，我简单聊下</p><ol><li>腾讯云能查看6个月的历史记录，CPU、内存、硬盘、网络这些默认都可以查看，而且还可以指定和历史哪段时间进行对比</li><li>GCP默认能查看CPU和网络情况，安装Ops Agent后能查看内存和硬盘信息，并且历史查看时间比腾讯云还要长，我查看过超过半年的</li><li>AWS默认开启基本监控，可以查看CPU和网络使用情况，最长可以查看超过1年，想要查看更多信息需要开启消息监控，配合 CloudWatch 可能产生一些费用</li><li>阿里云默认可以查看CPU、内存、硬盘、网络这些信息，但是显示的友好性比不过腾讯云，最长只能查看一个月的</li></ol><p>再有一点就是费用的查询上，也就是详细账单的查看，腾讯云相比阿里云也略胜一筹吧，阿里云查看实例费用时不显示标签，只能看实例ID阅读不友好，AWS的费用查看比较笼统，只有服务项的费用，没有对应到实例</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>互联网时代，云服务百花齐放，AWS、腾讯云、阿里云、GCP、Microsoft Azure蓬勃发展</li><li>各家服务可以平替，<code>ECS</code> vs <code>CVM</code> vs <code>EC2</code> vs <code>Compute Engine</code> vs <code>Azure VM</code></li><li>虽然各家的服务长得很像，但是也各有所长，在服务器监控方面首推腾讯云</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143321440" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>唯有病痛带来的生理痛苦是真实的，其他的痛苦全部都是由你的价值观决定的。忠、孝、礼、义、廉、耻，悔、悟是否重要，完全在于你怎么看它~</p><p>2024-11-1 17:22:48</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;本来被腾讯云的配置调整折腾两天了，一个实例在不同的界面有不同的价格，调整配置前后我都弄不清到底便宜了多少，有些区域因老架构缺货不得不选择低配的新架构，结果费用反而上涨了，比如本来想把&lt;code&gt;S2 32核 128G内存&lt;/code&gt;的机器降低的到 &lt;code&gt;24核96G内存&lt;/code&gt;，但S2高内存实例已经下架了，只能使用选择S5，好家伙！新的 S5.6XLARGE96 比目前用的 S2.8XLARGE128 还贵，配置降了1/3，价格反而上去了，真是“降本”降了个寂寞。&lt;/p&gt;
    
    </summary>
    
      <category term="Thinking" scheme="http://AlbertGithubHome.github.io/blog/categories/Thinking/"/>
    
    
      <category term="aws" scheme="http://AlbertGithubHome.github.io/blog/tags/aws/"/>
    
      <category term="阿里云" scheme="http://AlbertGithubHome.github.io/blog/tags/%E9%98%BF%E9%87%8C%E4%BA%91/"/>
    
      <category term="cloud" scheme="http://AlbertGithubHome.github.io/blog/tags/cloud/"/>
    
      <category term="云" scheme="http://AlbertGithubHome.github.io/blog/tags/%E4%BA%91/"/>
    
      <category term="腾讯云" scheme="http://AlbertGithubHome.github.io/blog/tags/%E8%85%BE%E8%AE%AF%E4%BA%91/"/>
    
      <category term="gcp" scheme="http://AlbertGithubHome.github.io/blog/tags/gcp/"/>
    
  </entry>
  
  <entry>
    <title>SVN克隆或更新遇到Error: Checksum mismatch for xxx</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/10/29/SVN%E5%85%8B%E9%9A%86%E6%88%96%E6%9B%B4%E6%96%B0%E9%81%87%E5%88%B0Error-Checksum-mismatch-for-xxx/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/10/29/SVN克隆或更新遇到Error-Checksum-mismatch-for-xxx/</id>
    <published>2024-10-29T15:45:00.000Z</published>
    <updated>2024-12-28T16:29:54.820Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p><code>TortoiseSVN</code> 作为版本控制常用的工具，有一个更为人们熟知的名字 <code>SVN</code>，客观的讲SVN的门槛相比Git而言还是低一些的，用来存储一些文件并保留历史记录比较方便，但使用SVN还是会经常需要这样那样的问题，特别是当仓库很大的时候，提示的问题往往让初学者、甚至是经常使用的人一脸懵，比如这个 <code>Checksum mismatch for</code> 问题，通常SVN问题都有一个终极解法，那就是重新克隆一份再操作，这就像大多数的电脑问题重启后就能解决一样，不过偶尔有一些情景不适合采用重新克隆的解法，我们就得想一个更精确的办法去解决了。</p><a id="more"></a><h1 id="问题的产生"><a href="#问题的产生" class="headerlink" title="问题的产生"></a>问题的产生</h1><p>在克隆一个300多G的库时，报了下面这个错误，总结来说这个错误表述的就是“心有所求而不得”，期望得到一个md5是 <code>2c448c9f40b0dd561539b80ec3cfcaa2</code> 的 <code>bundle_103_cp_delafere_e_buff_wuqi.ab</code>文件，但是下载后的文件计算md5得到的是 <code>a2e41ec2779a23d88b31e4237ad43ceb</code>，并不是自己想要的，提示如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Error: Checksum mismatch <span class="keyword">for</span></span><br><span class="line">Error:  <span class="string">'E:\gameproject\prefabs\bundle_103_cp_delafere_e_buff_wuqi.ab'</span>:</span><br><span class="line">Error:</span><br><span class="line">Error:    expected:  2c448c9f40b0dd561539b80ec3cfcaa2</span><br><span class="line">Error:      actual:  a2e41ec2779a23d88b31e4237ad43ceb</span><br></pre></td></tr></table></figure><h1 id="探索解决方案"><a href="#探索解决方案" class="headerlink" title="探索解决方案"></a>探索解决方案</h1><p>先说说网上一些常见的解决办法：</p><ol><li>删除了整个库，重新克隆下载，这个方法99%可行，但是整个库太大了，我不想重新下载</li><li><code>Clean up</code> 之后继续更新，我试过了不行</li><li>先在出错的文件夹下执行 <code>svn update --set-depth empty</code> 删除文件夹下所有文件，再执行 <code>svn update --set-depth infinity</code> 更新，这是流传的最广的解决办法，在我这不行</li><li>先在 <code>Update to revision...</code> 中选择 Update Depth 为 <code>Only this item</code> 更新，然后再选择 <code>Fully recursive</code> 更新，原理上和上一步类似，在我这也不行</li></ol><p>其实仔细分析这几种解决方案的本质，都是把错误的文件删除掉再重新更新，但是为什么不起作用呢？</p><p>从我最终解决问题的方法来看是，针对我遇到的问题，上面提到的一些方法并没有将错误的文件成功删除，简单分析下svn克隆和更新的过程，很可能不准确，但是我不在此处不深究，暂时还用不到这一块。</p><p>我们在传输文件时为了保证文件的完整性，经常会用一些摘要算法比如md5、sha1等来计算文件摘要，通过比较文件摘要来判断文件传输是否完整，而SVN这个工具每时每刻都在上传下载传输文件，所以也用到了摘要比对的技术。</p><p>以较新的SVN版本为例，是将sha1和md5存到了<code>.svn/wc.db</code>中（太老的版本md5是存储到文件中的），下载时先更新 <code>wc.db</code>，然后将原始文件的元文件下载到 <code>.svn/pristine</code> 文件夹中，然后比较这些文件的md5值与 <code>wc.db</code> 记录的md5值是否一致，如果一样就从元文件中提取出真正的文件放到库中，这个文件就算下载完了，如果不一致就会报出上面的错误。</p><p>分析过后我们只要找到错误的元文件，删除后重新下载就解决了。</p><h1 id="正式的解决方法"><a href="#正式的解决方法" class="headerlink" title="正式的解决方法"></a>正式的解决方法</h1><p>首先打开从 <code>.svn/wc.db</code> 中查找的元文件的名字，可以使用你熟悉的工具，我用的是 <code>SQLiteSpy.exe</code>有图形界面方便一下，最基础的也可以用 <code>sqlite3.exe</code> 这种命令行的工具，执行以下sql语句：</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">select</span> * <span class="keyword">from</span> NODES <span class="keyword">where</span> local_relpath = <span class="string">'prefabs/bundle_103_cp_delafere_e_buff_wuqi.ab'</span></span><br></pre></td></tr></table></figure><p>执行过后可以查到checksum列的值为 <code>$sha1$f9fd60e244a004cc30fcc83e8e59e7466b3dca6a</code>，这就是元文件的名字，也可以通过下面的语句查到md5值</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">select</span> * <span class="keyword">from</span> PRISTINE <span class="keyword">where</span> <span class="keyword">checksum</span> = <span class="string">'$sha1$f9fd60e244a004cc30fcc83e8e59e7466b3dca6a'</span>;</span><br></pre></td></tr></table></figure><p>结果中md5_checksum列的值为 <code>$md5$2c448c9f40b0dd561539b80ec3cfcaa2</code> 就是报错中提到的那个期望值了。</p><p>接下来进入 <code>.svn/pristine/f9</code> 文件夹，删除 <code>f9fd60e244a004cc30fcc83e8e59e7466b3dca6a.svn-base</code> 文件，然后在仓库根目录更新就正常了</p><h1 id="背后的故事"><a href="#背后的故事" class="headerlink" title="背后的故事"></a>背后的故事</h1><p>问题解决了，那么产生的这种问题的原因是什么呢？为什么会md5值不一致呢？我们知道文件的md5值是根据文件内容计算出来的，下载后的文件md5不是预期的值说明文件内容发生了变化。</p><p>我用比较工具对比了正常的文件和出错的文件发现，两个大小都为29949字节的文件，其中有一个字节发生了偏差，正确的文件该字节值为 <code>2F</code>，而错误的文件对应值为 <code>2E</code>，就弄错了这一个字节，导致SVN的文件比对报错了。</p><p>这一个字节差异产生的原因，可能有很多种，比如传输过程中数据损坏、压缩或加密问题、数据损坏或硬盘问题，我这里大概率是因为下载时机器重启的原因。</p><p>这让我想起了7、8年前下载游戏服务器包无法启动的问题，当时下载完启动各种异常，比较文件大小也是一样的，但是计算md5后发现不同，应该也是传输过程中发生了错误。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>SVN遇到的问题有90%都可以通过完整删除后重新克隆下载的方式解决</li><li>老版本的文件checksum值存储到 <code>.svn/entries</code> 文件中，较新版本的文件将这些值存储在 <code>.svn/wc.db</code> 中</li><li>通过 <code>select * from NODES where local_relpath = &#39;xxx&#39;</code> 方式可以从 <code>.svn/wc.db</code> 中查到文件的 sha1值</li><li>在 <code>svn/pristine</code> 中找到 <code>$sha1$.svn-base</code> 删除即可重新下载了</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143218353" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>待到秋来九月八，我花开后百花杀。有些句子在经历了一些事以后，自然而然就有了新的理解，这与儿时死记硬背下来的赏析是完全不同的~</p><p>2024-10-29 10:08:10</p></blockquote><!-- https://www.jianshu.com/p/eb64119de4b5 -->]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;&lt;code&gt;TortoiseSVN&lt;/code&gt; 作为版本控制常用的工具，有一个更为人们熟知的名字 &lt;code&gt;SVN&lt;/code&gt;，客观的讲SVN的门槛相比Git而言还是低一些的，用来存储一些文件并保留历史记录比较方便，但使用SVN还是会经常需要这样那样的问题，特别是当仓库很大的时候，提示的问题往往让初学者、甚至是经常使用的人一脸懵，比如这个 &lt;code&gt;Checksum mismatch for&lt;/code&gt; 问题，通常SVN问题都有一个终极解法，那就是重新克隆一份再操作，这就像大多数的电脑问题重启后就能解决一样，不过偶尔有一些情景不适合采用重新克隆的解法，我们就得想一个更精确的办法去解决了。&lt;/p&gt;
    
    </summary>
    
      <category term="SVN" scheme="http://AlbertGithubHome.github.io/blog/categories/SVN/"/>
    
    
      <category term="svn" scheme="http://AlbertGithubHome.github.io/blog/tags/svn/"/>
    
      <category term="Checksum" scheme="http://AlbertGithubHome.github.io/blog/tags/Checksum/"/>
    
      <category term="mismatch" scheme="http://AlbertGithubHome.github.io/blog/tags/mismatch/"/>
    
      <category term="sqlite" scheme="http://AlbertGithubHome.github.io/blog/tags/sqlite/"/>
    
      <category term="svn-base" scheme="http://AlbertGithubHome.github.io/blog/tags/svn-base/"/>
    
  </entry>
  
  <entry>
    <title>Redis内存碎片自动回收</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/10/24/Redis%E5%86%85%E5%AD%98%E7%A2%8E%E7%89%87%E8%87%AA%E5%8A%A8%E5%9B%9E%E6%94%B6/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/10/24/Redis内存碎片自动回收/</id>
    <published>2024-10-24T13:06:19.000Z</published>
    <updated>2024-12-28T16:27:09.315Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>今天说的是一个老问题了，关于 Redis 内存碎片的回收，以前用过几次，今天再次使用时发现自己没总结，没有可以拿来直接用的命令，花了一些时间去查找和测试，为了以后使用时更方便，我决定把整理一下放在这，等待再次相遇。</p><a id="more"></a><h1 id="预制命令"><a href="#预制命令" class="headerlink" title="预制命令"></a>预制命令</h1><ol><li>查询内存情况</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><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></pre></td><td class="code"><pre><span class="line">[root@server ~]<span class="comment"># redis-cli -a "password" info memory</span></span><br><span class="line"><span class="comment"># Memory</span></span><br><span class="line">used_memory:48320108840</span><br><span class="line">used_memory_human:45.00G</span><br><span class="line">used_memory_rss:56677724160</span><br><span class="line">used_memory_rss_human:52.79G</span><br><span class="line">used_memory_peak:57105514920</span><br><span class="line">used_memory_peak_human:53.18G</span><br><span class="line">used_memory_peak_perc:84.62%</span><br><span class="line">used_memory_overhead:71864532</span><br><span class="line">used_memory_startup:786616</span><br><span class="line">used_memory_dataset:48248244308</span><br><span class="line">used_memory_dataset_perc:99.85%</span><br><span class="line">total_system_memory:135025369088</span><br><span class="line">total_system_memory_human:125.75G</span><br><span class="line">used_memory_lua:37888</span><br><span class="line">used_memory_lua_human:37.00K</span><br><span class="line">maxmemory:0</span><br><span class="line">maxmemory_human:0B</span><br><span class="line">maxmemory_policy:noeviction</span><br><span class="line">mem_fragmentation_ratio:1.17</span><br><span class="line">mem_allocator:jemalloc-4.0.3</span><br><span class="line">active_defrag_running:0</span><br><span class="line">lazyfree_pending_objects:0</span><br></pre></td></tr></table></figure><ol><li>查询是否开启了自动清理碎片</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[root@server ~]<span class="comment"># redis-cli -a "password" config get activedefrag</span></span><br><span class="line">1) <span class="string">"activedefrag"</span></span><br><span class="line">2) <span class="string">"no"</span></span><br></pre></td></tr></table></figure><ol><li>开启碎片自动清理</li></ol><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">[root@server ~]<span class="comment"># redis-cli -a "password" config set activedefrag yes</span></span><br><span class="line">OK</span><br></pre></td></tr></table></figure><ol><li>关闭碎片自动清理</li></ol><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">[root@server ~]<span class="comment"># redis-cli -a "password" config set activedefrag no</span></span><br><span class="line">OK</span><br></pre></td></tr></table></figure><h1 id="聊聊背后的东西"><a href="#聊聊背后的东西" class="headerlink" title="聊聊背后的东西"></a>聊聊背后的东西</h1><p>上面已经把命令都烹饪好了，接下来可以聊聊背后的东西。</p><p>先学个单词 <code>defrag</code> /dɪˈfræɡ/，“整理磁盘碎片”的意思，后面的内容大部分都和这个单词有关。</p><p>Redis 的数据都存储在内存中，当我们的应用程序频繁修改 Redis 中的数据，特别是删除大量的Key时，就有可能会导致 Redis 产生内存碎片，或者是一些内存剩余，其实Redis删除Key之后使用的内存肯定少了，但是它不一定会释放出来，有时也算是一种预先分配的机制，就比如他的数据只有1个G，但是有可能申请1.2个G的空间，这样在添加新的key时，通常立马就可以使用，不必再去申请新的内存了。</p><p>过多的内存碎片会降低 Redis 的内存使用率，通过Redis 的 <code>info memory</code> 命令，查到内存使用率 <code>mem_fragmentation_ratio:1.17</code>，这个值的计算比较简单：</p><blockquote><p>mem_fragmentation_ratio = used_memory_rss / used_memory</p></blockquote><p>其中 <code>used_memory</code> 表示 Redis 存储数据的内存大小，<code>used_memory_rss</code> 表示操作系统实际分配给 Redis 进程的内存大小。</p><p>Redis 从 4.0 版本开始，提供了自动碎片整理的功能，可以通过配置开启碎片自动整理，也可以</p><p>如果你使用的是 Redis 4.0 以下版本，只能通过重启实例来解决<br>如果你使用的是 Redis 4.0 版本，它正好提供了自动碎片整理的功能，可以通过配置开启碎片自动整理，开启内存碎片整理必然会占用服务器的计算资源，这可能会导致 Redis 性能下降。所以是否开启这个功能，最好提前测试评估</p><p>究其原因，Redis 的碎片整理工作也是在主线程中执行的，当其进行碎片整理时，肯定会会消耗 CPU 计算资源，产生更多的耗时，从而可能影响到客户端的请求。</p><p>Redis 碎片整理的参数配置如下：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 开启自动内存碎片整理（总开关）</span></span><br><span class="line">activedefrag no</span><br><span class="line"><span class="comment"># 内存使用 100MB 以下，不进行碎片整理</span></span><br><span class="line">active-defrag-ignore-bytes 100mb</span><br><span class="line"><span class="comment"># 内存碎片率超过 10%，开始碎片整理</span></span><br><span class="line">active-defrag-threshold-lower 10</span><br><span class="line"><span class="comment"># 内存碎片率超过 100%，尽最大努力碎片整理</span></span><br><span class="line">active-defrag-threshold-upper 100</span><br><span class="line"><span class="comment"># 内存碎片整理占用 CPU 资源最小百分比</span></span><br><span class="line">active-defrag-cycle-min 1</span><br><span class="line"><span class="comment"># 内存碎片整理占用 CPU 资源最大百分比</span></span><br><span class="line">active-defrag-cycle-max 25</span><br></pre></td></tr></table></figure><p>本机查询情况如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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">[root@server ~]<span class="comment"># redis-cli -a "password" config get activedefrag</span></span><br><span class="line">1) <span class="string">"activedefrag"</span></span><br><span class="line">2) <span class="string">"yes"</span></span><br><span class="line">[root@server ~]<span class="comment"># redis-cli -a "password" config get active-defrag-ignore-bytes</span></span><br><span class="line">1) <span class="string">"active-defrag-ignore-bytes"</span></span><br><span class="line">2) <span class="string">"104857600"</span></span><br><span class="line">[root@server ~]<span class="comment"># redis-cli -a "password" config get active-defrag-threshold-lower</span></span><br><span class="line">1) <span class="string">"active-defrag-threshold-lower"</span></span><br><span class="line">2) <span class="string">"10"</span></span><br><span class="line">[root@server ~]<span class="comment"># redis-cli -a "password" config get active-defrag-cycle-min</span></span><br><span class="line">1) <span class="string">"active-defrag-cycle-min"</span></span><br><span class="line">2) <span class="string">"25"</span></span><br><span class="line">[root@server ~]<span class="comment"># redis-cli -a "password" config get active-defrag-cycle-max</span></span><br><span class="line">1) <span class="string">"active-defrag-cycle-max"</span></span><br><span class="line">2) <span class="string">"75"</span></span><br><span class="line">[root@server ~]<span class="comment"># redis-cli -a "password" config get active-defrag-max-scan-fields</span></span><br><span class="line">(empty list or <span class="built_in">set</span>)</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>Redis 从 4.0 版本开始，提供了自动碎片整理的功能，可以通过配置开启碎片自动整理</li><li>内存查询 <code>info memory</code>， 查询碎片整理是否开启 <code>config get activedefrag</code></li><li>开启碎片整理 <code>config set activedefrag yes</code></li><li>关闭碎片整理 <code>config set activedefrag no</code></li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143192496" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>不畏浮云遮望眼，只缘身在最高层。真的是这样吗？如果你太高了浮云遮住了你向下望的眼睛呢？太低看不到高处，而太高又看不清地处了呀~</p><p>2024-10-24 20:15:17</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;今天说的是一个老问题了，关于 Redis 内存碎片的回收，以前用过几次，今天再次使用时发现自己没总结，没有可以拿来直接用的命令，花了一些时间去查找和测试，为了以后使用时更方便，我决定把整理一下放在这，等待再次相遇。&lt;/p&gt;
    
    </summary>
    
      <category term="Redis" scheme="http://AlbertGithubHome.github.io/blog/categories/Redis/"/>
    
    
      <category term="Redis" scheme="http://AlbertGithubHome.github.io/blog/tags/Redis/"/>
    
      <category term="defrag" scheme="http://AlbertGithubHome.github.io/blog/tags/defrag/"/>
    
      <category term="config" scheme="http://AlbertGithubHome.github.io/blog/tags/config/"/>
    
      <category term="memory" scheme="http://AlbertGithubHome.github.io/blog/tags/memory/"/>
    
      <category term="framentation" scheme="http://AlbertGithubHome.github.io/blog/tags/framentation/"/>
    
  </entry>
  
  <entry>
    <title>修复WordPress前后端分离时无法预览文章的问题</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/10/24/%E4%BF%AE%E5%A4%8DWordPress%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E6%97%B6%E6%97%A0%E6%B3%95%E9%A2%84%E8%A7%88%E6%96%87%E7%AB%A0%E7%9A%84%E9%97%AE%E9%A2%98/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/10/24/修复WordPress前后端分离时无法预览文章的问题/</id>
    <published>2024-10-23T17:30:00.000Z</published>
    <updated>2024-12-28T16:27:25.028Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>惊喜总在不经意间来临？前段时间刚刚总结了<a href="https://blog.csdn.net/albertsh/article/details/142577286" target="_blank" rel="noopener">WordPress迁移遇到的问题及解决办法</a>，费了九牛二虎之力似乎一切都走向正轨了，今天则又冒出点新花样，待发布的文章居然不能预览！这不都是常规功能吗？难道因为我迁移之后就不好使了，根据遇到的情况分析之后发现不是这样的，这个预览一直就不好使，已经过去5年了，也是有点醉了，好像到我手里就坏了一样，其实一直就不能用，5年前别人挖的坑我也要填，我就是一个填坑勇士。</p><a id="more"></a><h1 id="问题表现"><a href="#问题表现" class="headerlink" title="问题表现"></a>问题表现</h1><p>问题最直接的表现就是文章保存草稿后，点击预览按钮跳转的不是刚刚编辑的文章预览页面，而是已发布的文章列表，很明显是跳转的链接不对，从已发布的文章链接来看，这个链接格式肯定是编辑过，通过查找资料我发现可以通过 WordPress后台菜单中的 <code>设置</code> -&gt; <code>固定链接</code> 来修改固定样式，查看后发现发布链接的样式已经从 <code>https://008ct.top/?p=123</code> 定义为 <code>https://008ct.top/%category%/%year%/%post_id%/</code> 也就是在链接中增加了分类和年份信息。</p><p>再看预览链接的样式为 <code>https://008ct.top/?p=123&amp;preview=true</code> 和默认样式很相似，所以我就找了半天哪里能有修改预览样式的页面，转了一圈无果，后来发现后台的地址和前台地址设置不同，地址中多了一段 <code>cms</code>，完整预览格式应该为 <code>https://008ct.top/cms/?p=123&amp;preview=true</code>，默认打开的地址肯定是不行的，需要修改预览链接样式，朝这个方向努力后发现可以通过钩子函数来自定义预览链接。</p><h1 id="解决问题"><a href="#解决问题" class="headerlink" title="解决问题"></a>解决问题</h1><p>我们可以使用下面这个过滤钩子preview_post_link来自定义预览链接，把下面的代码放到主题文件夹里的functions.php文件中，路径类似 <code>/cms/wp-content/themes/xxxx/functions.php</code></p><figure class="highlight php"><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="function"><span class="keyword">function</span> <span class="title">aw_custom_preview_link</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> get_option(<span class="string">'siteurl'</span>) . <span class="string">"/?p="</span> . get_the_ID() . <span class="string">"?preview=true"</span>;</span><br><span class="line">&#125;</span><br><span class="line">add_filter(<span class="string">'preview_post_link'</span>, <span class="string">'aw_custom_preview_link'</span>);</span><br></pre></td></tr></table></figure><p>其中 <code>get_option(&#39;siteurl&#39;)</code> 获取的是后台的地址，也就是 <code>https://008ct.top/cms</code> 这部分，如果想获取前台的地址可以使用 <code>get_option(&#39;home&#39;)</code>，这些都是在WordPress后台 <code>设置</code> -&gt; <code>常规</code> 页面里设置的，修改过后无需重启项目，直接访问待发布的文章，预览链接就可以使用啦。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>WordPress的可定制性真的很强，最近接触后深有体会</li><li>WordPress可通过<code>设置</code> -&gt; <code>固定链接</code>来设置文章的访问地址</li><li>WordPress可通过<code>设置</code> -&gt; <code>常规</code> 来设置前台和后台的访问地址</li><li>WordPress可通过钩子preview_post_link来自定义预览链接</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/143027711" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p>故善人者，不善人之师；不善人者，善人之资。不贵其师，不爱其资，虽智大迷，是谓要妙</p><p>2024-10-23 20:03:53</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;惊喜总在不经意间来临？前段时间刚刚总结了&lt;a href=&quot;https://blog.csdn.net/albertsh/article/details/142577286&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;WordPress迁移遇到的问题及解决办法&lt;/a&gt;，费了九牛二虎之力似乎一切都走向正轨了，今天则又冒出点新花样，待发布的文章居然不能预览！这不都是常规功能吗？难道因为我迁移之后就不好使了，根据遇到的情况分析之后发现不是这样的，这个预览一直就不好使，已经过去5年了，也是有点醉了，好像到我手里就坏了一样，其实一直就不能用，5年前别人挖的坑我也要填，我就是一个填坑勇士。&lt;/p&gt;
    
    </summary>
    
      <category term="Web" scheme="http://AlbertGithubHome.github.io/blog/categories/Web/"/>
    
    
      <category term="WordPress" scheme="http://AlbertGithubHome.github.io/blog/tags/WordPress/"/>
    
      <category term="预览" scheme="http://AlbertGithubHome.github.io/blog/tags/%E9%A2%84%E8%A7%88/"/>
    
      <category term="Web" scheme="http://AlbertGithubHome.github.io/blog/tags/Web/"/>
    
      <category term="preview" scheme="http://AlbertGithubHome.github.io/blog/tags/preview/"/>
    
      <category term="functions.php" scheme="http://AlbertGithubHome.github.io/blog/tags/functions-php/"/>
    
  </entry>
  
  <entry>
    <title>陷入产出危机的我聊聊近况</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/10/18/%E9%99%B7%E5%85%A5%E4%BA%A7%E5%87%BA%E5%8D%B1%E6%9C%BA%E7%9A%84%E6%88%91%E8%81%8A%E8%81%8A%E8%BF%91%E5%86%B5/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/10/18/陷入产出危机的我聊聊近况/</id>
    <published>2024-10-17T17:30:00.000Z</published>
    <updated>2024-12-28T16:27:34.465Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在总结文章时，我把自己当做一个内容产出者，当这样一个身份进入每天按部就班的平稳状态时会陷入一种焦虑，产生一种居然没有什么可写的感觉，如果任由这种焦虑泛滥，便会陷入一种无限内耗的状态，也不是无所事事，只是觉得做重复的工作没有看见提升就让人焦虑，性格使然。</p><a id="more"></a><p>我当然知道它的危害，所以我得找点事情来做，找点常规工作以外的事情来做，今天在依旧找不到话题的情况下，我决定写写最近一段时间的感受和经历，有些知识点之前已经总结过了，今天主要是在整体唠叨唠叨，写写最近一段时间干了点什么。</p><h1 id="我的多重身份"><a href="#我的多重身份" class="headerlink" title="我的多重身份"></a>我的多重身份</h1><h2 id="作为IT网管"><a href="#作为IT网管" class="headerlink" title="作为IT网管"></a>作为IT网管</h2><p>这是一个我在离职前就能预见到的身份，离开了之前庞大的机构，进入一个小小的初创团队，什么事情都得自己干，选网络运营商，配路由，搭建内部DNS服务器，甚至要亲自买机柜，买PDU，接网线，装系统等等等等，刚刚搭建完服务器还要盯着各个服务器的温度，功率太大导致跳闸还得呼哧呼哧搬着机器找一个合适的位置，反正网管的活我都干，能干的不能干的最后也都变成了能干的。</p><h2 id="作为运维人员"><a href="#作为运维人员" class="headerlink" title="作为运维人员"></a>作为运维人员</h2><p>之前需要服务器，开个防火墙类似的，找运维说一声，或者提个单子等着就行了，现在不行了，机器得自己买，防火墙得自己配，费用要自己算，一切路都得自己趟，今年之前偶尔在阿里云或腾讯云上买个服务器玩玩，最近把AWS和GCP玩的66的，唯一差一点就是 <code>Microsoft Azure</code> 还没有用过，暂时还没有需求吧。</p><p>这么来说吧，用了一圈我发现各家云服务器的常用功能类似，只是每个人起了不同的名字，比如 AWS中的 <code>EC2</code> 在腾讯云叫 <code>CVM（Cloud Virtual Machine）</code>，在阿里云叫 <code>ECS（Elastic Compute Service)</code> 在GCP 叫 <code>Compute Engine</code>，其他的服务也是类似的情况。</p><p>最近AWS用的比较多，我就把AWS的服务列举一下：</p><ul><li>EC2：云服务器</li><li>RDS：关系型数据库</li><li>Amazon MemoryDB：内存数据</li><li>Route 53：DNS解析</li><li>WAF &amp; Shield：应用防火墙</li><li>CloudFront：类CDN产品</li><li>S3：对象存储服务</li><li>IAM：安全控制服务</li><li>CloudWatch：监控和管理服务</li></ul><p>此外用到的还有：</p><ul><li>AMI：预配置的虚拟机镜像</li><li>ALB：负载均衡服务</li><li>SSL：安全协议证书</li></ul><p>还有一些小的概念，比如硬盘的gp2还是gp3，安全组的配置，弹性IP，在GCP中叫静态IP，密钥的创建和丢失密钥的处理方法，还有目标组，NameServer等等概念，反正搞这些东西的那段时间可把我忙坏了。</p><h2 id="作为Web开发人员"><a href="#作为Web开发人员" class="headerlink" title="作为Web开发人员"></a>作为Web开发人员</h2><p>关于这个身份，我之前想到了一点点，但是没想到这段时间此类工作占比如此之重，接手了原来完整的一个网站，而且他还是我之前都没听说过的结构框架编写的，例如Nextjs框架，React库，服务器客户端组件、Vercel部署，Supabase存储等，每一项都花了不少时间，后来又迁移了Appsmith，使用了Docker，反正是磕磕绊绊的把这些东西接过来了。</p><p>再后来又接了一个WordPress的活，把原来的项目完整迁移过来，配置本地的STMP发送邮件服务，为了测试自己又完整的搭建了一遍WorldPress网站，还跑去GoDaddy上买了个域名，Route53完成DNS解析，ALB转接流量，SSL分配证书，又完整的搞了一遍。</p><h2 id="作为游戏开发人员"><a href="#作为游戏开发人员" class="headerlink" title="作为游戏开发人员"></a>作为游戏开发人员</h2><p>这个本应该属于我的角色，这段时间却花费时间较少，只能在前面这些工作的间隙抽空过来处理，当然线上的问题肯定优先解决，老项目也开发了几个较小的功能，同时做了一些优化和功能BUG的修复，新项目刚刚又在开发新功能了，也在梳理之前的框架，清理冗余的逻辑，精简模块的实现。</p><p>选购云服务器也是这一部分不可缺少的工作，自从来了这个小团队已经买了10来个服务器实例，用途各式各样，手法也越发熟练了。</p><p>哦对了，一开始还捣鼓了一阵儿打包的问题，把苹果各类机器和版本试了个遍，那是一段没有前半夜回家的日子。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>最近这4个月是近几年最匪夷所思的一段时间，仿佛什么事情都赶在一起了</li><li>这段时间游戏开发、网站开发、买服务器、装机器、接网线似乎什么都干</li><li>这段经历虽然不一定多彩，但肯定算的上多样，有些事情不经历靠书本是学不会的</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/142577286" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p> 这人生啊，又开始给我加强度了</p><p>虽然我年纪轻轻就谈人生显得有点可笑，但是每个看似深思熟虑的思考在高纬度看来同样可笑至极，蝼蚁罢了~</p><p>2024-10-17 20:30:25</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;在总结文章时，我把自己当做一个内容产出者，当这样一个身份进入每天按部就班的平稳状态时会陷入一种焦虑，产生一种居然没有什么可写的感觉，如果任由这种焦虑泛滥，便会陷入一种无限内耗的状态，也不是无所事事，只是觉得做重复的工作没有看见提升就让人焦虑，性格使然。&lt;/p&gt;
    
    </summary>
    
      <category term="Thinking" scheme="http://AlbertGithubHome.github.io/blog/categories/Thinking/"/>
    
    
      <category term="Web" scheme="http://AlbertGithubHome.github.io/blog/tags/Web/"/>
    
      <category term="危机" scheme="http://AlbertGithubHome.github.io/blog/tags/%E5%8D%B1%E6%9C%BA/"/>
    
      <category term="产出" scheme="http://AlbertGithubHome.github.io/blog/tags/%E4%BA%A7%E5%87%BA/"/>
    
      <category term="云服务器" scheme="http://AlbertGithubHome.github.io/blog/tags/%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    
      <category term="AWS" scheme="http://AlbertGithubHome.github.io/blog/tags/AWS/"/>
    
  </entry>
  
  <entry>
    <title>AWS上迁移WordPress遭遇若干问题及处理办法</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/09/27/AWS%E4%B8%8A%E8%BF%81%E7%A7%BBWordPress%E9%81%AD%E9%81%87%E8%8B%A5%E5%B9%B2%E9%97%AE%E9%A2%98%E5%8F%8A%E5%A4%84%E7%90%86%E5%8A%9E%E6%B3%95/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/09/27/AWS上迁移WordPress遭遇若干问题及处理办法/</id>
    <published>2024-09-26T17:00:00.000Z</published>
    <updated>2024-09-28T07:23:52.684Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>前段时间不是在<a href="https://blog.csdn.net/albertsh/article/details/142427271" target="_blank" rel="noopener">迁移AWS</a>嘛，采用了分享AMI的方式，废了九牛二虎之力，终于在没有密钥的情况下成功登录了转移后的EC2实例，那么我们肯定不单单是为了转移一个EC2实例而大费周章，实际目的还是为了里面的数据和服务，具体来说就是一套WordPress网站，对于一个新的知识和事物围绕它的有很多知识点，一旦发散的过快就发现东西太多了，什么也讲不明白，造成虎头蛇尾的现象，做技术内容输出的人经常有这种力不从心的感觉。</p><a id="more"></a><p>为了避免这种现象，我尽量保持克制，不随意发散，按着时间先后发生的主线，来说说遇到了哪些问题，又是怎么解决的，解决方案或许不完美，只是提供一种思路。</p><h1 id="问题的产生"><a href="#问题的产生" class="headerlink" title="问题的产生"></a>问题的产生</h1><p>前文提到我已经把原来的AWS实例迁移到新的账号下了，其实繁杂的工作才刚刚起步，首先是访问IP，为了每次重启后IP地址不发生变化，我们不能采用自动分配的公共IP，而要申请弹性IP关联到对应的实例上，注意弹性IP关联到正在运行的实例上时不收费，但是如果不关联或者关联的实例不运行，则需要支付费用，大约$0.005/小时。</p><p>当然这都是小问题，真正的问题还在后面，比如域名需要转移，域名的解析需要重新配置，SSL证书要重新申请，花费时间最多的还是网站用户上报信息发送邮件功能不可用的问题，不过还好，凭借这知识积累和渊博的互联网，基本都找到了解决方案，下面逐项来介绍一下。</p><h1 id="域名转移与DNS解析"><a href="#域名转移与DNS解析" class="headerlink" title="域名转移与DNS解析"></a>域名转移与DNS解析</h1><p>域名转移据说在国外非常顺畅，但这次不是常用的GoDaddy，也不是上次虐我千百遍的JPDirect，是一个叫Onamae服务网站，使用这个网站最大的难点不是技术上的，而是语言层面的，全网站只有日文，凭借着我的有道词典和小伙伴的日语助攻，域名顺利转移，但是我废了九牛二虎之力也没找到原来的DNS记录配置在哪里，找不到DNS记录意味着我无法将域名解析到新的EC2。</p><p>后来想起了Vercel验证域名的经历，这个域名不会也把域名服务器改到别处了吧，一查果然是改到aws了，那必然是用了Route53啊。</p><p><strong>Amazon Route 53</strong> 是 AWS 提供的高度可用和可扩展的云域名系统（DNS）服务，主要用于将用户访问的域名（如 <code>example.com</code>）映射到网络资源（如 EC2 实例、负载均衡器、S3 存储桶等）。Route 53 提供域名注册、DNS 解析、流量管理、以及健康检查等功能。</p><p>Route53 提供高度可用的 DNS 解析服务，将域名映射到 AWS 资源或其他 IP 地址，支持 A 记录（IPv4 地址）、AAAA 记录（IPv6 地址）、CNAME、MX 等常见 DNS 记录类型。支持多种路由策略，如简单路由、加权路由、基于延迟的路由、地理位置路由、故障转移路由等，帮助实现流量优化和高可用性。</p><p>每月前 25 个托管区域每个 $0.50，超过部分按 $0.10/托管区域/月收费。首 10 亿次查询为 $0.40 每百万次查询，超过部分按 $0.20 每百万次查询计费。在全球多个 AWS 区域提供服务，确保最快速的 DNS 解析速度。</p><p>总的来说就是Onamae甩锅了，有人查询DNS记录，Onamae指着AWS说你问他，我不管了，这个域名由他全权负责。</p><h1 id="解析域名到EC2与启用SSL"><a href="#解析域名到EC2与启用SSL" class="headerlink" title="解析域名到EC2与启用SSL"></a>解析域名到EC2与启用SSL</h1><p>现在是个网站就HTTPS，也不管是不是有隐私数据，反正我就得加上显得安全，那我们也随大流别特立独行了，去搞个SSL证书吧，虽然给官网买个好点的证书也付得起，但毕竟免费的更具性价比，在AWS上有免费的证书可用，访问ACM即可申请。</p><p><strong>AWS Certificate Manager (ACM)</strong> 是 Amazon Web Services 提供的服务，用于轻松管理 SSL/TLS 证书，来保护网站和应用程序的网络流量。ACM 自动化了证书的申请、颁发、续订、以及配置过程，从而简化了安全管理。</p><p>ACM 支持免费申请 <strong>公有 SSL/TLS 证书</strong>，用于保护基于 HTTPS 协议的流量。支持单域名、多域名和通配符证书。可以将 ACM 证书快速部署到多个 AWS 服务上，如 <strong>Elastic Load Balancers (ELB/ALB/NLB)</strong>、<strong>CloudFront</strong>、<strong>API Gateway</strong>、和 <strong>Elastic Beanstalk</strong>。</p><p>申请时，需要验证域名的所有权，ACM 支持 <strong>DNS 验证</strong> 和 <strong>Email 验证</strong>，证书可以直接部署在 ALB、CloudFront 等 AWS 服务上，无需手动上传证书，ACM 会自动检测到即将到期的证书，并在过期前完成自动续订。ACM 是一个简化 SSL/TLS 证书管理的服务，特别适合不希望手动处理证书申请、安装和续订的用户。它与 AWS 其他服务深度集成，确保用户在 AWS 上可以轻松实现网站和应用的安全性。</p><p>看了这些介绍是不是心动了，申请步骤也非常方便，基本上填写一个域名<code>example.com</code> 其他参数默认就可以了，前文提到ACM需要验证域名的所有权，这一点AWS做的非常方便，之前我们已经修改Onamae购买的域名NS指向了Route53，这里可以选择<strong>DNS 验证</strong>，在申请SSL证书后会有一个按钮，将验证的DNS记录添加到Route53，一步到位非常的人性化，添加完验证DNS记录后，证书状态会显示为 <code>等待验证</code>，这个过程我等待了1-2小时，然后状态切换成了 <code>已颁发</code>，可能因为我使用新买的域名进行的测试。</p><p>进行到这遇到了一个难题，SSL给谁用？如果我在Route53中将域名解析到EC2，那么我这个SSL证书就得放到EC2上，不管是配置到Nginx、Apache，还是应用程序自己处理都比较麻烦，所以我们充分利用AWS的服务架构，申请一个ELB，将SSL证书分配到ELB上，然后Route53将域名解析到ELB，ELB收到请求后再将流量转发到EC2。</p><h2 id="申请ALB完成转发"><a href="#申请ALB完成转发" class="headerlink" title="申请ALB完成转发"></a>申请ALB完成转发</h2><p>AWS ELB（Elastic Load Balancing）是亚马逊云服务（AWS）提供的一项负载均衡服务，主要分为ALB、NLB、CLB，旨在自动分配进入应用程序的流量，以确保高可用性和可靠性。ELB 可以帮助你管理和优化应用程序的流量，同时确保后端服务器不会因过载而失效。</p><p>ELB 能够根据流量自动扩展和缩减负载均衡能力，以适应流量的变化，支持 SSL/TLS 加密，确保数据在传输过程中安全。集成了 AWS CloudWatch，可以实时监控负载均衡器的性能和流量，并提供访问日志。支持 HTTP、HTTPS、TCP 和 WebSocket 等多种协议。</p><h3 id="ELB分类"><a href="#ELB分类" class="headerlink" title="ELB分类"></a>ELB分类</h3><ol><li><p><strong>应用负载均衡器（ALB）</strong>：</p><ul><li>最适合 HTTP 和 HTTPS 流量，能够根据请求的内容进行智能路由。</li><li>支持基于内容的路由、SSL 终止、WebSocket 和 HTTP/2。</li><li>提供更细粒度的流量控制和监控功能。</li></ul></li><li><p><strong>网络负载均衡器（NLB）</strong>：</p><ul><li>设计用于处理 TCP 流量，能够提供极低的延迟和高吞吐量。</li><li>可以处理数百万个请求，并支持静态 IP 地址。</li><li>适合高性能、低延迟的应用场景，如游戏和实时数据处理。</li></ul></li><li><p><strong>经典负载均衡器（CLB）</strong>：</p><ul><li>早期的负载均衡器，支持 HTTP、HTTPS 和 TCP。</li><li>提供基本的负载均衡功能，但不具备 ALB 和 NLB 的高级功能。</li><li>目前已不再推荐用于新应用，AWS 建议用户使用 ALB 或 NLB。</li></ul></li></ol><p>因为使用的是 HTTP 和 HTTPS 流量的应用程序，所以需要选择 ALB，NLB是不会解析到应用层协议的，使用ALB可以进行SSL终止，那什么是SSL终止呢？</p><p><strong>SSL终止</strong>（SSL Termination）是在负载均衡器或代理服务器等中间设备上处理SSL/TLS加密和解密的技术。</p><h3 id="SSL终止及优点"><a href="#SSL终止及优点" class="headerlink" title="SSL终止及优点"></a>SSL终止及优点</h3><p><strong>SSL终止</strong>指的是当客户端与服务器之间的连接使用SSL/TLS加密时，SSL连接在中间设备（如负载均衡器）上终止（解密）。之后，终止SSL加密的数据将作为未加密的数据在中间设备和后端服务器之间传输。</p><ul><li><strong>性能提升</strong>：后端服务器不需要处理SSL/TLS加解密工作，减轻了服务器的负担，特别是对于高流量的网站，能够提高整体的性能和响应速度。</li><li><strong>集中管理SSL证书</strong>：SSL证书只需要在负载均衡器等设备上配置，无需在每台后端服务器上安装SSL证书，简化了证书的管理和更新流程。</li><li><strong>简化架构</strong>：SSL终止简化了网络架构，尤其适合需要将流量分配给多台服务器的负载均衡环境。</li><li><strong>支持多种协议</strong>：可以支持HTTP、WebSocket等不加密协议的通信形式，而客户端仍然认为其连接是安全的。</li></ul><h3 id="SSL终止的工作流程"><a href="#SSL终止的工作流程" class="headerlink" title="SSL终止的工作流程"></a>SSL终止的工作流程</h3><ol><li>客户端发起HTTPS请求。</li><li>负载均衡器或代理服务器接收请求，并在此解密SSL加密的数据。</li><li>解密后，负载均衡器将未加密的数据转发给后端服务器（通过HTTP等协议）。</li><li>服务器处理请求并将结果返回给负载均衡器。</li><li>负载均衡器将响应数据重新加密，并返回给客户端。</li></ol><p>在创建ALB时有几个需要注意的点，面向互联网还是内部？IPv4还是双栈？网络映射到哪？安全组怎么配？侦听器和路由是什么？这些问题看着深奥，其实一点也不简单，我用大白话试着解释一下？</p><ul><li>面向互联网还是内部，就是问给公网用户访问还是内网访问，这个就是字面意思</li><li>IPv4还是双栈，就是问只用IPv4还是需要支持IPv6，我不太懂，选了双栈，功能多总没坏处</li><li>网络映射到哪，就是选一个子网和至少两个可用区域，为了说明流量的去向</li><li>安全组怎么配，按照EC2的规则配就行了，这个就是EC2的前面的一道门，注意开启443端口的流入哈</li><li>侦听器和路由是什么，这个是个重点，就是说监听某个端口，然后将流量导入到目标组，按照我们的目的肯定是监听443，然后导向EC2实例的80啊，选择443时就可以使用前文里申请的SSL证书了</li></ul><p>回答了上面的问题，又引出了新的问题，我选择的子网不让我选择双栈怎么办？目标组又是什么？</p><p>如果你选择的是双栈（IPv4/IPv6），确保子网有 IPv6 CIDR 块。<strong>CIDR块</strong>（Classless Inter-Domain Routing Block）是一种表示IP地址范围的方式。它用于描述一个网络及其子网的大小，并且广泛应用于现代网络，特别是在分配和管理IP地址时。如果没有就去子网里添加。</p><p>目标组就是把流量目标组到一起，定义一个目标组，目的就是精确表示流量去到的地方，我目前的需求是导向到具体的EC2，所以目标组里可以只选择一个EC2实例，其实你也可以选择多个实例去做类似负载均衡的需求</p><p>到这里ALB基本就配置完了，这时可以将这个ALB的DNS信息复制添加到Route53里，以别名的形式添加，这一套路径就走下来了</p><p>用于访问域名，交给Route53解析地址，Route53将ALB地址返回，流量以HTTPS形式到达ALB，使用证书解密出数据，数据以HTTP形式从ALB到达目标组中的EC2实例，完成一次请求，EC2不用关心证书的问题，多数一句，这样的EC2如果不直接访问可以不配置公网IP和弹性IP。</p><p>进行到这里网站跑起来了，访问也正常了，貌似问题都解决了，但旅途怎会如此顺利？接下来就将迎来业务引发的问题——发邮件不成功，经历了这次的折腾我看到了更本质的东西。</p><h1 id="邮件发送问题"><a href="#邮件发送问题" class="headerlink" title="邮件发送问题"></a>邮件发送问题</h1><p>当你不熟悉一件事情的时候总会认为它很神秘，就像我之前认为邮件特别复杂、特别庞大一样，经过这次折腾之后发现它的轮廓清晰了起来，再说问题之前，我们先来看一段历史。</p><p>1987 年 9 月14 日晚上，北京车道沟十号院中一座树木掩映的小楼里，李澄炯教授等13 位中、德科学家聚拢在一台西门子7760 大型计算机周围，成功发出中国第一封电子邮件。 这座小楼，就是中国兵器工业计算机应用技术研究所的所在地。</p><blockquote><p>“Across the Great Wall we can reach every corner in the world”（“越过长城，走向世界”）。这是西方世界第一次通过互联网听到中国的声音。</p></blockquote><p>看到Great Wall和这个句子含义总联想到另一个词，有点讽刺~</p><p>现在看来，邮件本质是数据的发送，从数据层面来看与HTTP请求并无不同，只是使用了另一种更适合它的协议，将数据传递到目的地罢了。</p><p>我很早就不觉得HTTP神秘了，为什么呢？因为整天接触它，但是我在前两天还在觉得Email神秘，因为平时用的少也没有过多研究过，这次趁着解决问题稍微拓展了一下，感觉知识面又变宽了一点点。</p><p>好了，接下来说遇到的问题，迁移的WordPress有一个提交表单发邮件的功能，使用了一个叫 <code>MW WP WPForm</code> 的插件，可以<a href="https://mw-wp-form.web-soudan.co.jp/manual/auto_mail/" target="_blank" rel="noopener">自动回复邮件</a>以及<a href="https://mw-wp-form.web-soudan.co.jp/manual/admin_mail/" target="_blank" rel="noopener">向管理员发送表单邮件</a>，但是我将WordPress迁移到新的AWS实例后，这个功能失效了</p><p>官方也给出了一些<a href="https://mw-wp-form.web-soudan.co.jp/faq/" target="_blank" rel="noopener">FAQ</a></p><blockquote><p>“There was an error trying to send your message. Please try again later.” is a message that appears when an email fails to send. There are two main reasons why this message appears:</p><ul><li>WordPress itself cannot send emails</li><li>The email body is not set on the MW WP Form form editing screen</li></ul></blockquote><p>虽然不能直接解决我的问题，但是我发现了一件事，就是这个插件本身不能发送邮件，它要求WordPress必须本身可以发送邮件才可以，怎么理解这句话呢？首先你要理解插件的作用，插件是在原项目或作品上通过加载的方式丰富完善原来的内容，本身并不能提供一些基础的能力。</p><p>因为插件一般与源系统是解耦的，所以只能通过系统提供的一些钩子或事件来改善原系统功能，就以 <code>MW WP WPForm</code> 这个插件为例，WordPress本身是可以利用 PHP 的 <code>WP_Mail</code> 发送邮件的，但是相对来说使用较复杂，需要传递很多参数，而 <code>MW WP WPForm</code> 就是将这些参数便利化的插件，当我收到你要发邮件的请求时由插件负责组装参数和调用函数，大大方便了邮件发送的操作。</p><p>但是如果本身调用 <code>WP_Mail</code> 都发不了邮件，那么这个插件也就无能为力了。</p><p>其实 <code>WP_Mail</code> 只是为 WordPress 提供了发送邮件的功能逻辑，并不是发邮件的基础架构，以Linux为例，与邮件相关的命令有 <code>mail</code>、<code>sendmail</code>、<code>postfix</code> 等，其中 <code>mail</code> 是一个邮件客户端，用于发送和接收邮件，可以这样使用 <code>mail -s &quot;Subject&quot; user@example.com</code>，<code>sendmail</code> 和 <code>postfix</code> 是邮件传输代理（MTA），主要用于处理邮件的发送和接收。在现代 Linux 系统中，<code>postfix</code> 通常被认为是比 <code>sendmail</code> 更好的选择，因为它更安全、易于管理且性能优越。</p><p>这些才是Linux系统发送邮件的基础结构，WordPress的 <code>WP_Mail</code> 函数主要依赖于 PHP 的 <code>mail()</code> 函数来发送邮件，而PHP 的 <code>mail()</code> 函数通过系统的邮件传输代理（通常是 <code>sendmail</code> 或 <code>postfix</code>）来发送邮件，确保适当的 MTA 已安装并配置正确，才能保证 PHP 可以成功发送邮件。</p><p>初步分析是这台机器邮件服务没有配置好，那就查查log吧，打开 <code>/var/log/maillog</code> 文件看到以下字样：</p><blockquote><p>Sep 26 16:14:29 ip-192-168-2-26 postfix/smtp[9198]: connect to mx2.qq.com[240d:c040:1:40::133]:25: Network is unreachable</p></blockquote><p>咦？25号端口咋不通呢？看了看安全规则也没限制25号端口，还有就是我现在的系统是直接镜像过来的，配置应该一模一样才对，为什么之前的实例可以发送邮件，而镜像过来的新实例就不行呢？</p><p>后来我又创建一个简单实例，然后搭建了WordPress，依旧发不了邮件，然后我就查了一下发现<a href="https://aws.amazon.com/cn/ec2/faqs/" target="_blank" rel="noopener">AWS-FAQ</a>：</p><figure class="highlight plain"><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">EC2 SMTP 端点策略的变化</span><br><span class="line">问：ID 格式会发生怎样的变化？</span><br><span class="line"></span><br><span class="line">自 2020 年 1 月 7 日起，Amazon EC2 开始推出一项变化来限制默认情况下通过端口 25 的电子邮件流量，</span><br><span class="line">从而防止客户和其他收件人收到垃圾邮件和电子邮件滥用。端口 25 通常用作发送电子邮件的默认 SMTP 端口。</span><br><span class="line">在过去请求并取消了端口 25 限制的 AWS 账户将不受此变化影响。</span><br></pre></td></tr></table></figure><p>原来是AWS把新实例的25端口封锁了，之前的实例是2019年申请的没有受到影响，进一步查询，发现不只AWS这样做，阿里云也封锁了25号端口，要是申请解封会花一些时间还不一定成功，我还是想想别的办法。</p><h2 id="解决邮件问题"><a href="#解决邮件问题" class="headerlink" title="解决邮件问题"></a>解决邮件问题</h2><p>既然25号端口不好用，我就用SMTP的SSL端口465吧，可是怎么改变发送的端口呢？我查到了WordPress的 <code>WP MIAL SMTP</code> 插件，填写qq邮箱的SMPT服务器，添加qq邮箱和授权码，就搞定了，真是太方便，设置完终于可以发邮件了，泪流满面，天知道这个过程我持续了多久，熬了多少个夜晚。</p><p>这个插件的安装我是在测试实例时搞得，正当我想要在正式实例上安装 <code>WP MIAL SMTP</code> 插件时，提示我WordPress版本5.2.4太低无法安装，之前安装的那个测试环境是6.2.4，天塌了呀，那没办法升级WordPress吧，找到升级按钮那么一点，开始转圈圈，然后提示我升级失败，天又塌了呀。</p><p>鬼知道我都遇到了什么，既然不让我安装 <code>WP MIAL SMTP</code>，那我安装个别的呗，后来找到了 <code>Easy WP SMTP</code> 可以在当前版本安装，整个使用过程和 <code>WP MIAL SMTP</code> 几乎一毛一样，看起来就像换了个主题，结果就是一样好使，事情还没完。</p><p>刚刚配置的是我的QQ邮箱，但是这里总不能把邮件都扔我邮箱吧，最终要换成企业微信邮箱，但是当我配置为公司企业微信邮箱再提交表单发邮件时，虽然邮件正常回复给QQ邮箱了，但是插件 <code>WP MIAL SMTP</code> 测试页面报了一个警告和错误</p><blockquote><p>Domain Check Results</p><ul><li>spf<blockquote><p>Action Needed: It doesn’t look like the SPF record required by your SMTP server has been added to your domain. Please contact your SMTP server provider for details on how to find the SPF record, and how to add this record to your domain’s DNS.</p></blockquote></li><li>dmarc<blockquote><p>Action Recommended: It doesn’t look like DMARC has been set up on your domain (xxx.com). We recommend using the DMARC protocol because it helps protect your domain from &gt;&gt;unauthorized use. Please check out this step-by-step guide for details on how to add the DMARC record to your domain’s DNS.</p></blockquote></li></ul></blockquote><p>其实一开始我也没当回事，查了<code>Easy WP SMTP</code>插件的<a href="https://easywpsmtp.com/docs/how-to-create-dmarc-record/" target="_blank" rel="noopener">文档</a>，还提供了一个<a href="https://mxtoolbox.com/SuperTool.aspx" target="_blank" rel="noopener">查询DNS信息的工具</a>,但是我没有放在心上，不撞南墙怎会回头，后来再给Gmail发邮件的时候，邮件被退回了，错误原因写的还算清楚</p><figure class="highlight plain"><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">无法发送到 yyy@gmail.com</span><br><span class="line">退信原因    发件人（support@xxx.com）域名的DNS记录未设置或设置错误导致对方拒收此邮件。</span><br><span class="line">host gmail-smtp-in.l.google.com[172.253.118.27] said: 550-5.7.26 Your email has been blocked because</span><br><span class="line">the sender is unauthenticated. Gmail requires all senders to authenticate with either SPF or DKIM.</span><br><span class="line">Authentication results:  DKIM = did not pass  SPF [xxx.com] with ip: [18.169.211.239] = did not pass</span><br><span class="line">For instructions on setting up authentication,</span><br><span class="line">go to  https://support.google.com/mail/answer/81126#authentication d9443c01a7336-20af16e543csi13995695ad.54 - gsmtp (in reply to end of DATA command)</span><br><span class="line">解决方案    请通知你的邮箱管理员为邮箱域名设置正确的DNS(SPF、DKIM、DMARC)记录。</span><br><span class="line">详细请见 http://service.exmail.qq.com/cgi-bin/help?subtype=1&amp;&amp;no=1000580&amp;&amp;id=20012。</span><br></pre></td></tr></table></figure><p>在自己企业微信的域名上添加两行记录就可以了，这样就可以顺利的发邮件给GMail了。</p><figure class="highlight plain"><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">记录类型：TXT</span><br><span class="line">记录值为：v=spf1 include:spf.mail.qq.com ~all</span><br><span class="line"></span><br><span class="line">主机记录： _dmarc</span><br><span class="line">记录类型：TXT</span><br><span class="line">记录值: v=DMARC1; p=none; rua=mailto:mailauth-reports@qq.com</span><br></pre></td></tr></table></figure><p><strong>SPF（Sender Policy Framework）</strong> 是一种电子邮件验证协议，用于防止邮件伪造和垃圾邮件。SPF 的验证原理基于 DNS（域名系统）记录。<strong>DMARC（Domain-based Message Authentication, Reporting &amp; Conformance）</strong> 是一种邮件身份验证协议，用于增强电子邮件的安全性，防止邮件伪造和钓鱼攻击。它建立在 SPF（Sender Policy Framework）和 DKIM（DomainKeys Identified Mail）之上，通过提供发送方的政策声明和报告机制来实现。</p><p>有关SPF和DMARC的详细原理可以自己去扩展，这里只做简单介绍了。</p><p>本来事情到这里就比较完美了，但是架不住技术人有一个爱折腾的心，我不装插件行不行呢？</p><h2 id="原生解决邮件问题"><a href="#原生解决邮件问题" class="headerlink" title="原生解决邮件问题"></a>原生解决邮件问题</h2><h3 id="临时测试"><a href="#临时测试" class="headerlink" title="临时测试"></a>临时测试</h3><p>既然装了个插件能解决发邮件的问题，那我能不能自己搞个简单配置文件来做（貌似写代码的人有技术洁癖），说干就干，我先是在测试的WordPress上进行，版本6.2.4，ChatGPT提示我直接在主题下面的 <code>functions.php</code> 文件里加代码就行，路径 <code>/wp-content/themes/你的主题名/functions.php</code></p><figure class="highlight php"><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="function"><span class="keyword">function</span> <span class="title">configure_smtp</span><span class="params">( PHPMailer $phpmailer )</span> </span>&#123;</span><br><span class="line">    $phpmailer-&gt;isSMTP();</span><br><span class="line">    $phpmailer-&gt;Host       = <span class="string">'smtp.qq.com'</span>;       <span class="comment">// 你的 SMTP 服务器</span></span><br><span class="line">    $phpmailer-&gt;SMTPAuth   = <span class="keyword">true</span>;</span><br><span class="line">    $phpmailer-&gt;Port       = <span class="number">465</span>;                 <span class="comment">// SMTP 端口，465 是 SSL 的常用端口</span></span><br><span class="line">    $phpmailer-&gt;Username   = <span class="string">'707070901@qq.com'</span>;  <span class="comment">// 你的 SMTP 用户名</span></span><br><span class="line">    $phpmailer-&gt;Password   = <span class="string">'SMTPjyiqwhzlSMTP'</span>;  <span class="comment">// 你的 SMTP 密码</span></span><br><span class="line">    $phpmailer-&gt;SMTPSecure = <span class="string">'ssl'</span>;               <span class="comment">// 加密类型，使用 ssl</span></span><br><span class="line">    $phpmailer-&gt;From       = <span class="string">'707070901@qq.com'</span>;  <span class="comment">// 发件人地址</span></span><br><span class="line">    $phpmailer-&gt;FromName   = <span class="string">'Your Name'</span>;         <span class="comment">// 发件人姓名</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">add_action( <span class="string">'phpmailer_init'</span>, <span class="string">'configure_smtp'</span> );</span><br></pre></td></tr></table></figure><p>一测试发邮件，网站崩溃了，最后在开头加了一句 <code>use PHPMailer\PHPMailer\PHPMailer;</code> 问题解决</p><h3 id="正式修改"><a href="#正式修改" class="headerlink" title="正式修改"></a>正式修改</h3><p>后来我又到正式的WordPress上修改，这次为6.2.4版本，但是这个WordPress主题是自定义的，我没找到 <code>functions.php</code>，ChatGPT又给我出了主意，让我自己创建一个我没听，而是采用了他说的另一种方法，使用 mu-plugins 文件夹，如果你确保这段代码始终运行（甚至在你切换主题时），可以使用 WordPress 的 mu-plugins 机制（”must-use plugins”）。</p><p>在/var/www/sanctus-senki.com/cms/wp-content/下新建文件夹 <code>mu-plugins</code></p><p>在这个文件夹下新建文件 <code>custom-smtp.php</code> 编写下面内容，这个版本不能在开头加 <code>use PHPMailer\PHPMailer\PHPMailer;</code>代码段，否则报错</p><figure class="highlight php"><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="meta">&lt;?php</span></span><br><span class="line"><span class="comment">// Custom SMTP configuration for WordPress</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">configure_smtp</span><span class="params">( PHPMailer $phpmailer )</span> </span>&#123;</span><br><span class="line">    $phpmailer-&gt;isSMTP();</span><br><span class="line">    $phpmailer-&gt;Host       = <span class="string">'smtp.qq.com'</span>;       <span class="comment">// 你的 SMTP 服务器</span></span><br><span class="line">    $phpmailer-&gt;SMTPAuth   = <span class="keyword">true</span>;</span><br><span class="line">    $phpmailer-&gt;Port       = <span class="number">465</span>;                 <span class="comment">// SMTP 端口，465 是 SSL 的常用端口</span></span><br><span class="line">    $phpmailer-&gt;Username   = <span class="string">'707070901@qq.com'</span>;  <span class="comment">// 你的 SMTP 用户名</span></span><br><span class="line">    $phpmailer-&gt;Password   = <span class="string">'SMTPjyiqwhzlSMTP'</span>;  <span class="comment">// 你的 SMTP 密码</span></span><br><span class="line">    $phpmailer-&gt;SMTPSecure = <span class="string">'ssl'</span>;               <span class="comment">// 加密类型，使用 ssl</span></span><br><span class="line">    <span class="comment">//$phpmailer-&gt;From       = '707070901@qq.com';// 发件人地址（这里不强制发件人可以显示转发哦）</span></span><br><span class="line">    $phpmailer-&gt;FromName   = <span class="string">'Your Name'</span>;         <span class="comment">// 发件人姓名</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">add_action( <span class="string">'phpmailer_init'</span>, <span class="string">'configure_smtp'</span> );</span><br><span class="line"><span class="meta">?&gt;</span></span><br></pre></td></tr></table></figure><p>好了，这件事告一段落了，大道至简，经历了由简到繁，由繁化简的历程，才明白简约的可贵。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>AWS Route53 提供高度可用的 DNS 解析服务，将域名映射到 AWS 资源或其他 IP 地址</li><li>ACM 支持免费申请 <strong>公有 SSL/TLS 证书</strong>，用于保护基于 HTTPS 协议的流量</li><li>AWS ELB（Elastic Load Balancing）是AWS提供的一项负载均衡服务，主要分为ALB、NLB、CLB，旨在自动分配进入应用程序的流量，以确保高可用性和可靠性</li><li>Linux系统下 <code>mail</code> 是一个邮件客户端，用于发送和接收邮件，<code>sendmail</code> 和 <code>postfix</code> 是邮件传输代理（MTA），主要用于处理邮件的发送和接收</li><li>自 2020 年 1 月 7 日起，Amazon EC2 开始限制默认情况下通过端口 25 的电子邮件流量</li><li>记住WordPress的 <code>MW WP WPForm</code>、<code>WP MIAL SMTP</code>、<code>Easy WP SMTP</code> 插件，你或许以后用的上</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/142427271" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p> 过钢者易折，善柔者不败</p><p>不必为人性感到意外，哪个人强大了不对弱者动武，哪个女人漂亮了不被男人惦记，<br>利刃在手，易起杀心，权大无边，总想乱来</p><p>2024-9-26 20:26:55</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;前段时间不是在&lt;a href=&quot;https://blog.csdn.net/albertsh/article/details/142427271&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;迁移AWS&lt;/a&gt;嘛，采用了分享AMI的方式，废了九牛二虎之力，终于在没有密钥的情况下成功登录了转移后的EC2实例，那么我们肯定不单单是为了转移一个EC2实例而大费周章，实际目的还是为了里面的数据和服务，具体来说就是一套WordPress网站，对于一个新的知识和事物围绕它的有很多知识点，一旦发散的过快就发现东西太多了，什么也讲不明白，造成虎头蛇尾的现象，做技术内容输出的人经常有这种力不从心的感觉。&lt;/p&gt;
    
    </summary>
    
      <category term="Practical" scheme="http://AlbertGithubHome.github.io/blog/categories/Practical/"/>
    
    
      <category term="aws" scheme="http://AlbertGithubHome.github.io/blog/tags/aws/"/>
    
      <category term="ALB" scheme="http://AlbertGithubHome.github.io/blog/tags/ALB/"/>
    
      <category term="Route53" scheme="http://AlbertGithubHome.github.io/blog/tags/Route53/"/>
    
      <category term="WordPress" scheme="http://AlbertGithubHome.github.io/blog/tags/WordPress/"/>
    
      <category term="ACM" scheme="http://AlbertGithubHome.github.io/blog/tags/ACM/"/>
    
      <category term="postfix" scheme="http://AlbertGithubHome.github.io/blog/tags/postfix/"/>
    
  </entry>
  
  <entry>
    <title>AWS镜像迁移登录问题二三则</title>
    <link href="http://AlbertGithubHome.github.io/blog/2024/09/22/AWS%E9%95%9C%E5%83%8F%E8%BF%81%E7%A7%BB%E7%99%BB%E5%BD%95%E9%97%AE%E9%A2%98%E4%BA%8C%E4%B8%89%E5%88%99/"/>
    <id>http://AlbertGithubHome.github.io/blog/2024/09/22/AWS镜像迁移登录问题二三则/</id>
    <published>2024-09-21T16:26:14.000Z</published>
    <updated>2024-09-28T07:21:26.652Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>关于云服务器之前用的多的还是国内的阿里云和腾讯云，也用过一点点的华为云，而今年接触的AWS和GCP比较多，使用场景渐渐多了起来，遇到的问题也渐渐多了起来，之前这些都是有专门的运维同事负责的，而现在降本增效之后只能自己上了，这两条在捣鼓AWS，就先记录一些AWS上遇到的问题和解决办法吧，关于问题我尽量贴一些原始的报错，便于有相同问题的人进行对照。</p><a id="more"></a><h1 id="EC2镜像迁移"><a href="#EC2镜像迁移" class="headerlink" title="EC2镜像迁移"></a>EC2镜像迁移</h1><p>此文章的本意是记录遇到的问题和解决办法，所以我在这不会详细描述怎样做镜像，自己去搜索文档吧，要实在找不到可以留言等有空我帮你找下，说说镜像有什么用，在AWS中的系统镜像被称为AMI，全称Amazon Machine Image，可以简单的连接为操作系统，不过要更好用一些。</p><p>现实中我们买一台新电脑是先挑硬件组装好，然后安装操作系统，Windows、Linux都可以，如果是云服务器比如AWS，也是先选配置比如CPU、内存、硬盘等等，然后选择一个AMI就可以安装使用了，如果你操作很熟练的话，大概几秒钟就可以得到一台可以立即使用的特定配置的云服务器。</p><p>这里的镜像可以选择官方Windows、Linux、Mac等官方支持的系统，也可以选择第三方的，甚至是自己可以定制的，比如你的项目大量使用Ubuntu+XXX的机器，每次创建Ubuntu实例后都要安装XXX软件，这就很麻烦，你可以使用安装好XXX的实例创建映像，保存下来，以后创建新的实例时直接使用这个AMI就很方便了，有点像Docker，但这比Docker要重度一些。</p><p>而我使用这个映像的情景是项目的迁移，将AWS实例从一个账号迁移到另一个账号，这里有几个问题需要注意：</p><ol><li>AMI可以分享，但只能在同一个区域内，如果跨区域只能先复制到同区域再进行分享</li><li>使用AMI镜像时需要分配大于制作镜像时磁盘大小，所以在创建实例时磁盘应该尽可能合理，否则分配了100G实际只用3G，那么用这个AMI最少也要分配100G才行</li><li>使用共享AMI先要浏览获取基础映像的授权，我在这遇到了一个问题，选择了另一个账号的AMI，但是启动失败，提示错误如下：</li></ol><blockquote><p>In order to use this AWS Marketplace product you need to accept terms and subscribe. To do so please visit <a href="https://aws.amazon.com/marketplace/pp?sku=aw0evgkw8e5c1q413zgy5pjce" target="_blank" rel="noopener">https://aws.amazon.com/marketplace/pp?sku=aw0evgkw8e5c1q413zgy5pjce</a></p></blockquote><p>其实你访问提供的<a href="https://aws.amazon.com/marketplace/pp?sku=aw0evgkw8e5c1q413zgy5pjce" target="_blank" rel="noopener">链接</a>会看到产品的详细信息和定价。你需要阅读并接受产品条款，然后点击“Continue”或类似的按钮完成订阅。之后你就可以返回 AWS 管理控制台，再次尝试启动实例，这时候应该可以成功启动。</p><h1 id="无法登录古老的AWS实例"><a href="#无法登录古老的AWS实例" class="headerlink" title="无法登录古老的AWS实例"></a>无法登录古老的AWS实例</h1><p>无法登录的原因，并不一定是因为AWS出问题了，AWS本身出问题不能说完全没有，但是这种情况概率比较小，来聊聊我遇到的情况。</p><p>接手别的项目组的AWS，密钥已经无法找到，这种情况我已经遇到了两次，都是经了几手的项目，密钥早已经找不到了，我们知道这个密钥只能在创建时下载一次，丢了就几乎无法再登录这个实例了，很多时候需要删除重建了。</p><p>为什么说几乎呢？因为没有密钥也不是完全没有办法再登录进去，我在这走了很多弯路，你可以先检测一下这几个问题，对你或许有帮助</p><ol><li>再找找原来的密钥，如果能找到这是最方便的途径</li><li>使用AWS网站后台的“连接”按钮，来直连实例</li><li>检查实例的安全规则，看看22端口是否放行，是否使用其他端口来运行ssh服务了</li></ol><p>查询端口是否连通的办法有 <code>telnet ip port</code> 或 <code>nmap -p port ip</code> 等多种办法</p><p>前两种方法我都试了行不通，而第三种遇到问题时还没意识到，所以我使用了“必杀技”，将无法连接的实例硬盘分离，然后挂载到其他可以登录的实例，然后挂载修改ssh密钥，再挂载回去重新登录即可，挂载到其他实例的命令如下，根据实际情况调整</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">mkdir /mnt/temp</span><br><span class="line">sudo mount /dev/xvdb1 /mnt/temp</span><br></pre></td></tr></table></figure><p>这时你可能会遇到报错，提示你挂载报错，大概率是UUID重复了，可以修改挂载命令 <code>sudo mount -o nouuid /dev/xvdb1 /mnt/temp</code></p><p>但我修改后还是不能登录，后来发现端口22不通，我一度以为这个实例没有启动ssh服务，尝试了多种方法，走了很多弯路，比如 <code>chroot</code> 或者修改用户数据添加启动ssh命令的脚本但都没有效果，最终发现是ssh服务器被改到了2022端口，废了九牛二虎之力终于可以访问ssh服务了，但还是认证失败</p><p>ssh登录时添加 <code>-vvv</code> 选项，输出以下关键调试信息</p><figure class="highlight plain"><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">debug3: send packet: type 50</span><br><span class="line">debug2: we sent a publickey packet, wait for reply</span><br><span class="line">debug3: receive packet: type 51</span><br><span class="line">debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic</span><br><span class="line">debug2: we did not send a packet, disable method</span><br></pre></td></tr></table></figure><p>关键是 <code>receive packet: type 51</code> 这一句表示publickey验证失败，如果能登录这里应该是 <code>receive packet: type 52</code>，原因大概率就是publickey出了问题，我是因为没把publickey放到服务器上，不过我也很奇怪，创建实例时明明选了密钥，可能因为一开始启动报错就没放上去，导致了后面一系列弯路</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>AWS 和 GCP 是全球主流的云服务，Microsoft 的 Azure至今还没用过</li><li>如果想重用一个云服务环境，可以将这个实例创建成一个AMI保存</li><li>如果无法登录实例看看安全规则有没有换ssh的端口</li><li>如果密钥真的丢了就把实例的硬盘挂载到其他的实例上修改密钥吧</li></ul><hr><center><a href="https://blog.csdn.net/albertsh/article/details/141725939" target="_blank" rel="noopener"> ==&gt;&gt; 反爬链接，请勿点击，原地爆炸，概不负责！&lt;&lt;== </a></center><hr><blockquote><p> 你我皆凡人，生在人世间。终日奔波苦，一刻不得闲。既然不是仙，难免有杂念。道义放两旁，利字摆中间~</p><p>无德无行而取厚利，必有奇祸；善行善德而受磨难，必有后福；修德以配位，自谦以远祸。以色相交者，色衰而爱弛；以利相交者，利尽而交疏；以势相交者，势倾而交绝；以道相交者，天荒而地老；以德相交者，地久而天长；你用什么吸引人，你吸引的就是什么样的人。</p><p>2024-9-22 00:10:51</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;关于云服务器之前用的多的还是国内的阿里云和腾讯云，也用过一点点的华为云，而今年接触的AWS和GCP比较多，使用场景渐渐多了起来，遇到的问题也渐渐多了起来，之前这些都是有专门的运维同事负责的，而现在降本增效之后只能自己上了，这两条在捣鼓AWS，就先记录一些AWS上遇到的问题和解决办法吧，关于问题我尽量贴一些原始的报错，便于有相同问题的人进行对照。&lt;/p&gt;
    
    </summary>
    
      <category term="Practical" scheme="http://AlbertGithubHome.github.io/blog/categories/Practical/"/>
    
    
      <category term="aws" scheme="http://AlbertGithubHome.github.io/blog/tags/aws/"/>
    
      <category term="云计算" scheme="http://AlbertGithubHome.github.io/blog/tags/%E4%BA%91%E8%AE%A1%E7%AE%97/"/>
    
      <category term="AMI" scheme="http://AlbertGithubHome.github.io/blog/tags/AMI/"/>
    
      <category term="ssh" scheme="http://AlbertGithubHome.github.io/blog/tags/ssh/"/>
    
      <category term="密钥丢失" scheme="http://AlbertGithubHome.github.io/blog/tags/%E5%AF%86%E9%92%A5%E4%B8%A2%E5%A4%B1/"/>
    
      <category term="mount" scheme="http://AlbertGithubHome.github.io/blog/tags/mount/"/>
    
  </entry>
  
</feed>
