<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>jingsam</title>
  
  
  <link href="https://jingsam.github.io/atom.xml" rel="self"/>
  
  <link href="https://jingsam.github.io/"/>
  <updated>2022-08-13T14:10:48.352Z</updated>
  <id>https://jingsam.github.io/</id>
  
  <author>
    <name>jingsam</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>拓扑排序原理</title>
    <link href="https://jingsam.github.io/2020/08/11/topological-sort.html"/>
    <id>https://jingsam.github.io/2020/08/11/topological-sort.html</id>
    <published>2020-08-11T07:36:15.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>最近在开发一个流程化建模的功能，即用户将多个算子的输入输出连接起来构成流程图，完成建模并执行。其中涉及到一个重要的技术过程，是将算子按照输入输出的依赖关系进行排序，对于任意一个算子，其依赖的前驱算子都排在前面，保证算子执行的时候输入条件已准备好。</p><p>实际上，上面构建的流程化模型是一个有向无环图（Directed Acyclic Graph, DAG）。顾名思义，DAG有三大特点：“有向”、“无环”、“图”。DAG首先是一种“图”，这种“图”是有方向性的，并且没有环路。</p><p>DAG是一个专业术语，虽然对于一般人比较陌生，但它实际上隐藏在生活中的方方面面。例如，一个工程项目有多个子项目组成，子项目之间有先后关系，一个子项目的开展必须保证前面的项目完成；工厂里面生产的产品，产品由若干部件组成，每个部件再由多个零件组成，组装时必须保证一定的先后顺序。在软件领域，我们也会不知不觉地碰到DAG，例如程序模块之间的依赖关系、makefile脚本、区块链、git分支等。</p><p>按照依赖关系对DAG的顶点进行排序，使得对每一条有向边<code>(u, v)</code>，均有<code>u</code>（在排序记录中）比<code>v</code>先出现，这种排序就是<strong>拓扑排序</strong>。</p><p>例如，下图中<code>1 → 2 → 4 → 3 → 5</code>是一个正确的拓扑排序，每个节点都在它说依赖的节点后面。而<code>1 → 2 → 3 → 4 → 5</code>则不满足拓扑排序的要求，<code>3</code>依赖于<code>4</code>却出现在前面。</p><img src="/2020/08/11/topological-sort/toposort.png" class=""><h2 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a>基本原理</h2><p>对于有向图进行拓扑排序要解决两个问题：一是要判断待排序的有向图是不是无环；二是按照依赖关系生成正确的序列。</p><p>我们可以从入度着手，选择一个入度为0的节点，说明该节点不依赖于其他节点，可以放在结果序列最前面。然后，删除该节点和节点的边，找出下一个入度为0的节点，依次放到结果序列中。重复以上过程，即可完成拓扑排序。这种方法可称之为<strong>入度方法</strong>。</p><p>同样，我们也可以从出度着手，选择一个出度为0的节点，说明没有任何节点依赖该节点，可以放到结果序列最末尾。然后，删除该节点和节点的边，找出下一个出度为0的节点，依次放到结果序列尾部。重复以上过程，即可完成拓扑排序。这种方法可称之为<strong>出度方法</strong>。</p><p>不管是入度方法还是出度方法，如果最后还存在入（出）度不为0的节点，或者结果序列中的节点个数不等于有向图中的节点个数，说明有向图中存在环。</p><p>按照<a href="https://en.wikipedia.org/wiki/Topological_sorting">维基百科</a>的说法，拓扑排序主要有Kahn算法和DFS（深度优先搜索）算法两种。Kahn算法采用入度方法，以循环迭代方法实现；DFS算法采用出度方法，以递归方法实现。Kahn算法和DFS算法的复杂度是一样的，算法过程也是等价的，不分优劣，因为本质上<code>循环迭代 + 栈 = 递归</code>。</p><h2 id="Kahn算法"><a href="#Kahn算法" class="headerlink" title="Kahn算法"></a>Kahn算法</h2><p>Kahn算法采用入度方法，其算法过程如下：</p><ol><li>选择入度为0的节点，输出到结果序列中；</li><li>删除该节点以及该节点的边；</li><li>重复执行步骤1和2，直到所有节点输出到结果序列中，完成拓扑排序；如果最后还存在入度不为0的节点，说明有向图中存在环，无法进行拓扑排序。</li></ol><p>下图是对Kahn算法的演绎：</p><img src="/2020/08/11/topological-sort/kahn.png" class=""><h2 id="DFS算法"><a href="#DFS算法" class="headerlink" title="DFS算法"></a>DFS算法</h2><p>DFS算法采用出度算法，其算法过程如下：</p><ol><li>对有向图进行深度优先搜索；</li><li>在执行深度优先搜索时，若某个顶点不能继续前进，即顶点的出度为0，则将此顶点入栈。</li><li>最后对栈中的序列进行逆排序，即完成拓扑排序；如果深度优先搜索时，碰到已遍历的节点，说明存在环。</li></ol><p>下图是对DFS算法的演绎：</p><img src="/2020/08/11/topological-sort/dfs.png" class=""><h2 id="One-more-thing"><a href="#One-more-thing" class="headerlink" title="One more thing"></a>One more thing</h2><p>仔细观察发现，Kahn算法并不一定局限于入度方法，同样适用于出度方法，过程为先选择一个出度为0的节点输出，然后删除该节点和节点的边，重复“选择-删除”过程直到没有出度为0的节点输出，最终序列的逆排序即是拓扑排序结果。这个过程实际上是将原来的有向图的边反向，原来的入度变出度、出度边入度，Kahn算法将出度当成入度处理，最后我们再将结果逆序就还原了拓扑排序结果。</p><p>同样，DFS算法也不一定局限于出度算法，我们同样可以将有向图反向，入度变出度、出度边入度。由于我们对有向图反向，所以需要对结果做两次逆序操作，两次逆序操作相当于不需要逆序操作，所以结果序列刚好是拓扑排序结果。</p><p>最后，我将在下篇文章中使用Javascript来分别实现Kahn算法和DFS算法。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p><a href="https://en.wikipedia.org/wiki/Topological_sorting">Topological sorting</a><br><a href="https://www.cnblogs.com/en-heng/p/5085690.html">有向无环图的拓扑排序</a><br><a href="https://www.cxyxiaowu.com/1084.html">浅谈什么是图拓扑排序</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近在开发一个流程化建模的功能，即用户将多个算子的输入输出连接起来构成流程图，完成建模并执行。其中涉及到一个重要的技术过程，是将算子按照输入输出的依赖关系进行排序，对于任意一个算子，其依赖的前驱算子都排在前面，保证算子执行的时候输入条件已准备好。&lt;/p&gt;
&lt;p&gt;实际上，上面</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>加速Docker多阶段构建的方法</title>
    <link href="https://jingsam.github.io/2020/08/07/speed-up-docker-multi-stage-builds.html"/>
    <id>https://jingsam.github.io/2020/08/07/speed-up-docker-multi-stage-builds.html</id>
    <published>2020-08-07T04:18:56.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>使用多阶段构建（multi-stage build）是减小docker镜像体积很有效的方法，例如拿一个常见的Java应用为例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">FROM maven:3.6-jdk-8-alpine</span><br><span class="line">WORKDIR /app</span><br><span class="line">COPY pom.xml .</span><br><span class="line">RUN mvn -e -B dependency:resolve</span><br><span class="line">COPY src ./src</span><br><span class="line">RUN mvn -e -B package</span><br><span class="line">CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/app/target/app.jar&quot;]</span><br></pre></td></tr></table></figure><p>粗看之下似乎没有改进的空间，但是细看之后发现镜像里安装了完整的jdk环境，包含java的sdk和runtime。实际上，运行jar包只需要java runtime，sdk是多余的。</p><p>通过使用多阶段构建，将应用的编译环境和运行环境分离，可以极大地减少最终镜像的体积。例如，对将上面的Dockerfile修改为多阶段构建：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">FROM maven:3.6-jdk-8-alpine AS builder</span><br><span class="line">WORKDIR /app</span><br><span class="line">COPY pom.xml .</span><br><span class="line">RUN mvn -e -B dependency:resolve</span><br><span class="line">COPY src ./src</span><br><span class="line">RUN mvn -e -B package</span><br><span class="line"></span><br><span class="line">FROM openjdk:8-jre-alpine</span><br><span class="line">COPY --from=builder /app/target/app.jar /</span><br><span class="line">CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]</span><br></pre></td></tr></table></figure><p>以上Dockerfile生成的最终镜像只包含runtime，不再有sdk。</p><p>但是凡事皆有两面性，多阶段构建虽然能够减小镜像体积，但是构建的速度慢了许多。原因在于：一是相比于原先的单阶段构建，多了一些构建步骤；二是缓存失效，多阶段编译之后只保留最终镜像的缓存，中间镜像的缓存丢失。其中缓存失效的问题在CI环境中尤为显著。</p><p>加快多阶段构建的措施有两项：并行构建和保留缓存。</p><h2 id="并行构建"><a href="#并行构建" class="headerlink" title="并行构建"></a>并行构建</h2><p>如果把多阶段构建中各阶段之间的依赖关系画出来，实际上是一个有向无环图（DAG, Directed Acyclic Graph）。在图中，有些节点之间是没有前后关系的，意味着某些阶段可以并行构建。</p><p>从Docker 18.09开始引入了并行构建，启用方法有两种：</p><ol><li>临时启用：设置环境变量<code>DOCKER_BUILDKIT=1</code>；</li><li>默认启用：在<code>/etc/docker/daemon.json</code>中设置<code>&#123; &quot;features&quot;: &#123; &quot;buidkit&quot;: true &#125;&#125;</code>。</li></ol><h2 id="保留缓存"><a href="#保留缓存" class="headerlink" title="保留缓存"></a>保留缓存</h2><p>保留缓存意思是不仅保留最终镜像的缓存，还保留中间镜像的缓存。</p><p><code>docker build</code>有两个与缓存相关的参数：<code>--cache-from</code>和<code>BUILDKIT_INLINE_CACHE=1</code>。<code>--cache-from</code>表示可以指定镜像作为缓存源，可以指定多个镜像，指定后会从镜像仓库自动拉取本地不存在的镜像。<code>BUILDKIT_INLINE_CACHE=1</code>表示在构建时将缓存的元数据打包到镜像中，使之可以作为缓存源。默认构建的镜像不包含缓存的元数据，不能被<code>--cache-from</code>使用。</p><p>还是拿文章开头的java应用为例：</p><p>首先将编译阶段的镜像进行构建和上传：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker build --build-arg BUILDKIT_INLINE_CACHE=1 \</span><br><span class="line">  --cache-from=builder \</span><br><span class="line">  --target builder \</span><br><span class="line">  --tag builder .</span><br><span class="line">docker push builder</span><br></pre></td></tr></table></figure><p>然后将最终阶段利用前一阶段和当前阶段的缓存进行构建：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker build --build-arg BUILDKIT_INLINE_CACHE=1 \</span><br><span class="line">  --cache-from=builder \</span><br><span class="line">  --cache-from=app \</span><br><span class="line">  --tag app .</span><br><span class="line">docker push app</span><br></pre></td></tr></table></figure><p>通过以上过程，就可以在多阶段构建过程中充分利用缓存来加快构建速度。</p><p>本文完。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;使用多阶段构建（multi-stage build）是减小docker镜像体积很有效的方法，例如拿一个常见的Java应用为例：&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pr</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>使用GDAL完成影像切片</title>
    <link href="https://jingsam.github.io/2020/07/15/raster-tiling-with-gdal.html"/>
    <id>https://jingsam.github.io/2020/07/15/raster-tiling-with-gdal.html</id>
    <published>2020-07-15T04:12:10.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>2020年已经过去一半多了，今天才更新2020年的第一篇博客文章，惭愧！如果能找什么借口的话，过去的一段时间把精力放在了<a href="https://www.yuque.com/geoway-vision/vision">可视化团队知识库</a>的建设上面，忽视了博客的建设。</p><p>知识库里面的文章的主要目的其一是作为团队工程经验的积累，其二是让团队新人能够快速掌握时空大数据可视化相关的知识。基于以上目的，知识库里面的文章偏向于科普向和入门向，注重于知识的广度而不是深度，放到博客上似乎有些“低技术含量”了。同时，平时在做文章选题时，经常会在知识库和博客之间犹豫徘徊，犹豫来犹豫去，最后写作的热情也就没有了😂。</p><p>不过最近我的想法又有了些变化，“低技术含量”的文章不见得让本博客掉价。每个人的技术专精的方向不一样，我认为简单的技术，读者可能不太了解，需要一些“低技术含量”的文章帮助他快速了解和入门。所以，今后本博客的文章不会太刻意去追求技术深度，只要我觉得有价值的点，都会形成文章分享出来。</p><p>今天的这篇文章就是一篇“低技术含量”的文章，主要讲使用GDAL来完成遥感影像的切片。</p><p>对了，最近我更新了<a href="/assets/%E7%AE%80%E5%8E%86_GIS_%E7%A0%94%E5%8F%91%E4%B8%BB%E7%AE%A1_%E5%BD%AD%E9%87%91%E9%87%91_%E6%AD%A6%E6%B1%89%E5%A4%A7%E5%AD%A6_18771991849.pdf">简历</a>，如果您有什么好的工作机会介绍给我，那就太感谢了🙏。</p><p>前段时间做一个可视化大屏，需要对遥感影像进行切片。由于近几年一直在研究矢量瓦片技术，对栅格切片这块的工具比较生疏。在网上搜了一下，发现合适的工具并不多。要么是诸如GeoServer这样的巨无霸，要么就是一些只支持限定数据源和限定切片规则的小工具。仔细研究了下GDAL，发现组合使用栅格格式转换工具<code>gdal_translate</code>和金字塔生成工具<code>gdaladdo</code>就能够实现支持任意数据源和任意切片规则的影像切片工作流。</p><p>开始之前，需要选定一种存储栅格瓦片的容器格式。看了一下发现选择并不多，暂时时只发现MBTiles、GeoPackage和COG（Cloud Optimized GeoTIFF）三种。MBtiles只支持EPSG:3857一种切片规则，不符合需要支持任意切片规则的要求。COG则是基于GeoTiff的一种Hack实现，技术过程过于复杂，也不予以考虑。所以能够选择的实际上只有一种：GeoPackage。</p><p>接下来使用如下命令就可以将影像转换为栅格瓦片，并输出到GeoPackage中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gdal_translate -of GPKG chengdu.tif chengdu.gpkg -co TILING_SCHEME=InspireCRS84Quad</span><br></pre></td></tr></table></figure><p>以上命令中，<code>-of</code>用来指定输出格式。<code>TILING_SCHEME</code>用来指定切片规则，比较常用的是<code>GoogleMapsCompatible</code>（EPSG:3857）和<code>InspireCRS84Quad</code>（EPSG:4326）,也可以自定义切片规则，具体做法请查看<a href="https://gdal.org/drivers/raster/gpkg.html#creation-options">文档</a>。</p><p>需要注意的是，以上命令只是根据切片规则选择一个合适的级别，生成一个单一级别的瓦片，并不是我们需要的多级别的瓦片。生成多级别的瓦片，需要使用接下来的命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gdaladdo chengdu.gpkg</span><br></pre></td></tr></table></figure><p>如果GeoPackage里面有多个瓦片集，可以使用以下命令来指定为某个瓦片集生成金字塔：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gdaladdo GPKG:chengdu.gpkg:layername</span><br></pre></td></tr></table></figure><p>从GDAL 3.2开始还支持并行，使用以下命令使用所有的CPU来加速生成：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gdaladdo chengdu.gpkg --config GDAL_NUM_THREADS ALL_CPUS</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;2020年已经过去一半多了，今天才更新2020年的第一篇博客文章，惭愧！如果能找什么借口的话，过去的一段时间把精力放在了&lt;a href=&quot;https://www.yuque.com/geoway-vision/vision&quot;&gt;可视化团队知识库&lt;/a&gt;的建设上面，忽视了博客的</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>《会说谎的地图》</title>
    <link href="https://jingsam.github.io/2019/11/10/how-to-lie-with-maps.html"/>
    <id>https://jingsam.github.io/2019/11/10/how-to-lie-with-maps.html</id>
    <published>2019-11-10T03:12:04.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>最近读了一本两百多页的小书《会说谎的地图》（How to Lie with Maps），之前就听说过这本书但总逮不到机会拜读。最近买其他书的时候，恰好遇到打折便入手，断断续续花了一周时间读完，果然物超所值。</p><p>当正常人听到某些话，一般会在心里掂量一下是不是真话，并不会毫不保留地接受；否则，那就是容易被骗的傻子。每当电视上一些机构发布的统计数字，有分辨力的听众，也得先搞清楚统计方法再评判统计数字是不是真实。拿到一本书，有批判精神的读者，总是要质疑论据能不能支撑观点。所以，当前大众对于话语、统计数字、书本这些信息来源有良好的警惕性，知道假话、错误的数字、误导人的观点都有可能存在于这些信息来源中。但是，当面对的信息来源是地图时，大众的警惕性似乎就没有那么普遍了。</p><p>《会说谎的地图》这本书告诉我们一个事实：地图也会说谎，而且说谎的程度并不亚于常规的信息来源。地图是现实世界的概括和抽象，并不是与现实世界一一对应的比例模型（从这个角度来说，遥感影像并不能称为地图，除非在上面加绘地理要素）。制图员在概括和抽象的过程中，有可能会根据预设观点，对要素进行适应性地取舍或者夸大。例如，我们经常在各种房产传单中看到一些简略的地图，那幅地图会告诉客户房产的地段好、交通方便。如果你认为地图是可靠的信息来源的话，等你到实地考察一番，很可能会让你大跌眼镜：地图上葱葱郁郁的绿化现在是光秃秃的小树苗，地图上近在迟尺的商场，需要搭乘半个小时以上的公交。所以说，地图只是制图员（或者背后的金主爸爸）一种观点表达，并不是完全权威可靠的信息来源，大众对于地图同样需要保持警惕性。</p><p>《会说谎的地图》列举了一些常见的地图“骗术”，下面讲一讲印象比较深刻的几种。</p><p>第一种是利用地图投影形变，突出某些地区。例如，我们常见的网络地图通常采用的Web Mercator投影，会对高纬度地区进行夸大，所以我们会发现俄罗斯、格陵兰岛大得惊人。如果地图上一个区域的面积比重大，那么人们会潜意识地认为这个地区的重要性、话语权比较大。所以，别有用心的地区或国家，会利用地图投影的形变特点，夸大其国土面积，以增强在世界地区的影响力。好在稍有地图常识的人，很容易识别出这种骗术。</p><p>第二种是对地图要素进行有目的性地选择，以美化或者劣化环境要素。前面说过，地图是对现实世界的抽象概括，制图过程涉及到大量的要素取舍。对于由政府编制的地形图，尚且对要素取舍有严格的规定；而对于由商业公司制作的各类专用图，则没有规范惯例可循，制图的目的基本上是服务于特定商业诉求。例如，一个房地产的宣传挂图上，绝不对出现如化工厂、垃圾中转站这些“不和谐”要素，以证明社区环境优美；反之，一个环保组织的地图上，则不会放过这些“不和谐”要素，以证明社区环境恶化程度，提醒人们关注环保问题。这类骗术的识别，首先是要识别出地图提供方的利益关系，其次是要与官方提供的标准图进行对比。</p><p>第三种是变换统计口径，以呈现出特定的分布。同一份统计数据，按照不同的统计口径，有可能会出现完全不同的空间分布特征。例如，每到美国大选，共和党和民主党都会公布支持率分布的“红蓝”选战地图，我们会发现两张地图会呈现完全相反的趋势。那到底那张图是准确的呢？如果没有统一的统计标准，很难说哪张图是正确的。共和党辩解说是按照参议员占比来统计的，民主党则说是按照选民占比来统计的，各自都有理。这种骗术的识别，需要搞清楚统计口径。</p><p>第四种是选择不同的统计单元，抹平区域差异。将人口密度按照省、市、县、乡、村等不同粒度的统计单元进行统计，会得到不同的区域分布特征。统计单元越大，统计值的区域分异规律就会难以显现。某些别有用心的制图者，还会“巧用”统计单元合并策略，将一些极值区域合并到相邻区域，以掩饰某些问题。这类骗术的识别，需要认识到统计单元粒度越大越能掩盖问题，尽量参考统计粒度更小的地图。</p><p>第五种是使用不同的分段策略，呈现出特定的趋势。我们经常对统计值进行分段，然后在地图上使用不同颜色来表示统计值的高低。对统计分段的方法有很多种，例如自然断点法是按照“内间距最小、外间距最大”的原则进行分段，等间距法则是按照相等的间距进行分段。没有办法说哪一种分段方式科学合理，每种分段方法各自有适用的场景。制图者在做这类专题图时，可以有意或者无意地混淆各类分段的适用场景，选择一种能够表达观点的分段方法，使得地图呈现出特定的趋势。这类骗术的识别，需要读者仔细审查地图的图例，看选择的分段策略是不是合理的。另外，使用连续分布的渐变色，可能比经过加工后的分段颜色更可靠。</p><p>以上就是对我印象比较深的几种骗术，当然在《会说谎的地图》中还有更多的骗术，例如使用鲜艳醒目的颜色突出重点、使用虚假的地名误导抄袭者，感兴趣的读者可以去阅读本书了解。总而言之，《会说谎的地图》告诉我们，要对地图有批判精神，不要满盲目地相信地图。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近读了一本两百多页的小书《会说谎的地图》（How to Lie with Maps），之前就听说过这本书但总逮不到机会拜读。最近买其他书的时候，恰好遇到打折便入手，断断续续花了一周时间读完，果然物超所值。&lt;/p&gt;
&lt;p&gt;当正常人听到某些话，一般会在心里掂量一下是不是真话，</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>利用ssh反向穿透实现内网服务器自动部署</title>
    <link href="https://jingsam.github.io/2019/06/14/deploy-with-autossh.html"/>
    <id>https://jingsam.github.io/2019/06/14/deploy-with-autossh.html</id>
    <published>2019-06-14T06:36:17.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>当前持续集成的流行，大大提高了开发迭代的速度。自动化部署作为持续集成的最后一公里，对于完成整个闭环至关重要。如果生产服务器部署至公网且开通了ssh端口，那么测试服务器完成测试打包后，可以很方便地利用ssh将部署包推送到生产服务器上完成部署。但是，如果生产服务器部署在内网，测试服务器部署在外网，那么测试服务器就无法得知生产服务器的IP，无法利用ssh远程登录实现自动部署。</p><p>本文利用ssh的反向穿透技术，来实现内网服务器的自动部署。</p><h2 id="技术原理"><a href="#技术原理" class="headerlink" title="技术原理"></a>技术原理</h2><p>ssh一般用来客户端远程登录到服务器上，而ssh反向穿透“反其道而行之”，由服务端主动发起请求连接客户端，然后在客户端打开一个端口，之后发往客户端的数据包将会转发到服务端。例如在服务端执行：</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">ssh -NR 22022:localhost:22 clientUser@clientMachine</span><br></pre></td></tr></table></figure><p>连接成功后，发往客户端<code>22022</code>的数据包将被转发到服务端的<code>22</code>端口上。这时，如果客户端具有公网IP，那么我们就可以利用客户端机器作为跳板，远程登录到内网服务器上，命令如下：</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">ssh -p 22022 serverUser@clientMachine</span><br></pre></td></tr></table></figure><p>通过ssh的反向穿透技术，让内网服务器借用公网客户机的IP，实现内网服务器公网可见。在此基础上，实现内网服务器自动部署的原理如下图：</p><img src="/2019/06/14/deploy-with-autossh/ssh-reverse-proxy.png" class=""><p>上述原理很简单，但具体操作起来，还是有许多细节要考虑，下面对此一一说明。</p><h2 id="持久化连接"><a href="#持久化连接" class="headerlink" title="持久化连接"></a>持久化连接</h2><p>通过ssh在第①步建立了生产服务器<code>prodServer</code>与跳板机<code>jumpServer</code>的连接隧道，但是这条隧道如果长时间没有数据包传输，那么ssh会主动关闭连接，之后测试服务器<code>testServer</code>就不能再通过跳板机连接到生产服务器。</p><p>为了保持生产服务器与跳板机的连接隧道不断开，需要ssh自动重连。ssh不支持自动重连功能，幸好有autossh帮我们完成这项工作。</p><p>autossh不属于系统自带工具，我们需要在生产服务器安装它，Ubuntu上的安装命令如下：</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 apt-get install autossh</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">autossh -M 0 -f -o <span class="string">&quot;ServerAliveInterval=30&quot;</span> -o <span class="string">&quot;ServerAliveCountMax=3&quot;</span> -o <span class="string">&quot;ExitOnForwardFailure=yes&quot;</span> -NR 22022:localhost:22 jump@jumpServer</span><br></pre></td></tr></table></figure><p>上述命令中，除了<code>-M 0</code>和<code>-f</code>是autossh的参数外，其他参数都原样传递给ssh，其中<code>-M 0</code>表示不另开端口监测ssh，<code>-f</code>表示后台运行。auotssh以前是靠<code>-M</code>另开一个端口发送心跳数据包，由于新版ssh（protocol 2）内建了心跳功能，所以不再推荐另开端口。上面的命令使用<code>ServerAliveInterval</code>和<code>ServerAliveCountMax</code>两个参数，表示客户端向服务端每30秒发送一次心跳数据包，如果发3次还没响应，那么断开连接。我们也可以在服务端的<code>/etc/ssh/sshd_config</code>配置文件中添加<code>ClientAliveInterval 30</code>和<code>ClientAliveCountMax 3</code>参数后，重启sshd，表示由服务端向客户端发送心跳数据包。<code>ExitOnForwardFailure</code>表示ssh转发失败后，关闭连接并退出，这样autossh才能监测到错误并重启ssh连接。</p><p>执行上述命令后，我们只能在<code>jumpServer</code>上登录到<code>prodServer</code>。如果我们想要从任意机器上远程登录到<code>prodServer</code>，需要在<code>jumpServer</code>的<code>/etc/ssh/sshd_config</code>中添加<code>GatewayPorts yes</code>参数并重启sshd，之后执行：</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">ssh -p 22022 prod@jumpServer</span><br></pre></td></tr></table></figure><p>如果<code>prodServer</code>重启后，我们希望<code>autossh</code>也能随系统启动，此时需要将<code>autossh</code>添加到启动项中，以下是以systemd为例，新建一个<code>/lib/systemd/system/autossh.service</code>配置文件来添加autossh启动项：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">[Unit]</span><br><span class="line">Description=autossh</span><br><span class="line">Wants=network-online.target sshd.service</span><br><span class="line">After=network-online.target sshd.service</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">Type=simple</span><br><span class="line">User=geoeye</span><br><span class="line">Environment=&quot;AUTOSSH_GATETIME=0&quot;</span><br><span class="line">ExecStart=/usr/bin/autossh -M 0 -o &quot;ServerAliveInterval=30&quot; -o &quot;ServerAliveCountMax=3&quot; -NR 22022:localhost:22 jingsam@www.foxgis.com</span><br><span class="line">ExecStop=/bin/kill $MAINPID</span><br><span class="line">Restart=on-failure</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure><p>注意上面启动autossh的命令中，要把<code>-f</code>选项去掉，并且autossh使用绝对路径。</p><p>最后执行以下命令执行来启动autossh：</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 systemctl <span class="built_in">enable</span> autossh</span><br><span class="line">sudo systemctl start autossh</span><br></pre></td></tr></table></figure><h2 id="免密登录"><a href="#免密登录" class="headerlink" title="免密登录"></a>免密登录</h2><p>不管是<code>prodServer</code>在系统启动时反向连接到<code>jumpServer</code>，还是<code>testServer</code>完成自动测试后推送部署包到<code>prodServer</code>，这些过程都是后台自动执行，我们没有机会去输入登录密码。因此，我们需要在三者之间配置ssh key，实现免密登录。</p><p>首先是<code>prodServer</code>连接到<code>jumpServer</code>，这一步比较简单，只需在<code>prodServer</code>使用<code>ssh-copy-id</code>命令将公钥复制到<code>jumpServer</code>即可。</p><p>然后是<code>testServer</code>连接到<code>prodServer</code>，这一步稍微有点复杂，主要是因为<code>testServer</code>一般是动态创建的虚拟机或容器，测试完就删除了，所以没办法提前将<code>testServer</code>的公钥复制到<code>prodServer</code>。解决的思路是使用任意机器登录一次<code>prodServer</code>并将该机器的公钥复制到<code>prodServer</code>，然后将该该机器的私钥复制到<code>testServer</code>，让<code>testServer</code>伪装成为该机器登录<code>prodServer</code>。Gitlab、Travis-ci、Circle CI各自有不同的方法安全地传输ssh key，具体参考相应的文档。Gitlab可以在project和group中的“设置-&gt;CI/CD-&gt;Variables”中设置环境变量，如下图：</p><img src="/2019/06/14/deploy-with-autossh/gitlab-variables.png" class=""><p>设置好之后，配置<code>.gitlab-ci.yml</code>：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">deploy:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy</span></span><br><span class="line">  <span class="attr">before_script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&#x27;which ssh-agent || ( apt-get update -y &amp;&amp; apt-get install openssh-client -y )&#x27;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">eval</span> <span class="string">$(ssh-agent</span> <span class="string">-s)</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;$SSH_PRIVATE_KEY&quot;</span> <span class="string">|</span> <span class="string">tr</span> <span class="string">-d</span> <span class="string">&#x27;\r&#x27;</span> <span class="string">|</span> <span class="string">ssh-add</span> <span class="bullet">-</span> <span class="string">&gt;</span> <span class="string">/dev/null</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">~/.ssh</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">chmod</span> <span class="number">700</span> <span class="string">~/.ssh</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">ssh-keyscan</span> <span class="string">www.foxgis.com</span> <span class="string">&gt;&gt;</span> <span class="string">~/.ssh/known_hosts</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">chmod</span> <span class="number">644</span> <span class="string">~/.ssh/known_hosts</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&#x27;[[ -f /.dockerenv ]] &amp;&amp; echo -e &quot;Host *\n\tStrictHostKeyChecking no\n\n&quot; &gt; ~/.ssh/config&#x27;</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">ssh</span> <span class="string">-p</span> <span class="number">22022</span> <span class="string">prod@jumpServer</span> <span class="comment"># 这里写具体的部署逻辑</span></span><br><span class="line">  <span class="attr">only:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">master</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文借助ssh反向穿透技术，实现了内网服务器与测试服务器的互通，并且利用autossh解决了ssh持久连接的问题，以及利用复制私钥来实现测试服务器到内网服务器的免密登录。最终，结合这些技术，实现了内网服务的自动化部署。</p><p>在实际测试过程中，ssh的反向穿透连接速度慢而且不是很稳定，下一步研究webhook技术来实现自动部署。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;当前持续集成的流行，大大提高了开发迭代的速度。自动化部署作为持续集成的最后一公里，对于完成整个闭环至关重要。如果生产服务器部署至公网且开通了ssh端口，那么测试服务器完成测试打包后，可以很方便地利用ssh将部署包推送到生产服务器上完成部署。但是，如果生产服务器部署在内网，测</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>简单搞定Nginx日志分割</title>
    <link href="https://jingsam.github.io/2019/01/15/nginx-access-log.html"/>
    <id>https://jingsam.github.io/2019/01/15/nginx-access-log.html</id>
    <published>2019-01-15T10:32:52.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>nginx日志分割是很常见的运维工作，关于这方面的文章也很多，通常无外乎两种做法：一是采用cron定期执行shell脚本对日志文件进行归档；二是使用专门日志归档工作logrotate。</p><p>第一种写shell脚本的方法用得不多，毕竟太原始。相比之下，使用logrotate则要省心得多，配置logrotate很简单。关于如何配置logrotate不是本文要讲的内容，感兴趣的话可以自行搜索。</p><p>虽然大多数Linux发行版都自带了logrotate，但在有些情况下不见得安装了logrotate，比如nginx的docker镜像、较老版本的Linux发行版。虽然我们可以使用包管理器安装logrotate，但前提是服务器能够访问互联网，企业内部的服务器可不一定能够联网。</p><p>其实我们有更简单的方法，从nginx 0.7.6版本开始<code>access_log</code>的路径配置可以包含变量，我们可以利用这个特性来实现日志分割。例如，我们想按天来分割日志，那么我们可以这样配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">access_log logs/access-$logdate.log main;</span><br></pre></td></tr></table></figure><p>那么接下来的问题是我们怎么提取出<code>$logdate</code>这个变量？网上有建议使用下面的方法：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">if ($time_iso8601 ~ &quot;^(\d&#123;4&#125;)-(\d&#123;2&#125;)-(\d&#123;2&#125;)&quot;) &#123;</span><br><span class="line">  set $year $1;</span><br><span class="line">  set $month $2;</span><br><span class="line">  set $day $3;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">access_log logs/access-$year-$month-$day.log main;</span><br></pre></td></tr></table></figure><p>上面的方法有两个问题：一是如果<code>if</code>条件不成立，那么<code>$year</code>、<code>$month</code>和<code>$month</code>这三个变量将不会被设置，那么日志将会记录到<code>access-$year-$month-$day.log</code>这个文件中；二是<code>if</code>只能出现在<code>server</code>和<code>location</code>块中，而<code>access_log</code>通常会配置到顶层的<code>http</code>块中，这时候<code>if</code>就不适用。</p><p>如果要在<code>http</code>块中设置<code>access_log</code>，更好的方法是使用<code>map</code>指令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">map $time_iso8601 $logdate &#123;</span><br><span class="line">  &#x27;~^(?&lt;ymd&gt;\d&#123;4&#125;-\d&#123;2&#125;-\d&#123;2&#125;)&#x27; $ymd;</span><br><span class="line">  default                       &#x27;date-not-found&#x27;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">access_log logs/access-$logdate.log main;</span><br></pre></td></tr></table></figure><p><code>map</code>指令通过设置默认值，保证<code>$logdate</code>始终有值，并且可以出现在<code>http</code>块中，完美地解决了<code>if</code>指令的问题。</p><p>最后，为了提高日志的效率，建议配置<code>open_log_file_cache</code>，完整的日志分割配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">log_format  main  &#x27;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#x27;</span><br><span class="line">                  &#x27;$status $body_bytes_sent &quot;$http_referer&quot; &#x27;</span><br><span class="line">                  &#x27;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#x27;;</span><br><span class="line"></span><br><span class="line">map $time_iso8601 $logdate &#123;</span><br><span class="line">  &#x27;~^(?&lt;ymd&gt;\d&#123;4&#125;-\d&#123;2&#125;-\d&#123;2&#125;)&#x27; $ymd;</span><br><span class="line">  default                       &#x27;date-not-found&#x27;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">access_log logs/access-$logdate.log main;</span><br><span class="line">open_log_file_cache max=10;</span><br></pre></td></tr></table></figure><p>本文完。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;nginx日志分割是很常见的运维工作，关于这方面的文章也很多，通常无外乎两种做法：一是采用cron定期执行shell脚本对日志文件进行归档；二是使用专门日志归档工作logrotate。&lt;/p&gt;
&lt;p&gt;第一种写shell脚本的方法用得不多，毕竟太原始。相比之下，使用logro</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>离线安装npm包的几种方法</title>
    <link href="https://jingsam.github.io/2018/11/24/npm-package-offline-install.html"/>
    <id>https://jingsam.github.io/2018/11/24/npm-package-offline-install.html</id>
    <published>2018-11-24T08:56:19.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>这段时间的工作主题就是Linux<br>下的“离线部署”，包括mongo、mysql、postgresql、nodejs、nginx等软件的离线部署。平常在服务器上借助apt-get就能轻松搞定的事情，在离线环境下就变得异常艰难。<a href="/2018/11/24/npm-package-offline-install.html">上一篇文章</a>讲了使用snap离线安装软件的方式，但对于npm包怎么离线部署，snap是无能为力的。本篇文章就来讲一讲离线安装npm包的几种方法。</p><p>接下来的部分，我将以离线安装pm2为例来进行说明。pm2是一个进程守护程序，用于启动node集群和服务进程出错时自动重启，在生产环境下部署nodejs应用一般都会使用到。</p><h1 id="使用npm-link"><a href="#使用npm-link" class="headerlink" title="使用npm link"></a>使用<code>npm link</code></h1><p>使用<code>npm link</code>的方式是最常用的方法，具体做法是在联网机器上下载pm2的源码并安装好依赖，拷贝到离线服务器上，最后借助<code>npm link</code>将pm2链接到全局区域。</p><p>首先，将pm2的源代码克隆下来：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ git clone https://github.com/Unitech/pm2.git</span><br></pre></td></tr></table></figure><p>然后进入到pm2项目中，安装好所有的依赖：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ cd pm2</span><br><span class="line">$ npm install</span><br></pre></td></tr></table></figure><p>将安装好依赖的pm2文件夹拷贝到目标服务器上，进入pm2目录链接到全局区域：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ cd pm2</span><br><span class="line">$ npm link</span><br></pre></td></tr></table></figure><p>这种方式最关键的是借助<code>npm link</code>完成链接，但<code>npm link</code>这条命令本意是设计给开发人员调试用的。但开发人员开发某个全局命令工具的时候，通过将命令从本地工程目录链接到全局，这样调试的时候，可以实时查看本地代码在全局环境下的执行情况。所以，<code>npm link</code>的项目需要安装所有的依赖，包括<code>dependencies</code>以及<code>devDependencies</code>，而我们如果只是使用而不是开发某个包的话，正常情况下不应该安装<code>devDependencies</code>。</p><p>总而言之，这种方式优点是比较简单，缺点是安装了不需要的<code>devDependencies</code>，对于有“洁癖”的人是难以忍受的。</p><h1 id="使用npm-bundle"><a href="#使用npm-bundle" class="headerlink" title="使用npm bundle"></a>使用<code>npm bundle</code></h1><p>那有什么方法相比于上一种方法更干净呢？答案是使用npm-bundle工具将pm2的所有依赖打包，然后到目标服务器上使用<code>npm install &lt;tarball file&gt;</code>安装。</p><p>首先在联网机器上安装npm-bundle工具：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install -g npm-bundle</span><br></pre></td></tr></table></figure><p>然后打包pm2：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm-bundle pm2</span><br></pre></td></tr></table></figure><p>上面的命令会生成一个tgz的包文件，复制到目标服务器上安装：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install -g ./pm2-3.2.2.tgz</span><br></pre></td></tr></table></figure><p>npm-bundle的本质是借助<code>npm pack</code>来实现打包的。<code>npm pack</code>会打包包本身以及<code>bundledDependencies</code>中的依赖，npm-bundle则是将pm2的所有<code>dependencies</code>记录到<code>bundledDependencies</code>，来实现所有依赖的打包。</p><p>这种方式不需要安装多余的<code>devDependencies</code>，并且不需要克隆pm2的源码，比第一种方法更方便。</p><p>更新：<code>npm-bundle</code>对于scoped packages的处理有bug，不能正确地打包，这时考虑采用第一种方式。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;这段时间的工作主题就是Linux&lt;br&gt;下的“离线部署”，包括mongo、mysql、postgresql、nodejs、nginx等软件的离线部署。平常在服务器上借助apt-get就能轻松搞定的事情，在离线环境下就变得异常艰难。&lt;a href=&quot;/2018/11/24/n</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Ubuntu离线部署snap软件包</title>
    <link href="https://jingsam.github.io/2018/11/22/install-snaps-offline.html"/>
    <id>https://jingsam.github.io/2018/11/22/install-snaps-offline.html</id>
    <published>2018-11-22T13:03:07.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>Ubuntu借助包管理器apt-get安装软件包很方便，前提是服务器要能够联网。政府或企业内网的服务器，通常是不与互联网连通的，这时候部署软件只能借助文件拷贝的方式，感觉回到了原始时代。更大的问题是，要部署的软件包需要先安装很多依赖，依赖自己可能还有依赖，并且各种依赖还有版本要求。如果通过手动下载各个依赖包的方法部署，对于复杂的软件，变成了不可能的任务。</p><p>在离线部署方面，Windows明显比Linux做得好，Windows软件包通常会将软件所需的依赖打包，部署时只需拷贝一个软件安装包即可。那Linux有没有类似Windows软件安装包的东西呢？幸运的是，Ubuntu提供了snap软件包机制，可以用来简化离线部署。</p><p>snap软件包类似于windows的软件安装包，将所需的依赖都统一打包到软件包中，部署时只需拷贝snap文件。另外，snap也加强了安全隔离机制，通过注册软件包的签名和权限控制信息，使得snap软件运行在“沙盒”环境中。</p><p>从Ubuntu 16.04起，snap环境是自带的，可以直接使用。如果是早于16.04的版本且服务器不能联网，安装snap环境很困难，你只能自求多福了。下面以安装docker为例，来说明离线安装snap包的方法。</p><h1 id="下载"><a href="#下载" class="headerlink" title="下载"></a>下载</h1><p>首先，我们需要在能联网的机器上将相关的snap包下载下来。不仅要下载docker软件包，还需要下载core软件包。core软件包是snap的核心运行时，几乎所有的snap包都依赖core运行时，Ubuntu 16.04自带了snap环境却没安装core运行时，实在是让人有些搞不懂。</p><p>下载有snap包两种方式。一种方法是在能联网的Ubuntu上使用<code>snap download</code>命令下载：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">$ snap download core</span><br><span class="line">Fetching snap &quot;core&quot;</span><br><span class="line">Fetching assertions for &quot;core&quot;</span><br><span class="line">Install the snap with:</span><br><span class="line">   snap ack core_5897.assert</span><br><span class="line">   snap install core_5897.snap</span><br><span class="line">$ snap download docker</span><br><span class="line">Fetching snap &quot;docker&quot;</span><br><span class="line">Fetching assertions for &quot;docker&quot;</span><br><span class="line">Install the snap with:</span><br><span class="line">   snap ack docker_321.assert</span><br><span class="line">   snap install docker_321.snap</span><br></pre></td></tr></table></figure><p>以上命令将会得到<code>.assert</code>和<code>.snap</code>两类文件，其中<code>.assert</code>是软件包的元数据信息，包括签名和权限控制信息，<code>.snap</code>是实际的安装文件。</p><p>另外一种方法是到<a href="https://uappexplorer.com/snaps">uApp Explorer</a>网站上下载，好处是不需要有Ubuntu环境，缺点是只能下载<code>.snap</code>文件，无法下载<code>.assert</code>文件。</p><h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><p>安装snap包的方法很简单：将软件包拷贝到服务器上，安装时首先注册<code>.assert</code>，然后再安装<code>.assert</code>。对于首次安装，需要安装core和docker两个软件包：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ sudo snap ack core_5897.assert</span><br><span class="line">$ sudo snap install core_5897.snap</span><br><span class="line">$ sudo snap ack docker_321.assert</span><br><span class="line">$ sudo snap install docker_321.snap</span><br></pre></td></tr></table></figure><p>这样就完成了docker的安装。</p><p>要是我们是从<a href="https://uappexplorer.com/snaps">uApp Explorer</a>网站上下载的软件包，缺少<code>.assert</code>文件怎么办？其实我们可以使用<code>snap install --dangerous</code>方式安装：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ sudo snap install core.snap --dangerous</span><br><span class="line">$ sudo snap install docker.snap --dangerous</span><br></pre></td></tr></table></figure><p>如<code>--dangerous</code>所提示的是，这种模式有些“危险”。这是因为缺少<code>.assert</code>文件所描述的签名信息和权限控制信息，意味着软件不是在“沙盒”环境下执行的，运行过程不受控。其实，大可不必紧张，只要snap包来源可信，一般没什么问题，毕竟以前咱们没有snap包的时候不都是这么干的么。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Ubuntu借助包管理器apt-get安装软件包很方便，前提是服务器要能够联网。政府或企业内网的服务器，通常是不与互联网连通的，这时候部署软件只能借助文件拷贝的方式，感觉回到了原始时代。更大的问题是，要部署的软件包需要先安装很多依赖，依赖自己可能还有依赖，并且各种依赖还有版</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Docker容器访问宿主机网络</title>
    <link href="https://jingsam.github.io/2018/10/16/host-in-docker.html"/>
    <id>https://jingsam.github.io/2018/10/16/host-in-docker.html</id>
    <published>2018-10-16T02:26:26.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<h1 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h1><p>最近部署一套系统，使用nginx作反向代理，其中nginx是使用docker方式运行：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -d --name nginx $PWD:/etc/nginx -p 80:80 -p 443:443 nginx:1.15</span><br></pre></td></tr></table></figure><p>需要代理的API服务运行在宿主机的<code>1234</code>端口，<code>nginx.conf</code>相关配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  location /api &#123;</span><br><span class="line">    proxy_pass http://localhost:1234</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>结果访问的时候发现老是报<code>502 Bad Gateway</code>错误，错误日志显示无法连接到upstream。</p><p>仔细想一想，<code>nginx.conf</code>中的<code>localhost</code>似乎有问题。由于nginx是运行在docker容器中的，这个<code>localhost</code>是容器的localhost，而不是宿主机的localhost。</p><p>到这里，就出现了本文要解决的问题：如何从容器中访问到宿主机的网络？通过搜索网络，有如下几种方法：</p><h1 id="使用宿主机IP"><a href="#使用宿主机IP" class="headerlink" title="使用宿主机IP"></a>使用宿主机IP</h1><p>在Linux下安装Docker的时候，会在宿主机安装一个虚拟网卡<code>docker0</code>，我们可以使用宿主机在<code>docker0</code>上的IP地址来代替<code>localhost</code>。</p><p>首先，使用如下命令查询宿主机IP地址：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ ip addr show docker0</span><br><span class="line">3: docker0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default</span><br><span class="line">    link/ether 02:42:d5:4c:f2:1e brd ff:ff:ff:ff:ff:ff</span><br><span class="line">    inet 172.17.0.1/16 scope global docker0</span><br><span class="line">       valid_lft forever preferred_lft forever</span><br><span class="line">    inet6 fe80::42:d5ff:fe4c:f21e/64 scope link</span><br><span class="line">       valid_lft forever preferred_lft forever</span><br></pre></td></tr></table></figure><p>可以发现宿主机的IP是<code>172.17.0.1</code>，那么将<code>proxy_pass http://localhost:1234</code>改为<code>proxy_pass http://172.17.0.1:1234</code>就可以解决<code>502 Bad Gateway</code>错误。</p><p>但是，在Windows和macOS平台下并没有<code>docker0</code>虚拟网卡，这时候可以使用<code>host.docker.internal</code>这个特殊的DNS名称来解析宿主机IP。</p><p>由此发现，不同系统下宿主机的IP是不同的，所以使用宿主机IP，不能跨环境通用。</p><h1 id="使用host网络"><a href="#使用host网络" class="headerlink" title="使用host网络"></a>使用host网络</h1><p>Docker容器运行的时候有<code>host</code>、<code>bridge</code>、<code>none</code>三种网络可供配置。默认是<code>bridge</code>，即桥接网络，以桥接模式连接到宿主机；<code>host</code>是宿主网络，即与宿主机共用网络；<code>none</code>则表示无网络，容器将无法联网。</p><p>当容器使用<code>host</code>网络时，容器与宿主共用网络，这样就能在容器中访问宿主机网络，那么容器的<code>localhost</code>就是宿主机的<code>localhost</code>。</p><p>在docker中使用<code>--network host</code>来为容器配置<code>host</code>网络：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -d --name nginx --network host nginx</span><br></pre></td></tr></table></figure><p>上面的命令中，没有必要像前面一样使用<code>-p 80:80 -p 443:443</code>来映射端口，是因为本身与宿主机共用了网络，容器中暴露端口等同于宿主机暴露端口。</p><p>使用host网络不需要修改<code>nginx.conf</code>，仍然可以使用<code>localhost</code>，因而通用性比上一种方法好。但是，由于<code>host</code>网络没有<code>bridge</code>网络的隔离性好，使用<code>host</code>网络安全性不如<code>bridge</code>高。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>本文提出了使用宿主机IP和使用host网络两种方法，来实现从容器中访问宿主机的网络。两种方法各有优劣，使用宿主机IP隔离性更好，但通用性不好；使用host网络，通用性好，但带来了暴露宿主网络的风险。</p>]]></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;最近部署一套系统，使用nginx作反向代理，其中nginx是使用docker方式运行：&lt;/p&gt;
&lt;figure class=&quot;highlig</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>手动挡模式申请Let&#39;s Encrypt通配符证书</title>
    <link href="https://jingsam.github.io/2018/10/12/lets-encrypt.html"/>
    <id>https://jingsam.github.io/2018/10/12/lets-encrypt.html</id>
    <published>2018-10-12T04:00:00.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>本篇文章要讲的内容是申请Let’s Encrypt通配符证书，但是标题中加一个“手动挡”模式是什么意思呢？我们拿学车为例，当我们学会了开手动挡，开自动挡自然不在话下。同理，如果我们弄明白了手动申请Let’s Encrypt证书的步骤，以后使用自动化工具自然也是手到擒来。</p><p>网上关于申请Let’s Encrypt证书的文章多如牛毛，本篇文章的不同在于以下几点：</p><ol><li>使用手动模式申请证书；</li><li>申请的是通配符证书，一个证书适用多个域名；</li><li>使用Docker来完成申请操作，避免安装一大堆软件污染系统；</li><li>可以在本地电脑上完成申请操作，更灵活。</li></ol><h1 id="验证域名的三种模式"><a href="#验证域名的三种模式" class="headerlink" title="验证域名的三种模式"></a>验证域名的三种模式</h1><p>申请证书要解决的一个关键问题是：如何证明域名是你所拥有的？Let’s Encrypt提供了三种模式：http、dns、tls-sni。</p><p>http模式就是Let’s Encrypt给你一个随机字符串，你需要在Web服务器的<code>/.well-known/acme-challenge/</code>服务路径下放置一个以该字符串命名的文件，当Let’s Encrypt能够访问到这个文件时，证明你是这个域名的所有者。</p><p>dns模式同样是Let’s Encrypt给你一个随机字符串，你需要以该字符串建立一个DNS TXT记录，当Let’s Encrypt查询域名的TXT记录时，发现得到的字符串一致，则证明你是这个域名的所有者。</p><p>tls-sni模式没有仔细研究过，似乎通过SSL加密传输。http走的是80端口，tls-sni走的是443端口。由于当前的tls-sni似乎有漏洞，该模式一度遭禁用，所以不推荐采用此模式，本文也不加以讨论。</p><p>http模式和dns模式各有优劣，适用于不同的场景。http模式不需要操作DNS记录，只需要新建一个文件就可以完成验证，带来的限制就是验证过程必须操作服务器；dns模式不需要操作服务器，只需添加DNS TXT记录就行，缺点是必须登录到域名提供商的页面上修改DNS记录。不管是http模式还是dns模式，申请证书的操作都是可以在任意电脑上完成，申请完之后再将证书复制到服务器上。http模式验证过程需要操作服务器，dns模式验证过程需要操作DNS。</p><p>由于我们要申请的是通配符证书，必须使用dns模式验证，为什么呢？以通配符域名<code>*.example.com</code>为例，它包含的域名千千万万，不太可能使用http模式去验证每个域名。dns模式的验证能力更强，如果用户具有操作<code>example.com</code>域名的DNS记录权限，那当然是拥有通配符域名<code>*.example.com</code>。</p><p>搞清楚申请域名关键问题，接下来说明申请域名的步骤。</p><h1 id="申请域名的步骤"><a href="#申请域名的步骤" class="headerlink" title="申请域名的步骤"></a>申请域名的步骤</h1><p>申请let’s encrypt的客户端软件很多，这里采用的是官方推荐的客户端certbot。运行certbot需要python环境，还需要在系统全局环境安装一些python包。考虑到证书的有效期是90天，申请证书并不是一个频繁的操作，我不想因为一个低频使用的软件污染我的全局系统，因此采用Docker作为运行环境。</p><p>申请let’s encrypt只需要一条命令，之后certbot通过交互的方式询问申请信息，很人性化：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -it --rm --name certbot \</span><br><span class="line">    -v $PWD:/etc/letsencrypt \</span><br><span class="line">    certbot/certbot \</span><br><span class="line">    certonly --manual --preferred-challenges=dns-01 \</span><br><span class="line">    --server=https://acme-v02.api.letsencrypt.org/directory</span><br></pre></td></tr></table></figure><p>上述命令中，<code>-v $PWD:/etc/letsencrypt</code>表示把当前文件夹映射到docker中的<code>/etc/letsencrypt</code>文件夹，这样certbot生成的证书将出现在当前文件夹中；<code>certonly</code>表示证书申请子命令，还有<code>renew</code>、<code>revoke</code>、<code>delete</code>等其他子命令；<code>--manual</code>是手动申请模式；<code>--preferred-challenges=dns-01</code>表示采用dns模式验证，默认采用http模式验证；<code>--server=https://acme-v02.api.letsencrypt.org/directory</code>表示指向ACME V2版本服务器，默认指向ACME V1版本服务器，但只有ACME V2才支持通配符证书。</p><p>接着需要提供一个email地址用于接收证书更新和安全问题提醒：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Saving debug log to /var/log/letsencrypt/letsencrypt.log</span><br><span class="line">Plugins selected: Authenticator manual, Installer None</span><br><span class="line">Enter email address (used for urgent renewal and security notices) (Enter &#x27;c&#x27; to</span><br><span class="line">cancel): XXX@XX.com</span><br></pre></td></tr></table></figure><p>需要同意用户协议：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">Please read the Terms of Service at</span><br><span class="line">https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must</span><br><span class="line">agree in order to register with the ACME server at</span><br><span class="line">https://acme-v02.api.letsencrypt.org/directory</span><br><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">(A)gree/(C)ancel: A</span><br></pre></td></tr></table></figure><p>询问你需不需要接收电子前哨基金会（EFF）的新闻、活动，可以选择不接收：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">Would you be willing to share your email address with the Electronic Frontier</span><br><span class="line">Foundation, a founding partner of the Let&#x27;s Encrypt project and the non-profit</span><br><span class="line">organization that develops Certbot? We&#x27;d like to send you email about our work</span><br><span class="line">encrypting the web, EFF news, campaigns, and ways to support digital freedom.</span><br><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">(Y)es/(N)o: N</span><br></pre></td></tr></table></figure><p>接下来就需要填写要申请证书的域名，这里需要注意一点的是<code>*.example.com</code>并不包括裸域名<code>example.com</code>，如果申请的证书需要囊括<code>example.com</code>，就必须要同时包含<code>example.com</code>和<code>*.example.com</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Please enter in your domain name(s) (comma and/or space separated)  (Enter &#x27;c&#x27;</span><br><span class="line">to cancel): example.com, *.example.com</span><br><span class="line">Obtaining a new certificate</span><br><span class="line">Performing the following challenges:</span><br><span class="line">dns-01 challenge for example.com</span><br><span class="line">dns-01 challenge for example.com</span><br></pre></td></tr></table></figure><p>申请证书的电脑的IP会被记录，可能是防治滥用吧，需要同意记录IP：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">NOTE: The IP of this machine will be publicly logged as having requested this</span><br><span class="line">certificate. If you&#x27;re running certbot in manual mode on a machine that is not</span><br><span class="line">your server, please ensure you&#x27;re okay with that.</span><br><span class="line"></span><br><span class="line">Are you OK with your IP being logged?</span><br><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">(Y)es/(N)o: Y</span><br></pre></td></tr></table></figure><p>然后开始使用dns模式验证域名，这时候需要登录到你的域名服务商的DNS编辑页面上，在<code>_acme-challenge.example.com</code>增加两个TXT记录，一个用来验证<code>example.com</code>，另一个用来验证<code>*.example.com</code>。TXT记录的内容就是命令行中提供的随机字符串：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br><span class="line">Please deploy a DNS TXT record under the name</span><br><span class="line">_acme-challenge.example.com with the following value:</span><br><span class="line"></span><br><span class="line">FvMdm......6scC4</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"></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.example.com with the following value:</span><br><span class="line"></span><br><span class="line">H1UHs......Lc90en4dY</span><br><span class="line"></span><br><span class="line">Before continuing, verify the record is deployed.</span><br><span class="line">- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span><br></pre></td></tr></table></figure><p>TXT记录修改好之后，使用<code>dig -t TXT _acme-challenge.example.com</code>来验证TXT记录是否生效了。如果确认生效了，则按回车完成证书的申请：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">Press Enter to Continue</span><br><span class="line">Waiting for verification...</span><br><span class="line">Cleaning up challenges</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/example.com/fullchain.pem</span><br><span class="line">   Your key file has been saved at:</span><br><span class="line">   /etc/letsencrypt/live/example.com/privkey.pem</span><br><span class="line">   Your cert will expire on 2019-01-10. To obtain a new or tweaked</span><br><span class="line">   version of this certificate in the future, simply run certbot</span><br><span class="line">   again. To non-interactively renew *all* of your certificates, run</span><br><span class="line">   &quot;certbot renew&quot;</span><br><span class="line"> - Your account credentials have been saved in your Certbot</span><br><span class="line">   configuration directory at /etc/letsencrypt. You should make a</span><br><span class="line">   secure backup of this folder now. This configuration directory will</span><br><span class="line">   also contain certificates and private keys obtained by Certbot so</span><br><span class="line">   making regular backups of this folder is ideal.</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&#x27;s Encrypt:   https://letsencrypt.org/donate</span><br><span class="line">   Donating to EFF:                    https://eff.org/donate-le</span><br></pre></td></tr></table></figure><p>上面的信息告诉我们，证书保存在<code>/etc/letsencrypt/live/example.com/fullchain.pem</code>，证书的秘钥保存在<code>/etc/letsencrypt/live/example.com/privkey.pem</code>，我们可以把这两个文件拷贝到服务器上用于设置https。</p><h1 id="证书更新"><a href="#证书更新" class="headerlink" title="证书更新"></a>证书更新</h1><p>证书的有效期是90天，在证书失效前30天之内，Let’s Encrypt会发邮件通知我们。</p><p>虽然有专门的<code>renew</code>命令用来自动更新证书，但是我们申请的是通配符证书，仍然需要手动验证域名。因此，我们需要重新运行原来申请证书的命令来更新证书：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -it --rm --name certbot \</span><br><span class="line">    -v $PWD:/etc/letsencrypt \</span><br><span class="line">    certbot/certbot \</span><br><span class="line">    certonly --manual --preferred-challenges=dns-01 \</span><br><span class="line">    --server=https://acme-v02.api.letsencrypt.org/directory</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>本文通过手动模式一步一步操作，完成了通配符域名的申请。只要理解了let’s encrypt验证域名的http模式和dns模式的原理，申请域名就能做到心中有数。下一篇将讲一讲在nginx环境下，如何使用申请到的证书配置https。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;本篇文章要讲的内容是申请Let’s Encrypt通配符证书，但是标题中加一个“手动挡”模式是什么意思呢？我们拿学车为例，当我们学会了开手动挡，开自动挡自然不在话下。同理，如果我们弄明白了手动申请Let’s Encrypt证书的步骤，以后使用自动化工具自然也是手到擒来。&lt;/</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Git内部原理之Git引用</title>
    <link href="https://jingsam.github.io/2018/10/12/git-reference.html"/>
    <id>https://jingsam.github.io/2018/10/12/git-reference.html</id>
    <published>2018-10-12T03:48:03.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>这篇文章本应该在6月份就完成，拖了4个月之后，终于鼓起勇气捡起来，实在惭愧。坚持写文章就像长跑，途中跑起来基本是靠惯性，如果停下来再起跑就很累很困难。</p><p>闲话不多说，本篇继续承接前文讲一讲Git内部原理，本篇的主题是Git引用的原理。</p><p>首先来搞清楚什么是Git引用，前文讲了Git提交对象的哈希、存储原理，理论上我们只要知道该对象的hash值，就能往前推出整个提交历史，例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ git log --pretty=oneline 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br><span class="line">3ac728ac62f0a7b5ac201fd3ed1f69165df8be31 third commit</span><br><span class="line">d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c second commit</span><br><span class="line">db1d6f137952f2b24e3c85724ebd7528587a067a first commit</span><br></pre></td></tr></table></figure><p>现在问题来了，提交对象的这40位hash值不好记忆，Git引用相当于给40位hash值取一个别名，便于识别和读取。Git引用对象都存储在<code>.git/refs</code>目录下，该目录下有3个子文件夹<code>heads</code>、<code>tags</code>和<code>remotes</code>，分别对应于HEAD引用、标签引用和远程引用，下面分别讲一讲每种引用的原理。</p><h1 id="HEAD引用"><a href="#HEAD引用" class="headerlink" title="HEAD引用"></a>HEAD引用</h1><p>HEAD引用是用来指向每个分支的最后一次提交对象，这样切换到一个分支之后，才能知道分支的“尾巴”在哪里。HEAD引用存储在<code>.git/refs/heads</code>目录下，有多少个分支，就有相应的同名HEAD引用对象。例如代码库里面有<code>master</code>和<code>test</code>两个分支，那么<code>.git/refs/heads</code>目录下就存在<code>master</code>和<code>test</code>两个文件，分别记录了分支的最后一次提交。</p><p>HEAD引用的内容就是提交对象的hash值，理论上我们可以手动地构造一个HEAD引用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ echo &quot;3ac728ac62f0a7b5ac201fd3ed1f69165df8be31&quot; &gt; .git/refs/heads/master</span><br></pre></td></tr></table></figure><p>Git提供了一个专有命令<code>update-ref</code>，用来查看和修改Git引用对象，当然也包括HEAD引用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git update-ref refs/heads/master 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br><span class="line">$ git update-ref refs/heads/master</span><br><span class="line">3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br></pre></td></tr></table></figure><p>上面的命令我们将<code>master</code>分支的HEAD指向了<code>3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</code>，现在用<code>git log</code>查看下<code>master</code>的提交历史，可以发现最后一次提交就是所更新的hash值：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ git log --pretty=oneline master</span><br><span class="line">3ac728ac62f0a7b5ac201fd3ed1f69165df8be31 (HEAD -&gt; master) third commit</span><br><span class="line">d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c second commit</span><br><span class="line">db1d6f137952f2b24e3c85724ebd7528587a067a first commit</span><br></pre></td></tr></table></figure><p>同理，可以使用同样的方法更新<code>test</code>分支的HEAD：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ git update-ref refs/heads/test d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c</span><br><span class="line">$ git log --pretty=oneline test</span><br><span class="line">d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c (test) second commit</span><br><span class="line">db1d6f137952f2b24e3c85724ebd7528587a067a first commit</span><br></pre></td></tr></table></figure><p><code>.git/refs/heads</code>目录下存储了每个分支的HEAD，那怎么知道代码库当前处于哪个分支呢？这就需要一个代码库级别的HEAD引用。<code>.git/HEAD</code>这个文件就是整个代码库级别的HEAD引用。我们先查看一下<code>.git/HEAD</code>文件的内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ cat .git/HEAD</span><br><span class="line">ref: refs/heads/master</span><br></pre></td></tr></table></figure><p>我们发现<code>.git/HEAD</code>文件的内容不是40位hash值，而像是指向<code>.git/refs/heads/master</code>。尝试切换到<code>test</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git checkout test</span><br><span class="line">$ cat .git/HEAD</span><br><span class="line">ref: refs/heads/test</span><br></pre></td></tr></table></figure><p>切换分支后，<code>.git/HEAD</code>文件的内容也跟着指向<code>.git/refs/heads/test</code>。<code>.git/HEAD</code>也是HEAD引用对象，与一般引用不同的是，它是“符号引用”。符号引用类似于文件的快捷方式，链接到要引用的对象上。</p><p>Git提供专门的命令<code>git symbolic-ref </code>，用来查看和更新符号引用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ git symbolic-ref HEAD refs/heads/master</span><br><span class="line">$ git symbolic-ref HEAD refs/heads/test</span><br></pre></td></tr></table></figure><p>至此，我们分析了两种HEAD引用，一种是分支级别的HEAD引用，用来记录各分支的最后一次提交，存储在<code>.git/refs/heads</code>目录下，使用<code>git update-ref</code>来维护；一种是代码库级别的HEAD引用，用来记录代码库所处的分支，存储在<code>.git/HEAD</code>文件，使用<code>git symbolic-ref</code>来维护。</p><h1 id="标签引用"><a href="#标签引用" class="headerlink" title="标签引用"></a>标签引用</h1><p>标签引用，顾名思义就是给Git对象打标签，便于记忆。例如，我们可以将某个提交对象打v1.0标签，表示是1.0版本。标签引用都存储在<code>.git/refs/tags</code>里面。</p><p>标签引用和HEAD引用本质是Git引用对象，同样使用<code>git update-ref</code>来查看和修改：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git update-ref refs/tags/v1.0 d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c</span><br><span class="line">$ cat .git/refs/tags/v1.0</span><br><span class="line">d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c</span><br></pre></td></tr></table></figure><p>还有一种标签引用称为“附注引用”，可以为标签添加说明信息。上面的标签引用打了一个<code>v1.0</code>的标签表示发布1.0版本，有时候发布软件的时候除了版本号信息，还要写更新说明。附注引用就是用来实现打标签的同时，也可以附带说明信息。</p><p>附注引用是怎么实现的呢？与常规标签引用不同的是，它不直接指向提交对象，而是新建一个Git对象存储到<code>.git/objects</code>中，用来记录附注信息，然后附注标签指向这个Git对象。</p><p>使用<code>git tag</code>建立一个附注标签：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git tag -a v1.1 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31 -m &quot;test tag&quot;</span><br><span class="line">$ cat .git/refs/tags/v1.1</span><br><span class="line">8be4d8e4e8e80711dd7bae304ccfa63b35a6eb8c</span><br></pre></td></tr></table></figure><p>使用<code>git cat-file</code>来查看附注标签所指向的Git对象：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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 8be4d8e4e8e80711dd7bae304ccfa63b35a6eb8c</span><br><span class="line">object 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br><span class="line">type commit</span><br><span class="line">tag v1.1</span><br><span class="line">tagger jingsam &lt;jing-sam@qq.com&gt; 1529481368 +0800</span><br><span class="line"></span><br><span class="line">test tag</span><br></pre></td></tr></table></figure><p>可以看到，上面的Git对象存储了我们填写的附注信息。</p><p>总之，普通的标签引用和附注引用同样都是存储的是40位hash值，指向一个Git对象，所不同的是普通的标签引用是直接指向提交对象，而附注标签是指向一个附注对象，附注对象再指向具体的提交对象。</p><p>另外，本质上标签引用并不是只可以指向提交对象，实际上可以指向任何Git对象，即可以给任何Git对象打标签。</p><h1 id="远程引用"><a href="#远程引用" class="headerlink" title="远程引用"></a>远程引用</h1><p>远程引用，类似于<code>.git/refs/heads</code>中存储的本地仓库各分支的最后一次提交，在<code>.git/refs/remotes</code>是用来记录多个远程仓库各分支的最后一次提交。</p><p>我们可以使用<code>git remote</code>来管理远程分支：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ git remote add origin git@github.com:jingsam/git-test.git</span><br></pre></td></tr></table></figure><p>上面添加了一个<code>origin</code>远程分支，接下来我们把本地仓库的<code>master</code>推送到远程仓库上：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ git push origin master</span><br><span class="line">Counting objects: 9, done.</span><br><span class="line">Delta compression using up to 4 threads.</span><br><span class="line">Compressing objects: 100% (5/5), done.</span><br><span class="line">Writing objects: 100% (9/9), 720 bytes | 360.00 KiB/s, done.</span><br><span class="line">Total 9 (delta 0), reused 0 (delta 0)</span><br><span class="line">To github.com:jingsam/git-test.git</span><br><span class="line"> * [new branch]      master -&gt; master</span><br></pre></td></tr></table></figure><p>这时候在<code>.git/refs/remotes</code>中的远程引用就会更新：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ cat .git/refs/remotes/origin/master</span><br><span class="line">3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br></pre></td></tr></table></figure><p>和本地仓库的<code>master</code>比较一下，发现是一模一样的，表示远程分支和本地分支是同步的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ cat .git/refs/heads/master</span><br><span class="line">3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br></pre></td></tr></table></figure><p>由于远程引用也是Git引用对象，所以理论上也可以使用<code>git update-ref</code>来手动维护。但是，我们需要先把代码与远程仓库进行同步，在远程仓库中找到对应分支的HEAD，然后使用<code>git update-ref</code>进行更新，过程比较麻烦。而我们在执行<code>git pull</code>或<code>git push</code>这样的高层命令的时候，远程引用会自动更新。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>到这里，三种Git引用都已分析完毕。总的来说，三种Git引用都统一存储到<code>.git/refs</code>目录下，Git引用中的内容都是40位的hash值，指向某个Git对象，这个对象可以是任意的Git对象，可以是数据对象、树对象、提交对象。三种Git引用都可以使用<code>git update-ref</code>来手动维护。</p><p>三种Git引用对象所不同的是，分别存储于<code>.git/refs/heads</code>、<code>.git/refs/tags</code>、<code>.git/refs/remotes</code>,存储的文件夹不同，赋予了引用对象不同的功能。HEAD引用用来记录本地分支的最后一次提交，标签引用用来给任意Git对象打标签，远程引用正式用来记录远程分支的最后一次提交。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;这篇文章本应该在6月份就完成，拖了4个月之后，终于鼓起勇气捡起来，实在惭愧。坚持写文章就像长跑，途中跑起来基本是靠惯性，如果停下来再起跑就很累很困难。&lt;/p&gt;
&lt;p&gt;闲话不多说，本篇继续承接前文讲一讲Git内部原理，本篇的主题是Git引用的原理。&lt;/p&gt;
&lt;p&gt;首先来搞清楚</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Git内部原理之Git对象存储</title>
    <link href="https://jingsam.github.io/2018/06/15/git-storage.html"/>
    <id>https://jingsam.github.io/2018/06/15/git-storage.html</id>
    <published>2018-06-15T09:50:05.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>在<a href="http://jingsam.github.io/2018/06/10/git-hash.html">Git内部原理之Git对象哈希</a>中，讲解了Git对象hash的原理，接下来的这篇文章讲一讲Git对象如何存储。</p><h1 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h1><p>数据对象、树对象和提交对象都是存储在<code>.git/objects</code>目录下，目录的结构如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">.git</span><br><span class="line">|-- objects</span><br><span class="line">    |-- 01</span><br><span class="line">    |   |-- 55eb4229851634a0f03eb265b69f5a2d56f341</span><br><span class="line">    |-- 1f</span><br><span class="line">    |   |-- 7a7a472abf3dd9643fd615f6da379c4acb3e3a</span><br><span class="line">    |-- 83</span><br><span class="line">        |-- baae61804e65cc73a7201a7252750c76066a30</span><br></pre></td></tr></table></figure><p>从上面的目录结构可以看出，Git对象的40位hash分为两部分：头两位作为文件夹，后38位作为对象文件名。所以一个Git对象的存储路径规则为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.git/objects/hash[0, 2]/hash[2, 40]</span><br></pre></td></tr></table></figure><p>这里就产生了一个疑问：为什么Git要这么设计目录结构，而不直接用Git对象的40位hash作为文件名？原因是有两点：</p><p>1.有些文件系统对目录下的文件数量有限制。例如，FAT32限制单目录下的最大文件数量是65535个，如果使用U盘拷贝Git文件就可能出现问题。<br>2.有些文件系统访问文件是一个线性查找的过程，目录下的文件越多，访问越慢。</p><p>在在<a href="http://jingsam.github.io/2018/06/10/git-hash.html">Git内部原理之Git对象哈希</a>中，我们知道Git对象会在原内容前加个一个头部：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">store = header + content</span><br></pre></td></tr></table></figure><p>Git对象在存储前，会使用zlib的deflate算法进行压缩，即简要描述为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">zlib_store = zlib.deflate(store)</span><br></pre></td></tr></table></figure><p>压缩后的<code>zlib_store</code>按照Git对象的路径规则存储到<code>.git/objects</code>目录下。</p><p>总结下Git对象存储的算法步骤：</p><p>1.计算<code>content</code>长度，构造<code>header</code>;<br>2.将<code>header</code>添加到<code>content</code>前面，构造Git对象；<br>3.使用sha1算法计算Git对象的40位hash码；<br>4.使用zlib的deflate算法压缩Git对象；<br>5.将压缩后的Git对象存储到<code>.git/objects/hash[0, 2]/hash[2, 40]</code>路径下;</p><h1 id="Nodejs实现"><a href="#Nodejs实现" class="headerlink" title="Nodejs实现"></a>Nodejs实现</h1><p>接下来，我们使用Nodejs来实现<code>git hash-object -w</code>的功能，即计算Git对象的hash值并存储到Git文件系统中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">const fs = require(&#x27;fs&#x27;)</span><br><span class="line">const crypto = require(&#x27;crypto&#x27;)</span><br><span class="line">const zlib = require(&#x27;zlib&#x27;)</span><br><span class="line"></span><br><span class="line">function gitHashObject(content, type) &#123;</span><br><span class="line">  // 构造header</span><br><span class="line">  const header = `$&#123;type&#125; $&#123;Buffer.from(content).length&#125;\0`</span><br><span class="line"></span><br><span class="line">  // 构造Git对象</span><br><span class="line">  const store = Buffer.concat([Buffer.from(header), Buffer.from(content)])</span><br><span class="line"></span><br><span class="line">  // 计算hash</span><br><span class="line">  const sha1 = crypto.createHash(&#x27;sha1&#x27;)</span><br><span class="line">  sha1.update(store)</span><br><span class="line">  const hash = sha1.digest(&#x27;hex&#x27;)</span><br><span class="line"></span><br><span class="line">  // 压缩Git对象</span><br><span class="line">  const zlib_store = zlib.deflateSync(store)</span><br><span class="line"></span><br><span class="line">  // 存储Git对象</span><br><span class="line">  fs.mkdirSync(`.git/objects/$&#123;hash.substring(0, 2)&#125;`)</span><br><span class="line">  fs.writeFileSync(`.git/objects/$&#123;hash.substring(0, 2)&#125;/$&#123;hash.substring(2, 40)&#125;`, zlib_store)</span><br><span class="line"></span><br><span class="line">  console.log(hash)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 调用入口</span><br><span class="line">gitHashObject(process.argv[2], process.argv[3])</span><br></pre></td></tr></table></figure><p>最后，测试下能否正确存储Git对象：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ node index.js &#x27;hello, world&#x27; blob</span><br><span class="line">8c01d89ae06311834ee4b1fab2f0414d35f01102</span><br><span class="line">$ git cat-file -p 8c01d89ae06311834ee4b1fab2f0414d35f01102</span><br><span class="line">hello, world</span><br></pre></td></tr></table></figure><p>由此可见，我们生成了一个合法的Git数据对象，证明算法是正确的。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在&lt;a href=&quot;http://jingsam.github.io/2018/06/10/git-hash.html&quot;&gt;Git内部原理之Git对象哈希&lt;/a&gt;中，讲解了Git对象hash的原理，接下来的这篇文章讲一讲Git对象如何存储。&lt;/p&gt;
&lt;h1 id=&quot;原理&quot;&gt;&lt;</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Git内部原理之Git对象哈希</title>
    <link href="https://jingsam.github.io/2018/06/09/git-hash.html"/>
    <id>https://jingsam.github.io/2018/06/09/git-hash.html</id>
    <published>2018-06-09T23:25:18.000Z</published>
    <updated>2022-08-13T14:10:48.352Z</updated>
    
    <content type="html"><![CDATA[<p>在上一篇<a href="https://jingsam.github.io/2018/06/03/git-objects.html">文章</a>中，将了数据对象、树对象和提交对象三种Git对象，每种对象会计算出一个hash值。那么，Git是如何计算出Git对象的hash值？本文的内容就是来解答这个问题。</p><h1 id="Git对象的hash方法"><a href="#Git对象的hash方法" class="headerlink" title="Git对象的hash方法"></a>Git对象的hash方法</h1><p>Git中的数据对象、树对象和提交对象的hash方法原理是一样的，可以描述为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">header = &quot;&lt;type&gt; &quot; + content.length + &quot;\0&quot;</span><br><span class="line">hash = sha1(header + content)</span><br></pre></td></tr></table></figure><p>上面公式表示，Git在计算对象hash时，首先会在对象头部添加一个<code>header</code>。这个<code>header</code>由3部分组成：第一部分表示对象的类型，可以取值<code>blob</code>、<code>tree</code>、<code>commit</code>以分别表示数据对象、树对象、提交对象；第二部分是数据的字节长度；第三部分是一个空字节，用来将<code>header</code>和<code>content</code>分隔开。将<code>header</code>添加到<code>content</code>头部之后，使用<code>sha1</code>算法计算出一个40位的hash值。</p><p>在手动计算Git对象的hash时，有两点需要注意：<br>1.<strong><code>header</code>中第二部分关于数据长度的计算，一定是字节的长度而不是字符串的长度</strong>；<br>2.<strong><code>header + content</code>的操作并不是字符串级别的拼接，而是二进制级别的拼接</strong>。</p><p>各种Git对象的hash方法相同，不同的在于：<br>1.头部类型不同，数据对象是<code>blob</code>，树对象是<code>tree</code>，提交对象是<code>commit</code>；<br>2.数据内容不同，数据对象的内容可以是任意内容，而树对象和提交对象的内容有固定的格式。</p><p>接下来分别讲数据对象、树对象和提交对象的具体的hash方法。</p><h2 id="数据对象"><a href="#数据对象" class="headerlink" title="数据对象"></a>数据对象</h2><p>数据对象的格式如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">blob &lt;content length&gt;&lt;NULL&gt;&lt;content&gt;</span><br></pre></td></tr></table></figure><p>从上一篇<a href="https://jingsam.github.io/2018/06/03/git-objects.html">文章</a>中我们知道，使用<code>git hash-object</code>可以计算出一个40位的hash值，例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;what is up, doc?&quot; | git hash-object --stdin</span><br><span class="line">bd9dbf5aae1a3862dd1526723246b20206e5fc37</span><br></pre></td></tr></table></figure><p>注意，上面在<code>echo</code>后面使用了<code>-n</code>选项，用来阻止自动在字符串末尾添加换行符，否则会导致实际传给<code>git hash-object</code>是<code>what is up, doc?\n</code>，而不是我们直观认为的<code>what is up, doc?</code>。</p><p>为验证前面提到的Git对象hash方法，我们使用<code>openssl sha1</code>来手动计算<code>what is up, doc?</code>的hash值：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;blob 16\0what is up, doc?&quot; | openssl sha1</span><br><span class="line">bd9dbf5aae1a3862dd1526723246b20206e5fc37</span><br></pre></td></tr></table></figure><p>可以发现，手动计算出的hash值与<code>git hash-object</code>计算出来的一模一样。</p><p>在Git对象hash方法的注意事项中，提到**<code>header</code>中第二部分关于数据长度的计算，一定是字节的长度而不是字符串的长度**。由于<code>what is up, doc?</code>只有英文字符，在UTF8中恰好字符的长度和字节的长度都等于16，很容易将这个长度误解为字符的长度。假设我们以<code>中文</code>来试验：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;中文&quot; | git hash-object --stdin</span><br><span class="line">efbb13322ba66f682e179ebff5eeb1bd6ef83972</span><br><span class="line">$ echo -n &quot;blob 2\0中文&quot; | openssl sha1</span><br><span class="line">d1dc2c3eed26b05289bddb857713b60b8c23ed29</span><br></pre></td></tr></table></figure><p>我们可以看到，<code>git hash-object</code>和<code>openssl sha1</code>计算出来的hash值根本不一样。这是因为<code>中文</code>两个字符作为UTF格式存储后的字符长度不是2，具体是多少呢？可以使用<code>wc</code>来计算：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;中文&quot; | wc -c</span><br><span class="line">       6</span><br></pre></td></tr></table></figure><p><code>中文</code>字符串的字节长度是6，重新手动计算发现得出的hash值就能对应上了：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;blob 6\0中文&quot; | openssl sha1</span><br><span class="line">efbb13322ba66f682e179ebff5eeb1bd6ef83972</span><br></pre></td></tr></table></figure><h2 id="树对象"><a href="#树对象" class="headerlink" title="树对象"></a>树对象</h2><p>树对象的内容格式如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tree &lt;content length&gt;&lt;NUL&gt;&lt;file mode&gt; &lt;filename&gt;&lt;NUL&gt;&lt;item sha&gt;...</span><br></pre></td></tr></table></figure><p>需要注意的是，<code>&lt;item sha&gt;</code>部分是二进制形式的sha1码，而不是十六进制形式的sha1码。</p><p>我们从上一篇<a href="https://jingsam.github.io/2018/06/03/git-objects.html">文章</a>摘出一个树对象做实验，其内容如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">100644 blob 83baae61804e65cc73a7201a7252750c76066a30  test.txt</span><br></pre></td></tr></table></figure><p>我们首先使用<code>xxd</code>把<code>83baae61804e65cc73a7201a7252750c76066a30</code>转换成为二进制形式，并将结果保存为<code>sha1.txt</code>以方便后面做追加操作：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;83baae61804e65cc73a7201a7252750c76066a30&quot; | xxd -r -p &gt; sha1.txt</span><br><span class="line">$ cat tree-items.txt</span><br><span class="line">���a�Ne�s� rRu</span><br><span class="line">              vj0%</span><br></pre></td></tr></table></figure><p>接下来构造content部分，并保存至文件<code>content.txt</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;100644 test.txt\0&quot; | cat - sha1.txt &gt; content.txt</span><br><span class="line">$ cat content.txt</span><br><span class="line">100644 test.txt���a�Ne�s� rRu</span><br><span class="line">                             vj0%</span><br></pre></td></tr></table></figure><p>计算content的长度：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ cat content.txt | wc -c</span><br><span class="line">      36</span><br></pre></td></tr></table></figure><p>那么最终该树对象的内容为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;tree 36\0&quot; | cat - content.txt</span><br><span class="line">tree 36100644 test.txt���a�Ne�s� rRu</span><br><span class="line">                                    vj0%</span><br></pre></td></tr></table></figure><p>最后使用<code>openssl sha1</code>计算hash值，可以发现和实验的hash值是一样的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;tree 36\0&quot; | cat - content.txt | openssl sha1</span><br><span class="line">d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br></pre></td></tr></table></figure><h2 id="提交对象"><a href="#提交对象" class="headerlink" title="提交对象"></a>提交对象</h2><p>提交对象的格式如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">commit &lt;content length&gt;&lt;NUL&gt;tree &lt;tree sha&gt;</span><br><span class="line">parent &lt;parent sha&gt;</span><br><span class="line">[parent &lt;parent sha&gt; if several parents from merges]</span><br><span class="line">author &lt;author name&gt; &lt;author e-mail&gt; &lt;timestamp&gt; &lt;timezone&gt;</span><br><span class="line">committer &lt;author name&gt; &lt;author e-mail&gt; &lt;timestamp&gt; &lt;timezone&gt;</span><br><span class="line"></span><br><span class="line">&lt;commit message&gt;</span><br></pre></td></tr></table></figure><p>我们从上一篇<a href="https://jingsam.github.io/2018/06/03/git-objects.html">文章</a>摘出一个提交对象做实验，其内容如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ echo &#x27;first commit&#x27; | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">db1d6f137952f2b24e3c85724ebd7528587a067a</span><br><span class="line">$ git cat-file -p db1d6f137952f2b24e3c85724ebd7528587a067a</span><br><span class="line">tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">author jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line">committer jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line"></span><br><span class="line">first commit</span><br></pre></td></tr></table></figure><p>这里需要注意的是，由于<code>echo &#39;first commit&#39;</code>没有添加<code>-n</code>选项，因此实际的提交信息是<code>first commit\n</code>。使用<code>wc</code>计算出提交内容的字节数：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">author jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line">committer jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line"></span><br><span class="line">first commit\n&quot; | wc -c</span><br><span class="line">     163</span><br></pre></td></tr></table></figure><p>那么，这个提交对象的<code>header</code>就是<code>commit 163\0</code>，手动把头部添加到提交内容中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">commit 163\0tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">author jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line">committer jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line"></span><br><span class="line">first commit\n</span><br></pre></td></tr></table></figure><p>使用<code>openssl sha1</code>计算这个上面内容的hash值：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ echo -n &quot;commit 163\0tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">author jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line">committer jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line"></span><br><span class="line">first commit\n&quot; | openssl sha1</span><br><span class="line">db1d6f137952f2b24e3c85724ebd7528587a067a</span><br></pre></td></tr></table></figure><p>可以看见，与实验的hash值是一样的。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这篇文章详细地分析了Git中的数据对象、树对象和提交对象的hash方法，可以发现原理是非常简单的。数据对象和提交对象打印出来的内容与存储内容组织是一模一样的，可以很直观的理解。对于树对象，其打印出来的内容和实际存储是有区别的，增加了一些实现上的难度。例如，使用二进制形式的hash值而不是直观的十六进制形式，我现在还没有从已有资料中搜到这么设计的理由，这个问题留待以后解决。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在上一篇&lt;a href=&quot;https://jingsam.github.io/2018/06/03/git-objects.html&quot;&gt;文章&lt;/a&gt;中，将了数据对象、树对象和提交对象三种Git对象，每种对象会计算出一个hash值。那么，Git是如何计算出Git对象的hash</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Git内部原理之Git对象</title>
    <link href="https://jingsam.github.io/2018/06/03/git-objects.html"/>
    <id>https://jingsam.github.io/2018/06/03/git-objects.html</id>
    <published>2018-06-03T09:50:55.000Z</published>
    <updated>2022-08-13T14:10:48.348Z</updated>
    
    <content type="html"><![CDATA[<p>最近在读《Pro Git》这本书，其中有一章讲Git的内部原理，写得非常好，读完之后对于Git的理解会提升到一个新的层次。今后，我会写一系列的关于Git内部原理的文章，以帮助读者加深对Git的认识。内容主要参考《Pro Git》这本书，但不同的是，我会对内容进行重新组织，以使大家更容易理解。</p><p>这篇文章的主题的Git对象。</p><p><strong>从根本上来讲，Git是一个内容寻址的文件系统，其次才是一个版本控制系统。</strong>记住这点，对于理解Git的内部原理及其重要。所谓“内容寻址的文件系统”，意思是根据文件内容的hash码来定位文件。这就意味着同样内容的文件，在这个文件系统中会指向同一个位置，不会重复存储。</p><p>Git对象包含三种：数据对象、树对象、提交对象。Git文件系统的设计思路与linux文件系统相似，即将文件的内容与文件的属性分开存储，文件内容以“装满字节的袋子”存储在文件系统中，文件名、所有者、权限等文件属性信息则另外开辟区域进行存储。在Git中，数据对象相当于文件内容，树对象相当于文件目录树，提交对象则是对文件系统的快照。</p><p>下面的章节，会分别对每种对象进行说明。开始说明之前，先初始化一个Git文件系统：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ mkdir git-test</span><br><span class="line">$ cd git-test</span><br><span class="line">$ git init</span><br></pre></td></tr></table></figure><p>接下来的操作都会在<code>git-test</code>这个目录中进行。</p><h1 id="数据对象"><a href="#数据对象" class="headerlink" title="数据对象"></a>数据对象</h1><p>数据对象是文件的内容，不包括文件名、权限等信息。Git会根据文件内容计算出一个hash值，以hash值作为文件索引存储在Git文件系统中。由于相同的文件内容的hash值是一样的，因此Git将同样内容的文件只会存储一次。<code>git hash-object</code>可以用来计算文件内容的hash值，并将生成的数据对象存储到Git文件系统中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">$ echo &#x27;version 1&#x27; | git hash-object -w --stdin</span><br><span class="line">83baae61804e65cc73a7201a7252750c76066a30</span><br><span class="line">$ echo &#x27;version 2&#x27; | git hash-object -w --stdin</span><br><span class="line">1f7a7a472abf3dd9643fd615f6da379c4acb3e3a</span><br><span class="line">$ echo &#x27;new file&#x27; | git hash-object -w --stdin</span><br><span class="line">fa49b077972391ad58037050f2a75f74e3671e92</span><br></pre></td></tr></table></figure><p>上面示例中，<code>-w</code>表示将数据对象写入到Git文件系统中，如果不加这个选项，那么只计算文件的hash值而不写入；<code>--stdin</code>表示从标准输入中获取文件内容，当然也可以指定一个文件路径代替此选项。</p><p>上面讲数据对象写入到Git文件系统中，那如何读取数据对象呢？<code>git cat-file</code>可以用来实现所有Git对象的读取，包括数据对象、树对象、提交对象的查看：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30</span><br><span class="line">version 1</span><br><span class="line">$ git cat-file -t 83baae61804e65cc73a7201a7252750c76066a30</span><br><span class="line">blob</span><br></pre></td></tr></table></figure><p>上面示例中，<code>-p</code>表示查看Git对象的内容，<code>-t</code>表示查看Git对象的类型。</p><p>通过这一节，我们能够对Git文件系统中的数据对象进行读写。但是，我们需要记住每一个数据对象的hash值，才能访问到Git文件系统中的任意数据对象，这显然是不现实的。数据对象只是解决了文件内容存储的问题，而文件名的存储则需要通过下一节的树对象来解决。</p><h1 id="树对象"><a href="#树对象" class="headerlink" title="树对象"></a>树对象</h1><p>树对象是文件目录树，记录了文件获取目录的名称、类型、模式信息。使用<code>git update-index</code>可以为数据对象指定名称和模式，然后使用<code>git write-tree</code>将树对象写入到Git文件系统中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt</span><br><span class="line">$ git write-tree</span><br><span class="line">d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br></pre></td></tr></table></figure><p><code>--add</code>表示新增文件名，如果第一次添加某一文件名，必须使用此选项；<code>--cacheinfo &lt;mode&gt; &lt;object&gt; &lt;path&gt;</code>是要添加的数据对象的模式、hash值和路径，<code>&lt;path&gt;</code>意味着为数据对象不仅可以指定单纯的文件名，也可以使用路径。另外要注意的是，使用<code>git update-index</code>添加完文件后，一定要使用<code>git write-tree</code>写入到Git文件系统中，否则只会存在于index区域。</p><p>树对象仍然可以使用<code>git cat-file</code>查看：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">100644 blob 83baae61804e65cc73a7201a7252750c76066a30  test.txt</span><br><span class="line">$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">tree</span><br></pre></td></tr></table></figure><p>上面表示这个树对象只有<code>test.txt</code>这个文件，接下来我们将<code>version 2</code>的数据对象指定为<code>test.txt</code>，并添加一个新文件<code>new.txt</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ git update-index --cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt</span><br><span class="line">$ git update-index --add --cacheinfo 100644 fa49b077972391ad58037050f2a75f74e3671e92 new.txt</span><br><span class="line">$ git write-tree</span><br><span class="line">0155eb4229851634a0f03eb265b69f5a2d56f341</span><br></pre></td></tr></table></figure><p>查看树对象<code>0155eb</code>，可以发现这个树对象有两个文件了：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341</span><br><span class="line">100644 blob fa49b077972391ad58037050f2a75f74e3671e92  new.txt</span><br><span class="line">100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a  test.txt</span><br></pre></td></tr></table></figure><p>我们甚至可以使用<code>git read-tree</code>，将已添加的树对象读取出来，作为当前树的子树：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">$ git write-tree</span><br><span class="line">3c4e9cd789d88d8d89c1073707c3585e41b0e614</span><br></pre></td></tr></table></figure><p><code>--prefix</code>表示把子树对象放到哪个目录下。查看树对象，可以发现当前树对象有一个文件夹和两个文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614</span><br><span class="line">040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579  bak</span><br><span class="line">100644 blob fa49b077972391ad58037050f2a75f74e3671e92  new.txt</span><br><span class="line">100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a  test.txt</span><br></pre></td></tr></table></figure><p>最终，整个树对象的结构如下图：</p><img src="/2018/06/03/git-objects/2018-06-03-1.png" class=""><p>树对象解决了文件名的问题，而且，由于我们是分阶段提交树对象的，树对象可以看做是开发阶段源代码目录树的一次次快照，因此我们可以是用树对象作为源代码版本管理。但是，这里仍然有问题需要解决，即我们需要记住每个树对象的hash值，才能找到个阶段的源代码文件目录树。在源代码版本控制中，我们还需要知道谁提交了代码、什么时候提交的、提交的说明信息等，接下来的提交对象就是为了解决这个问题的。</p><h1 id="提交对象"><a href="#提交对象" class="headerlink" title="提交对象"></a>提交对象</h1><p>提交对象是用来保存提交的作者、时间、说明这些信息的，可以使用<code>git commit-tree</code>来将提交对象写入到Git文件系统中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ echo &#x27;first commit&#x27; | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">db1d6f137952f2b24e3c85724ebd7528587a067a</span><br></pre></td></tr></table></figure><p>上面<code>commit-tree</code>除了要指定提交的树对象，也要提供提交说明，至于提交的作者和时间，则是根据环境变量自动生成，并不需要指定。这里需要提醒一点的是，读者在测试时，得到的提交对象hash值一般和这里不一样，这是因为提交的作者和时间是因人而异的。</p><p>提交对象的查看，也是使用<code>git cat-file</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ git cat-file -p db1d6f137952f2b24e3c85724ebd7528587a067a</span><br><span class="line">tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line">author jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line">committer jingsam &lt;jing-sam@qq.com&gt; 1528022503 +0800</span><br><span class="line"></span><br><span class="line">first commit</span><br></pre></td></tr></table></figure><p>上面是属于首次提交，那么接下来的提交还需要指定使用<code>-p</code>指定父提交对象，这样代码版本才能成为一条时间线：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ echo &#x27;second commit&#x27; | git commit-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 -p db1d6f137952f2b24e3c85724ebd7528587a067a</span><br><span class="line">d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c</span><br></pre></td></tr></table></figure><p>使用<code>git cat-file</code>查看一下新的提交对象，可以看到相比于第一次提交，多了<code>parent</code>部分：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ git cat-file -p d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c</span><br><span class="line">tree 0155eb4229851634a0f03eb265b69f5a2d56f341</span><br><span class="line">parent db1d6f137952f2b24e3c85724ebd7528587a067a</span><br><span class="line">author jingsam &lt;jing-sam@qq.com&gt; 1528022722 +0800</span><br><span class="line">committer jingsam &lt;jing-sam@qq.com&gt; 1528022722 +0800</span><br><span class="line"></span><br><span class="line">second commit</span><br></pre></td></tr></table></figure><p>最后，我们再将树对象<code>3c4e9c</code>提交：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ echo &#x27;third commit&#x27; | git commit-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614 -p d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c</span><br><span class="line">3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br></pre></td></tr></table></figure><p>使用<code>git log</code>可以查看整个提交历史：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><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">$ git log --stat 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br><span class="line">commit 3ac728ac62f0a7b5ac201fd3ed1f69165df8be31</span><br><span class="line">Author: jingsam &lt;jing-sam@qq.com&gt;</span><br><span class="line">Date:   Sun Jun 3 18:47:29 2018 +0800</span><br><span class="line"></span><br><span class="line">    third commit</span><br><span class="line"></span><br><span class="line"> bak/test.txt | 1 +</span><br><span class="line"> 1 file changed, 1 insertion(+)</span><br><span class="line"></span><br><span class="line">commit d4d2c6cffb408d978cb6f1eb6cfc70e977378a5c</span><br><span class="line">Author: jingsam &lt;jing-sam@qq.com&gt;</span><br><span class="line">Date:   Sun Jun 3 18:45:22 2018 +0800</span><br><span class="line"></span><br><span class="line">    second commit</span><br><span class="line"></span><br><span class="line"> new.txt  | 1 +</span><br><span class="line"> test.txt | 2 +-</span><br><span class="line"> 2 files changed, 2 insertions(+), 1 deletion(-)</span><br><span class="line"></span><br><span class="line">commit db1d6f137952f2b24e3c85724ebd7528587a067a</span><br><span class="line">Author: jingsam &lt;jing-sam@qq.com&gt;</span><br><span class="line">Date:   Sun Jun 3 18:41:43 2018 +0800</span><br><span class="line"></span><br><span class="line">    first commit</span><br><span class="line"></span><br><span class="line"> test.txt | 1 +</span><br><span class="line"> 1 file changed, 1 insertion(+)</span><br></pre></td></tr></table></figure><p>最终的提交对象的结构如下图：</p><img src="/2018/06/03/git-objects/2018-06-03-2.png" class=""><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>Git中的数据对象解决了数据存储的问题，树对象解决了文件名存储问题，提交对象解决了提交信息的存储问题。从Git设计中可以看出，Linus对一个源代码版本控制系统做了很好的抽象和解耦，每种对象解决的问题都很明确，相比于使用一种数据结构，无疑更灵活和更易维护。每种Git对象都有一个hash值，这个值是怎么计算出来的？Git的各种对象是如何存储的？这些问题将在下一篇文章中讲解。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近在读《Pro Git》这本书，其中有一章讲Git的内部原理，写得非常好，读完之后对于Git的理解会提升到一个新的层次。今后，我会写一系列的关于Git内部原理的文章，以帮助读者加深对Git的认识。内容主要参考《Pro Git》这本书，但不同的是，我会对内容进行重新组织，以</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>手动更新Homebrew formula</title>
    <link href="https://jingsam.github.io/2018/05/23/edit-a-brew-formula.html"/>
    <id>https://jingsam.github.io/2018/05/23/edit-a-brew-formula.html</id>
    <published>2018-05-23T06:24:01.000Z</published>
    <updated>2022-08-13T14:10:48.348Z</updated>
    
    <content type="html"><![CDATA[<p>Homebrew是macOS上的软件包管理神器，类似于Ubuntu的apt-get，是使用mac作为开发机时的必装软件之一。Homebrew的软件包术语叫Formula，中文解释就是”配方”。Homebrew有“家酿、自制”的意思，80年代硅谷有一个著名的计算机俱乐部叫“Homebrew Homebrew Computer Club”，苹果的创始人Steve Jobs和Steve Wozniak都曾是这个俱乐部的活跃分子。“家酿”一个东西当然得有“配方”，所以这个取名很形象。</p><p>使用homebrew安装软件很方便，一条命令<code>brew install your-formula-name</code>就可以搞定。更新formula到最新版本，使用<code>brew upgrade your-formula-name</code>即可。</p><p>但是，各种formula是人工维护的，当软件包更新后，formula不见得能及时更新到最新版本。本文就以gdal为例，说明如何手动编辑formula文件，以此来将软件更新到最新版本。</p><h1 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a>基本原理</h1><p>Homebrew中的每个软件包都是通过一个<code>formula.rb</code>文件来配置软件的源代码URL、依赖、编译规则和选项，例如以下是gdal的formula文件：</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Gdal</span> &lt; <span class="title class_ inherited__">Formula</span></span><br><span class="line">  desc <span class="string">&quot;Geospatial Data Abstraction Library&quot;</span></span><br><span class="line">  homepage <span class="string">&quot;http://www.gdal.org/&quot;</span></span><br><span class="line">  url <span class="string">&quot;https://download.osgeo.org/gdal/2.2.4/gdal-2.2.4.tar.xz&quot;</span></span><br><span class="line">  sha256 <span class="string">&quot;6f75e49aa30de140525ccb58688667efe3a2d770576feb7fbc91023b7f552aa2&quot;</span></span><br><span class="line">  revision <span class="number">2</span></span><br><span class="line"></span><br><span class="line">  bottle <span class="keyword">do</span></span><br><span class="line">    rebuild <span class="number">2</span></span><br><span class="line">    sha256 <span class="string">&quot;00b28455769c3d5d6ea13dc119f213f320c247489cb2ce9d03f7791d4b53919b&quot;</span> =&gt; <span class="symbol">:high_sierra</span></span><br><span class="line">    sha256 <span class="string">&quot;1365de6a18caeb84d6a50e466a63be9c7541b1fab21edfc3012812157464f2c0&quot;</span> =&gt; <span class="symbol">:sierra</span></span><br><span class="line">    sha256 <span class="string">&quot;8c0fd81eda5a91c8a75a78795f96b6dd9c53e74974bd38cc004b55a44ae95932&quot;</span> =&gt; <span class="symbol">:el_capitan</span></span><br><span class="line">  <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">  ....</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>以上是一个ruby文件，好在并不需要懂ruby的语法就能看懂formula文件。<code>desc</code>和<code>homepage</code>都是描述性信息，不对软件安装产生什么影响。<code>url</code>是软件源代码的位置，编译安装时从此位置将源代码下载下来。<code>sha256</code>是源代码包的校验码，这是保证下载下来的包不被篡改。<code>revision</code>是修订版本号，主要用来保持版本号不变的情况下，对软件包打补丁，每打一次补丁，修订版本号就自增一次。当使用<code>brew install</code>安装软件包时，除非使用<code>--build-from-source</code>强制指定使用源代码安装，大部分情况下，brew会下载编译好的二进制包，这样安装起来更快。<code>bottle</code>选项就记录了各版本macOS下预编译的二进制包的校验码，这部分内容是homebrew的自动集成工具自动维护的，我们并不需要编辑修改。</p><p>更改上面的<code>url</code>和<code>sha256</code>，即可将formula的配置更新到任意版本。编辑好后，使用<code>brew install</code>或者<code>brew upgrade</code>进行安装或者更新升级。</p><p>Homebrew实际上是通过git来管理formula配置文件的，因此我们还可以发送Pull request，将我们的更新推送到GitHub上，让别人也能够方便地更新软件包。</p><h1 id="具体步骤"><a href="#具体步骤" class="headerlink" title="具体步骤"></a>具体步骤</h1><p>这部分以更新gdal 2.2.4到2.3.0为例，来说明手动更新formula的步骤。由于写作本文时，gdal已经更新到2.3.0，所以某些步骤的输出可能与本文不一致，但不妨碍理解更新步骤。</p><h2 id="编辑配置"><a href="#编辑配置" class="headerlink" title="编辑配置"></a>编辑配置</h2><p>使用<code>brew edit gdal</code>即可打开<code>gdal.rb</code>开始编辑，我们将<code>url</code>更新为2.3.0版本的源代码链接：</p><figure class="highlight ruby"><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">class</span> <span class="title class_">Gdal</span> &lt; <span class="title class_ inherited__">Formula</span></span><br><span class="line">  desc <span class="string">&quot;Geospatial Data Abstraction Library&quot;</span></span><br><span class="line">  homepage <span class="string">&quot;http://www.gdal.org/&quot;</span></span><br><span class="line">  url <span class="string">&quot;https://download.osgeo.org/gdal/2.3.0/gdal-2.3.0.tar.xz&quot;</span></span><br><span class="line">  sha256 <span class="string">&quot;6f75e49aa30de140525ccb58688667efe3a2d770576feb7fbc91023b7f552aa2&quot;</span></span><br><span class="line">  revision <span class="number">2</span></span><br><span class="line"></span><br><span class="line">  ...</span><br></pre></td></tr></table></figure><p>理论上我们还需要更新<code>sha256</code>，使它和<code>url</code>相匹配。但是<code>sha256</code>需要我们使用工具计算或者从发布网站上找，不是很方便。我们可以通过下一步的安装调试，来自动计算<code>sha256</code>。</p><h2 id="安装调试"><a href="#安装调试" class="headerlink" title="安装调试"></a>安装调试</h2><p>使用<code>brew install gdal --verbose --debug --build-from-source</code>来安装调试gdal的formula，如果已经安装旧版本的gdal，那么使用<code>brew upgrade gdal --verbose --debug --build-from-source</code>。<code>--verbose</code>表示显示详细输出，便于调试；<code>--debug</code>打开调试；<code>--build-from-source</code>强制从源代码编译。</p><p>安装过程中，会报sha256校验码不匹配的警告，并打印出<code>url</code>所指向的源代码包的sha256校验值，这是因为在上一步我们并没有修改<code>sha256</code>，配置文件中<code>sha256</code>还是gdal 2.2.4版本的。这时候重新使用<code>brew edit gdal</code>打开并编辑formula文件，将<code>sha256</code>更改为正确的校验值。</p><p>最后，再安装调试，经过漫长的编译，成功地安装上了gdal的最新版本。</p><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>测试包括对软件包的测试和对formula文件的测试。使用<code>brew test gdal</code>可以测试gdal的功能是否正常，使用<code>brew audit --strict gdal</code>测试formula文件是否正确。运行<code>brew audit</code>时，会报告<code>revision 2</code>需被删除，这是因为当前homebrew线上库中还没有gdal 2.3.0，意味着本地端的gdal应该是第一版本，不存在修订版本之说。同样地，使用<code>brew edit gdal</code>打开并删除<code>revision</code>部分，然后再重新测试。</p><h2 id="推送更新"><a href="#推送更新" class="headerlink" title="推送更新"></a>推送更新</h2><p>通过上述步骤，我们完成了gdal的手动更新，如果将更新推送到homebrew的线上库中，那么其他人就可以方便地更新到最新版本。并且推送到线上库后，homebrew的自动集成工具会自动地编译生成二进制包，这样就不需要从源代码编译那么耗时了，可谓是利人利己。</p><p>由于homebrew是在GitHub上协作的，所以更新一个formula就和发一个Pull request是一样的，基本步骤如下：</p><p>1.使用<code>cd $(brew --repository homebrew/homebrew-core)</code>切换到本地的homebrew-core目录；<br>2.使用<code>git commit</code>提交自己的修改；<br>3.把<a href="https://github.com/Homebrew/homebrew-core">https://github.com/Homebrew/homebrew-core</a> fork一份；<br>4.使用<code>git remote add</code>命令添加自己的fork的homebrew-core库；<br>5.使用<code>git push</code>推送将本地提交推送到自己的fork的homebrew-core库中；<br>6.在GitHub网页上发起Pull request。</p><h2 id="一键更新"><a href="#一键更新" class="headerlink" title="一键更新"></a>一键更新</h2><p>上面一步步完成了编辑配置、安装调试、测试、推送更新，操作起来有些繁琐。但其实homebrew还提供了一个工具，能够一键完成上面4个步骤，命令如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew bump-formula-pr gdal --URL=https://download.osgeo.org/gdal/2.3.0/gdal-2.3.0.tar.xz --audit --strict</span><br></pre></td></tr></table></figure><p><code>brew bump-formula-pr</code>自动的修改formula配置文件、检查文件错误、提交并推送更新，其中提交并推送更新的过程需要使用<code>hub</code>来在终端上操作GitHub，可以使用<code>brew install hub</code>来安装这个工具。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Homebrew是macOS上的软件包管理神器，类似于Ubuntu的apt-get，是使用mac作为开发机时的必装软件之一。Homebrew的软件包术语叫Formula，中文解释就是”配方”。Homebrew有“家酿、自制”的意思，80年代硅谷有一个著名的计算机俱乐部叫“H</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>linux进程后台运行</title>
    <link href="https://jingsam.github.io/2018/05/07/linux-nohup.html"/>
    <id>https://jingsam.github.io/2018/05/07/linux-nohup.html</id>
    <published>2018-05-07T09:25:15.000Z</published>
    <updated>2022-08-13T14:10:48.348Z</updated>
    
    <content type="html"><![CDATA[<p>在linux上启动Web服务，当退出终端后，Web服务进程也会随着关闭。产生这种问题的原因在于，当用户注销或者网络断开后，终端后收到挂断信号（SIGHUP）,并向子进程广播SIGHUP信号，子进程收到SIGHUP信号而关闭。因此，让linux后台持续运行的方法有以下几种：</p><p>1.改变子进程的所属的父进程，只要父进程不关闭，子进程也不会关闭；<br>2.让子进程忽略挂断信号，即使收到SIGHUP信号，也任性地继续运行；<br>3.不向子进程广播SIGHUP信号，子进程收不到SIGHUP信号，因而不会关闭。</p><h1 id="第一种方式"><a href="#第一种方式" class="headerlink" title="第一种方式"></a>第一种方式</h1><p>使用<code>setsid</code>可以新开一个session运行进程，此session不从属于当前终端，因此终端关闭时进程也不会退出。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&gt;setsid ping www.baidu.com</span><br><span class="line">&gt;ps -ef | grep www.baidu.com</span><br><span class="line">501 57697     1   0  5:55下午 ttys000    0:00.01 ping www.baidu.com</span><br></pre></td></tr></table></figure><p>从上面可以看出，ping进程的父进程是1，即init进程，因此只要电脑不关机，ping进程就不会停止。</p><p>linux下自带<code>setsid</code>这个命令，但是macOS上并没有这个命令。此时，可以结合使用<code>()</code>和<code>&amp;</code>达到同样的效果。<code>()</code>可以新开一个subshell，<code>&amp;</code>让命令后台运行。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&gt;(ping www.baidu.com &amp;)</span><br><span class="line">&gt;ps -ef | grep www.baidu.com</span><br><span class="line">501 57781     1   0  6:01下午 ttys000    0:00.00 ping www.baidu.com</span><br></pre></td></tr></table></figure><p>可以看到，效果与<code>setsid</code>是一样的。</p><h1 id="第二种方式"><a href="#第二种方式" class="headerlink" title="第二种方式"></a>第二种方式</h1><p>使用<code>nohup</code>可以使进程忽略SIGHUP信号，这种方式也是最常用的。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&gt;nohup ping www.baidu.com &amp;</span><br><span class="line">[1]  + 59193 exit 127   nohup www.baidu.com</span><br><span class="line"></span><br><span class="line">&gt;ps -ef | grep www.baidu.com</span><br><span class="line">501 59193 39100   0  6:05下午 ttys000    0:00.01 ping www.baidu.com</span><br></pre></td></tr></table></figure><p>可以看到，ping进程的父进程并不为1，nohup是让进程忽略SIGHUP信号实现进程不退出的。</p><p>需要注意的是，当在<code>zsh</code>中使用<code>nohup</code>时，退出终端时会提示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">zsh: you have running jobs.</span><br></pre></td></tr></table></figure><p>再次强行退出，那么进程仍然会被干掉。这时候，采用以下命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nohup ping www.baidu.com &amp;!</span><br></pre></td></tr></table></figure><h1 id="第三种方式"><a href="#第三种方式" class="headerlink" title="第三种方式"></a>第三种方式</h1><p>使用nohup的前提是，进程以nohup来启动。但是，如果启动时忘记了以nohup启动，有什么方法在不停止进程的情况，让它继续后台运行呢？接下来就要将另外一个命令：<code>disown</code>。<code>disown</code>的原理是，将子进程从终端任务队列中移除，所以即使终端挂断，子进程也收不到SIGHUP信号。</p><p>假设现在使用ping命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&gt;ping www.baidu.com</span><br><span class="line">64 bytes from 14.215.177.38: icmp_seq=0 ttl=51 time=19.642 ms</span><br><span class="line">64 bytes from 14.215.177.38: icmp_seq=1 ttl=51 time=97.976 ms</span><br><span class="line">64 bytes from 14.215.177.38: icmp_seq=2 ttl=51 time=88.996 ms</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>这时采用<code>Ctrl + z</code>使它进入后台，使用<code>jobs</code>查看后台进程：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&gt;jobs</span><br><span class="line">[1]  + suspended  ping www.baidu.com</span><br></pre></td></tr></table></figure><p>可以看到虽然ping进程进入后台，但是进程被挂起了，没有继续运行。使用<code>bg</code>命令可以使他在后台继续运行：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&gt;bg %1</span><br><span class="line">[1]  + 58174 continued  ping www.baidu.com</span><br><span class="line">&gt;jobs</span><br><span class="line">[1]  + running    ping www.baidu.com</span><br></pre></td></tr></table></figure><p>通过组合<code>Ctrl + z</code>和<code>bg</code>，成功地将前台进程变为了后台进程。为了让进程不随终端关闭而终止，还差最后一步：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&gt;disown %1</span><br><span class="line">&gt;jobs</span><br></pre></td></tr></table></figure><p>上面使用<code>jobs</code>命令查看任务队列，发现ping进程不在任务队列中，意味着进程不会收到SIGHUP信号。</p><h1 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h1><p>以上我们通过三种方式，避免进程随着终端关闭而被杀掉：</p><ol><li><code>setsid</code>改变父进程，只要父进程不关闭，进程就可以持续运行；</li><li><code>nohup</code>使得进程忽略SIGHUP信号，父进程即使发送挂断信号，进程也不会终止；</li><li><code>disown</code>将进程从任务队列中移除，保证进程收不到SIGHUP信号。</li></ol><p>但是，以上种种方法只是避免了进程受到SIGHUP信号的影响，进程的持续运行还需要一些其他环境，例如stdin、stdout以及stderr。通常从终端启动的进程，会继承终端的stdin、stdout和stderr。当终端断掉之后，stdin、stdout和stderr也会随着消失，若此时后台进程需要读写stdin、stdout、stderr，该进程将会暂停或者挂住。所以，为保证进程正常后台运行，最好启动时对输入输出重定向：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&gt;ping www.baidu.com &gt; a.log 2&gt;&amp;1 &amp;</span><br></pre></td></tr></table></figure><p>此时，将stdout和stderr重定向到文件<code>a.log</code>中，文件<code>a.log</code>不受终端关闭的影响。如果进程依赖于stdin，意思是进程需要由于键盘输入，那就说明这是个交互式程序，交互式程序后台运行就没多大意义了。</p><h1 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h1><p><a href="https://www.ibm.com/developerworks/cn/linux/l-cn-nohup/index.html">Linux 技巧：让进程在后台可靠运行的几种方法</a><br><a href="https://unix.stackexchange.com/questions/3886/difference-between-nohup-disown-and">Difference between nohup, disown and &amp;</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在linux上启动Web服务，当退出终端后，Web服务进程也会随着关闭。产生这种问题的原因在于，当用户注销或者网络断开后，终端后收到挂断信号（SIGHUP）,并向子进程广播SIGHUP信号，子进程收到SIGHUP信号而关闭。因此，让linux后台持续运行的方法有以下几种：&lt;</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Windows Server 2012评估版本延长使用期限</title>
    <link href="https://jingsam.github.io/2018/03/13/windows-server-rearm.html"/>
    <id>https://jingsam.github.io/2018/03/13/windows-server-rearm.html</id>
    <published>2018-03-13T07:00:51.000Z</published>
    <updated>2022-08-13T14:10:48.348Z</updated>
    
    <content type="html"><![CDATA[<p>最近一台Windows Server 2012 R2数据库服务器隔一个小时就关机，恰巧碰到要演示，这种突发情况让我手忙脚乱。（别问我为什么要用Windows Server做服务器，客户要求的）网上搜了下，发现是Windows授权到期了，需要激活。</p><p>网上确实有很多Windows Server破解激活工具，但是有各种各样的工具，不知道哪个能起作用。而且，这些工具大部分杀毒软件都报有病毒，不太敢用。最重要的是，破解激活涉及到版权问题，道义上过不去。</p><p>由于我使用的是Windows Server评估版本，评估版本可以重置5次试用期，所以加上安装的那次，那么理论了最多可以试用1080天，差不多3年。</p><p>重置方法很简单，以管理员身份打开命令提示符，输入以下命令即可重置试用期，获得180天试用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">slmgr.vbs /rearm</span><br></pre></td></tr></table></figure><p><strong>重置成功后需要重启一次才能生效。</strong></p><p>通过以下命令可以查看剩余的试用时间和可重置次数：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">slmgr.vbs /dlv</span><br></pre></td></tr></table></figure><p>那么重置次数试用完之后怎么办呢？据说可以通过改注册表的方式获得额外的重置次数，但是这种方法可能违反了系统试用协议。具体的做法是使用<code>regedit</code>打开注册表编辑器，找到：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">HKEY_LOCAL_MACHINE -&gt; SOFTWARE -&gt; Microsoft -&gt; Windows NT -&gt; CurrentVersion -&gt; SoftwareProtectionPlatform</span><br></pre></td></tr></table></figure><p>将键<code>SkipRearm</code>的值设为1，再用<code>slmgr.vbs /rearm</code>继续重置，据说这种方法可以使用8次。</p><h1 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h1><p><a href="http://www.vixual.net/blog/archives/180">Windows Server 2012 評估版與延長使用期限</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近一台Windows Server 2012 R2数据库服务器隔一个小时就关机，恰巧碰到要演示，这种突发情况让我手忙脚乱。（别问我为什么要用Windows Server做服务器，客户要求的）网上搜了下，发现是Windows授权到期了，需要激活。&lt;/p&gt;
&lt;p&gt;网上确实有很</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>关于package.json中main字段的指向问题</title>
    <link href="https://jingsam.github.io/2018/03/12/npm-main.html"/>
    <id>https://jingsam.github.io/2018/03/12/npm-main.html</id>
    <published>2018-03-12T11:58:22.000Z</published>
    <updated>2022-08-13T14:10:48.348Z</updated>
    
    <content type="html"><![CDATA[<p><code>package.json</code>中的<code>main</code>字段指向的是Library的入口，通常有3个选择：</p><p>1.指向源代码入口文件，如<code>src/index.js</code>;<br>2.指向打包后的开发版本，如<code>dist/library.js</code>;<br>3.指向打包后的发布版本，如<code>dist/library.min.js</code>。</p><p>引用Library的方式也分为两种：</p><p>1.通过script标签直接引用，适用于简单页面；<br>2.通过require或import方式引用，需要借助打包工具打包，适用于复杂页面。</p><p>本文探讨一下<code>main</code>字段如何指定，才能兼顾各种引用方式。</p><h1 id="指向源代码入口文件"><a href="#指向源代码入口文件" class="headerlink" title="指向源代码入口文件"></a>指向源代码入口文件</h1><p>第一种方式指向源码入口，这种情况仅适用于require方式引用。由于指向的是源代码，需要库使用者借助打包工具如webpack，自行对库进行打包。此方式存在以下问题：</p><p>1.webpack配置babel-loader一般会排除node_modules，意味着不会对library进行转译，可能会导致打包后的代码中包含ES6代码，造成低版本浏览器兼容问题；<br>2.如果library的编译需要一些特别的loader或loader配置，使用者需要在自己的配置中加上这些配置，否则会造成编译失败；<br>3.使用者的打包工具需要收集library的依赖，造成打包编译速度慢，影响开发体验。</p><p>总的来说，第一种方式需要使用者自行对library进行编译打包，对使用者造成额外的负担，因此源代码入口文件不适宜作为库的入口。但是，如果library的目标运行环境只是node端，由于node端不需要对源代码进行编译打包，所以这种情况下可以使用<code>src/index.js</code>作为库入口。</p><h1 id="指向打包后的开发版本"><a href="#指向打包后的开发版本" class="headerlink" title="指向打包后的开发版本"></a>指向打包后的开发版本</h1><p>开发版本的主要作用是便于调试，文件体积并不是开发版本所关心的问题，这是因为开发版本通常是托管在localhost上，文件大小基本没影响。</p><p>开发版本主要通过以下手段来方便调试，提升开发体验：</p><p>1.预先进行依赖收集和babel转译，即使用者不再需要对library进行这两步工作了，提高编译打包的效率；<br>2.尽量保留源代码的格式，保证开发版本里面的源代码基本可读；<br>3.保留警告信息，对开发者对库的错误或不合理调用进行提示。</p><p>其中第3点是通过库代码中添加如下类似代码实现的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">if (process.env.NODE_ENV === &#x27;development&#x27;) &#123;</span><br><span class="line">  console.warn(&#x27;Some useful warnings.&#x27;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>生成开发版本的似乎，webpack的DefinePlugin会将<code>process.env.NODE_ENV</code>替换为<code>development</code>，所以以上代码变为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">if (&#x27;development&#x27; === &#x27;development&#x27;) &#123;</span><br><span class="line">  console.warn(&#x27;Some useful warnings.&#x27;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就表示上述条件一直成立，warning信息会显示出来。</p><p>最近和iview的开发者争论一件事，即在生成library的开发版本的时候，<code>NODE_ENV</code>应该设置为<code>development</code>还是<code>production</code>。他们认为应该设置为<code>production</code>，理由是可以减小开发版本的体积。假设DefinePlugin将<code>process.env.NODE_ENV</code>替换为<code>production</code>，之前的示例代码会变为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">if (&#x27;production&#x27; === &#x27;development&#x27;) &#123;</span><br><span class="line">  console.warn(&#x27;Some useful warnings.&#x27;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就意味着你使用库开发应用时，不会看到任何警告信息，这不利于提前发现错误。可能有的人会说，我的源代码中没有<code>if (process.env.NODE_ENV === &#39;development&#39;) &#123;&#125;</code>这类代码，所以设置为<code>production</code>也不会有任何问题呀。殊不知，虽然你的源代码中这种没有这类提示代码，但是你的devependencies里面可能会有啊，这样做就会关闭依赖中的warning信息。</p><p>可能又有疑问：“引用开发版本的包体积很大，岂不是让我的应用打包上线版本很大？”其实完全不用担心，因为应用打包为上线版本时，会经过两个额外的工作：</p><p>1.使用DefinePlugin将<code>process.env.NODE_ENV</code>替换为<code>production</code>，关闭所有警告信息；<br>2.使用UglifyJsPlugin对应用代码进行minify，减小应用体积。同时会删除<code>if (&#39;production&#39; === &#39;development&#39;) &#123;&#125;</code>这类永远不会执行的代码，进一步减小应用体积。</p><p>所以，在开发时应用开发版本，不必担心最后的应用体积。但是如果开发时是以script标签的方式引用库的开发版本，上线时应该替换为响应的发布版本。</p><h1 id="指向打包后的发布版本"><a href="#指向打包后的发布版本" class="headerlink" title="指向打包后的发布版本"></a>指向打包后的发布版本</h1><p>发布版本追求的是尽量减小体积，因为相比于JS引擎解析的时间，网络传输是最慢的，所以要通过减小库的体积，减少网络传输的时间。</p><p>减小发布版本的文件体积，主要是通过将<code>process.env.NODE_ENV</code>设置为<code>production</code>，然后再使用UglifyJsPlugin对应用代码进行minify以及删除永不执行的代码。</p><p>那么将库的发布版本作为入口文件合不合适呢？显然不合适，因为发布版本的是经过高度压缩精简的，代码完全不可读，应用开发阶段难以调试。</p><p>发布版本是适用于在应用上线时，通过script标签形式引用。</p><h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><p>通过上面的分析，可以发现将库的开发版本作为库的入口才是正确合理的做法，即设置<code>&quot;main&quot;: &quot;dist/library.js&quot;</code>。而作为库的开发者，也要遵循约定，生成库的开发版本的时候，使用<code>development</code>环境变量，保留警告信息。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;中的&lt;code&gt;main&lt;/code&gt;字段指向的是Library的入口，通常有3个选择：&lt;/p&gt;
&lt;p&gt;1.指向源代码入口文件，如&lt;code&gt;src/index.js&lt;/code&gt;;&lt;br&gt;2.指向打包后的开发版本，如&lt;co</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Library项目的Webpack配置</title>
    <link href="https://jingsam.github.io/2018/03/06/webpack.html"/>
    <id>https://jingsam.github.io/2018/03/06/webpack.html</id>
    <published>2018-03-06T06:24:48.000Z</published>
    <updated>2022-08-13T14:10:48.348Z</updated>
    
    <content type="html"><![CDATA[<p>知乎上有个提问，叫“如何成为高级Webpack配置工程师”，戏谑Webpack已经复杂到成为一门专业学问了。但Webpack确实是非常复杂的，一般人只能做到入门而无法精通。Webpack的复杂性在于要完成各种各样的功能，即不仅要处理js、css、html、图片、字体等各种格式的前端资源，还要对这些资源进行转译、精简、提取、分割、打包等一系列操作。</p><p>Webpack在当前前端工程化中占有很重要的地位，前端工程化是为了提高前端开发的效率，但是前端工程化中的工具链配置的复杂度也逐渐提高。有时候新开一个项目，光是配置这些工具就要花一两天，这对于小型项目有点得不偿失。在配置工具链的时间花费大，诚然webpack本身配置参数多，我认为更重要的原因在于平时过于依赖于vue-cli、react-scripts这类自动化生成工具，没有具体地了解每个配置项的作用。</p><p>最近要开发一个可视化的JS库，但是vue-cli、react-scripts这类自动化生成工具主要针对的是SPA，对开发Library支持不好，因而尝试下手动配置webpack、eslint、babel、prettier这些工具。好在开发的是Library，主要处理JS代码，如果是SPA，那就需要处理各种各样的前端资源，还是建议使用vue-cli、react-scripts。</p><p>本文主要是做步骤记录，用于指导以后进行简单项目的手动配置，因而本篇文章不探讨深度内容。</p><h1 id="生成package-json"><a href="#生成package-json" class="headerlink" title="生成package.json"></a>生成package.json</h1><p>这块没什么好说的，借助<code>npm init</code>或<code>yarn init</code>可以快速地生成<code>package.json</code>文件。我通常习惯于先在<code>scripts</code>中把主要的开发命令写出来，以下是我的<code>scripts</code>配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;scripts&quot;: &#123;</span><br><span class="line">    &quot;build-dev&quot;: &quot;webpack --config build/webpack.dev.config.js&quot;,</span><br><span class="line">    &quot;build-prod&quot;: &quot;webpack --config build/webpack.prod.config.js&quot;,</span><br><span class="line">    &quot;build&quot;: &quot;npm run build-dev &amp;&amp; npm run build-prod&quot;,</span><br><span class="line">    &quot;start&quot;: &quot;webpack-dev-server --config build/webpack.dev.config.js&quot;,</span><br><span class="line">    &quot;lint&quot;: &quot;eslint src/*.js&quot;,</span><br><span class="line">    &quot;format&quot;: &quot;prettier-eslint --write src/*.js&quot;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的<code>scripts</code>配置也体现了下文要讲的工程目录结构，即<code>build</code>里面放webpack配置文件，<code>src</code>里面放源代码，详细的内容在下一节描述。</p><p><code>main</code>默认是<code>index.js</code>，即指向源代码的入口文件。但是本项目主要开发的库是作为其他项目的node_modules，一般不会再对库进行转译，所以为了方便将本库集成到前段工程项目中，<code>main</code>应该指向转译好的UMD格式文件。本项目的<code>main</code>配置为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;main&quot;: &quot;dist/geoeye.js&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="规划目录结构"><a href="#规划目录结构" class="headerlink" title="规划目录结构"></a>规划目录结构</h1><p>我认为工程的目录结构非常重要，它能够反映代码的模块划分，好的目录结构让人赏心悦目、容易理解。我按照以下目录进行组织：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">build  // 编译配置</span><br><span class="line">  |- webpack.dev.config.js</span><br><span class="line">  |- webpack.prod.config.js</span><br><span class="line">dist  // 编译好的库文件</span><br><span class="line">  |- geoeye.js</span><br><span class="line">  |- geoeye.min.js</span><br><span class="line">  |- geoeye.min.js.map</span><br><span class="line">src   // 源代码</span><br><span class="line">debug // 用于调试</span><br><span class="line">test  // 测试</span><br><span class="line">package.json</span><br></pre></td></tr></table></figure><h1 id="配置webpack"><a href="#配置webpack" class="headerlink" title="配置webpack"></a>配置webpack</h1><p>Webpack 4刚发布，据说简化了配置，所以本项目就来尝尝鲜。Webpack 4相比原来需要额外安装一个webpack-cli，并且要求node版本不小于6.11.5，安装命令如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev webpack webpack-cli</span><br></pre></td></tr></table></figure><p>接下来就要配置<code>webpack.dev.config.js</code>和<code>webpack.prod.config.js</code>。webpack官方文档推荐用一个<code>webpack.common.config.js</code>提取公共配置后，再用<code>webpack-merge</code>合并。由于本项目配置比较简单，重复的地方不多，所以就不用引入额外的复杂度了。如果项目的配置复杂到同一种配置需要重复3次以上，那么还是需要采用<code>webpack-merge</code>合并的，因为同时更改3个地方很容易出错。</p><p>本项目的<code>webpack.dev.config.js</code>的配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><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></pre></td><td class="code"><pre><span class="line">const path = require(&#x27;path&#x27;)</span><br><span class="line">const webpack = require(&#x27;webpack&#x27;)</span><br><span class="line"></span><br><span class="line">module.exports = &#123;</span><br><span class="line">  entry: &#x27;./src/index.js&#x27;,</span><br><span class="line">  output: &#123;</span><br><span class="line">    path: path.join(__dirname, &#x27;../dist&#x27;),</span><br><span class="line">    filename: &#x27;geoeye.js&#x27;,</span><br><span class="line">    library: &#x27;geoeye&#x27;,</span><br><span class="line">    libraryTarget: &#x27;umd&#x27;,</span><br><span class="line">    umdNamedDefine: true</span><br><span class="line">  &#125;,</span><br><span class="line">  devtool: &#x27;cheap-module-eval-source-map&#x27;,</span><br><span class="line">  devServer: &#123;</span><br><span class="line">    contentBase: &#x27;./debug&#x27;,</span><br><span class="line">    publicPath: &#x27;/dist/&#x27;,</span><br><span class="line">    hot: true,</span><br><span class="line">    open: true,</span><br><span class="line">    overlay: true</span><br><span class="line">  &#125;,</span><br><span class="line">  module: &#123;</span><br><span class="line">    rules: [</span><br><span class="line">      &#123;</span><br><span class="line">        test: /\.js$/,</span><br><span class="line">        loader: &#x27;babel-loader&#x27;,</span><br><span class="line">        exclude: /node_modules/</span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;,</span><br><span class="line">  mode: &#x27;development&#x27;,</span><br><span class="line">  plugins: [</span><br><span class="line">    new webpack.HotModuleReplacementPlugin()</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>本项目的<code>webpack.prod.config.js</code>的配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><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">const path = require(&#x27;path&#x27;)</span><br><span class="line">const webpack = require(&#x27;webpack&#x27;)</span><br><span class="line"></span><br><span class="line">module.exports = &#123;</span><br><span class="line">  entry: &#x27;./src/index.js&#x27;,</span><br><span class="line">  output: &#123;</span><br><span class="line">    path: path.join(__dirname, &#x27;../dist&#x27;),</span><br><span class="line">    filename: &#x27;geoeye.min.js&#x27;,</span><br><span class="line">    library: &#x27;geoeye&#x27;,</span><br><span class="line">    libraryTarget: &#x27;umd&#x27;,</span><br><span class="line">    umdNamedDefine: true</span><br><span class="line">  &#125;,</span><br><span class="line">  devtool: &#x27;source-map&#x27;,</span><br><span class="line">  module: &#123;</span><br><span class="line">    rules: [</span><br><span class="line">      &#123;</span><br><span class="line">        test: /\.js$/,</span><br><span class="line">        loader: &#x27;babel-loader&#x27;,</span><br><span class="line">        exclude: /node_modules/</span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;,</span><br><span class="line">  mode: &#x27;production&#x27;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上有以下几个地方需要注意：</p><p>1.<code>entry</code>中使用相对于当前目录时，<code>./</code>不能省略，即<code>./src/index.js</code>不能写为<code>src/index.js</code>。<br>2.<code>output</code>中的<code>path</code>一定要是绝对路径；<br>3.<code>libraryTarget</code>设为<code>umd</code>以兼容浏览器和commonjs环境；<br>4.webpack 4新引入了<code>mode</code>配置，会自动做一些优化，可以为<code>development</code>或<code>production</code>，不能省略<code>mode</code>的配置；<br>5.<code>mode</code>为<code>development</code>时，HotModuleReplacementPlugin不是默认载入的，所以为了使开发时候能够热替换，需要手动加上这个配置；</p><h1 id="配置babel"><a href="#配置babel" class="headerlink" title="配置babel"></a>配置babel</h1><p>babel负责将高语言特性JS源代码转译为低语言特性JS代码，以兼容低版本浏览器，当前推荐采用<code>babel-preset-env</code>，它能根据要兼容的浏览器版本，有选择性地转译，而不是像以前一样统统转译为ES5。</p><p>使用以下命令安装babel以及配套工具：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev babel-core babel-preset-env babel-loader</span><br></pre></td></tr></table></figure><p><code>.babelrc</code>配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;presets&quot;: [</span><br><span class="line">    [</span><br><span class="line">      &quot;env&quot;,</span><br><span class="line">      &#123;</span><br><span class="line">        &quot;targets&quot;: &#123;</span><br><span class="line">          &quot;browsers&quot;: [&quot;last 2 versions&quot;, &quot;ie 11&quot;]</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="配置eslint和prettier"><a href="#配置eslint和prettier" class="headerlink" title="配置eslint和prettier"></a>配置eslint和prettier</h1><p>eslint能够检查源代码中的格式错误以及少量的语法错误，prettier是用来自动地格式化代码。它们的主要区别在于，eslint主要用来检查代码格式，prettier主要用来修复代码格式。虽然<code>eslint --fix</code>也能自动修复一些格式错误，但只能修复少数几种格式错误，功能十分有限。prettier的格式修复功能很强，但是如果代码中有错误，例如有尾逗号、引用未知变量，prettier不管这些，仍然帮你格式化，这就让你很难提早发现代码中的错误。</p><p>eslint和prettier相爱相杀，让它们和谐相处，才能更好地为我们提供服务。总体思想是eslint的检查规则尽量与prettier的格式规则保持一致，代码先用prettier格式化之后再用<code>eslint --fix</code>修复并检查。</p><p>需要先安装一下几个包：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev eslint prettier eslint-config-prettier eslint-plugin-prettier prettier-eslint-cli</span><br></pre></td></tr></table></figure><p><code>eslint-config-prettier</code>是用来将prettier的格式化规则作为eslint的检查规则，<code>eslint-plugin-prettier</code>则是用来对比prettier格式化前后，代码中出现的错误。<code>prettier-eslint-cli</code>是用来依次执行prettier和<code>eslint --fix</code>，自动格式化代码。</p><p><code>.eslintrc</code>的配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;env&quot;: &#123;</span><br><span class="line">    &quot;browser&quot;: true,</span><br><span class="line">    &quot;es6&quot;: true</span><br><span class="line">  &#125;,</span><br><span class="line">  &quot;parserOptions&quot;: &#123;</span><br><span class="line">    &quot;sourceType&quot;: &quot;module&quot;</span><br><span class="line">  &#125;,</span><br><span class="line">  &quot;extends&quot;: [&quot;prettier&quot;],</span><br><span class="line">  &quot;plugins&quot;: [&quot;prettier&quot;],</span><br><span class="line">  &quot;rules&quot;: &#123; &quot;prettier/prettier&quot;: &quot;error&quot; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要指出的是如果使用了ES6的<code>import</code>和<code>export</code>，则需要配置<code>&quot;sourceType&quot;: &quot;module&quot;</code>。</p><p>我是无分号党，<code>.prettierrc</code>配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;singleQuote&quot;: true,</span><br><span class="line">  &quot;semi&quot;: false</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="配置-gitignore"><a href="#配置-gitignore" class="headerlink" title="配置.gitignore"></a>配置.gitignore</h1><p><code>.gitignore</code>用来排除不需要git管理的文件，配置如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">.DS_Store</span><br><span class="line">node_modules/</span><br><span class="line">dist/</span><br><span class="line">npm-debug.log*</span><br><span class="line">yarn-debug.log*</span><br><span class="line">yarn-error.log*</span><br><span class="line"></span><br><span class="line"># Editor directories and files</span><br><span class="line">.idea</span><br><span class="line">.vscode</span><br><span class="line">*.suo</span><br><span class="line">*.ntvs*</span><br><span class="line">*.njsproj</span><br><span class="line">*.sln</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>配置一个Library开发环境，分为生成package.json、规划目录结构、配置webpack、配置babel、配置eslint和prettier、配置.gitignore几个步骤。本文仅仅是流水账记录，深度不够，写到后面我都觉得乏味了，以后不写这种水文章了，匿了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;知乎上有个提问，叫“如何成为高级Webpack配置工程师”，戏谑Webpack已经复杂到成为一门专业学问了。但Webpack确实是非常复杂的，一般人只能做到入门而无法精通。Webpack的复杂性在于要完成各种各样的功能，即不仅要处理js、css、html、图片、字体等各种格</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>使用pkg打包Node.js应用</title>
    <link href="https://jingsam.github.io/2018/03/02/pkg.html"/>
    <id>https://jingsam.github.io/2018/03/02/pkg.html</id>
    <published>2018-03-02T03:23:39.000Z</published>
    <updated>2022-08-13T14:10:48.348Z</updated>
    
    <content type="html"><![CDATA[<p>Node.js应用不需要经过编译过程，可以直接把源代码拷贝到部署机上执行，确实比C++、Java这类编译型应用部署方便。然而，Node.js应用执行需要有运行环境，意味着你需要先在部署机器上安装Node.js。虽说没有麻烦到哪里去，但毕竟多了一个步骤，特别是对于离线环境下的部署机，麻烦程度还要上升一级。假设你用Node.js写一些小的桌面级工具软件，部署到客户机上还要先安装Node.js，有点“大炮打蚊子”的感觉。更严重的是，如果部署机器上游多个Node.js应用，而且这些应用要依赖于不同的Node.js版本，那就更难部署了。</p><p>理想的情况是将Node.js打包为一个单独的可执行文件，部署的时候直接拷贝过去就行了。除了部署方便外，因为不需要再拷贝源代码了，还有利于保护知识产权。</p><p>将Node.js打包为可执行文件的工具有pkg、nexe、node-packer、enclose等，从打包速度、使用便捷程度、功能完整性来说，pkg是最优秀的。这篇文章就来讲一讲半年来我使用pkg打包Node.js应用的一些经验。</p><p>pkg的打包原理简单来说，就是将js代码以及相关的资源文件打包到可执行文件中，然后劫持<code>fs</code>里面的一些函数，使它能够读到可执行文件中的代码和资源文件。例如，原来的<code>require(&#39;./a.js&#39;)</code>会被劫持到一个虚拟目录<code>require(&#39;/snapshot/a.js&#39;)</code>。</p><h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><p>pkg既可以全局安装也可以局部安装，推荐采用局部安装：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install pkg --save-dev</span><br></pre></td></tr></table></figure><h1 id="用法"><a href="#用法" class="headerlink" title="用法"></a>用法</h1><p>pkg使用比较简单，执行下<code>pkg -h</code>就可以基本了解用法，基本语法是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pkg [options] &lt;input&gt;</span><br></pre></td></tr></table></figure><p><code>&lt;input&gt;</code>可以通过三种方式指定：</p><p>1.一个脚本文件，例如<code>pkg index.js</code>;<br>2.<code>package.json</code>，例如<code>pkg package.json</code>，这时会使用<code>package.json</code>中的<code>bin</code>字段作为入口文件；<br>3.一个目录，例如<code>pkg .</code>，这时会寻找指定目录下的<code>package.json</code>文件，然后在找<code>bin</code>字段作为入口文件。</p><p><code>[options]</code>中可以指定打包的参数：<br>1.<code>-t</code>指定打包的目标平台和Node版本，如<code>-t node6-win-x64,node6-linux-x64,node6-macos-x64</code>可以同时打包3个平台的可执行程序；<br>2.<code>-o</code>指定输出可执行文件的名称，但如果用<code>-t</code>指定了多个目标，那么就要用<code>--out-path</code>指定输出的目录；<br>3.<code>-c</code>指定一个JSON配置文件，用来指定需要额外打包脚本和资源文件，通常使用<code>package.json</code>配置。</p><p>使用pkg的最佳实践是：在<code>package.json</code>中的<code>pkg</code>字段中指定打包参数，使用<code>npm scripts</code>来执行打包过程，例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  ...</span><br><span class="line">  &quot;bin&quot;: &quot;./bin/www&quot;,</span><br><span class="line">  &quot;scripts&quot;: &#123;</span><br><span class="line">    &quot;pkg&quot;: &quot;pkg . --out-path=dist/&quot;</span><br><span class="line">  &#125;,</span><br><span class="line">  &quot;pkg&quot;: &#123;</span><br><span class="line">    &quot;scripts&quot;: [...]</span><br><span class="line">    &quot;assets&quot;: [...],</span><br><span class="line">    &quot;targets&quot;: [...]</span><br><span class="line">  &#125;,</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>scripts</code>和<code>assets</code>用来配置未打包进可执行文件的脚本和资源文件，文件路径可以使用glob通配符。这里就浮现出一个问题：为什么有的脚本和资源文件打包不进去呢？</p><p>要回答这个问题，就涉及到pkg打包文件的机制。按照pkg文档的说法，pkg只会自动地打包相对于<code>__dirname</code>、<code>__filename</code>的文件，例如<code>path.join(__dirname, &#39;../path/to/asset&#39;)</code>。至于<code>require()</code>，因为require本身就是相对于<code>__dirname</code>的，所以能够自动打包。假设文件中有以下代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">require(&#x27;./build/&#x27; + cmd + &#x27;.js&#x27;)</span><br><span class="line">path.join(__dirname, &#x27;views/&#x27; + viewName)</span><br></pre></td></tr></table></figure><p>这些路径都不是常量，pkg没办法帮你自动识别要打包哪个文件，所以文件就丢失了，所以这时候就使用<code>scripts</code>和<code>assets</code>来告诉pkg，这些文件要打包进去。那么我们怎么知道哪些文件没有被打包呢？难倒要一行行地去翻源代码吗？其实很简单，只需要把打包好的文件运行下，报错信息一般就会告诉你缺失哪些文件，并且pkg在打包过程中也会提示一些它不能自动打包的文件。</p><h1 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h1><p>如果说pkg还有哪儿还可以改进的地方，那就是无法自动打包二进制模块<code>*.node</code>文件。如果你的项目中引用了二进制模块，如sqlite3，那么你需要手动地将<code>*.node</code>文件复制到可执行文件同一目录，我通常使用命令<code>cp node_modules/**/*.node .</code>一键完成。但是，如果你要跨平台打包，例如在windows上打包linux版本，相应的二进制模块也要换成linux版本，通常需要你手动的下载或者编译。</p><p>那为什么pkg不能将二进制模块打包进去呢？我猜想是require载入一个js文件和node文件，它们的机制是不一样的。另外从设计来说，不自动打包二进制模块也是合理的，因为二进制模块都是平台相关的。如果我在windows上生成一个linux文件，那么就需要拉取linux版本的<code>.node</code>文件，这是比较困难的。并且有些二进制模块不提供预编译版本，需要安装的时候编译，pkg再牛也不可能模拟一个其他平台的编译环境吧。nexe可以自动打包二进制模块，但是只能打包当前平台和当前版本的可执行文件。这意味着如果Node.js应用引用了二进制包，那么这个应用就不能跨平台打包了，所以我认为这方面，nexe不能算是一个好的设计。</p><p>还有一点就是关于项目中的配置文件处理，比如数据库连接参数、环境变量等。因为这些配置文件会跟着不同的部署环境进行更改，所以为了方便更改，一般不希望把配置文件打包到exe。为了避免pkg自动地将配置文件打包到exe中，代码中不要采用以下方式读取配置文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fs.readFile(path.join(__dirname, &#x27;./config.json&#x27;)), callback)</span><br></pre></td></tr></table></figure><p>而是采用相对于<code>process.CWD()</code>的方法读取：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">fs.readFile(path.join(process.CWD(), &#x27;./config.json&#x27;), callback)</span><br><span class="line"></span><br><span class="line">// 或者</span><br><span class="line">fs.readFile(&#x27;./config.json&#x27;, callback)</span><br></pre></td></tr></table></figure><p>如果配置文件是js格式的，那么不要直接<code>require(&#39;./config&#39;)</code>，而是采用动态require：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const config = require(process.CWD() + &#x27;./config&#x27;)</span><br></pre></td></tr></table></figure><p>另外要提及的是pkg打包之后动态载入js文件会有安全性问题，即用户可以在js文件写任何处理逻辑，注入到打包后的exe中。例如，可以读取exe里面的虚拟文件系统，把源代码导出来。所以，尽量不要采用JS作为配置文件，也不要动态载入js模块。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Node.js应用不需要经过编译过程，可以直接把源代码拷贝到部署机上执行，确实比C++、Java这类编译型应用部署方便。然而，Node.js应用执行需要有运行环境，意味着你需要先在部署机器上安装Node.js。虽说没有麻烦到哪里去，但毕竟多了一个步骤，特别是对于离线环境下的</summary>
      
    
    
    
    
  </entry>
  
</feed>
