<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>wastecat</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://lengzzz.com/</id>
  <link href="https://lengzzz.com/" rel="alternate"/>
  <link href="https://lengzzz.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, wastecat</rights>
  <title>/home/wastecat</title>
  <updated>2026-09-20T08:00:50.217Z</updated>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="rclone" scheme="https://lengzzz.com/tags/rclone/"/>
    <category term="OverlayFS" scheme="https://lengzzz.com/tags/OverlayFS/"/>
    <category term="kodi" scheme="https://lengzzz.com/tags/kodi/"/>
    <category term="多媒体" scheme="https://lengzzz.com/tags/%E5%A4%9A%E5%AA%92%E4%BD%93/"/>
    <content>
      <![CDATA[<p>之前我的家庭媒体中心一直是使用本地 nas 存高清片源 + tmm（TinyMediaManager）做媒体搜刮器 + 电视上安装 kodi 做播放器的方案，但是这种方式的缺点是总得自己花时间下载片源。后来用了一段时间别人的 emby 服务，然鹅现在也用不了了。索性自己用 rclone 挂载网盘来管理片源，于是这周末折腾了下媒体管理中心，踩坑各种不同的方案之后选择了一个网上不太常见但个人感觉很好用的方案，于是记录一下。</p><p><img src="/images/IMG_9433.PNG" alt="kodi 截图"></p><span id="more"></span><h2 id="方案对比"><a href="#方案对比" class="headerlink" title="方案对比"></a>方案对比</h2><table><thead><tr><th>方案</th><th>是否能播放原始文件</th><th>搜刮</th><th>硬件解码</th></tr></thead><tbody><tr><td>plex</td><td>需付费直接放弃</td><td></td><td></td></tr><tr><td>emby</td><td>大多数ok，因字幕等原因会触发转码</td><td>速度一般，无法手动搜索</td><td>需付费开启</td></tr><tr><td>jellyfin</td><td>大多数ok，因字幕等原因会触发转码</td><td>速度一般，准确性一般，无法手动搜索</td><td>可开启</td></tr><tr><td>tmm + kodi</td><td>可以</td><td>速度较快，可手动搜索，提升准确性</td><td>取决于电视</td></tr></tbody></table><p>我的需求其实比较简单，观影设备只有客厅和卧室的电视，没有在移动端、web端看剧的需求。需要支持多端能同步观影记录。需要能尽可能播放原始文件，一方面是对画质的要求，另一方面是考虑家庭服务器的负载。</p><p>一开始尝试了 emby ，搜刮直接花了一个白天的时间，然后发现海报怎么都加载不出来，另外很多视频都会走转码（我就很奇怪为啥，我电视端明明支持这种格式，服务端仍然会转码），emby 的硬件解码需要付费开启，遂放弃。然后尝试了 jellyfin，这个转码倒是比较完美的支持硬件解码了，然鹅搜刮还是难用，一是慢，二是不好进行微调。最后还是用回了 kodi。下面是 kodi 方案的教程。</p><h2 id="rclone-配置"><a href="#rclone-配置" class="headerlink" title="rclone 配置"></a>rclone 配置</h2><p>rclone 的作用是把各种网盘挂载到 Linux 服务器上，这样访问网盘的文件就像是访问本地的文件夹一样方便。</p><h4 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h4><p>安装的话直接按照官方的方法，一条命令完事：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl https://rclone.org/install.sh | <span class="hljs-built_in">sudo</span> bash<br></code></pre></td></tr></table></figure><h4 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h4><p>安装好了之后首先需要配置下，在终端执行 <code>rclone config</code> 命令后是一个交互式的命令行界面，能看懂英文的话照着提示一步一步来就行了，主要就是中间会给你一个url，在浏览器里用你的Google账号打开然后进行授权，得到一个授权码填到 rclone 里就行了，很简单就不赘述了，详细可以参考这个教程： <a href="https://ley.best/rclone/">https://ley.best/rclone/</a> </p><h4 id="挂载"><a href="#挂载" class="headerlink" title="挂载"></a>挂载</h4><p>挂载同样是一条命令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">rclone mount g: /home/gdrive-lower -copy-links --no-gzip-encoding --no-check-certificate --allow-other --allow-non-empty --<span class="hljs-built_in">umask</span> 000 --daemon<br></code></pre></td></tr></table></figure><ul><li>g: 代表初始化的时候给网盘起的名字</li><li>&#x2F;home&#x2F;gdrive-lower：代表要挂载的位置，需要先创建出这个目录</li><li>其他参数暂时不用管</li><li>这个命令会阻塞，为了开机自启动，可以创建一个systemd服务，或者更简单的放在rc.local文件里也行</li></ul><p>挂载好了之后，可以发现 &#x2F;home&#x2F;gdrive-lower 目录里已经是网盘上的文件了。不过都是只读的，我们搜刮到额外的信息海报等信息是无法存储到这个目录的，所以就需要 Linux 上另一个神器 OverlayFS 来解决这个问题。</p><h2 id="OverlayFS-配置"><a href="#OverlayFS-配置" class="headerlink" title="OverlayFS 配置"></a>OverlayFS 配置</h2><p>OverlayFS 是 Linux 的一个文件系统，用途很广泛，比如 openwrt、docker 的文件系统就基于这个。简单讲一下他的作用。</p><p>OverlayFS 可以多个目录进行合并成一个新的目录，比如a、b两个目录里分别有 1.mp4 和 1.nfo 文件，那么他们合并成的新目录就同时包含了1.mp4和1.nfo两个文件。另外 OverlayFS 也有层级的概念，如果a、b目录有同名的文件，那么更高层级的目录的文件会优先被访问。</p><p>由于 OverlayFS 这么好用的特性，很适合用在我这个场景，能方便的把只读的目录变成可读写的。</p><p><img src="/images/20171029124743310.jpeg" alt="overlayfs 原理"></p><h4 id="创建必要目录"><a href="#创建必要目录" class="headerlink" title="创建必要目录"></a>创建必要目录</h4><p>首先创建两个目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> ~/gdrive-upper ~/gdrive-work /home/gdrive<br></code></pre></td></tr></table></figure><ul><li>~&#x2F;gdrive-upper：作为上层目录，对谷歌云的读写都会保存到这里，而不会真正写入谷歌云的挂载点</li><li>~&#x2F;gdrive-work：存储临时文件</li><li>&#x2F;home&#x2F;gdrive：合并出的新目录</li></ul><h4 id="挂载-OverlayFS"><a href="#挂载-OverlayFS" class="headerlink" title="挂载 OverlayFS"></a>挂载 OverlayFS</h4><p>同样是一条命令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> mount -t overlay overlay -o lowerdir=/home/gdrive-lower,upperdir=～/gdrive-upper,workdir=～/gdrive-work /home/gdrive<br></code></pre></td></tr></table></figure><ul><li>lowerdir&#x3D;&#x2F;home&#x2F;gdrive-lower：代表底层目录，写操作不会实际写到这里，读操作优先读取上层目录，读不到的才会读这里（比如视频文件）</li><li>upperdir&#x3D;～&#x2F;gdrive-upper：代表上层目录，读写优先对这里操作，会保存nfo、poster等文件</li><li>workdir&#x3D;～&#x2F;gdrive-work：临时目录，系统需要的，我们不用管他的作用</li><li>&#x2F;home&#x2F;gdrive：合并出的新目录，我们后续只对他进行访问</li></ul><h4 id="后续使用"><a href="#后续使用" class="headerlink" title="后续使用"></a>后续使用</h4><blockquote><p>下面samba 的 docker 有点问题，会进行 chown 修改文件所有者，这会导致文件被写入到upper上层目录，会使服务器硬盘爆炸。我最后使用的 ssh 协议（sftp）来访问，kodi上需要安装下 sftp 插件</p></blockquote><p>既然已经搞定了读写的问题了，那么后续无论是要通过 smb 协议还是 ftp 协议访问都是配置一下的事了，<del>我这里使用的是 docker 启动一个 smb 服务器：</del></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker run -d \<br>    --name samba \<br>    -e TZ=Asia/Shanghai \<br>    --net host \<br>    -v /home/gdrive:/mount \<br>    dperson/samba -p \<br>    -u <span class="hljs-string">&quot;xxx;xxx&quot;</span> \<br>    -s <span class="hljs-string">&quot;gdrive;/mount;yes;no&quot;</span> \<br>    -S<br></code></pre></td></tr></table></figure><ul><li>-v &#x2F;home&#x2F;gdrive:&#x2F;mount：这里填写合并后的目录，改成你自己的</li><li>-u “xxx;xxx”：samba的用户名密码，自己改一个</li></ul><h2 id="TinyMediaManager"><a href="#TinyMediaManager" class="headerlink" title="TinyMediaManager"></a>TinyMediaManager</h2><p>TinyMediaManager 是一个很好用的媒体搜刮器，能很方便的搜挂电影、电视剧的片名、介绍、海报、缩略图等等，并且存储成 kodi、emby 能读取的 nfo 文件。</p><p><img src="/images/WechatIMG4.png" alt="overlayfs 原理"></p><p>我们基本上只会用到左上角的两个按钮。</p><h4 id="更新源"><a href="#更新源" class="headerlink" title="更新源"></a>更新源</h4><p>在设置里首先配置下要搜刮的目录，然后点击左上角的更新源按钮，他就开始自动搜索媒体文件了。</p><h4 id="搜刮"><a href="#搜刮" class="headerlink" title="搜刮"></a>搜刮</h4><p>等搜索结束后，点击筛选，在里面选择新增季或新增电影，能筛选出刚刚新搜刮到的媒体文件，然后全选他们，点击搜索&amp;剐削按钮旁边的下拉菜单，点击自动匹配，它会自动一个一个下载 nfo 和海报文件，最后匹配度低于 75% （在设置里可以调整）的视频，会提示给你来手动进行搜索匹配，速度挺快的，300 部电影十几分钟就搞好了。</p><p>另外搜刮有个小技巧，如果你的文件名也是和我一样是中文+英文混合命名的，并且包含什么 <code>x264</code> <code>web-dl</code> 之类的标签，那搜挂起大概率会被这些词干扰而什么都搜刮不到。所以 tmm 在搜挂时支持从文件名中剔除掉一些字符，而且支持正则表达式。我直接把 <code>[a-z]*</code> 作为敏感词，可以提出掉所有的英文字符，只用中文来搜刮，这样准确率高很多。</p><p>配置如下图：</p><p><img src="/images/WechatIMG5.png" alt="tmm 搜挂无效字符"></p><h4 id="docker"><a href="#docker" class="headerlink" title="docker"></a>docker</h4><p>每次使用电脑可能会有所不便，毕竟个人电脑不能24小时开机，而服务器可以，其实 tmm 也支持docker:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker run \<br>    --name=tinyMediaManager \<br>    --add-host=api.themoviedb.org:13.224.161.90 \<br>    --add-host=image.tmdb.org:104.16.61.155 \<br>    --add-host=api.themoviedb.org:13.35.67.86 \<br>    --add-host=www.themoviedb.org:54.192.151.79 \<br>    --<span class="hljs-built_in">env</span> ENABLE_CJK_FONT=1 \<br>    --<span class="hljs-built_in">env</span> USER_ID=0 \<br>    --<span class="hljs-built_in">env</span> GROUP_ID=0 \<br>    --<span class="hljs-built_in">env</span> TZ=Asia/Shanghai \<br>    -v /home/gdrive:/media \<br>    -p 5800:5800 \<br>    -p 5900:5900 \<br>    romancin/tinymediamanager:latest<br></code></pre></td></tr></table></figure><p>启动后进入到容器里，<code>vi /etc/cont-init.d/10-cjk-font.sh</code> 编辑这个文件，把 <a href="http://dl-cdn.alpinelinux.org/">http://dl-cdn.alpinelinux.org</a> 替换为 <a href="http://mirrors.tuna.tsinghua.edu.cn/">http://mirrors.tuna.tsinghua.edu.cn/</a> 这样下载中文字体会快很多。然后重启容器。（如果你的服务器本身能正常访问这些域名，则不需要这一步，上面命令里的 <code>--add-host</code> 也可以删去）</p><p>启动后就可以通过浏览器打开 http:&#x2F;&#x2F;[容器ip]:5800&#x2F; 访问 tmm 了，这样开启搜刮任务后个人电脑就可以关机了，等有时间了再用浏览器看结果。事实上 docker 内部启动了个 xorg 桌面服务，然后通过 http 来访问 vnc，还是稍微有点卡顿的。</p><h2 id="kodi"><a href="#kodi" class="headerlink" title="kodi"></a>kodi</h2><p>我有个特殊的需求是需要在客厅和卧室的不同电视里同步播放进度，kodi 默认是把媒体库存储到本地的 sqlite 里，但也支持存储到远端 mysql。mysql 服务器可以用docker启动，也可以用群晖 nas 里的 MariaDB 软件包。如果安装教程较多，不再赘述。</p><p>针对 kodi 需要写一个 advancedsettings.xml 配置文件：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">advancedsettings</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">videodatabase</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">type</span>&gt;</span>mysql<span class="hljs-tag">&lt;/<span class="hljs-name">type</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">host</span>&gt;</span>192.168.1.123<span class="hljs-tag">&lt;/<span class="hljs-name">host</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">port</span>&gt;</span>3306<span class="hljs-tag">&lt;/<span class="hljs-name">port</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">user</span>&gt;</span>kodi<span class="hljs-tag">&lt;/<span class="hljs-name">user</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">pass</span>&gt;</span>kodi<span class="hljs-tag">&lt;/<span class="hljs-name">pass</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">videodatabase</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">videolibrary</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">importwatchedstate</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">importwatchedstate</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">importresumepoint</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">importresumepoint</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">videolibrary</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">advancedsettings</span>&gt;</span><br></code></pre></td></tr></table></figure><p>上面的 ip 地址用户名密码改成你自己的 mysql 地址。</p><p>把这个文件存储到每个电视的 <code>Android/data/org.xbmc.kodi/files/.kodi/userdata/</code> 下面，重启 kodi 后他搜挂到的信息就存储到 mysql 了。</p><p>（访问电视的文件，可以安装一个 ES文件浏览器，然后开启 ftp 访问）</p><p>另外推荐一个很好看的 kodi 皮肤 <code>Arctic: Zephyr - Reloaded</code>，在系统自带的插件库里就能安装，个人感觉比 emby、jellyfin 的 android app 好用&#x2F;好看多了 😂</p><p><img src="/images/IMG_9431.PNG" alt="kodi 截图"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>当然这个方案也不是完美的，只是比较切合我个人的需求，比如搜刮不能自动进行，需要定期手工打开 TinyMediaManager 进行搜刮。另外对 iOS 移动端支持的不好，appstore 无法直接安装 kodi。</p>]]>
    </content>
    <id>https://lengzzz.com/2021/08/17/kodi/</id>
    <link href="https://lengzzz.com/2021/08/17/kodi/"/>
    <published>2021-08-17T10:54:00.000Z</published>
    <summary>
      <![CDATA[<p>之前我的家庭媒体中心一直是使用本地 nas 存高清片源 + tmm（TinyMediaManager）做媒体搜刮器 + 电视上安装 kodi 做播放器的方案，但是这种方式的缺点是总得自己花时间下载片源。后来用了一段时间别人的 emby 服务，然鹅现在也用不了了。索性自己用 rclone 挂载网盘来管理片源，于是这周末折腾了下媒体管理中心，踩坑各种不同的方案之后选择了一个网上不太常见但个人感觉很好用的方案，于是记录一下。</p>
<p><img src="/images/IMG_9433.PNG" alt="kodi 截图"></p>]]>
    </summary>
    <title>rclone挂载网盘 + OverlayFS + tmm + kodi打造家庭媒体中心</title>
    <updated>2026-09-20T08:00:50.217Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="闲扯" scheme="https://lengzzz.com/tags/%E9%97%B2%E6%89%AF/"/>
    <content>
      <![CDATA[<p>这里是一只废喵，时隔多年，我又准备写博客了。这次搬家到了 GitHub pages，用上了hexo。整体感觉还不错，简单记录下折腾 hexo 的经历。</p><h2 id="安装步骤"><a href="#安装步骤" class="headerlink" title="安装步骤"></a>安装步骤</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm install hexo-cli -g<br>hexo init blog<br><span class="hljs-built_in">cd</span> blog<br>npm install<br></code></pre></td></tr></table></figure><p>这几步是在本机上安装好 hexo，并且生成一个 hexo 项目文件。</p><span id="more"></span><h2 id="配置主题"><a href="#配置主题" class="headerlink" title="配置主题"></a>配置主题</h2><p>默认的主题不太喜欢，换了个叫 <a href="https://github.com/CodeDaraW/Hacker">Hacker</a> 的。</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><code class="hljs bash">git <span class="hljs-built_in">clone</span> https://github.com/CodeDaraW/Hacker themes/Hacker<br><span class="hljs-built_in">rm</span> -rf themes/Hacker/.git<br></code></pre></td></tr></table></figure><p>这样就安装主题到 theme 目录了，当然也可以下载 zip，手动解压缩。</p><p>之后编辑 <code>_config.yml</code> 文件，修改成 <code>theme: Hacker</code>。</p><p>另外，主题本身也有一些配置，放在 <code>./themes/Hacker/_config.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><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></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">menu:</span><br>  <span class="hljs-attr">Home:</span> <span class="hljs-string">/</span><br>  <span class="hljs-attr">Archives:</span> <span class="hljs-string">/archives</span><br>  <span class="hljs-attr">About:</span> <span class="hljs-string">/about</span><br>  <span class="hljs-attr">RSS:</span> <span class="hljs-string">/atom.xml</span><br><br><span class="hljs-comment"># gitment</span><br><span class="hljs-attr">gitment:</span> <span class="hljs-literal">false</span><br><span class="hljs-attr">gitment_owner:</span><br><span class="hljs-attr">gitment_repo:</span><br><span class="hljs-attr">gitment_client_id:</span><br><span class="hljs-attr">gitment_client_secret:</span><br><br><span class="hljs-comment"># gitalk</span><br><span class="hljs-attr">gitalk:</span> <span class="hljs-literal">false</span><br><span class="hljs-attr">gitalk_owner:</span><br><span class="hljs-attr">gitalk_admin:</span> []<br><span class="hljs-attr">gitalk_repo:</span><br><span class="hljs-attr">gitalk_client_id:</span><br><span class="hljs-attr">gitalk_client_secret:</span><br><br><span class="hljs-comment"># valine comment</span><br><span class="hljs-attr">valine:</span> <span class="hljs-literal">false</span><br><span class="hljs-attr">leancloud_id:</span><br><span class="hljs-attr">leancloud_key:</span><br><br><span class="hljs-comment"># disqus comment</span><br><span class="hljs-attr">disqus:</span> <span class="hljs-literal">true</span><br><span class="hljs-attr">disqus_shortname:</span> <span class="hljs-string">lengzzz</span><br><br><span class="hljs-comment"># google analytics</span><br><span class="hljs-attr">googleTrackId:</span> <span class="hljs-string">UA-47471611-1</span><br><br></code></pre></td></tr></table></figure><h2 id="配置CI"><a href="#配置CI" class="headerlink" title="配置CI"></a>配置CI</h2><p>travis-ci 能帮你在每次 push 时自动部署。基本上按照 <a href="https://hexo.io/zh-cn/docs/github-pages.html">https://hexo.io/zh-cn/docs/github-pages.html</a> 这个教程就可以了，但是有个坑是目前 GitHub pages 只支持用 master 分支了，所以写文章的分支就不能是 master 了，我这里用的是 build 分支。相应的，<code>.travis.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><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">sudo:</span> <span class="hljs-literal">false</span><br><span class="hljs-attr">language:</span> <span class="hljs-string">node_js</span><br><span class="hljs-attr">node_js:</span><br>  <span class="hljs-bullet">-</span> <span class="hljs-number">10</span> <br><span class="hljs-attr">cache:</span> <span class="hljs-string">npm</span><br><span class="hljs-attr">branches:</span><br>  <span class="hljs-attr">only:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">build</span> <span class="hljs-comment"># 重点：这里分支改成写文章的分支</span><br><span class="hljs-attr">script:</span><br>  <span class="hljs-bullet">-</span> <span class="hljs-string">ln</span> <span class="hljs-string">theme.config.yml</span> <span class="hljs-string">themes/Hacker/_config.yml</span><br>  <span class="hljs-bullet">-</span> <span class="hljs-string">hexo</span> <span class="hljs-string">generate</span> <br><span class="hljs-attr">deploy:</span><br>  <span class="hljs-attr">provider:</span> <span class="hljs-string">pages</span><br>  <span class="hljs-attr">skip-cleanup:</span> <span class="hljs-literal">true</span><br>  <span class="hljs-attr">github-token:</span> <span class="hljs-string">$GH_TOKEN</span><br>  <span class="hljs-attr">keep-history:</span> <span class="hljs-literal">true</span><br>  <span class="hljs-attr">target_branch:</span> <span class="hljs-string">master</span> <span class="hljs-comment"># 重点：这里新增一个target_branch选项，值设定为 master</span><br>  <span class="hljs-attr">on:</span><br>    <span class="hljs-attr">branch:</span> <span class="hljs-string">build</span> <span class="hljs-comment"># 重点：这里分支改成写文章的分支</span><br>  <span class="hljs-attr">local-dir:</span> <span class="hljs-string">public</span><br><br></code></pre></td></tr></table></figure><h2 id="域名"><a href="#域名" class="headerlink" title="域名"></a>域名</h2><p>Github pages 支持自定义域名，需要将www的域名CNAME设定为 <code>xxx.github.io</code>，另外需要把@域名（就是裸的没有任何子域名的，比如本站就是lengzzz.com）的A记录修改为下列IP：</p><figure class="highlight accesslog"><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><code class="hljs accesslog"><span class="hljs-number">185.199.108.153</span><br><span class="hljs-number">185.199.109.153</span><br><span class="hljs-number">185.199.110.153</span><br><span class="hljs-number">185.199.111.153</span><br></code></pre></td></tr></table></figure><p>都设定好之后，在项目里 source 下新增一个 <code>CNAME</code> 文件（全大写），里面写上域名就ok了。</p><p>之后在 GitHub 项目的 setting 里，也将 Custom domain 里配置上域名。之后 GitHub 会自动签发域名的 ssl 证书。最好也将 setting 里的 Enforce HTTPS 勾上，这样能强制使用 https。</p><h2 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h2><p>大概等十几分钟后，使用域名 <a href="https://lengzzz.com/">https://lengzzz.com/</a> 就能访问到了。</p>]]>
    </content>
    <id>https://lengzzz.com/2020/04/29/hello-world/</id>
    <link href="https://lengzzz.com/2020/04/29/hello-world/"/>
    <published>2020-04-29T04:00:00.000Z</published>
    <summary>
      <![CDATA[<p>这里是一只废喵，时隔多年，我又准备写博客了。这次搬家到了 GitHub pages，用上了hexo。整体感觉还不错，简单记录下折腾 hexo 的经历。</p>
<h2 id="安装步骤"><a href="#安装步骤" class="headerlink" title="安装步骤"></a>安装步骤</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">npm install hexo-cli -g<br>hexo init blog<br><span class="hljs-built_in">cd</span> blog<br>npm install<br></code></pre></td></tr></table></figure>

<p>这几步是在本机上安装好 hexo，并且生成一个 hexo 项目文件。</p>]]>
    </summary>
    <title>你好世界</title>
    <updated>2026-09-20T08:00:50.217Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="openwrt" scheme="https://lengzzz.com/tags/openwrt/"/>
    <category term="折腾" scheme="https://lengzzz.com/tags/%E6%8A%98%E8%85%BE/"/>
    <category term="路由器" scheme="https://lengzzz.com/tags/%E8%B7%AF%E7%94%B1%E5%99%A8/"/>
    <category term="lede" scheme="https://lengzzz.com/tags/lede/"/>
    <content>
      <![CDATA[<p>家里一直用的是网件的 R6220 路由器，之前一直用的是原厂固件，因为本人不太喜欢在主路由器上装奇奇怪怪的东西，另外是因为 R6220 一直都没有好用的固件可以刷，前几天逛论坛发现竟然已经有人破解了 R6220 可以刷 LEDE 和 pandora 了，忍不住手痒又去刷了一波固件。此文记录一下过程。</p><p><a href="/notename/" title="install lede on netgear r6220"></a></p><span id="more"></span><p>[toc]</p><blockquote><p><strong>请全程使用网线连接进行操作，操作时千万不要断电，因为 R6220 使用 NAND flash，刷坏后无法使用普通编程器修复，刷机有风险，请扶稳方向盘谨慎驾驶</strong></p></blockquote><h2 id="1-开启路由器telnet"><a href="#1-开启路由器telnet" class="headerlink" title="1. 开启路由器telnet"></a>1. 开启路由器telnet</h2><p>在浏览器打开 <a href="http://192.168.1.1/setup.cgi?todo=debug">http://192.168.1.1/setup.cgi?todo=debug</a> 这个网址，记着把 192.168.1.1 替换成你路由器的 ip 地址，成功后会显示 <code>Debug Enabled !</code> 说明已经成功。</p><p>使用 Mac 或 Linux 的用户，打开终端，执行 <code>telnet 192.168.1.1</code> ，可以登录到路由器的管理shell里，使用 windows 的用户，可以<a href="https://the.earth.li/~sgtatham/putty/latest/w32/putty.exe">点这里</a>下载 putty，然后如下图，Host Name填路由器ip，port写23，connection type 选 telnet：</p><p><img src="/images/1a797a80091c5a2290a090407be0801f.png" alt="image_1bnv9ksf91joebhc5uvi451l14p.png-84.3kB"></p><p>点击 open 之后，会出现一个黑屏窗口，显示 <code>R6220 login:</code> ，输入 <code>root</code> 按回车，会显示一个大的 logo 和一个 <code>#</code> 号（如下），此时说明已经登录成功了。</p><figure class="highlight gherkin"><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><code class="hljs gherkin">Welcome to<br>    _______  _______  ___     __  ____   _  _   ___<br>    |<span class="hljs-string">  ___  \</span>|<span class="hljs-string">   __  </span>||<span class="hljs-string">   </span>|<span class="hljs-string">   </span>|<span class="hljs-string">__</span>||<span class="hljs-string">    \ </span>|<span class="hljs-string"> </span>||<span class="hljs-string"> </span>|<span class="hljs-string"> /  /</span><br>    |<span class="hljs-string"> </span>|<span class="hljs-string">___</span>|<span class="hljs-string"> </span>||<span class="hljs-string">  </span>|<span class="hljs-string">__</span>|<span class="hljs-string"> </span>||<span class="hljs-string">   </span>|<span class="hljs-string">__  __ </span>|<span class="hljs-string">     \</span>|<span class="hljs-string"> </span>||<span class="hljs-string"> </span>|<span class="hljs-string">/  /</span><br>    |<span class="hljs-string">   _   /</span>|<span class="hljs-string">   _   </span>||<span class="hljs-string">      </span>||<span class="hljs-string">  </span>||<span class="hljs-string"> </span>|<span class="hljs-string">\     </span>||<span class="hljs-string">     \</span><br>    |<span class="hljs-string">__</span>|<span class="hljs-string"> \__\</span>|<span class="hljs-string">__</span>|<span class="hljs-string"> </span>|<span class="hljs-string">__</span>||<span class="hljs-string">______</span>||<span class="hljs-string">__</span>||<span class="hljs-string">_</span>|<span class="hljs-string"> \____</span>||<span class="hljs-string">_</span>|<span class="hljs-string">\___\</span><br>                     =System Architecture Department=<br><span class="hljs-comment">#</span><br></code></pre></td></tr></table></figure><h2 id="2-备份eeprom"><a href="#2-备份eeprom" class="headerlink" title="2. 备份eeprom"></a>2. 备份eeprom</h2><blockquote><p>此步骤可跳过，但有一定几率无法使用 Wi-Fi</p></blockquote><p>准备一个格式化成 fat32 的U盘（最好重新格式化，卷标写成U），插入 R6220 的USB口中，然后在刚才的黑窗口里输入命令：<code>ls /mnt/shares</code></p><p>会显示出你刚才优盘的卷标名，如果重新格式化成 U 了，那么就会显示一个 U。</p><p>下一步，执行 <code>cd /mnt/shares/优盘的卷标名</code> ，同理如果刚才显示 U 这里就应该执行 <code>cd /mnt/shares/U</code>。</p><p>如果没有报错的话就进行下一步：</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs routeros">dd <span class="hljs-attribute">if</span>=/dev/mtd10 <span class="hljs-attribute">of</span>=./mtd10.bin<br></code></pre></td></tr></table></figure><p>这是为了把 eeprom 备份到U盘中。如果执行之后，显示一个 <code>#</code> 说明成功了。现在把U盘拔下来插到电脑上，可以看到一个 mtd10.bin 文件，把它复制到电脑上备份。</p><h2 id="3-刷写不死-bootloader-pb-boot"><a href="#3-刷写不死-bootloader-pb-boot" class="headerlink" title="3. 刷写不死 bootloader pb-boot"></a>3. 刷写不死 bootloader pb-boot</h2><p>从这里下载 pb-boot：</p><blockquote><p>链接: <a href="https://pan.baidu.com/s/1jIONs6i">https://pan.baidu.com/s/1jIONs6i</a> 密码: nyn9</p></blockquote><p>把这个文件放到U盘里，插入到路由器中。</p><p>使用 telnet 登陆到路由器，和第二步一样，执行 <code>cd /mnt/shares/U</code> 进入到U盘文件夹。执行下面的命令：</p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">mtd_write</span> write pb-boot-r6220-<span class="hljs-number">20170801</span>.img Bootloader<br></code></pre></td></tr></table></figure><p>成功后会显示：</p><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs dart"><span class="hljs-title class_">Unlocking</span> <span class="hljs-title class_">Bootloader</span> ...<br><span class="hljs-title class_">Writing</span> from pb-boot-r6220.bin to <span class="hljs-title class_">Bootloader</span> ...  [w]<br></code></pre></td></tr></table></figure><p>接下来路由器关机，然后用针扎着reset键开机，开机后发现电源灯和wan口灯呼吸闪烁，说明进入了 bootloader，在浏览器中输入 192.168.1.1 会看到 pb-boot 的界面：</p><p><img src="/images/6f4b994612ac748b39bf5768d96db680.png" alt="image_1bnvblmm81d0t1hgfpfu1h0ft4116.png-107.3kB"></p><p>现在说明不死 bootloader 已经刷好了，可以随意折腾路由器而不怕刷坏了，无论何时感觉刷坏了，都可以用针扎着reset键开机，来反复刷机重置系统。</p><blockquote><p>小知识：bootloader 是在操作系统运行之前先运行的一个小程序，类似电脑的 bios，折腾路由器时就算把系统搞坏了，只要 bootloader 没被破坏，都可以刷回出厂设置的。</p></blockquote><h3 id="3-1-mac-用户会遇到的坑"><a href="#3-1-mac-用户会遇到的坑" class="headerlink" title="3.1 mac 用户会遇到的坑"></a>3.1 mac 用户会遇到的坑</h3><p>使用 mac 的用户再刷写了 pb-boot 之后，会发现无法获取到 IP 地址，可以这样操作：</p><p>在<strong>系统偏好设置</strong>中点网络，把网卡设置成下图所示：</p><p><img src="/images/3e9a5f6d55c07d805533d03e2d3b8a24.png" alt="image_1bnvc6apsorr15en13he1l7s1mei1j.png-107.3kB"></p><p>打开<strong>终端</strong>反复执行如下命令：</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><code class="hljs bash"><span class="hljs-built_in">sudo</span> arp -ad<br><span class="hljs-built_in">sudo</span> arp -s 192.168.1.1 ff:ff:ff:ff:ff:ff<br></code></pre></td></tr></table></figure><p>第一次可能会要求你输密码，输入即可</p><p>每执行一遍就再浏览器中刷新一次 192.168.1.1 ，直到浏览器能打开网页后停止执行命令。</p><h2 id="4-刷-LEDE-官方固件"><a href="#4-刷-LEDE-官方固件" class="headerlink" title="4. 刷 LEDE 官方固件"></a>4. 刷 LEDE 官方固件</h2><p>一开始我是在 <a href="http://www.right.com.cn/forum/thread-208580-1-1.html">恩山无限论坛</a> 下载的网友编译的固件，他的固件本身集成了好多功能，不爱折腾的人刷他的固件用自带的功能就够用了，但是他的固件有个问题，固件的内核版本不是官方的版本，没办法安装官方软件仓库里的 kmod 软件包（可以强制安装，但是会大几率无限重启），另外固件的 opkg 好像也没有配置好，安装软件包会比较折腾。所以我建议还是先刷网友版，然后升级成官方版。</p><p>先从这里下载恩山网友版固件</p><blockquote><p>链接: <a href="https://pan.baidu.com/s/1c1YSCLy">https://pan.baidu.com/s/1c1YSCLy</a> 密码: 36vm</p></blockquote><p>然后在 192.168.1.1 里，点击 browse 选择刚才下载的网友版固件，再点击 Firmware update，等 1 分钟就刷好了。</p><p>再次刷新 192.168.1.1 网页，发现已经进入 lede 的界面了。</p><p><img src="/images/772ab39a5aa6a5091dc7111ab7983f0f.png" alt="image_1bnvcca3pf6dfb6177rr9b70o20.png-23.1kB"></p><p>用户名填 root，密码输admin，进入到管理界面中。</p><p>然后左侧点击<strong>网络</strong>、<strong>接口</strong>，把 WAN 口配置好，尝试一下能不能上去网。</p><p>对于不想折腾的朋友到此为止就可以了，恩山网友版的固件足够好用，喜欢折腾的朋友继续看下面（如果发现Wi-Fi无法正常使用的请看<a href="#8-%E6%89%BE%E4%B8%8D%E5%88%B0-wi-fi%E6%88%96wi-fi%E4%BF%A1%E5%8F%B7%E5%BE%88%E5%B7%AE">最下面</a>）。</p><p>点击左侧的<strong>系统</strong>，然后点击<strong>备份&#x2F;升级</strong>。现在咱们要升级成 lede 官方版。</p><p>从这里下载官方版：<a href="https://downloads.lede-project.org/snapshots/targets/ramips/mt7621/lede-ramips-mt7621-r6220-squashfs-sysupgrade.tar">https://downloads.lede-project.org/snapshots/targets/ramips/mt7621/lede-ramips-mt7621-r6220-squashfs-sysupgrade.tar</a></p><p>或者从中科大的镜像里下载：<a href="http://mirrors.ustc.edu.cn/lede/snapshots/targets/ramips/mt7621/lede-ramips-mt7621-r6220-squashfs-sysupgrade.tar">http://mirrors.ustc.edu.cn/lede/snapshots/targets/ramips/mt7621/lede-ramips-mt7621-r6220-squashfs-sysupgrade.tar</a></p><p>在刷写新的固件那一栏，选择刚下载的固件，然后点击刷写固件。</p><p>等 1 分钟后，路由器自动重启，此时官方固件已经刷好了。</p><h2 id="5-配置官方固件"><a href="#5-配置官方固件" class="headerlink" title="5. 配置官方固件"></a>5. 配置官方固件</h2><p>官方固件默认不开启Wi-Fi，默认不安装web管理界面，所以我们需要先使用命令行进行配置。</p><p>使用 <code>ssh root@192.168.1.1</code> 登陆，windows的朋友还使用 putty，但是 port 需填写 22，connection type 选 SSH。</p><p>然后输入 root 回车，再输入 admin 回车。</p><p><img src="/images/6450c2de39907259390b2e4f293b4ec4.png" alt="image_1bnvd5mqeofhlp61sa51q0c11fm2d.png-22.6kB"></p><p>接着会看到一个 LEDE 的 logo，说明登陆成功了，下一步安装web管理界面。</p><blockquote><p>以下内容选作</p></blockquote><p>因为 lede 默认的软件仓库可能会比较慢，所以我们可以换成中科大的源，先执行vim：</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs vim"><span class="hljs-keyword">vim</span> /etc/opkg/distfeeds.<span class="hljs-keyword">conf</span><br></code></pre></td></tr></table></figure><p>用 vim 打开 opkg 配置文件，然后多按几次键盘上的 <code>dd</code> 会发现之前的内容被删除了。之后按一下 键盘上的 <code>I</code> ，然后复制下面的内容，在终端里粘贴（putty用户直接在黑窗口里右键就可以了）</p><figure class="highlight awk"><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><code class="hljs awk">src<span class="hljs-regexp">/gz reboot_core http:/</span><span class="hljs-regexp">/mirrors.ustc.edu.cn/</span>lede<span class="hljs-regexp">/snapshots/</span>targets<span class="hljs-regexp">/ramips/m</span>t7621/packages<br>src<span class="hljs-regexp">/gz reboot_base http:/</span><span class="hljs-regexp">/mirrors.ustc.edu.cn/</span>lede<span class="hljs-regexp">/snapshots/</span>packages<span class="hljs-regexp">/mipsel_24kc/</span>base<br>src<span class="hljs-regexp">/gz reboot_luci http:/</span><span class="hljs-regexp">/mirrors.ustc.edu.cn/</span>lede<span class="hljs-regexp">/snapshots/</span>packages<span class="hljs-regexp">/mipsel_24kc/</span>luci<br>src<span class="hljs-regexp">/gz reboot_packages http:/</span><span class="hljs-regexp">/mirrors.ustc.edu.cn/</span>lede<span class="hljs-regexp">/snapshots/</span>packages<span class="hljs-regexp">/mipsel_24kc/</span>packages<br>src<span class="hljs-regexp">/gz reboot_routing http:/</span><span class="hljs-regexp">/mirrors.ustc.edu.cn/</span>lede<span class="hljs-regexp">/snapshots/</span>packages<span class="hljs-regexp">/mipsel_24kc/</span>routing<br>src<span class="hljs-regexp">/gz reboot_telephony http:/</span><span class="hljs-regexp">/mirrors.ustc.edu.cn/</span>lede<span class="hljs-regexp">/snapshots/</span>packages<span class="hljs-regexp">/mipsel_24kc/</span>telephony<br></code></pre></td></tr></table></figure><p>然后按一下键盘上的 ESC 键，然后依次输入 <code>冒号</code>、<code>w</code>、<code>q</code>、<code>回车键</code>，退出到 shell 界面，现在中科大源就配置好了。</p><blockquote><p>以上内容选作</p></blockquote><p>接下来执行opkg来安装web管理界面，中文翻译和material皮肤：</p><figure class="highlight mipsasm"><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><code class="hljs mipsasm">opkg update<br>opkg <span class="hljs-keyword">install </span>luci<br>opkg <span class="hljs-keyword">install </span>luci-i18n-<span class="hljs-keyword">base-zh-cn</span><br><span class="hljs-keyword"></span>opkg <span class="hljs-keyword">install </span>luci-theme-material<br></code></pre></td></tr></table></figure><p>短暂等待后就安装好了，在浏览器打开 192.168.1.1 就能看到路由器管理界面了。</p><blockquote><p>注意，此处有个小坑，如果在刚才升级到原版系统时，把 <strong>保留配置</strong> 的勾给去掉了，那么此时路由器是无法上网的，也就无法从软件仓库安装，所以如果你和我一样手贱去掉了那个勾，你现在可以把 R6220 接到一个已经配置好能上网的路由器的 LAN 口上，然后再进行 opkg install 操作。</p></blockquote><h2 id="6-安装upnp"><a href="#6-安装upnp" class="headerlink" title="6. 安装upnp"></a>6. 安装upnp</h2><p>官方版固件默认没有 upnp，需要ssh登陆到路由器执行下面命令安装：</p><figure class="highlight stata"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs stata">opkg <span class="hljs-keyword">update</span><br>opkg install luci-<span class="hljs-keyword">app</span>-upnp<br></code></pre></td></tr></table></figure><p>或者也可以在<strong>系统</strong>、<strong>软件包</strong>里安装 <code>luci-app-upnp</code></p><p>然后在 <strong>服务</strong>、<strong>UPNP</strong>里勾选 Start UPnP and NAT-PMP service，点击保存&amp;应用</p><h2 id="7-编译广告屏蔽大师（adbyby）"><a href="#7-编译广告屏蔽大师（adbyby）" class="headerlink" title="7. 编译广告屏蔽大师（adbyby）"></a>7. 编译广告屏蔽大师（adbyby）</h2><p><a href="https://github.com/kuoruan/luci-app-adbyby">广告屏蔽大师（英文名adbyby）</a>是一个很好用的 lede 应用，能屏蔽爱奇艺、优酷等一系列广告而且不用等待。</p><h3 id="7-1-下载SDK"><a href="#7-1-下载SDK" class="headerlink" title="7.1 下载SDK"></a>7.1 下载SDK</h3><p>中科大源：<a href="http://mirrors.ustc.edu.cn/lede/snapshots/targets/ramips/mt7621/lede-sdk-ramips-mt7621_gcc-5.4.0_musl.Linux-x86_64.tar.xz">http://mirrors.ustc.edu.cn/lede/snapshots/targets/ramips/mt7621/lede-sdk-ramips-mt7621_gcc-5.4.0_musl.Linux-x86_64.tar.xz</a></p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">wget</span> http://mirrors.ustc.edu.cn/lede/snapshots/targets/ramips/mt7621/lede-sdk-ramips-mt7621_gcc-<span class="hljs-number">5</span>.<span class="hljs-number">4</span>.<span class="hljs-number">0</span>_musl.Linux-x86_64.tar.xz<br><span class="hljs-attribute">tar</span> xvf lede-sdk-ramips-mt7621_gcc-<span class="hljs-number">5</span>.<span class="hljs-number">4</span>.<span class="hljs-number">0</span>_musl.Linux-x86_64.tar.xz<br></code></pre></td></tr></table></figure><h3 id="7-2-下载adbyby"><a href="#7-2-下载adbyby" class="headerlink" title="7.2 下载adbyby"></a>7.2 下载adbyby</h3><figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs crmsh">git <span class="hljs-keyword">clone</span> <span class="hljs-title">https</span>://github.com/kuoruan/luci-app-adbyby.git<br></code></pre></td></tr></table></figure><h3 id="7-3-更新feeds"><a href="#7-3-更新feeds" class="headerlink" title="7.3 更新feeds"></a>7.3 更新feeds</h3><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><code class="hljs bash"><span class="hljs-built_in">cd</span> ~/lede-sdk-ramips-mt7621_gcc-5.4.0_musl.Linux-x86_64<br>./scripts/feeds update -a<br></code></pre></td></tr></table></figure><h3 id="7-4-把-adbyby-放进-SDK"><a href="#7-4-把-adbyby-放进-SDK" class="headerlink" title="7.4 把 adbyby 放进 SDK"></a>7.4 把 adbyby 放进 SDK</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> ~/luci-app-adbyby<br><span class="hljs-built_in">cp</span> ./adbyby ~/lede-sdk-ramips-mt7621_gcc-5.4.0_musl.Linux-x86_64/package -r<br><span class="hljs-built_in">cp</span> ./luci-app-adbyby ~/lede-sdk-ramips-mt7621_gcc-5.4.0_musl.Linux-x86_64/feeds/luci/applications -r<br></code></pre></td></tr></table></figure><p>把 adbyby 放到 package 里，把 luci-app-adbyby 放到 feeds&#x2F;luci&#x2F;applications 里。</p><h3 id="7-5-再更新-feeds"><a href="#7-5-再更新-feeds" class="headerlink" title="7.5 再更新 feeds"></a>7.5 再更新 feeds</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> ~/lede-sdk-ramips-mt7621_gcc-5.4.0_musl.Linux-x86_64<br>./scripts/feeds update -a<br>./scripts/feeds install luci-app-adbyby<br></code></pre></td></tr></table></figure><h3 id="7-6-编译"><a href="#7-6-编译" class="headerlink" title="7.6 编译"></a>7.6 编译</h3><figure class="highlight ebnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs ebnf"><span class="hljs-attribute">make menuconfig</span><br></code></pre></td></tr></table></figure><p>会打开一个界面，在里面用上下键移动光标，用回车键选择，先选 LuCI，再选 Appications，然后找到 luci-app-adbyby 按两下空格使前面变成 <code>[*]</code> ，然后一直按 ESC 退出，退出前会询问是否保存，选 YES。</p><p>然后执行 </p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs routeros">make <span class="hljs-attribute">V</span>=99<br></code></pre></td></tr></table></figure><p>最后会在 bin 文件夹编译出 luci-app-adbyby.ipk 文件，上传到路由器的&#x2F;tmp 文件夹，执行 <code>opkg install luci-app-adbyby.ipk</code> 进行安装。</p><h2 id="8-找不到-Wi-Fi，或Wi-Fi信号很差"><a href="#8-找不到-Wi-Fi，或Wi-Fi信号很差" class="headerlink" title="8. 找不到 Wi-Fi，或Wi-Fi信号很差"></a>8. 找不到 Wi-Fi，或Wi-Fi信号很差</h2><p>如果刷了 lede 之后找不到Wi-Fi，可以把 eeprom 还原成原厂。</p><h3 id="8-1-把之前备份的-mtd10-bin-放到路由器中"><a href="#8-1-把之前备份的-mtd10-bin-放到路由器中" class="headerlink" title="8.1 把之前备份的 mtd10.bin 放到路由器中"></a>8.1 把之前备份的 <code>mtd10.bin</code> 放到路由器中</h3><p>在终端里执行：</p><figure class="highlight elixir"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs elixir">scp mtd10.bin root<span class="hljs-variable">@10</span>.<span class="hljs-number">0.0</span>.<span class="hljs-number">1</span><span class="hljs-symbol">:/tmp</span><br></code></pre></td></tr></table></figure><p>使用 windows 的用户可以下载一个 <a href="https://sourceforge.net/projects/winscp/files/WinSCP/5.9.6/WinSCP-5.9.6-Portable.zip/download">WinScp</a> 来远程复制文件：</p><p>File protocol 选择 scp，hostname输入路由器IP，port写22，username写root，password写密码。</p><p><img src="/images/b2fc85fcbac6955b1c2fc2c68fa01d89.png" alt="image_1bnvf7v6un84c0v11qm1vku1sd37.png-39.8kB"></p><p>点击login，然后再右侧窗口双击一下 <code>..</code> ，然后再双击 tmp 文件夹。然后再左侧窗口找到刚才备份的 mtd10.bin 文件，直接拖到右侧窗口的空白区域。</p><p><img src="/images/38b4a52764c74bf82b95b87032f00a2f.png" alt="image_1bnvfagtt1k5f105kcgr7r132a3k.png-193.8kB"></p><h3 id="8-2-还原-eeprom"><a href="#8-2-还原-eeprom" class="headerlink" title="8.2 还原 eeprom"></a>8.2 还原 eeprom</h3><p>使用 putty 或 ssh 登陆路由器，然后执行下面命令：</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs gradle">mtd -r <span class="hljs-keyword">write</span> <span class="hljs-regexp">/tmp/m</span>td10.bin factory<br></code></pre></td></tr></table></figure><p>成功后会自动重启。</p><p>然后在<strong>系统</strong>、<strong>备份&#x2F;升级</strong>里重新上传sysupgrade.tar，并且不勾选<strong>保留设置</strong>。</p>]]>
    </content>
    <id>https://lengzzz.com/2017/08/20/%E7%BD%91%E4%BB%B6R6220%E5%88%B7%E6%9C%BALEDE%EF%BC%88openwrt%EF%BC%89%E5%9B%BA%E4%BB%B6%E6%95%99%E7%A8%8B/</id>
    <link href="https://lengzzz.com/2017/08/20/%E7%BD%91%E4%BB%B6R6220%E5%88%B7%E6%9C%BALEDE%EF%BC%88openwrt%EF%BC%89%E5%9B%BA%E4%BB%B6%E6%95%99%E7%A8%8B/"/>
    <published>2017-08-20T07:22:39.000Z</published>
    <summary>
      <![CDATA[<p>家里一直用的是网件的 R6220 路由器，之前一直用的是原厂固件，因为本人不太喜欢在主路由器上装奇奇怪怪的东西，另外是因为 R6220 一直都没有好用的固件可以刷，前几天逛论坛发现竟然已经有人破解了 R6220 可以刷 LEDE 和 pandora 了，忍不住手痒又去刷了一波固件。此文记录一下过程。</p>
<p><a href="/notename/" title="install lede on netgear r6220"></a></p>]]>
    </summary>
    <title>网件 R6220 刷机 LEDE（openwrt）固件教程</title>
    <updated>2017-09-18T08:41:52.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="caddy" scheme="https://lengzzz.com/tags/caddy/"/>
    <category term="http" scheme="https://lengzzz.com/tags/http/"/>
    <category term="运维" scheme="https://lengzzz.com/tags/%E8%BF%90%E7%BB%B4/"/>
    <content>
      <![CDATA[<p>说起 HTTP server，使用最广泛的就是 apache 和 nginx 了，功能都非常强大，但相对而言，学习它们的配置是有一定难度的。最近发现了一个 golang 开发的 HTTP server，叫做 <a href="https://caddyserver.com/" title="Caddy is the HTTP&#x2F;2 web server with automatic HTTPS.">Caddy</a>，它配置起来十分简便，甚至可以 <a href="http://www.bilibili.com/video/av4219585/" title="Caddy - Automatic HTTPS in 28s">28 秒配置好一个支持 http2 的 server </a> ，而且对各种 http 新特性都支持的比较早（比如 http2、quic都有支持）。因此对于不用于生产环境只搭建个人博客是十分友好的，我就简单介绍下 caddy。</p><p><a href="/notename/" title="caddy HTTP&#x2F;2 web server guide for beginners"></a></p><span id="more"></span><p>[toc]</p><h2 id="1-安装"><a href="#1-安装" class="headerlink" title="1. 安装"></a>1. 安装</h2><p>用过 golang 的应该都知道，golang 程序基本上不会有各种依赖，都是光秃秃一个可执行程序，cp 到 <code>/usr/local/bin</code> 就算安装完成了，所以说安装 caddy 是很简单的，我给出三种方法。</p><h3 id="1-1-脚本安装"><a href="#1-1-脚本安装" class="headerlink" title="1.1 脚本安装"></a>1.1 脚本安装</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -s https://getcaddy.com | bash<br></code></pre></td></tr></table></figure><p>caddy 官方给出了一个安装脚本，执行上面的命令就可以一键安装 caddy，等执行结束后，使用 <code>which caddy</code>，可以看到 caddy 已经被安装到了 &#x2F;usr&#x2F;local&#x2F;bin&#x2F;caddy</p><h3 id="1-2-手动安装"><a href="#1-2-手动安装" class="headerlink" title="1.2 手动安装"></a>1.2 手动安装</h3><p><a href="https://caddyserver.com/download">https://caddyserver.com/download</a> 点这个链接进入到 caddy 官网的下载界面，网页左侧可以选择平台和插件，如果在 Linux 服务器上使用的话，platform 选择 Linux 64-bit 就可以了，plugins 如果暂时不需要的话，可以不选。然后点击下面的 DOWNLOAD 按钮，就下载到 caddy 了。同理，解压之后用 cp 命令放到 <code>/usr/local/bin/caddy</code> 就完成了安装。</p><p><img src="/images/f3ac2345c6df862838743d2dd0c02830.png" alt="image_1bn3mbk8jov2es3vu41l842ac9.png-177.7kB"></p><h3 id="1-3-源码安装"><a href="#1-3-源码安装" class="headerlink" title="1.3 源码安装"></a>1.3 源码安装</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go get github.com/mholt/caddy/caddy<br></code></pre></td></tr></table></figure><p>对于安装了 golang 编译器的同学，只需要执行 go get 就能到 $GOPATH&#x2F;bin 里，是否 cp 到 <code>/usr/local/bin</code> 里就看心情了。使用源码安装可以安装到最新版本的 caddy，功能上一般是最新的，而且因为是本地编译，性能可能会稍微高一些，但是可能会存在不稳定的现象。</p><h2 id="2-配置"><a href="#2-配置" class="headerlink" title="2. 配置"></a>2. 配置</h2><h3 id="2-1-临时文件服务器"><a href="#2-1-临时文件服务器" class="headerlink" title="2.1 临时文件服务器"></a>2.1 临时文件服务器</h3><p>Caddy 的配置文件叫做 <code>Caddyfile</code>，Caddy 不强制你把配置文件放到哪个特定文件夹，默认情况下，把 Caddyfile 放到当前目录就可以跑起来了，如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;localhost:8888&#x27;</span> &gt;&gt; Caddyfile<br><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;gzip&#x27;</span> &gt;&gt; Caddyfile<br><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;browse&#x27;</span> &gt;&gt; Caddyfile<br>caddy<br></code></pre></td></tr></table></figure><p>在随便一个目录里执行上面代码，然后在浏览器里打开 <a href="http://localhost:8888/">http://localhost:8888</a> 发现 caddy 已经启动了一个文件服务器。当临时需要一个 fileserver 的时候（比如共享文件），使用 caddy 会很方便。</p><h3 id="2-2-生产环境使用"><a href="#2-2-生产环境使用" class="headerlink" title="2.2 生产环境使用"></a>2.2 生产环境使用</h3><p>当然了，在生产环境使用的时候就不能这么草率的把配置文件放到当前目录了，一般情况下会放到 <code>/etc/caddy</code> 里。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> /etc/caddy<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">touch</span> /etc/caddy/Caddyfile<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chown</span> -R root:www-data /etc/caddy<br></code></pre></td></tr></table></figure><p>除了配置文件，caddy 会自动生成 ssl 证书，需要一个文件夹放置 ssl 证书。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> /etc/ssl/caddy<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chown</span> -R www-data:root /etc/ssl/caddy<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 0770 /etc/ssl/caddy<br></code></pre></td></tr></table></figure><p>因为 ssl 文件夹里会放置私钥，所以权限设置成 770 禁止其他用户访问。</p><p>最后，创建一下放置网站文件的目录，如果已经有了，就不需要创建了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> /var/www<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chown</span> www-data:www-data /var/www<br></code></pre></td></tr></table></figure><p>创建好这些文件和目录了之后，我们需要把 caddy 配置成一个服务，这样就可以开机自动运行，并且管理起来也方便。因为目前大多数发行版都使用 systemd 了，所以这里只讲一下如何配置 systemd，不过 caddy 也支持配置成原始的 sysvinit 服务，具体方法<a href="https://github.com/mholt/caddy/tree/master/dist/init/linux-sysvinit" title="SysVinit conf for Caddy">看这里</a>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> curl -s https://raw.githubusercontent.com/mholt/caddy/master/dist/init/linux-systemd/caddy.service -o /etc/systemd/system/caddy.service   <span class="hljs-comment"># 从 github 下载 systemd 配置文件</span><br><span class="hljs-built_in">sudo</span> systemctl daemon-reload        <span class="hljs-comment"># 重新加载 systemd 配置</span><br><span class="hljs-built_in">sudo</span> systemctl <span class="hljs-built_in">enable</span> caddy.service <span class="hljs-comment"># 设置 caddy 服务自启动</span><br><span class="hljs-built_in">sudo</span> systemctl status caddy.service <span class="hljs-comment"># 查看 caddy 状态</span><br></code></pre></td></tr></table></figure><h2 id="3-Caddyfile"><a href="#3-Caddyfile" class="headerlink" title="3. Caddyfile"></a>3. Caddyfile</h2><p>基本的安装配置搞定之后，最重要的就是如何写 Caddyfile了。可以直接 <code>vim /etc/caddy/Caddyfile</code> 来修改 Caddyfile，也可以再自己电脑上改好然后 rsync 到服务器上。如果修改了 Caddyfile 发现没有生效，是需要执行一下 <code>sudo systemctl restart caddy.service</code> 来重启 caddy 的。</p><h3 id="3-1-Caddyfile-的格式"><a href="#3-1-Caddyfile-的格式" class="headerlink" title="3.1 Caddyfile 的格式"></a>3.1 Caddyfile 的格式</h3><p>Caddfile的格式还是比较简单的，首先第一行必须是网站的地址，例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs Caddyfile">localhost:8080<br></code></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><code class="hljs Caddyfile">lengzzz.com<br></code></pre></td></tr></table></figure><p>地址可以带一个端口号，那么 caddy 只会在这个端口上开启 http 服务，而不会开启 https，如果不写端口号的话，caddy 会默认绑定 80 和 443 端口，同时启动 http 和 https 服务。</p><p>地址后面可以再跟一大堆指令（directive）。Caddyfile 的基本格式就是这样，由一个网站地址和指令组成，是不是很简单。</p><h3 id="3-2-指令"><a href="#3-2-指令" class="headerlink" title="3.2 指令"></a>3.2 指令</h3><p>指令的作用是为网站开启某些功能。指令的格式有三种，先说一下最简单的不带参数的指令比如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs Caddyfile">railgun.moe     # 没错，moe后缀的域名也可以哦<br>gzip<br></code></pre></td></tr></table></figure><p>第二行的 gzip 就是一个指令，它表示打开 gzip 压缩功能，这样网站在传输网页是可以降低流量。</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><code class="hljs Caddyfile">railgun.moe<br>gzip<br>log /var/log/caddy/access.log<br>tls lengz@lengzzz.com<br>root /var/www/<br></code></pre></td></tr></table></figure><p>第三行，log 指令会为网站开启 log 功能，log 指令后的参数告诉 caddy log 文件存放的位置。第四行的 tls 指令告诉 caddy 为网站开启 https 并自动申请证书，后面的 email 参数是告知 CA 申请人的邮箱。（caddy 会默认使用 let’s encrypt 申请证书并续约，很方便吧）</p><p>另外，简单参数也可能不只一个，比如 redir 指令：</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><code class="hljs Caddyfile">railgun.moe<br>gzip<br>log /var/log/caddy/access.log<br>tls /etc/ssl/cert.pem /etc/ssl/key.pem<br>root /var/www/<br>redir / https://lengzzz.com/archive/&#123;uri&#125; 301<br></code></pre></td></tr></table></figure><p>上面的 redir 指令带了三个参数，意思是把所有的请求使用 301 重定向到 <a href="https://lengzzz.com/archive/xxx%EF%BC%8C%E8%BF%99%E4%B8%AA%E6%8C%87%E4%BB%A4%E5%9C%A8%E7%BB%99%E7%BD%91%E7%AB%99%E6%8D%A2%E5%9F%9F%E5%90%8D%E7%9A%84%E6%97%B6%E5%80%99%E5%BE%88%E6%9C%89%E7%94%A8%E3%80%82%E5%8F%A6%E5%A4%96">https://lengzzz.com/archive/xxx，这个指令在给网站换域名的时候很有用。另外</a> tls 指令变了，不单单传 email一个参数， 而是分别传了证书和私钥的路径，这样的话 caddy 就不会去自动申请证书，而是使用路径给出的证书了。</p><p>在这个例子里还使用了 <code>{uri}</code> 这样的占位符（placeholder），详细的列表可以在这里查询到：<a href="https://caddyserver.com/docs/placeholders%E3%80%82">https://caddyserver.com/docs/placeholders。</a></p><p>最后一种指令是带复杂参数的，这种指令包含可能很多参数，所以需要用一对花括号包起来，比如 header 指令：</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></pre></td><td class="code"><pre><code class="hljs Caddyfile">railgun.moe<br>gzip<br>log /var/log/caddy/access.log<br>tls lengz@lengzzz.com<br>root /var/www/<br>header /api &#123;<br>    Access-Control-Allow-Origin  *<br>    Access-Control-Allow-Methods &quot;GET, POST, OPTIONS&quot;<br>    -Server<br>&#125;<br>fastcgi / 127.0.0.1:9000 php &#123;<br>    index index.php<br>&#125;<br>rewrite &#123;<br>    to &#123;path&#125; &#123;path&#125;/ /index.php?&#123;query&#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>6-10 行的 header 指令代表为所有的 <code>/api/xxx</code> 的请求加上 <code>Access-Control-Allow-Origin</code> 和 <code>Access-Control-Allow-Methods</code> 这两个 header，从而能支持 javascript <a href="https://lengzzz.com/note/cross-origin-http-request" title="跨源 HTTP 请求（CORS）">跨域访问</a> ，第 9 行代表删除 Server header，防止别人看到服务器类型。</p><p>11-13 行使用了 fastcgi 指令，代表把请求通过 fastcgi 传给 php，ruby 等后端程序。</p><p>14-15 行，使用了 rewrite 指令，这个指令的作用是 <strong>服务器内部重定向</strong> 在下面的参数 <code>to</code> 后面，又跟了三个参数，这个功能上有点类似 nginx 的 <code>try_files</code> 。告诉 caddy 需要先查看网址根目录 &#x2F;var&#x2F;www 里有没有 {path} 对应的文件，如果没有再查看有没有 {path} 对应的目录，如果都没有，则转发给 index.php 入口文件。这个功能一般会用在 PHP 的 MVC 框架上使用。</p><p>随着一步步完善这个 Caddyfile，目前这个版本的 Caddyfaile 已经可以直接在网站中使用了。</p><h3 id="3-3-多-HOST-网站"><a href="#3-3-多-HOST-网站" class="headerlink" title="3.3 多 HOST 网站"></a>3.3 多 HOST 网站</h3><p>刚才说的一直都是单个域名的网址，那么如果在同一个服务器上部署多个域名的网站呢？很简单，只需要在域名后面跟一个花括号扩起来就可以了，如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs Caddyfile">railgun.moe &#123;<br>    gzip<br>    log /var/log/caddy/railgun_moe.log<br>    tls lengz@lengzzz.com<br>    root /var/www/<br>    header /api &#123;<br>        Access-Control-Allow-Origin  *<br>        Access-Control-Allow-Methods &quot;GET, POST, OPTIONS&quot;<br>        -Server<br>    &#125;<br>    fastcgi / 127.0.0.1:9000 php &#123;<br>        index index.php<br>    &#125;<br>    rewrite &#123;<br>        to &#123;path&#125; &#123;path&#125;/ /index.php?&#123;query&#125;<br>    &#125;<br>&#125;<br>lengzzz.com &#123;<br>tls lengz@lengzzz.com<br>log /var/log/caddy/lengzzz_com.log<br>    redir / https://railgun.moe/&#123;uri&#125; 301<br>&#125;<br></code></pre></td></tr></table></figure><p>好了，基本的 caddy 配置就这些，详细的内容可以去官网上看文档学习。</p><p><a href="https://caddyserver.com/docs">https://caddyserver.com/docs</a></p><p>[EOF]</p>]]>
    </content>
    <id>https://lengzzz.com/2017/08/09/Caddy-%E6%96%B9%E4%BE%BF%E5%A4%9F%E7%94%A8%E7%9A%84HTTPSserver%E6%96%B0%E6%89%8B%E6%95%99%E7%A8%8B/</id>
    <link href="https://lengzzz.com/2017/08/09/Caddy-%E6%96%B9%E4%BE%BF%E5%A4%9F%E7%94%A8%E7%9A%84HTTPSserver%E6%96%B0%E6%89%8B%E6%95%99%E7%A8%8B/"/>
    <published>2017-08-09T11:51:49.000Z</published>
    <summary>
      <![CDATA[<p>说起 HTTP server，使用最广泛的就是 apache 和 nginx 了，功能都非常强大，但相对而言，学习它们的配置是有一定难度的。最近发现了一个 golang 开发的 HTTP server，叫做 <a href="https://caddyserver.com/" title="Caddy is the HTTP&#x2F;2 web server with automatic HTTPS.">Caddy</a>，它配置起来十分简便，甚至可以 <a href="http://www.bilibili.com/video/av4219585/" title="Caddy - Automatic HTTPS in 28s">28 秒配置好一个支持 http2 的 server </a> ，而且对各种 http 新特性都支持的比较早（比如 http2、quic都有支持）。因此对于不用于生产环境只搭建个人博客是十分友好的，我就简单介绍下 caddy。</p>
<p><a href="/notename/" title="caddy HTTP&#x2F;2 web server guide for beginners"></a></p>]]>
    </summary>
    <title>Caddy - 方便够用的 HTTPS server 新手教程</title>
    <updated>2020-04-29T15:46:45.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="lua" scheme="https://lengzzz.com/tags/lua/"/>
    <category term="coroutine" scheme="https://lengzzz.com/tags/coroutine/"/>
    <category term="协程" scheme="https://lengzzz.com/tags/%E5%8D%8F%E7%A8%8B/"/>
    <category term="移植" scheme="https://lengzzz.com/tags/%E7%A7%BB%E6%A4%8D/"/>
    <content>
      <![CDATA[<p>lua 语言最大的卖点之一就是他的协程（coroutine）了。但是在 lua5.1 中有一个文档都没提到的一个坑：协程<strong>只能</strong>在 lua 中使用，当调用 yield 时，如果当前的调用栈上有 c 代码，则会报错 <strong>“attempt to yield across metamethod&#x2F;C-call boundary”</strong>。目前有个第三方的 patch 叫做 <a href="http://coco.luajit.org/" title="Coco — True C Coroutines for Lua">luaCoco</a> 可以让 lua 支持 “真协程”。本文研究了 luaCoco 的内部实现，并把它移植到了 <a href="https://en.wikipedia.org/wiki/Tensilica">xtensa</a> 处理器上。</p><p><a href="/notename/" title="porting luaCoco"></a></p><p><img src="/images/7101caf8fc0075b8d52d5a15ad9f6cb9.png" alt="image_1bhbpomnrk0m58270a54k789.png-16.9kB"></p><span id="more"></span><p>[toc]</p><h2 id="setjmp-的实现"><a href="#setjmp-的实现" class="headerlink" title="setjmp 的实现"></a>setjmp 的实现</h2><p>在文章的开头需要先讲解一下c语言标准库中 <a href="http://www.cplusplus.com/reference/csetjmp/">setjmp</a> 的内部实现，因为之后的 luaCoco 的实现就是对 setjmp 的数据结构的一个 hack。</p><p>先看看 setjmp 的用法</p><figure class="highlight c"><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><code class="hljs c"><span class="hljs-meta">#inlcude &lt;stdio.h&gt;</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdlib.h&gt;</span></span><br><span class="hljs-meta">#inlcude &lt;setjmp.h&gt;</span><br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    jmp_buf buf;<br>    <span class="hljs-type">int</span> code = setjmp(buf);<br>    <span class="hljs-keyword">if</span> (code == <span class="hljs-number">0</span>) &#123;<br>        print(<span class="hljs-string">&quot;before jmp\n&quot;</span>);<br>        longjmp(buf, <span class="hljs-number">1024</span>);<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;jmp here, code: %d\n&quot;</span>, code);<br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>在调用 setjmp 的时候，会返回 0，从而会执行 <code>if (code == 0) {</code> 为 true 的 block，会打印出 <code>before jmp</code>。当执行了 longjmp 之后，程序的执行会重新跳转到 setjmp 那一行（第七行）然而这次 setjmp 的返回值 code 不再是 0，而是 longjmp 的第二个参数（1024），这样就会打印出 <code>jmp here, code: 1024</code>。</p><p>利用 setjmp 的这个功能能实现出很多有趣的东西，比如在c语言中做 <code>Exception</code>，但是 setjmp 是怎么实现的呢？我们去读一读源码。</p><p>libc 的实现有好多种，常见的比如 glibc、uclibc 和 musl-libc，但是我们这次读一读 newlib 的源码。newlib 也是一个 libc 的实现，常用于嵌入式开发中。</p><p>打开 inlcude&#x2F;machine&#x2F;setjmp-dj.h 文件，可以看到 jmp_buf 的定义：</p><figure class="highlight c"><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><code class="hljs c"><span class="hljs-comment">// from inlcude/machine/setjmp-dj.h</span><br><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> &#123;</span><br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> eax;<br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> ebx;<br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> ecx;<br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> edx;<br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> esi;<br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> edi;<br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> ebp;<br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> esp;<br>  <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> eip;<br>&#125; jmp_buf[<span class="hljs-number">1</span>];<br></code></pre></td></tr></table></figure><p>发现这个结构体是用来存储 cpu 的寄存器的。这里很好理解，因为要实现 长跳转（longjmp），必须要首先把跳转的目的地的<strong>现场</strong>先保存下来。</p><p>setjmp 函数做的工作就是保存现场：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><code class="hljs asm">// from machine/i386/setjmp.S<br> /*<br> **jmp_buf:<br> ** eax ebx ecx edx esi edi ebp esp eip<br> ** 0   4   8   12  16  20  24  28  32<br> */<br><br>       #include &quot;i386mach.h&quot;<br><br>        .global SYM (setjmp)<br>        .global SYM (longjmp)<br>       SOTYPE_FUNCTION(setjmp)<br>       SOTYPE_FUNCTION(longjmp)<br> <br>SYM (setjmp):<br><br>pushlebp<br>movlesp,ebp<br><br>pushledi<br>movl8 (ebp),edi<br><br>movleax,0 (edi)<br>movlebx,4 (edi)<br>movlecx,8 (edi)<br>movledx,12 (edi)<br>movlesi,16 (edi)<br><br>movl-4 (ebp),eax<br>movleax,20 (edi)    // edi<br><br>movl0 (ebp),eax<br>movleax,24 (edi)    // ebp<br><br>movlesp,eax<br>addl$12,eax<br>movleax,28 (edi)    // esp<br><br>movl4 (ebp),eax<br>movleax,32 (edi)    // eip (PC)<br><br>popledi<br>movl$0,eax<br>leave<br>ret<br><br></code></pre></td></tr></table></figure><center>![FullSizeRender 2.jpg-1881.4kB][2]<small>setjmp 的栈帧</small></center><p>看图解释一下代码： </p><ul><li>17 行：push ebp 到栈上（esp 同时下移 4 字节）</li><li>18 行：ebp 指向 esp</li><li>20 行：push edi 到栈上（esp 同时再下移 4 字节）</li><li>21 行：8(ebp) 保存的是 jmp_buf 的指针，先把它放到 edi 中</li><li>23 - 27 行：分别把 eax、ebx、ecx、edx、esi 保存到 jmp_buf 里</li><li>29 - 30 行：-4 (ebp) 是edi，如图</li><li>32 - 33 行：(ebp) 就是之前的ebp，如图</li><li>35 - 37 行：保存 esp</li><li>39 - 40 行：如图，保存 <code>return addr</code> 到 jmp_buf-&gt;eip</li></ul><p>longjmp 的实现是正好相反的：</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></pre></td><td class="code"><pre><code class="hljs asm">SYM (longjmp):<br>pushlebp<br>movlesp,ebp<br><br>movl8(ebp),edi/* get jmp_buf */<br>movl12(ebp),eax/* store retval in j-&gt;eax */<br>testleax,eax<br>jne0f<br>incleax<br>0:<br>movleax,0(edi)<br><br>movl24(edi),ebp<br><br>       __CLI<br>movl28(edi),esp<br><br>pushl32(edi)<br><br>movl0(edi),eax<br>movl4(edi),ebx<br>movl8(edi),ecx<br>movl12(edi),edx<br>movl16(edi),esi<br>movl20(edi),edi<br>       __STI<br><br>ret<br><br></code></pre></td></tr></table></figure><p>代码就不赘述了，基本上就是恢复<strong>现场</strong>、把longjmp的第二个参数作为返回值返回（7 - 9 行还有一个判断：如果参数为0的话，会把它改成 1）。</p><h2 id="LuaCoco-的实现"><a href="#LuaCoco-的实现" class="headerlink" title="LuaCoco 的实现"></a>LuaCoco 的实现</h2><p>首先在看代码之前，先简单讲解一下我对 LuaCoco 的理解。lua 的协程其实也就是为每个协程维护了一个 context（所谓 context，既程序执行到某个地方时的状态，包括寄存器、callstack）。当协程之前相互 yield 的时候，切换一下 context。但是 lua 没做好的地方是 lua 仅保存了 lua 程序的 context，而 c 代码的 context 是没有保存的。这是因为 lua 很追求使用 pure c，不希望在源码中加入过多平台相关的东西。保存 lua 的 context 是比较简单的，因为所有 lua 程序相关的数据结构都放在 lua 虚拟机里，只需要每个 coroutine 保存一份就好了，而取得／保存 c 代码的上下文是需要操作平台相关的寄存器的。LuaCoco 就是为每个常见的平台都做了一份保存 c 程序 context 的实现。</p><p>下面看 LuaCoco 的源码。</p><p>LuaCoco 是对 lua 源码的一个 patch，coco 改动了 lua 的以下文件：</p><p><img src="/images/d6fcc3dbe149d5f6cdc4eeb0fba867b1.png" alt="image_1bhcc3die5r3c7tg95ab069b1e.png-22.7kB"></p><p>最主要的文件就是 lcoco.c 和 lcoco.h 了：</p><p>我看源码一般喜欢先看一下 header 文件，瞄一眼 coco 大体上做了什么事情。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// from lcoco.h</span><br><span class="hljs-comment">/* Exported C API to add a C stack to a coroutine. */</span><br>LUA_API lua_State *<span class="hljs-title function_">lua_newcthread</span><span class="hljs-params">(lua_State *L, <span class="hljs-type">int</span> cstacksize)</span>;<br><br><span class="hljs-comment">/* Internal support routines. */</span><br>LUAI_FUNC <span class="hljs-type">void</span> <span class="hljs-title function_">luaCOCO_free</span><span class="hljs-params">(lua_State *L)</span>;<br>LUAI_FUNC <span class="hljs-type">int</span> <span class="hljs-title function_">luaCOCO_resume</span><span class="hljs-params">(lua_State *L, <span class="hljs-type">int</span> nargs)</span>;<br>LUAI_FUNC <span class="hljs-type">int</span> <span class="hljs-title function_">luaCOCO_yield</span><span class="hljs-params">(lua_State *L)</span>;<br>LUAI_FUNC <span class="hljs-type">int</span> <span class="hljs-title function_">luaCOCO_cstacksize</span><span class="hljs-params">(<span class="hljs-type">int</span> cstacksize)</span>;<br></code></pre></td></tr></table></figure><p>lcoco.h 里声明了 coco 定义的 4 个函数。很明显，lua_newcthread 是为了取代原生 lua 中的 lua_newthread 函数的。这一点可以在 lbaselib.c 文件的改动中看到。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// from lbaselib.c</span><br><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">luaB_cocreate</span> <span class="hljs-params">(lua_State *L)</span> &#123;<br><span class="hljs-meta">#<span class="hljs-keyword">ifdef</span> COCO_DISABLE</span><br>  lua_State *NL = lua_newthread(L);<br>  luaL_argcheck(L, lua_isfunction(L, <span class="hljs-number">1</span>) &amp;&amp; !lua_iscfunction(L, <span class="hljs-number">1</span>), <br>    <span class="hljs-number">1</span>, <span class="hljs-string">&quot;Lua function expected&quot;</span>);<br><span class="hljs-meta">#<span class="hljs-keyword">else</span></span><br>  <span class="hljs-type">int</span> cstacksize = luaL_optint(L, <span class="hljs-number">2</span>, <span class="hljs-number">0</span>);<br>  lua_State *NL = lua_newcthread(L, cstacksize);<br>  luaL_argcheck(L, lua_isfunction(L, <span class="hljs-number">1</span>) &amp;&amp;<br>                   (cstacksize &gt;= <span class="hljs-number">0</span> ? <span class="hljs-number">1</span> : !lua_iscfunction(L, <span class="hljs-number">1</span>)),<br>                <span class="hljs-number">1</span>, <span class="hljs-string">&quot;Lua function expected&quot;</span>);<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br>  lua_pushvalue(L, <span class="hljs-number">1</span>);  <span class="hljs-comment">/* move function to top */</span><br>  lua_xmove(L, NL, <span class="hljs-number">1</span>);  <span class="hljs-comment">/* move function from L to NL */</span><br>  <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>这里用宏判断了是否开启 coco，如果开启的话就使用新的 lua_newcthread，否则的话还使用原生的 lua_newthread。</p><p>那么，下一步就着重来看看 lua_newcthread 的实现。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">/* Add a C stack to a coroutine. */</span><br>lua_State *<span class="hljs-title function_">lua_newcthread</span><span class="hljs-params">(lua_State *OL, <span class="hljs-type">int</span> cstacksize)</span><br>&#123;<br>  lua_State *NL = lua_newthread(OL);<br><br>  <span class="hljs-keyword">if</span> (cstacksize &lt; <span class="hljs-number">0</span>)<br>    <span class="hljs-keyword">return</span> NL;<br>  <span class="hljs-keyword">if</span> (cstacksize == <span class="hljs-number">0</span>)<br>    cstacksize = defaultcstacksize;<br>  <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (cstacksize &lt; COCO_MIN_CSTACKSIZE)<br>    cstacksize = COCO_MIN_CSTACKSIZE;<br>  cstacksize &amp;= <span class="hljs-number">-16</span>;<br><br>  COCO_NEW(OL, NL, cstacksize, ((coco_MainFunc)(coco_main)))<br><br>  <span class="hljs-keyword">return</span> NL;<br>&#125;<br></code></pre></td></tr></table></figure><p>先看函数签名，发现比原生的 newthread 多了一个 cstacksize 参数，因为现在需要为 c 程序保存上下文，c 程序的执行需要一个 stack，所以每个 coroutine 都要有一个自己的 stack。这个 cstacksize 参数就是用来控制这个 stack 的大小的。</p><p>继续看代码，几个 if 的意思也很清晰，不表。发现重要的功能都封装到了 <code>COCO_NEW</code> 这个宏里面了。再一翻代码，这个宏又套了好几层宏和宏判断，搞得我很郁闷。所以我就使出了我的编译器大法！</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">gcc -E -DCOCO_USE_SETJMP -D__linux__ -D_I386_JMP_BUF_H -D__i386 lcoco.c &gt; _lcoco.c<br></code></pre></td></tr></table></figure><p>这个命令可以让编译器只执行预处理，说白了就是把 c 语言的宏全部展开。命令中定义的其他几个宏的意思分别是：使用 setjmp 实现协程、使用Linux架构、使用i386架构。</p><p>宏展开的结果如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs c">lua_State* <span class="hljs-title function_">lua_newcthread</span><span class="hljs-params">(lua_State* OL, <span class="hljs-type">int</span> cstacksize)</span> &#123;<br>  <span class="hljs-comment">// ...</span><br>  <span class="hljs-comment">// ... if check</span><br>  <br>  &#123;<br>    <span class="hljs-type">void</span>* ptr = luaM_realloc_(OL, <span class="hljs-literal">NULL</span>, <span class="hljs-number">0</span>, (cstacksize));<br>    coco_State* coco = ((coco_State* )(((<span class="hljs-type">char</span>* ) <span class="hljs-number">0</span>) + ((((<span class="hljs-type">char</span>* )(ptr) - (<span class="hljs-type">char</span>* ) <span class="hljs-number">0</span>) + (cstacksize) - <span class="hljs-keyword">sizeof</span>(coco_State)) &amp; <span class="hljs-number">-16</span>)));<br>    coco-&gt;allocptr = ptr;<br>    coco-&gt;allocsize = cstacksize; &#123;<br>      <span class="hljs-type">size_t</span>* stackptr = &amp; ((<span class="hljs-type">size_t</span>*) coco)[<span class="hljs-number">-1</span>];<br>      _setjmp(coco-&gt;ctx);<br>      coco-&gt;ctx-&gt;__pc = (((coco_MainFunc)(coco_main)));<br>      coco-&gt;ctx-&gt;__sp = (stackptr);<br>      coco-&gt;ctx-&gt;__bp = <span class="hljs-literal">NULL</span>;<br>      stackptr[<span class="hljs-number">1</span> - <span class="hljs-number">1</span>] = <span class="hljs-number">0xdeadc0c0</span>;<br>      coco-&gt;arg0 = (<span class="hljs-type">size_t</span>)(NL);<br>    &#125;(((coco_State**)(NL))[<span class="hljs-number">-1</span>]) = coco;<br>  &#125;<br><br>  <span class="hljs-keyword">return</span> NL;<br>&#125;<br></code></pre></td></tr></table></figure><p>可以看到，主要做的事情就是 alloc 了 coco_State 数据结构和 cstack。这里只申请了一次内存空间，然后通过指针操作分别把内存分成了 coco_State 和 cstack 两块。有点迷糊的可以看一下我画的图：</p><p><img src="/images/54aa11f5b1b9229bb14b4a0b83ea4a0d.jpeg" alt="WechatIMG23.jpeg-99.9kB"></p><p>申请好空间之后，11到16行初始化了 coco_State 的几个字段。</p><p>17行把 coco 的指针放到了 NL 的前面（我并不知道为什么可以这么做，反正就是可以，看的时候我一脸“还有这种操作？”的黑人问号）</p><center>![IMG_0002.JPG-49.3kB][5]</center><p>下面我们去看看 coco_State 的代码。</p><figure class="highlight c"><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><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coco_State</span> &#123;</span><br>  <span class="hljs-type">size_t</span> arg0;<br>  jmp_buf ctx;<br>  jmp_buf back;<br>  <span class="hljs-type">void</span>* allocptr;<br>  <span class="hljs-type">int</span> allocsize;<br>  <span class="hljs-type">int</span> nargs;<br>&#125;;<br></code></pre></td></tr></table></figure><p>经过宏替换之后，coco_State 长上面这样。</p><p>lua_newcthread里的代码初始化了 ctx，allocptr，allocsize，arg0 字段。其中，对 ctx 的初始化操作比较让人在意，首先是调用 setjmp 初始化了 ctx，之后对 ctx 内部的几个字段进行了魔改。</p><figure class="highlight c"><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><code class="hljs c">coco-&gt;ctx-&gt;__pc = (((coco_MainFunc)(coco_main)));<br>coco-&gt;ctx-&gt;__sp = (stackptr);<br>coco-&gt;ctx-&gt;__bp = <span class="hljs-literal">NULL</span>;<br></code></pre></td></tr></table></figure><p>有了之前 setjmp 的基础，这里就容易理解了，一旦对这个 ctx 调用 longjmp 的话，<strong>程序就会跳转到 <code>coco_main</code> 这个函数，并且把 stackptr 当作程序的 stack 来使用。这个 stack 的切换操作，其实就是程序 context 的切换</strong></p><p>我们先不看 <code>coco_main</code> 做了什么，先看看这个 longjmp 会在什么时候调用，动脑子想一想，应该会是再执行 resume 的时候调用 longjmp，果然不出所料：</p><figure class="highlight c"><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><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">luaCOCO_resume</span><span class="hljs-params">(lua_State* L, <span class="hljs-type">int</span> nargs)</span> &#123;<br>  coco_State* coco = (((coco_State**)(L))[<span class="hljs-number">-1</span>]);<br>  coco-&gt;nargs = nargs;<br>  <span class="hljs-keyword">if</span> (!_setjmp(coco-&gt;back)) _longjmp(coco-&gt;ctx, <span class="hljs-number">1</span>);<br><br>  <span class="hljs-keyword">if</span> (L-&gt;status != <span class="hljs-number">1</span>) &#123;<br>    luaM_realloc_(L, ((((coco_State**)(L))[<span class="hljs-number">-1</span>])-&gt;allocptr), ((((coco_State**)(L))[<span class="hljs-number">-1</span>])-&gt;allocsize), <span class="hljs-number">0</span>);<br>    (((coco_State**)(L))[<span class="hljs-number">-1</span>]) = <span class="hljs-literal">NULL</span>;<br>  &#125;<br><br>  <span class="hljs-keyword">return</span> L-&gt;status;<br>&#125;<br></code></pre></td></tr></table></figure><p>在 resume 的时候，首先使用 setjmp 把主线程的状态保存到 <code>coco-&gt;back</code> 里，然后调转到 <code>coco_main</code>。</p><p>下面看 <code>coco_main</code> 做了什么。</p><figure class="highlight c"><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><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">void</span> <span class="hljs-title function_">coco_main</span><span class="hljs-params">(lua_State* L)</span> &#123;<br>  coco_State* coco = (((coco_State**)(L))[<span class="hljs-number">-1</span>]);<br>  <span class="hljs-keyword">for</span> (;;) &#123;<br>    L-&gt;status = luaD_rawrunprotected(L, coco_start, L-&gt;top - (coco-&gt;nargs + <span class="hljs-number">1</span>));<br>    <span class="hljs-keyword">if</span> (L-&gt;status != <span class="hljs-number">0</span>) luaD_seterrorobj(L, L-&gt;status, L-&gt;top);<br>    <span class="hljs-keyword">if</span> (!_setjmp(coco-&gt;ctx)) _longjmp(coco-&gt;back, <span class="hljs-number">1</span>);<br>  &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>main 首先通过L取到了coco的指针（又一次黑人问号）。然后通过 luaD_rawrunprotected 调用了 lua 程序（也就是lua子线程）。之后判断子线程时候出错，设置错误码。最后，保存子线程的状态到 ctx，然后跳转会 back（调转到了luaCOCO_resume 第6行）。</p><p>继续回来看 luaCOCO_resume 函数，第 6 行后面宏展开前其实是这样：</p><figure class="highlight c"><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><code class="hljs c"><span class="hljs-keyword">if</span> (L-&gt;status != LUA_YIELD) &#123;<br>  COCO_FREE(L)<br>&#125;<br></code></pre></td></tr></table></figure><p>展开前的代码比较容易懂：如果子线程执行完了，就 free 掉，没啥可说的。</p><p>最后看看 yield：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">luaCOCO_yield</span><span class="hljs-params">(lua_State* L)</span> &#123;<br>  coco_State* coco = (((coco_State**)(L))[<span class="hljs-number">-1</span>]);<br>  L-&gt;status = <span class="hljs-number">1</span>;<br>  <span class="hljs-keyword">if</span> (!_setjmp(coco-&gt;ctx)) _longjmp(coco-&gt;back, <span class="hljs-number">1</span>);<br>  L-&gt;status = <span class="hljs-number">0</span>; &#123;<br>    StkId base = L-&gt;top - coco-&gt;nargs;<br>    StkId rbase = L-&gt;base;<br>    <span class="hljs-keyword">if</span> (rbase &lt; base) &#123;<br>      <span class="hljs-keyword">while</span> (base &lt; L-&gt;top) &#123;<br>        <span class="hljs-type">const</span> TValue* o2 = (base++);<br>        TValue* o1 = (rbase++);<br>        o1-&gt;value = o2-&gt;value;<br>        o1-&gt;tt = o2-&gt;tt;<br>        ((<span class="hljs-type">void</span>) <span class="hljs-number">0</span>);<br>      &#125;;<br>      L-&gt;top = rbase;<br>    &#125;<br>  &#125;<br>  L-&gt;base = L-&gt;ci-&gt;base;<br>  <span class="hljs-keyword">return</span> coco-&gt;nargs;<br>&#125;<br></code></pre></td></tr></table></figure><p>跳转那里和之前讲的一样，多了的东西没太明白，好像是在复制 lua_State 内部的数据。</p><p>LuaCoco 内部实现的内容基本上就这些了。</p><p>题外话：其实除了魔改 setjmp，LuaCoco还有三种实现，直接内连汇编、使用ucontext和使用fiber，大家可以通过执行 <code>gcc -E lcoco.c &gt; _lcoco.c</code> 加不同的宏定义来生成代码自行研究（其实原理都差不多）。</p><h2 id="xtensa-架构-ABI"><a href="#xtensa-架构-ABI" class="headerlink" title="xtensa 架构 ABI"></a>xtensa 架构 ABI</h2><p>这部分就简单讲一下带过了，毕竟不是那么通用的内容。</p><p>经常做移植的同学应该大多知道，移植的时候最重要的是需要了解这个 architecture 的 ABI（application binary interface）。Google 一番之后发现了<a href="http://0x04.net/~mwk/doc/xtensa.pdf" title="Xtensa® Instruction Set Architecture (ISA)">这个pdf</a>，不过先不急着看 ABI，先看一下这个 CPU 的寄存器吧。</p><p>xtensa 有 16 个 32 位的通用寄存器，名字分别叫 A0 ～ A15，一个 PC 程序计数器，再加上一个 SAR 寄存器（不知道干嘛用的，不过好像移植的话用不上），还算挺简单的设计，没有奇奇怪怪的东西。</p><p>接下来，再看看 xtensa 的 ABI，在 PDF 的 chapter 8 里找到了 关于 Xtensa ABI 的部分。xtensa 使用了两种ABI，一种叫 <code>Windowed Register</code> 另一种叫 <code>CALL0</code> 。NodeMCU 只会用到 CALL0 所以我们简单讲一下 CALL0。</p><p>先看表格：</p><p>| Register        | Use   |<br>|    |   |<br>| a0 | Return Address |<br>| a1 (sp) | Stack Pointer (callee-saved) |<br>| a2 – a7 | Function Arguments |<br>| a8 | Static Chain (see Section 8.1.8) |<br>| a12 – a15 | Callee-saved |<br>| a15 | Stack-Frame Pointer (optional) |</p><p>A0 保存了函数的返回地址，A1保存了栈指针，a2到a7一共6个寄存器用于传递函数的参数，更多的参数会在栈中传递，a12-a15需要被调用者自己保存。</p><p>看完了寄存器的使用，看一下 xtensa 的栈帧（stack frame）格式：</p><p><img src="/images/94e476dfa01186c4655281e79aedcfa1.png" alt="image_1bktgc5jiv8p1bqh1ot5pcf1v0i9.png-37.6kB"></p><p>可以看出来，xtensa 的指针也是向低地址方向增长的。在 SP 的上面会保存依次 <code>6个参数以外的参数</code> 、<code>局部变量</code> 等</p><h2 id="移植-LuaCoco"><a href="#移植-LuaCoco" class="headerlink" title="移植 LuaCoco"></a>移植 LuaCoco</h2><p>搞清楚 ABI 了，最后的工作就是 coding 了。</p><p>遇到的第一个问题是，因为我们要对 setjmp 进行 hack，而不是使用汇编，所以我们仅能操作内存，而不能操作寄存器，所以如何将 lua_State 传递给 coco_main 呢？最常见的方法就是使用 哑参数（dummy args）。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> COCO_MAIN_PARAMint _a, int _b, int _c, int _d, int _e, int _f, lua_State *L</span><br></code></pre></td></tr></table></figure><p>我们把 coco_main 的参数列表定义成这样，前面a到f的参数都是用不上的，这样就不用管 a2 到 a7 这几个寄存器了。所以我们只需要把L指针放到SP指的位置就好了。</p><figure class="highlight dns"><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><code class="hljs dns">#define COCO_PATCHCTX<span class="hljs-punctuation">(</span>coco, buf, func, stack, a0<span class="hljs-punctuation">)</span> \<br>  buf[<span class="hljs-number">0</span>] = <span class="hljs-punctuation">(</span>int<span class="hljs-punctuation">)</span><span class="hljs-punctuation">(</span>func<span class="hljs-punctuation">)</span><span class="hljs-comment">; \</span><br>  buf[<span class="hljs-number">1</span>] = <span class="hljs-punctuation">(</span>int<span class="hljs-punctuation">)</span><span class="hljs-punctuation">(</span>stack<span class="hljs-punctuation">)</span><span class="hljs-comment">; \</span><br>  stack[<span class="hljs-number">0</span>] = <span class="hljs-punctuation">(</span>size_t<span class="hljs-punctuation">)</span><span class="hljs-punctuation">(</span>a0<span class="hljs-punctuation">)</span><span class="hljs-comment">;</span><br></code></pre></td></tr></table></figure><p><code>stack[0] = (size_t)(a0);</code> 这一句中的 a0 就是 lua_State 的指针，把他放到了 SP[0] 这里。</p><p>第二个问题就是对 <code>jmp_buf</code> 进行 hack 了，我们需要搞明白 <code>jmp_buf</code> 里怎么放东西的，这需要看这个平台编译器的源码了。<a href="https://github.com/pfalcon/esp-open-sdk">https://github.com/pfalcon/esp-open-sdk</a> 这里是编译器的源代码，他是使用 crosstool 的，会一边下载源代码，一遍编译，下载好的源代码放在 crosstool-NG&#x2F;.build&#x2F;src&#x2F;newlib-2.0.0 ，正好我们看看 newlib 中 setjmp 的实现：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><code class="hljs asm">#else /* CALL0 ABI */<br><br>.text<br>.align4<br>.literal_position<br>.globalsetjmp<br>.typesetjmp, @function<br>setjmp:<br>s32ia0, a2, 0<br>s32ia1, a2, 4<br>s32ia12, a2, 8<br>s32ia13, a2, 12<br>s32ia14, a2, 16<br>s32ia15, a2, 20<br>movia2, 0<br>ret<br>.sizesetjmp, . - setjmp<br><br>.align4<br>.literal_position<br>.globallongjmp<br>.typelongjmp, @function<br>longjmp:<br>l32ia0, a2, 0<br>l32ia12, a2, 8<br>l32ia13, a2, 12<br>l32ia14, a2, 16<br>l32ia15, a2, 20<br>l32ia1, a2, 4<br>/* Return val ? val : 1.  */<br>movia2, 1<br>movneza2, a3, a3<br><br>ret<br>.sizelongjmp, .-longjmp<br><br>#endif /* CALL0 ABI */<br></code></pre></td></tr></table></figure><p>上面的 else 宏代表这段代码是使用 CALL0 ABI 才会被编译。看一下代码，可以了解到以下对应关系：</p><p>| jmp_buf | Register | Use |<br>|  |  |<br>| jup_buf[0] | a0 | Return Address |<br>| jmp_buf[1] | a1 | Stack Pointer (callee-saved) |<br>| jmp_buf[2] - jmp_buf[5] | a12 - a15 | Callee-saved |</p><p>所以这段代码也就不难理解了：</p><figure class="highlight dns"><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><code class="hljs dns">#define COCO_PATCHCTX<span class="hljs-punctuation">(</span>coco, buf, func, stack, a0<span class="hljs-punctuation">)</span> \<br>  buf[<span class="hljs-number">0</span>] = <span class="hljs-punctuation">(</span>int<span class="hljs-punctuation">)</span><span class="hljs-punctuation">(</span>func<span class="hljs-punctuation">)</span><span class="hljs-comment">; \</span><br>  buf[<span class="hljs-number">1</span>] = <span class="hljs-punctuation">(</span>int<span class="hljs-punctuation">)</span><span class="hljs-punctuation">(</span>stack<span class="hljs-punctuation">)</span><span class="hljs-comment">; \</span><br>  stack[<span class="hljs-number">0</span>] = <span class="hljs-punctuation">(</span>size_t<span class="hljs-punctuation">)</span><span class="hljs-punctuation">(</span>a0<span class="hljs-punctuation">)</span><span class="hljs-comment">;</span><br></code></pre></td></tr></table></figure><p>分别是把 <code>buf[0]</code> 和 <code>buf[1]</code> 改成 coco_main 和 我们为协程新申请的栈。</p><p>其他代码基本上是 copy coco 的，详细的可以看我 github：</p><p><a href="https://github.com/zwh8800/nodemcu-firmware/commit/1f31aa32901f07b6414c0471eb19f7bdd44d93a6">https://github.com/zwh8800/nodemcu-firmware/commit/1f31aa32901f07b6414c0471eb19f7bdd44d93a6</a> </p><p>[EOF] 基本上这次移植涉及到的内容就这些，完。</p>]]>
    </content>
    <id>https://lengzzz.com/2017/05/30/%E7%A7%BB%E6%A4%8DLuaCoco/</id>
    <link href="https://lengzzz.com/2017/05/30/%E7%A7%BB%E6%A4%8DLuaCoco/"/>
    <published>2017-05-30T04:13:43.000Z</published>
    <summary>
      <![CDATA[<p>lua 语言最大的卖点之一就是他的协程（coroutine）了。但是在 lua5.1 中有一个文档都没提到的一个坑：协程<strong>只能</strong>在 lua 中使用，当调用 yield 时，如果当前的调用栈上有 c 代码，则会报错 <strong>“attempt to yield across metamethod&#x2F;C-call boundary”</strong>。目前有个第三方的 patch 叫做 <a href="http://coco.luajit.org/" title="Coco — True C Coroutines for Lua">luaCoco</a> 可以让 lua 支持 “真协程”。本文研究了 luaCoco 的内部实现，并把它移植到了 <a href="https://en.wikipedia.org/wiki/Tensilica">xtensa</a> 处理器上。</p>
<p><a href="/notename/" title="porting luaCoco"></a></p>
<p><img src="/images/7101caf8fc0075b8d52d5a15ad9f6cb9.png" alt="image_1bhbpomnrk0m58270a54k789.png-16.9kB"></p>]]>
    </summary>
    <title>移植 LuaCoco</title>
    <updated>2017-07-13T09:11:08.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="闲扯" scheme="https://lengzzz.com/tags/%E9%97%B2%E6%89%AF/"/>
    <category term="golang" scheme="https://lengzzz.com/tags/golang/"/>
    <content>
      <![CDATA[<p>地址在这里：<a href="https://go.lengzzz.com/">https://go.lengzzz.com</a></p><p><a href="/notename/" title="godoc"></a></p><p><img src="/images/5970731776c90dd6af3c828ca79433c3.png" alt="image_1b68k36vh18er1h5ltlqn3o1b1pm.png-3.5kB"></p><p>点这个按钮哦</p>]]>
    </content>
    <id>https://lengzzz.com/2017/01/12/%E6%90%9E%E4%BA%86%E4%B8%AAgolang%E7%9A%84%E6%96%87%E6%A1%A3%E7%AB%99/</id>
    <link href="https://lengzzz.com/2017/01/12/%E6%90%9E%E4%BA%86%E4%B8%AAgolang%E7%9A%84%E6%96%87%E6%A1%A3%E7%AB%99/"/>
    <published>2017-01-12T05:56:42.000Z</published>
    <summary>
      <![CDATA[<p>地址在这里：<a href="https://go.lengzzz.com/">https://go.lengzzz.com</a></p>
<p><a href="/notename/" title="godoc"></a></p>
<p><img src="/image]]>
    </summary>
    <title>搞了个 golang 的文档站</title>
    <updated>2017-07-20T17:31:18.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="linux" scheme="https://lengzzz.com/tags/linux/"/>
    <category term="折腾" scheme="https://lengzzz.com/tags/%E6%8A%98%E8%85%BE/"/>
    <category term="嵌入式" scheme="https://lengzzz.com/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <content>
      <![CDATA[<p><a href="https://lengzzz.com/note/sync-video-from-yi-camera-to-synology-nas" title="小蚁摄像头实时同步视频到群晖 nas">上一篇 blog</a> 我利用 inotify-tools 和 rsync 两个工具实现了自动同步小蚁摄像机里拍摄的视频。不过今天翻网络又发现了另一种自动同步的解决方案，这个可以利用到群晖 nas 的 Surveillance Station 功能，使用效果更佳一些。这篇博客记录一下这次折腾过程。</p><p><a href="/notename/" title="sync video from yi camera to synology nas 2"></a></p><span id="more"></span><p>Surveillance Station 是群晖上的一个功能套件，可以管理网络摄像机，功能十分强大，而且原生支持很多品牌的网络摄像机，但是不支持小蚁摄像机。不过还好的是 Surveillance Station 支持 rtsp 协议，只要能在小蚁上开启 rtsp 服务就可以了。</p><center width="80%">![QQ20170104-1@2x.png-4220.7kB][1]<small>功能很强的Surveillance Station</small></center><p>这次没有自己编译 rtsp 服务，一是因为没有找到一个好用又轻量的，二是因为刚好找到一个俄罗斯的国际友人做的 <a href="https://github.com/fritz-smh/yi-hack">“小蚁Hack”项目</a> 里面正好有我想要的 rtsp 服务，我就直接拿来用了。</p><p>我们只需要用到他项目里的一个文件，叫做 rtspsvrM 可以在 <a href="https://raw.githubusercontent.com/fritz-smh/yi-hack/master/sd/test/rtspsvrM">https://raw.githubusercontent.com/fritz-smh/yi-hack/master/sd/test/rtspsvrM</a> 下载到。如果小蚁的系统版本比较老，可能需要 rtspsvrK 或者 rtspsvrI。我用的最新的 1.8.6.1 所以使用 M 版本的。具体的规则可以在 <a href="https://github.com/fritz-smh/yi-hack/blob/master/sd/test/equip_test.sh#L216">https://github.com/fritz-smh/yi-hack/blob/master/sd/test/equip_test.sh#L216</a> 这个脚本里找到，我就不赘述了。</p><p>下载好 rtspsvrM 文件后，放到 sd 卡根目录，然后再创建一个服务。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-meta">#!/bin/sh</span><br>/tmp/hd1/rtspsvrM &gt;&gt; /tmp/hd1/rtspsvrM.<span class="hljs-built_in">log</span> 2&gt;&amp;1 &amp;<br></code></pre></td></tr></table></figure><p>然后重启，执行下 <code>netstat -tuanp</code> ，可以看到 rtspsvrM 已经监听 554 端口了。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-comment"># netstat -tuanp</span><br>Active Internet connections (servers and established)<br>Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name<br>tcp        0      0 0.0.0.0:38888           0.0.0.0:*               LISTEN      1266/goolink<br>tcp        0      0 0.0.0.0:8554            0.0.0.0:*               LISTEN      1214/rtspsvrM<br>tcp        0      0 0.0.0.0:554             0.0.0.0:*               LISTEN      1214/rtspsvrM<br>tcp        0      0 0.0.0.0:21              0.0.0.0:*               LISTEN      1197/tcpsvd<br>tcp        0      0 0.0.0.0:18554           0.0.0.0:*               LISTEN      1214/rtspsvrM<br>tcp        0      0 10.0.0.224:49016        120.25.66.121:28622     ESTABLISHED 1266/goolink<br>tcp        0      1 10.0.0.224:53621        10.0.0.1:56688          LAST_ACK    -<br>tcp        0  36200 10.0.0.224:35958        10.0.0.222:873          ESTABLISHED 1837/rsync<br>tcp        0      0 10.0.0.224:554          10.0.0.222:53007        ESTABLISHED 1214/rtspsvrM<br>tcp        0      0 10.0.0.224:42065        42.62.94.185:80         ESTABLISHED 1629/cloud<br>tcp        0      0 :::80                   :::*                    LISTEN      1141/server<br>tcp        0      0 :::23                   :::*                    LISTEN      1204/telnetd<br>tcp        0   1087 ::ffff:10.0.0.224:23    ::ffff:10.0.0.220:46884 ESTABLISHED 1204/telnetd<br>udp        0      0 0.0.0.0:6970            0.0.0.0:*                           1214/rtspsvrM<br>udp        0      0 0.0.0.0:6972            0.0.0.0:*                           1214/rtspsvrM<br>udp        0      0 0.0.0.0:23887           0.0.0.0:*                           2596/p2p_tnp<br>udp        0      0 0.0.0.0:32108           0.0.0.0:*                           2596/p2p_tnp<br>udp        0      0 0.0.0.0:56944           0.0.0.0:*                           1266/goolink<br>udp        0      0 0.0.0.0:56501           0.0.0.0:*                           1266/goolink<br>udp        0      0 0.0.0.0:1500            0.0.0.0:*                           1266/goolink<br></code></pre></td></tr></table></figure><p>说明服务已经起来了。另外这个 rtspsvrM 虽然没有开源，但是他好像没有建立什么乱七八糟的网络连接，姑且认为它不会泄漏用户信息。</p><p>现在回到 nas 的管理界面中，打开 Surveillance Station，点击网络摄像机点新增。</p><p><img src="/images/c5056a2da99646ecea5ee8e10bf43334.png" alt="image_1b5k8dc0o19jf8bt1fe1abrtjr17.png-48.3kB"></p><p>之后把 IP、端口号填写上，品牌选择最上面的用户自定义。最后一个视频原路径很关键，需要填写成 <code>rtsp://10.0.0.224:554/ch0_0.h264</code> 把其中的 IP 地址替换成你摄像机的 IP 就可以了。</p><p>之后，就可以好好享受 Surveillance Station 的强大功能了。</p>]]>
    </content>
    <id>https://lengzzz.com/2017/01/04/%E5%B0%8F%E8%9A%81%E6%91%84%E5%83%8F%E5%A4%B4%E5%AE%9E%E6%97%B6%E5%90%8C%E6%AD%A5%E8%A7%86%E9%A2%91%E5%88%B0%E7%BE%A4%E6%99%96nas%EF%BC%882%EF%BC%89%E2%80%94%E2%80%94%E4%BD%BF%E7%94%A8rtsp%E5%8D%8F%E8%AE%AE%E5%90%8C%E6%AD%A5/</id>
    <link href="https://lengzzz.com/2017/01/04/%E5%B0%8F%E8%9A%81%E6%91%84%E5%83%8F%E5%A4%B4%E5%AE%9E%E6%97%B6%E5%90%8C%E6%AD%A5%E8%A7%86%E9%A2%91%E5%88%B0%E7%BE%A4%E6%99%96nas%EF%BC%882%EF%BC%89%E2%80%94%E2%80%94%E4%BD%BF%E7%94%A8rtsp%E5%8D%8F%E8%AE%AE%E5%90%8C%E6%AD%A5/"/>
    <published>2017-01-04T07:27:55.000Z</published>
    <summary>
      <![CDATA[<p><a href="https://lengzzz.com/note/sync-video-from-yi-camera-to-synology-nas" title="小蚁摄像头实时同步视频到群晖 nas">上一篇 blog</a> 我利用 inotify-tools 和 rsync 两个工具实现了自动同步小蚁摄像机里拍摄的视频。不过今天翻网络又发现了另一种自动同步的解决方案，这个可以利用到群晖 nas 的 Surveillance Station 功能，使用效果更佳一些。这篇博客记录一下这次折腾过程。</p>
<p><a href="/notename/" title="sync video from yi camera to synology nas 2"></a></p>]]>
    </summary>
    <title>小蚁摄像头实时同步视频到群晖 nas（2）—— 使用 rtsp 协议同步</title>
    <updated>2017-01-04T08:13:37.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="linux" scheme="https://lengzzz.com/tags/linux/"/>
    <category term="折腾" scheme="https://lengzzz.com/tags/%E6%8A%98%E8%85%BE/"/>
    <category term="嵌入式" scheme="https://lengzzz.com/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="交叉编译" scheme="https://lengzzz.com/tags/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91/"/>
    <content>
      <![CDATA[<p>之前买了个小蚁智能摄像机，原生只支持向小米路由器里同步视频，我只有一个群晖 nas 做网络存储，所以元旦放假在家研究了下怎么样“破解”小蚁摄像头使它能同步视频到 nas 上。本质上，小蚁摄像头也是一个 Linux 服务器，只不过是运行在 arm 上的嵌入式 Linux，所以 Linux 的整个生态环境都可以利用的上。我这次的解决方案是使用 Linux 上著名的 <a href="https://rsync.samba.org/">rsync</a> 做同步工具，但是必须编译出一个在 arm 上能用使用的 rsync。所以这篇文章的重点是 <strong>交叉编译</strong>。</p><p><a href="/notename/" title="sync video from Yi Camera to Synology nas"></a></p><span id="more"></span><h2 id="拿到小蚁摄像头的-shell"><a href="#拿到小蚁摄像头的-shell" class="headerlink" title="拿到小蚁摄像头的 shell"></a>拿到小蚁摄像头的 shell</h2><p>据说某些系统版本的小蚁摄像头默认没关闭 telnet 服务，那么在做以下事情之前可以先试试你的摄像头是不是已经开启了 telnet 。在终端里运行 <code>telnet xxx.xxx.xxx.xxx</code> xxx.xxx.xxx.xxx 是你摄像头的 ip 地址，可以在路由器管理界面中查到。如果出现 <code>(none) login:</code> 字样，说明你已经拿到了 shell，就不用做下面的操作了，直接跳到<a href="#jumpto-1">这里</a>。</p><ul><li>把小蚁摄像头里的内存卡取出来，在内存卡根目录建立一个文件夹 test</li><li>在 test 目录下创建文件 <code>equip_test.sh</code> 粘贴以下内容。</li></ul><figure class="highlight sh"><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><code class="hljs sh"><span class="hljs-meta">#!/bin/sh</span><br><span class="hljs-comment"># Telnet</span><br><span class="hljs-keyword">if</span> [ ! -f <span class="hljs-string">&quot;/etc/init.d/S88telnet&quot;</span> ]; <span class="hljs-keyword">then</span><br>    <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;#!/bin/sh&quot;</span> &gt; /etc/init.d/S88telnet<br>    <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;telnetd &amp;&quot;</span> &gt;&gt; /etc/init.d/S88telnet<br>    <span class="hljs-built_in">chmod</span> 755 /etc/init.d/S88telnet<br><span class="hljs-keyword">fi</span><br>dr=`<span class="hljs-built_in">dirname</span> <span class="hljs-variable">$0</span>`<br><span class="hljs-comment"># fix bootcycle</span><br><span class="hljs-built_in">mv</span> <span class="hljs-variable">$dr</span>/equip_test.sh <span class="hljs-variable">$dr</span>/equip_test.sh.moved<br>reboot<br></code></pre></td></tr></table></figure><p>根据网上的说法，<code>equip_test.sh</code> 会在开机的时候自动运行。</p><p>这个脚本的内容很简单，第一步创建 <code>/etc/init.d/S88telnet</code> 这个文件，内容如下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-meta">#!/bin/sh</span><br>telnetd &amp;<br></code></pre></td></tr></table></figure><p>这个文件相当于创建了一个 busybox-init 的 <code>服务</code> [^ref3]，和 ubuntu、CentOS 的服务类似，不过功能更简单一些，直接就是一个 shell 脚本。这个 shell 脚本开启了 telnetd 后台程序。</p><p>第二步是把自身重命名并且重启，避免每次摄像头开机重复运行。</p><div id="jumpto-1"></div><p>现在，可以把内存卡查到摄像头中开机，不出意外的话现在再次在终端里输入 <code>telnet xxx.xxx.xxx.xxx</code> 就可以看到 <code>(none) login:</code> 了，现在输入用户名 <code>root</code> 按回车，再输入密码 <code>1234qwer</code> 就可以进入小蚁摄像头的 shell 界面了。</p><h2 id="交叉编译-rsync"><a href="#交叉编译-rsync" class="headerlink" title="交叉编译 rsync"></a>交叉编译 rsync</h2><p>拿到 shell 之后先进去看了看系统信息，没想到这个小蚁摄像头的配置这么寒酸。。。? cpu 是 N 年前的 arm9 ，内存只有 32MiB ，你没看错，就是 32MiB，一点都不夸张，root 文件系统只有 3.5MiB 的空间，毛都放不了，只能想办法把东西放内存卡上。</p><figure class="highlight tcl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><code class="hljs tcl">$ uname -a<br>Linux (none) <span class="hljs-number">3.0</span><span class="hljs-number">.8</span> #<span class="hljs-number">1</span> Wed Apr <span class="hljs-number">30</span> <span class="hljs-number">16</span>:<span class="hljs-number">56</span>:<span class="hljs-number">49</span> CST <span class="hljs-number">2014</span> armv5tejl GNU/Linux<br>$ cat /<span class="hljs-keyword">proc</span>/cpuinfo<span class="hljs-title"></span><br><span class="hljs-title">Processor</span>       :<span class="hljs-title"> ARM926EJ-S</span> rev 5 (v5l)<span class="hljs-title"></span><br><span class="hljs-title">BogoMIPS</span>        : 218.72<span class="hljs-title"></span><br><span class="hljs-title">Features</span>        :<span class="hljs-title"> swp</span> half<span class="hljs-title"> thumb</span> fastmult<span class="hljs-title"> edsp</span> java<span class="hljs-title"></span><br><span class="hljs-title">CPU</span> implementer : 0x41<span class="hljs-title"></span><br><span class="hljs-title">CPU</span> architecture: 5TEJ<span class="hljs-title"></span><br><span class="hljs-title">CPU</span> variant     : 0x0<span class="hljs-title"></span><br><span class="hljs-title">CPU</span> part        : 0x926<span class="hljs-title"></span><br><span class="hljs-title">CPU</span> revision    : 5<span class="hljs-title"></span><br><span class="hljs-title"></span><br><span class="hljs-title">Hardware</span>        :<span class="hljs-title"> hi3518</span><br>Revision        : 0000<span class="hljs-title"></span><br><span class="hljs-title">Serial</span>          : 0000000000000000<br>$<span class="hljs-title"> cat</span> /<span class="hljs-keyword">proc</span>/meminfo<span class="hljs-title"></span><br><span class="hljs-title">MemTotal:</span>          35212<span class="hljs-title"> kB</span><br>MemFree:            1044<span class="hljs-title"> kB</span><br>Buffers:             268<span class="hljs-title"> kB</span><br>Cached:            10016<span class="hljs-title"> kB</span><br>SwapCached:            0<span class="hljs-title"> kB</span><br>Active:            11536<span class="hljs-title"> kB</span><br>Inactive:           5432<span class="hljs-title"> kB</span><br>Active(anon):       6996<span class="hljs-title"> kB</span><br>Inactive(anon):     2944<span class="hljs-title"> kB</span><br>Active(file):       4540<span class="hljs-title"> kB</span><br>Inactive(file):     2488<span class="hljs-title"> kB</span><br>Unevictable:           0<span class="hljs-title"> kB</span><br>Mlocked:               0<span class="hljs-title"> kB</span><br>SwapTotal:             0<span class="hljs-title"> kB</span><br>SwapFree:              0<span class="hljs-title"> kB</span><br>Dirty:               600<span class="hljs-title"> kB</span><br>Writeback:             0<span class="hljs-title"> kB</span><br>AnonPages:          6708<span class="hljs-title"> kB</span><br>Mapped:             5344<span class="hljs-title"> kB</span><br>Shmem:              3256<span class="hljs-title"> kB</span><br>Slab:               8152<span class="hljs-title"> kB</span><br>SReclaimable:       1192<span class="hljs-title"> kB</span><br>SUnreclaim:         6960<span class="hljs-title"> kB</span><br>KernelStack:         808<span class="hljs-title"> kB</span><br>PageTables:          692<span class="hljs-title"> kB</span><br>NFS_Unstable:          0<span class="hljs-title"> kB</span><br>Bounce:                0<span class="hljs-title"> kB</span><br>WritebackTmp:          0<span class="hljs-title"> kB</span><br>CommitLimit:       17604<span class="hljs-title"> kB</span><br>Committed_AS:     312508<span class="hljs-title"> kB</span><br>VmallocTotal:     966656<span class="hljs-title"> kB</span><br>VmallocUsed:       23856<span class="hljs-title"> kB</span><br>VmallocChunk:     935280<span class="hljs-title"> kB</span><br>$<span class="hljs-title"> df</span> -h<span class="hljs-title"></span><br><span class="hljs-title">Filesystem</span> <span class="hljs-title">               Size</span> <span class="hljs-title">     Used</span> Available<span class="hljs-title"> Use%</span> Mounted<span class="hljs-title"> on</span><br>/dev/root                 3.5M      2.7M    844.0K  76% /<span class="hljs-title"></span><br><span class="hljs-title">tmpfs</span>                    17.2M     16.0K     17.2M   0% /dev<br>/dev/mtdblock5            8.9M      8.1M    784.0K  91% /home<span class="hljs-title"></span><br><span class="hljs-title">tmpfs</span>                    32.0M    164.0K     31.8M   1% /tmp<br>/dev/hd1                 14.8G      4.1G     10.8G  27% /tmp/hd1<span class="hljs-title"></span><br><span class="hljs-title">tmpfs</span>                   512.0K     16.0K    496.0K   3% /home/mmap_tmpfs<span class="hljs-title"></span><br><span class="hljs-title">tmpfs</span>                    16.0M     36.0K     16.0M   0% /home/tmpfs<span class="hljs-title"></span><br><span class="hljs-title">tmpfs</span>                    16.0M      3.0M     13.0M  19% /home/jrview<br><br></code></pre></td></tr></table></figure><p>soc 用的好像是屁眼公司的 hi3518，小米在手机行业和华为干架那么厉害，芯片还是得用菊花厂的啊。</p><p>回归正题，我们开始交叉编译 rsync。一开始我用的是 ubuntu 上自带的交叉工具链 <code>gcc-arm-linux-gnueabi</code> 折腾半天编译出来的东西没法用，这才发现这个摄像头用的 libc 是 uClibc， 这种低端错误都犯了，之前自己做嵌入式的时候最常用的就是 uClibc 都忘记了?。</p><p>后来想办法下载一个 <code>arm-linux-uclibc-gcc</code> ubuntu 上好像没有现成的源可以下，Google 一下发现了这个 maillist <a href="http://lists.busybox.net/pipermail/buildroot/2010-January/031634.html%E3%80%82">http://lists.busybox.net/pipermail/buildroot/2010-January/031634.html。</a></p><p>这个帖子讨论的是一个叫 <a href="https://buildroot.org/">buildroot</a> 的东西。我研究了一下这个项目，它竟然可以一键 build 出整个嵌入式系统，包括 host 上运行的交叉工具链、bootloader、kernel、root filesystem甚至是各种软件包，其中就包括 rsync。那我还费心找什么工具链啊，直接用它就好了。</p><p>现在要做的就是找一台 Linux 机器，在上面运行以下命令，编译出 rsync。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash">wget https://buildroot.org/downloads/buildroot-2016.11.1.tar.gz<br>tar xvf buildroot-2016.11.1.tar.gz<br><span class="hljs-built_in">cd</span> buildroot-2016.11.1<br>make menuconfig<br>make<br></code></pre></td></tr></table></figure><p>执行 <code>make menuconfig</code> 之后会出现一个菜单（和编译内核的菜单用的同一个）。</p><ul><li>进入 <code>Target options</code> 再进入 <code>Target Architecture</code> 菜单，选择 <code>ARM (little endian)</code></li><li>进入 <code>Target Architecture Variant</code> 选择 <code>arm926t</code> 其他选项不用动，按两下 esc 退出来</li><li>进入 <code>Toolchain</code> 菜单，<code>C library</code> 选择 <code>uClibc-ng</code></li><li>进入 <code>Kernel Headers</code>，貌似没有 <code>3.0.8</code> 选个最低的 <code>Linux 3.2.x kernel headers</code> 吧</li><li>退出来进入 <code>Target packages</code>，在 <code>Networking applications</code> 里找到 <code>rsync</code> 按空格键打上勾</li><li>退出来进入 <code>Shell and utilities</code> 把 <code>inotify-tools</code> 打上勾，这个我们也要用到</li><li>按 tab 键，使光标移动到 <code>Save</code>，回车存盘退出。</li></ul><p>之后执行 make，经过漫长的等待，终于编译好了。进入到 buildroot-2016.11.1&#x2F;output&#x2F;target 文件夹可以看到整个根目录，在 <code>/usr/bin</code> 可以看到编译好的 rsync 。不过只把这个文件放到摄像头是不行的，因为还有 rsync 的动态链接库 so 文件也得放进去。</p><p>我直接 <code>tar zxcf target.tgz ./target</code> 把根目录打包，放到摄像头内。然后摄像头开机进入 shell，执行 <code>tar xvf target.tgz</code> 解包到内存卡里。然饿。。。现在也不能运行，因为必须把链接库的目录设置好，在 shell 里再执行一下 </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">$PATH</span>:/tmp/hd1/target/bin:/tmp/hd1/target/usr/bin<br><span class="hljs-built_in">export</span> LD_LIBRARY_PATH=<span class="hljs-variable">$LD_LIBRARY_PATH</span>:/tmp/hd1/target/lib:/tmp/hd1/target/usr/lib<br></code></pre></td></tr></table></figure><p>这个是把动态库的搜索路径设置好，具体可以看我<a href="https://lengzzz.com/note/linux-so-search-path">这篇文章</a>，现在，执行 rsync，竟然报错 <code>libz.so.1 not found</code> 。我进入发现只有 <code>libz.so.1.2.8</code> 并没有 <code>libz.so.1</code> 原来是因为内存卡是 fat32，不支持软连接，只好复制一份了?。<code>cp libz.so.1.2.8 libz.so.1</code> 这也是没有办法的事。</p><p>最后，执行一下 rsync，可以看到久违的帮助信息了，真不容易。</p><h2 id="自动同步脚本"><a href="#自动同步脚本" class="headerlink" title="自动同步脚本"></a>自动同步脚本</h2><p>rsync 只能同步一次文件，当文件保持同步之后就会退出，怎么样想个方法能让两端文件实时同步呢？答案是利用 <a href="https://github.com/rvoicilas/inotify-tools">inotify-tool</a>。这个工具利用了内核的通知系统，当文件进行改动之后，就会发出一个通知，此时再调用 rsync 进行同步就可以了。</p><p>所以把它写成了一个脚本[^ref4]：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-meta">#!/bin/sh</span><br><br><span class="hljs-built_in">export</span> LD_LIBRARY_PATH=<span class="hljs-variable">$LD_LIBRARY_PATH</span>:/tmp/hd1/target/lib:/tmp/hd1/target/usr/lib<br><span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">$PATH</span>:/tmp/hd1/target/bin:/tmp/hd1/target/usr/bin<br><span class="hljs-built_in">export</span> RSYNC_PASSWORD=<span class="hljs-string">&quot;xxxxx&quot;</span><br><br>SRC=/tmp/hd1/record/<br>DST=rsync://YiCamera@10.0.0.222/YiCamera/<br>nowtime=$(<span class="hljs-built_in">date</span> +%s)<br>inotifywait -mrq --timefmt <span class="hljs-string">&#x27;%s&#x27;</span> --format <span class="hljs-string">&#x27;%T&#x27;</span> -e modify,delete,create,attrib,move <span class="hljs-variable">$SRC</span> | <span class="hljs-keyword">while</span><br><span class="hljs-built_in">read</span> timestamp<br><span class="hljs-keyword">do</span>  <br>    diffnow=$((<span class="hljs-variable">$nowtime</span> - <span class="hljs-variable">$timestamp</span>))<br>    <span class="hljs-built_in">echo</span> <span class="hljs-variable">$nowtime</span> <span class="hljs-variable">$timestamp</span> <span class="hljs-variable">$diffnow</span><br><br>    <span class="hljs-keyword">if</span> [ <span class="hljs-variable">$diffnow</span> -lt 5 ]<br>    <span class="hljs-keyword">then</span><br>        <span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;start rsync&#x27;</span><br>        rsync -vzrtopg --delete <span class="hljs-variable">$SRC</span> <span class="hljs-variable">$DST</span><br>        nowtime=$(<span class="hljs-built_in">date</span> +%s)<br>    <span class="hljs-keyword">fi</span><br><span class="hljs-keyword">done</span><br><br></code></pre></td></tr></table></figure><p>解释一下这个脚本。前两句不用说了，第三局是导出一个系统变量，rsync 会读取这个变量拿到 rsync 的密码[^ref2]。之后是调用 inotifywait，当他发现文件的修改、删除、创建、修改属性时，会输出到标准输出中。</p><p>标准输出被重定向到管道中，<code>read files</code> 当接不到数据时会 block，当接收到数据之后会向下执行 rsync。</p><p>rsync 的参数 <code>-vzrtopg</code> 里的v是verbose，z是压缩，r是recursive，topg都是保持文件原有属性如属主、时间的参数，–delete参数会把原有getfile目录下的文件删除以保持客户端和服务器端文件系统完全一致<a href="%5BRsync%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%5D(http://wps2015.org/drops/drops/Rsync%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE.html)">^ref1</a>。</p><p>然后，在 <code>/etc/init.d</code> 创建一个服务：</p><figure class="highlight sh"><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><code class="hljs sh"><span class="hljs-built_in">cat</span> /etc/init.d/S90nasync<br><span class="hljs-comment">#!/bin/sh</span><br>/tmp/hd1/nasync.sh &gt;&gt; /tmp/hd1/nasync.log &amp;<br></code></pre></td></tr></table></figure><p>重启摄像头，你会发现，nas 里有自动同步过来的视频了。</p><p>[^ref2]: <a href="http://unix.stackexchange.com/questions/111526/rsync-without-prompt-for-password">rsync without prompt for password</a></p><p>[^ref3]: <a href="http://unix.stackexchange.com/questions/59018/create-and-control-start-up-scripts-in-busybox">Create and control start up scripts in BusyBox</a></p><p>[^ref4]: <a href="http://miaocbin.blog.51cto.com/689091/1662466">Linux-rsync+inotify 文件实时同步</a></p>]]>
    </content>
    <id>https://lengzzz.com/2017/01/02/%E5%B0%8F%E8%9A%81%E6%91%84%E5%83%8F%E6%9C%BA%E5%AE%9E%E6%97%B6%E5%90%8C%E6%AD%A5%E8%A7%86%E9%A2%91%E5%88%B0%E7%BE%A4%E6%99%96nas/</id>
    <link href="https://lengzzz.com/2017/01/02/%E5%B0%8F%E8%9A%81%E6%91%84%E5%83%8F%E6%9C%BA%E5%AE%9E%E6%97%B6%E5%90%8C%E6%AD%A5%E8%A7%86%E9%A2%91%E5%88%B0%E7%BE%A4%E6%99%96nas/"/>
    <published>2017-01-02T14:28:14.000Z</published>
    <summary>
      <![CDATA[<p>之前买了个小蚁智能摄像机，原生只支持向小米路由器里同步视频，我只有一个群晖 nas 做网络存储，所以元旦放假在家研究了下怎么样“破解”小蚁摄像头使它能同步视频到 nas 上。本质上，小蚁摄像头也是一个 Linux 服务器，只不过是运行在 arm 上的嵌入式 Linux，所以 Linux 的整个生态环境都可以利用的上。我这次的解决方案是使用 Linux 上著名的 <a href="https://rsync.samba.org/">rsync</a> 做同步工具，但是必须编译出一个在 arm 上能用使用的 rsync。所以这篇文章的重点是 <strong>交叉编译</strong>。</p>
<p><a href="/notename/" title="sync video from Yi Camera to Synology nas"></a></p>]]>
    </summary>
    <title>小蚁摄像机实时同步视频到群晖 nas</title>
    <updated>2017-01-04T07:33:01.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="linux" scheme="https://lengzzz.com/tags/linux/"/>
    <category term="AirPlay" scheme="https://lengzzz.com/tags/AirPlay/"/>
    <category term="" scheme="https://lengzzz.com/tags/%EF%A3%BF/"/>
    <content>
      <![CDATA[<p>AirPlay 是苹果设备上最方便的播放技术，可以很方便的把音频、视频串流到你的电视或音箱上。现在大部分智能电视都支持 AirPlay 投屏了，但是支持 AirPlay 的音响设备还是比较少见（and 贵）。那么，有没有比较廉价的搭建 AirPlay 音乐播放器的方式呢？那就是今天的主角 shairport-sync。</p><p><a href="/notename/" title="make your linux server a airplay player"></a></p><span id="more"></span><p>shairport 是一个音频 AirPlay receiver 服务器。但是不幸的是 shairport 的作者两年前停止更新了，就有了另一个开发者 fork 了 shairport 做出了 shairport-sync。</p><p>shairport-sync 基于 shairport，在此基础上还改进了音视频的同步的问题，这样使用 shairport-sync 播放视频时不会出现影音不同步的问题了。</p><h2 id="在-ubuntu-16-04-上安装-shairport-sync"><a href="#在-ubuntu-16-04-上安装-shairport-sync" class="headerlink" title="在 ubuntu 16.04 上安装 shairport-sync"></a>在 ubuntu 16.04 上安装 shairport-sync</h2><p>ubuntu 16.04 的软件仓库里已经集成了 shairport-sync，这样只需要执行 <code>apt install</code> 就可以安装了。</p><p>但是 shairport 还需要 avahi-daemon 这个服务，avahi-daemon 是开源的，它实现了苹果的 mDNS 协议（在苹果的设备上对应的服务是 Banjour）。shairport 需要在 avahi 上注册自己。</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><code class="hljs bash"><span class="hljs-built_in">sudo</span> apt install avahi-daemon<br><span class="hljs-built_in">sudo</span> apt install shairport-sync<br></code></pre></td></tr></table></figure><h2 id="配置-shairport-sync"><a href="#配置-shairport-sync" class="headerlink" title="配置 shairport-sync"></a>配置 shairport-sync</h2><p>shairport-sync 的配置非常简单，它的配置文件放在 <code>/etc/shairport-sync.conf</code> ，打开它之后会发现里面有很多配置项，我们只需要简单的配置下 name 就可以了，其他的选项不用动。</p><figure class="highlight abnf"><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><code class="hljs abnf">// General Settings<br><span class="hljs-attribute">general</span> <span class="hljs-operator">=</span><br>&#123;<br>    //...<br>    name <span class="hljs-operator">=</span> <span class="hljs-string">&quot;客厅的服务器&quot;</span><span class="hljs-comment">;</span><br>    //...<br>&#125;<span class="hljs-comment">;</span><br><br></code></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><code class="hljs bash"><span class="hljs-built_in">sudo</span> systemctl restart shairport-sync.service<br></code></pre></td></tr></table></figure><h2 id="不出声音的故障"><a href="#不出声音的故障" class="headerlink" title="不出声音的故障"></a>不出声音的故障</h2><p>安装之后有可能会不出声音，这是因为 shairport 的用户不在 audio 组了，这样的话 shairport 没有音频设备的权限，执行下面语句可以解决。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> usermod -aG audio shairport-sync<br></code></pre></td></tr></table></figure><h2 id="最后来一张效果图"><a href="#最后来一张效果图" class="headerlink" title="最后来一张效果图"></a>最后来一张效果图</h2><p><img src="/images/1ee3cfdd2eb20f717381a3c95775dec8.PNG" alt="IMG_1197.PNG-394.9kB"></p>]]>
    </content>
    <id>https://lengzzz.com/2016/11/18/%E6%8A%8A%E4%BD%A0%E7%9A%84Linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%89%93%E9%80%A0%E6%88%90AirPlay%E9%9F%B3%E4%B9%90%E6%92%AD%E6%94%BE%E5%99%A8/</id>
    <link href="https://lengzzz.com/2016/11/18/%E6%8A%8A%E4%BD%A0%E7%9A%84Linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%89%93%E9%80%A0%E6%88%90AirPlay%E9%9F%B3%E4%B9%90%E6%92%AD%E6%94%BE%E5%99%A8/"/>
    <published>2016-11-18T07:57:43.000Z</published>
    <summary>
      <![CDATA[<p>AirPlay 是苹果设备上最方便的播放技术，可以很方便的把音频、视频串流到你的电视或音箱上。现在大部分智能电视都支持 AirPlay 投屏了，但是支持 AirPlay 的音响设备还是比较少见（and 贵）。那么，有没有比较廉价的搭建 AirPlay 音乐播放器的方式呢？那就是今天的主角 shairport-sync。</p>
<p><a href="/notename/" title="make your linux server a airplay player"></a></p>]]>
    </summary>
    <title>把你的 Linux 服务器打造成 AirPlay 音乐播放器</title>
    <updated>2017-01-02T14:31:29.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="git" scheme="https://lengzzz.com/tags/git/"/>
    <category term="速查" scheme="https://lengzzz.com/tags/%E9%80%9F%E6%9F%A5/"/>
    <content>
      <![CDATA[<p>为女朋友总结了一些常用的 git 操作。这个速查默认：以 master 为主分支，开发新功能创建新分支。</p><p><a href="/notename/" title="girlfriend readable git quick"></a></p><p><img src="/images/56a0d8728d3325d70ca114f659975936.jpg" alt="atlassian-getting-git-right.jpg-94.4kB"></p><span id="more"></span><h2 id="1-开发一个功能"><a href="#1-开发一个功能" class="headerlink" title="1. 开发一个功能"></a>1. 开发一个功能</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><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><code class="hljs bash"><span class="hljs-comment"># 首先确保自己在 master 且代码是最新的</span><br>git checkout master<br>git pull<br><br><span class="hljs-comment"># 在最新的代码上建一个分支</span><br>git checkout -b xxx<br><br><span class="hljs-comment"># 在本地进行开发</span><br><span class="hljs-keyword">do</span> something<br>git commit -m <span class="hljs-string">&quot;zzz&quot;</span><br><br><span class="hljs-keyword">do</span> something<br>git commit -m <span class="hljs-string">&quot;yyy&quot;</span><br><br><span class="hljs-keyword">do</span> something<br>git commit -m <span class="hljs-string">&quot;www&quot;</span><br><br><span class="hljs-comment"># 开发完成，push开发分支</span><br>git push -u origin xxx<br><br><span class="hljs-comment"># 在代码审核工具上创建 pull request</span><br><br><span class="hljs-comment"># 根据别人的审核意见，修改代码</span><br><span class="hljs-keyword">do</span> something<br>git commit -m <span class="hljs-string">&quot;yyy&quot;</span><br><br><span class="hljs-keyword">do</span> something<br>git commit -m <span class="hljs-string">&quot;bbb&quot;</span><br><br><span class="hljs-comment"># 将修改后的代码push</span><br>git push<br><br><span class="hljs-comment"># 审核通过，按按钮合并 pr</span><br><br></code></pre></td></tr></table></figure><h2 id="2-审核过了之后有冲突"><a href="#2-审核过了之后有冲突" class="headerlink" title="2. 审核过了之后有冲突"></a>2. 审核过了之后有冲突</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 首先把 master 上的代码更新一下</span><br>git checkout master<br>git pull<br><br><span class="hljs-comment"># 然后把开发分支的代码 rebase 到最新的代码之上</span><br>git checkout xxx<br>git rebase master<br><br><span class="hljs-comment"># 这时，会出现冲突，打开文件，手动把文件修改正确</span><br><br><span class="hljs-comment"># 然后执行这个：</span><br>git add .<br>git rebase --<span class="hljs-built_in">continue</span><br><br><span class="hljs-comment"># 这时，开发分支 xxx 已经和 master 没有冲突了，push 上去</span><br>git push<br><br><span class="hljs-comment"># 在代码审核工具上，按按钮合并</span><br><br></code></pre></td></tr></table></figure><h2 id="3-开发了一半的功能，不想-commit-也不想丢掉"><a href="#3-开发了一半的功能，不想-commit-也不想丢掉" class="headerlink" title="3. 开发了一半的功能，不想 commit 也不想丢掉"></a>3. 开发了一半的功能，不想 commit 也不想丢掉</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 把修改暂存起来</span><br>git add .<br>git stash<br><br><span class="hljs-comment"># 可以查看刚刚暂存的信息</span><br>git stash list<br><br><span class="hljs-comment"># 现在需要继续开发，把暂存的东西 pop 出来</span><br>git stash pop<br><br><span class="hljs-comment"># 现在再看暂存列表，已经清空了</span><br>git stash list<br><br></code></pre></td></tr></table></figure><h2 id="4-只提交某几个文件，其他几个文件暂存起来"><a href="#4-只提交某几个文件，其他几个文件暂存起来" class="headerlink" title="4. 只提交某几个文件，其他几个文件暂存起来"></a>4. 只提交某几个文件，其他几个文件暂存起来</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 只提交这几个文件</span><br>git add 1.go 2.go 3.go<br>git commit -m <span class="hljs-string">&quot;zzz&quot;</span><br><span class="hljs-comment"># 看一下，剩下的文件确实未提交</span><br>git status<br><span class="hljs-comment"># 暂存</span><br>git add .<br>git stash<br><br></code></pre></td></tr></table></figure><h2 id="5-只提交某几行，其余行暂存"><a href="#5-只提交某几行，其余行暂存" class="headerlink" title="5. 只提交某几行，其余行暂存"></a>5. 只提交某几行，其余行暂存</h2><p>打开 sourcetree </p><p><img src="/images/40fd3596ffd1775197277b3916aa77ed.png" alt="image_1aps6uir211qg14423s41qjq85g9.png-47.5kB"></p><p>选中像提交的行，点按钮暂存行</p><p>然后：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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><code class="hljs bash"><span class="hljs-comment"># 只提交某几行</span><br>git commit -m <span class="hljs-string">&quot;zzz&quot;</span><br><span class="hljs-comment"># 看一下，剩下的行确实没提交</span><br>git status<br><span class="hljs-comment"># 暂存</span><br>git add .<br>git stash<br><br></code></pre></td></tr></table></figure><blockquote><p>已暂存的修改，叫做 <code>stashed changes</code> </p></blockquote><h2 id="6-已经-git-add-的文件，想变回未-add-的状态"><a href="#6-已经-git-add-的文件，想变回未-add-的状态" class="headerlink" title="6. 已经 git add 的文件，想变回未 add 的状态"></a>6. 已经 git add 的文件，想变回未 add 的状态</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">git reset HEAD 1.go<br><br><span class="hljs-comment"># 如果想把所有文件都未 add 的状态</span><br>git reset HEAD .<br></code></pre></td></tr></table></figure><blockquote><p>已经被 add 的修改，叫做 <code>staged changes</code>；未 add 的叫做 <code>unstaged changes</code> </p></blockquote><h2 id="7-已经修改，但未-add-的文件，想变回未修改的状态"><a href="#7-已经修改，但未-add-的文件，想变回未修改的状态" class="headerlink" title="7. 已经修改，但未 add 的文件，想变回未修改的状态"></a>7. 已经修改，但未 add 的文件，想变回未修改的状态</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">git checkout -- 1.go<br><br><span class="hljs-comment"># 如果想把所有文件都变回未修改的状态</span><br>git checkout -- .<br></code></pre></td></tr></table></figure><h2 id="8-已经-commit-了，但是不想要了，想回到上一个-commit-重新写"><a href="#8-已经-commit-了，但是不想要了，想回到上一个-commit-重新写" class="headerlink" title="8. 已经 commit 了，但是不想要了，想回到上一个 commit 重新写"></a>8. 已经 commit 了，但是不想要了，想回到上一个 commit 重新写</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 回到上一个 commit，把这个 commit 的修改变成 unstaged changes</span><br>git reset HEAD^<br><span class="hljs-comment"># 把 unstaged changes 变回未修改的状态</span><br>git checkout -- .<br><br><span class="hljs-comment"># 重新写</span><br></code></pre></td></tr></table></figure><h2 id="9-已经-commit，并且-push-了，但是不想要了，想回到上一个-commit-重新写"><a href="#9-已经-commit，并且-push-了，但是不想要了，想回到上一个-commit-重新写" class="headerlink" title="9. 已经 commit，并且 push 了，但是不想要了，想回到上一个 commit 重新写"></a>9. 已经 commit，并且 push 了，但是不想要了，想回到上一个 commit 重新写</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 同上，但是最后再push的时候需要加 -f</span><br><br>git push -f<br><br></code></pre></td></tr></table></figure><h2 id="9-上个方法太暴力了"><a href="#9-上个方法太暴力了" class="headerlink" title="9. 上个方法太暴力了"></a>9. 上个方法太暴力了</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 创建一个和上个提交完全相反的提交</span><br>git revert HEAD<br>git push<br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://lengzzz.com/2016/08/11/%E5%A5%B3%E6%9C%8B%E5%8F%8B%E9%83%BD%E8%83%BD%E7%9C%8B%E6%87%82%E7%9A%84git%E9%80%9F%E6%9F%A5/</id>
    <link href="https://lengzzz.com/2016/08/11/%E5%A5%B3%E6%9C%8B%E5%8F%8B%E9%83%BD%E8%83%BD%E7%9C%8B%E6%87%82%E7%9A%84git%E9%80%9F%E6%9F%A5/"/>
    <published>2016-08-11T06:19:56.000Z</published>
    <summary>
      <![CDATA[<p>为女朋友总结了一些常用的 git 操作。这个速查默认：以 master 为主分支，开发新功能创建新分支。</p>
<p><a href="/notename/" title="girlfriend readable git quick"></a></p>
<p><img src="/images/56a0d8728d3325d70ca114f659975936.jpg" alt="atlassian-getting-git-right.jpg-94.4kB"></p>]]>
    </summary>
    <title>女朋友都能看懂的 git 速查</title>
    <updated>2017-06-27T08:41:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="标准库" scheme="https://lengzzz.com/tags/%E6%A0%87%E5%87%86%E5%BA%93/"/>
    <category term="golang" scheme="https://lengzzz.com/tags/golang/"/>
    <category term="stream" scheme="https://lengzzz.com/tags/stream/"/>
    <category term="io" scheme="https://lengzzz.com/tags/io/"/>
    <content>
      <![CDATA[<p>本文对 golang 标准库中的 stream 进行了一些总结。</p><p><a href="/notename/" title="golang stream"></a></p><h2 id="Interfaces"><a href="#Interfaces" class="headerlink" title="Interfaces"></a>Interfaces</h2><p>在 golang 中，通过几个基本的 interface 对流操作进行了抽象。</p><h3 id="读写"><a href="#读写" class="headerlink" title="读写"></a>读写</h3><p>首先是最基本的Reader、Writer，定义了对于一个流来说最基本的操作：<strong>读、写</strong>。这两个 interface 定义在 <code>io</code> 包里。</p><figure class="highlight golang"><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><code class="hljs golang"><span class="hljs-keyword">type</span> Reader <span class="hljs-keyword">interface</span> &#123;<br>    Read(p []<span class="hljs-type">byte</span>) (n <span class="hljs-type">int</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><br><span class="hljs-keyword">type</span> Writer <span class="hljs-keyword">interface</span> &#123;<br>    Write(p []<span class="hljs-type">byte</span>) (n <span class="hljs-type">int</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><br></code></pre></td></tr></table></figure><span id="more"></span><h3 id="Seeker、ReaderAt、WriterAt、Closer"><a href="#Seeker、ReaderAt、WriterAt、Closer" class="headerlink" title="Seeker、ReaderAt、WriterAt、Closer"></a>Seeker、ReaderAt、WriterAt、Closer</h3><p>更进一步的，最常见的流就是文件了。对于文件来说，除了简单的读写操作之外，还有 <strong>Seek、ReadAt、WriteAt、Close</strong> 操作。标准库对这些操作也进行了抽象。</p><figure class="highlight golang"><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><code class="hljs golang"><span class="hljs-keyword">type</span> Seeker <span class="hljs-keyword">interface</span> &#123;<br>    Seek(offset <span class="hljs-type">int64</span>, whence <span class="hljs-type">int</span>) (<span class="hljs-type">int64</span>, <span class="hljs-type">error</span>)<br>&#125;<br><br><span class="hljs-keyword">type</span> ReaderAt <span class="hljs-keyword">interface</span> &#123;<br>    ReadAt(p []<span class="hljs-type">byte</span>, off <span class="hljs-type">int64</span>) (n <span class="hljs-type">int</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><br><span class="hljs-keyword">type</span> WriterAt <span class="hljs-keyword">interface</span> &#123;<br>    WriteAt(p []<span class="hljs-type">byte</span>, off <span class="hljs-type">int64</span>) (n <span class="hljs-type">int</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><br><span class="hljs-keyword">type</span> Closer <span class="hljs-keyword">interface</span> &#123;<br>    Close() <span class="hljs-type">error</span><br>&#125;<br></code></pre></td></tr></table></figure><h3 id="组合"><a href="#组合" class="headerlink" title="组合"></a>组合</h3><p>有了这些基础设施之后，就可以使用 golang 的<strong>组合</strong>大法了：</p><figure class="highlight golang"><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></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-keyword">type</span> ReadCloser <span class="hljs-keyword">interface</span> &#123;<br>    Reader<br>    Closer<br>&#125;<br><span class="hljs-keyword">type</span> ReadSeeker <span class="hljs-keyword">interface</span> &#123;<br>    Reader<br>    Seeker<br>&#125;<br><br><span class="hljs-keyword">type</span> WriteCloser <span class="hljs-keyword">interface</span> &#123;<br>    Writer<br>    Closer<br>&#125;<br><span class="hljs-keyword">type</span> WriteSeeker <span class="hljs-keyword">interface</span> &#123;<br>    Writer<br>    Seeker<br>&#125;<br><br><span class="hljs-keyword">type</span> ReadWriter <span class="hljs-keyword">interface</span> &#123;<br>    Reader<br>    Writer<br>&#125;<br><span class="hljs-keyword">type</span> ReadWriteCloser <span class="hljs-keyword">interface</span> &#123;<br>    Reader<br>    Writer<br>    Closer<br>&#125;<br><span class="hljs-keyword">type</span> ReadWriteSeeker <span class="hljs-keyword">interface</span> &#123;<br>    Reader<br>    Writer<br>    Seeker<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="杂项"><a href="#杂项" class="headerlink" title="杂项"></a>杂项</h3><p>其他还有一些不很常用的操作。</p><p><strong>写到一个Writer中</strong>、<strong>从一个Reader中读取</strong>。这两个操作会自动判断EOF，如果没有把所有数据写完／读完，就会继续写／读。</p><figure class="highlight golang"><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><code class="hljs golang"><span class="hljs-keyword">type</span> WriterTo <span class="hljs-keyword">interface</span> &#123;<br>    WriteTo(w Writer) (n <span class="hljs-type">int64</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><span class="hljs-keyword">type</span> ReaderFrom <span class="hljs-keyword">interface</span> &#123;<br>    ReadFrom(r Reader) (n <span class="hljs-type">int64</span>, err <span class="hljs-type">error</span>)<br>&#125;<br></code></pre></td></tr></table></figure><p>还有一些面向 <code>byte</code> 和 <code>rune</code> 的读写操作：</p><figure class="highlight golang"><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><code class="hljs golang"><span class="hljs-keyword">type</span> ByteReader <span class="hljs-keyword">interface</span> &#123;<br>    ReadByte() (c <span class="hljs-type">byte</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><span class="hljs-keyword">type</span> ByteScanner <span class="hljs-keyword">interface</span> &#123;<br>    ByteReader<br>    UnreadByte() <span class="hljs-type">error</span><br>&#125;<br><span class="hljs-keyword">type</span> ByteWriter <span class="hljs-keyword">interface</span> &#123;<br>    WriteByte(c <span class="hljs-type">byte</span>) <span class="hljs-type">error</span><br>&#125;<br><br><span class="hljs-keyword">type</span> RuneReader <span class="hljs-keyword">interface</span> &#123;<br>    ReadRune() (r <span class="hljs-type">rune</span>, size <span class="hljs-type">int</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><span class="hljs-keyword">type</span> RuneScanner <span class="hljs-keyword">interface</span> &#123;<br>    RuneReader<br>    UnreadRune() <span class="hljs-type">error</span><br>&#125;<br></code></pre></td></tr></table></figure><p><code>Scanner</code> 允许把一个读出的字节重新放回流中。这个操作有点类似 Peek 但是比 Peek 别扭一些。这种操作在做词法分析器的时候很有用。</p><p>下面是一些这些 interface 的实现。</p><h2 id="文件"><a href="#文件" class="headerlink" title="文件"></a>文件</h2><p>使用 <code>os.Open</code>、<code>os.OpenFile</code> 可以打开一个文件进行读写。它返回一个 <code>*os.File</code> 结构体，这个结构体实现了上面除了杂项外的接口。</p><h2 id="管道"><a href="#管道" class="headerlink" title="管道"></a>管道</h2><p>使用 <code>os.Pipe</code> 可以创建一个操作系统提供的管道（参见 unix 管道）。这个函数也是返回一个 <code>*os.File</code> 结构体。</p><h2 id="网络"><a href="#网络" class="headerlink" title="网络"></a>网络</h2><p><code>net.Conn</code> 是个 interface，他也实现了 <code>io.Reader</code>、<code>io.Writer</code>、<code>io.Closer</code> 这三个接口。</p><h2 id="内存流"><a href="#内存流" class="headerlink" title="内存流"></a>内存流</h2><p>有时候我们需要把一段内存当作流来处理，我们把这种设施叫做内存流。内存流在某些情况下非常有用。</p><h3 id="不阻塞的内存流"><a href="#不阻塞的内存流" class="headerlink" title="不阻塞的内存流"></a>不阻塞的内存流</h3><p>在 <code>strings</code> 包中，<code>strings.Reader</code> 实现了 <code>io.Reader</code> 、 <code>io.Seeker</code>、<code>io.ReaderAt</code>、<code>io.WriterTo</code>、<code>io.ByteScanner</code>、<code>io.RuneScanner</code> 这些接口。</p><p>可以将一个字符串当作一个<strong>只读流</strong>来使用。</p><p><code>bytes</code> 包中提供了一个比 <code>strings.Reader</code> 更高级的内存流－－ <code>bytes.Buffer</code>。它支持<strong>读写</strong>操作，同时还可以讲写入的数据转换成字符串来使用。这个结构体一般会被当做 golang 中的 StringBuilder 使用。</p><p>另外，如果需要将 []byte 转换为只读流，可以使用 <code>bytes.Reader</code> 它和 <code>strings.Reader</code> 类似。当数据只需要进行读操作时，使用这两个 Reader 会比 Buffer 要高效一些。</p><p>这些内存流都是非阻塞的，如果内存中没有数据了，会立即返回一个 EOF 错误。</p><h3 id="阻塞的内存流"><a href="#阻塞的内存流" class="headerlink" title="阻塞的内存流"></a>阻塞的内存流</h3><p>有时我们需要一个可以阻塞的内存流。当 buffer 中无数据的时候，Read 操作会被阻塞住；当 buffer 满时，Write 操作也会阻塞。</p><p><code>io.Pipe</code> 提供了这个功能。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Pipe</span><span class="hljs-params">()</span></span> (*PipeReader, *PipeWriter)<br></code></pre></td></tr></table></figure><p>使用 <code>io.Pipe</code> 函数创建一对 pipe，对 PipeReader 进行读操作，对<br>PipeWriter 进行写操作。</p><h2 id="杂七杂八的功能"><a href="#杂七杂八的功能" class="headerlink" title="杂七杂八的功能"></a>杂七杂八的功能</h2><p><code>io.LimitReader</code> 函数可以限制一个 Reader 的读取字节数</p><p><code>io.TeeReader</code> 可以在你读一个 Reader 的同时，将数据写入到一个 Writer 中：</p><figure class="highlight golang"><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><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">teeExample</span><span class="hljs-params">(input io.Reader)</span></span> &#123;<br>    backup := os.Create(<span class="hljs-string">&quot;xxx.log&quot;</span>)<br>    r := io.TeeReader(input, backup)<br>    fmt.Println(io.ReadAll(r))<br>&#125;<br></code></pre></td></tr></table></figure><p>这个例子可以将 input 的内容同时写到 console 和 xxx.log 文件中。</p><p><code>io.MultiReader</code>、<code>io.MultiWriter</code> 函数可以将多个 Reader 或 Writer 合并成一个 Reader 或 Writer。</p>]]>
    </content>
    <id>https://lengzzz.com/2016/07/18/%E6%80%BB%E7%BB%93golang%E5%AF%B9%E4%BA%8Estream%E7%9A%84%E6%8A%BD%E8%B1%A1/</id>
    <link href="https://lengzzz.com/2016/07/18/%E6%80%BB%E7%BB%93golang%E5%AF%B9%E4%BA%8Estream%E7%9A%84%E6%8A%BD%E8%B1%A1/"/>
    <published>2016-07-18T07:23:09.000Z</published>
    <summary>
      <![CDATA[<p>本文对 golang 标准库中的 stream 进行了一些总结。</p>
<p><a href="/notename/" title="golang stream"></a></p>
<h2 id="Interfaces"><a href="#Interfaces" class="headerlink" title="Interfaces"></a>Interfaces</h2><p>在 golang 中，通过几个基本的 interface 对流操作进行了抽象。</p>
<h3 id="读写"><a href="#读写" class="headerlink" title="读写"></a>读写</h3><p>首先是最基本的Reader、Writer，定义了对于一个流来说最基本的操作：<strong>读、写</strong>。这两个 interface 定义在 <code>io</code> 包里。</p>
<figure class="highlight golang"><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><code class="hljs golang"><span class="hljs-keyword">type</span> Reader <span class="hljs-keyword">interface</span> &#123;<br>    Read(p []<span class="hljs-type">byte</span>) (n <span class="hljs-type">int</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><br><span class="hljs-keyword">type</span> Writer <span class="hljs-keyword">interface</span> &#123;<br>    Write(p []<span class="hljs-type">byte</span>) (n <span class="hljs-type">int</span>, err <span class="hljs-type">error</span>)<br>&#125;<br><br></code></pre></td></tr></table></figure>]]>
    </summary>
    <title>总结 golang 对于 stream 的抽象</title>
    <updated>2016-08-10T06:24:10.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="并发" scheme="https://lengzzz.com/tags/%E5%B9%B6%E5%8F%91/"/>
    <category term="思考" scheme="https://lengzzz.com/tags/%E6%80%9D%E8%80%83/"/>
    <content>
      <![CDATA[<p>最近在项目里做了一个数据同步的功能。需要把两个数据库里的数据做同步，在业务不繁忙的时候跑一次，核对库里每一条数据，主要是为了保证<strong>数据最终一致</strong>，做的一个保底工作。之前项目里同步的代码是单线程的，我改成了基于线程池的，但是在上线前的一些问题却引起了我的思考。</p><p><a href="/notename/" title="concurrency roadblock how many threads should be in pool"></a></p><h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>程序的代码还是比较简单的，大概类似下面：</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-comment">// 两个参数分别为线程数量和总任务数</span><br>p := pool.NewPool(goroutineCount, <span class="hljs-built_in">len</span>(userIds))<br><span class="hljs-keyword">for</span> _, userId := <span class="hljs-keyword">range</span> userIds &#123;<br>p.Queue(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(job *pool.Job)</span></span> &#123;<br>userId := job.Params()[<span class="hljs-number">0</span>].(<span class="hljs-type">int64</span>)<br><br>tx, err := sess.Begin()<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-built_in">panic</span>(err)<br>&#125;<br><span class="hljs-keyword">defer</span> tx.RollbackUnlessCommitted()<br><span class="hljs-keyword">if</span> err := dao.SyncSingleUser(tx, userId); err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-built_in">panic</span>(err)<br>&#125;<br><br><span class="hljs-keyword">if</span> err := tx.Commit(); err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-built_in">panic</span>(err)<br>&#125;<br>&#125;, userId)<br>&#125;<br>errOccurs := <span class="hljs-literal">false</span><br><span class="hljs-keyword">for</span> result := <span class="hljs-keyword">range</span> p.Results() &#123;<br><span class="hljs-keyword">if</span> err, ok := result.(*pool.ErrRecovery); ok &#123;<br>glog.Errorln(err)<br>errOccurs = <span class="hljs-literal">true</span><br>&#125;<br>&#125;<br><span class="hljs-keyword">if</span> errOccurs &#123;<br><span class="hljs-keyword">return</span><br>&#125;<br></code></pre></td></tr></table></figure><p>为了确定线程的数量，我逐步增加线程数记录系统各项数值。下面是在一台 RMBP 上做的测试。</p><p>| T | QPS | CPU | DBCPU |<br>|  |  |  |  | |<br>| 1 | 9000 | 37 | 81 |<br>| 2 | 14000 | 60 | 150 |<br>| 3 | 15500 | 70 | 195 |<br>| 4 | 16500 | 90 | 260 |</p><p>可以发现随着线程数增加，性能会逐步提高，但是当线程数提高到一定程度之后，性能不增反降（这部分数据没放）。如果继续增加线程数进行测试，会发现系统的某些性能指标会被耗尽，程序开始报错。</p><figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs angelscript">recovering <span class="hljs-keyword">from</span> panic: dial tcp <span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span>:<span class="hljs-number">3306</span>: getsockopt: operation timed <span class="hljs-keyword">out</span><br></code></pre></td></tr></table></figure><p>为什么会出现这种情况呢？在讨论这些问题之前，先回顾一下 goroutine 的设计与实现。</p><h2 id="goroutine-设计"><a href="#goroutine-设计" class="headerlink" title="goroutine 设计"></a>goroutine 设计</h2><p>事实上，在 golang 中的 goroutine 已经不是传统意义上的线程了。传统的操作系统提供的线程在使用时有一些限制，超过一定数量之后会对性能造成影响。而 golang 提供的 goroutine 是一种 green thread。</p><p>Green thread 和 NodeJS 的异步回调方案有类似的地方，都在底层调用的系统的异步 IO 系统调用。</p><ul><li>异步回调方案：所有 IO 操作的 API 都设计成异步回调形式，底层调用异步的系统调用（如epool、kqueue等）。同时，也可能提供同步版本的 IO 操作（如fs.readFileSync）</li><li>Green thread 方案：所有 IO 操作底层实际上事异步调用，但是在语言中却表现的像一个同步调用。当 IO 操作需要等待时，语言的 runtime 自动调度系统级线程到另一个 Green thread 中。写代码的时候感觉好像是同步的，仿佛在同一个线程完成的，但实际上系统可能切换了线程，但程序对此无感。</li></ul><p>由此可见，Green thread 在语言设计上比异步回调方案要略胜一筹，不会出现“冲击波”的现象（具体在 NodeJS 中如何避免“冲击波”代码可以看我<a href="https://lengzzz.com/note/in-depth-analysis-of-await-and-async">这篇文章<small>await &amp; async 深度剖析</small></a>），但在性能上实际是差不多的，都避免了大量使用操作系统级的线程带来的性能问题，同时又能充分的利用 CPU。</p><p>但是，今天我想谈的问题并不是 CPU 利用率／线程数量的问题，这个问题已经被上述两种设计方案比较完美的解决了。</p><h2 id="CPU-以外的资源瓶颈"><a href="#CPU-以外的资源瓶颈" class="headerlink" title="CPU 以外的资源瓶颈"></a>CPU 以外的资源瓶颈</h2><p>在实际中，更多遇到的是 cpu ／线程数量之外的资源瓶颈。比如锁、数据库链接、tcp链接。举个例子，假如做个爬虫应用，每个 goroutine 爬一个网页，golang 虽然号称百万级别的 goroutine ，但是你每个 goroutine 里面创建一个 tcp 链接，不到 5 万个 goroutine 就会把系统的 tcp 资源耗尽。</p><p>回到文章最上面的情况，在 goroutine 里调用了 <code>dao.SyncSingleUser</code> 函数，这个是一个数据库操作，不可避免的会有 socket 操作、硬盘操作。如果将所有数据库操作都无脑的放到 goroutine 中执行，当资源出现瓶颈之后，大量 goroutine 会阻塞或报错。</p><p>因此，我在项目里使用了一个 goroutine 池，来确保不会过多使用系统资源导致崩溃。那么问题来了，<strong>池里究竟该放多少线程</strong>？</p><p>又回到了最初使用系统线程时遇到的问题，不过这次导致问题的不再是线程资源，而是其他资源瓶颈（如 tcp、数据库链接 等）。这些资源比线程资源更加复杂，更加难以把控。更加难做 benchmark，也就更难找出一个通用的方法来解决实际场景的问题。</p><h2 id="没有银弹"><a href="#没有银弹" class="headerlink" title="没有银弹"></a>没有银弹</h2><p>思考过后，我在网上开始搜索解决方案。发现这篇文章的作者和我做了类似的思考：<a href="http://jolestar.com/parallel-programming-model-thread-goroutine-actor/">《并发之痛 Thread，Goroutine，Actor》</a>。作者最后得出 Actor 模型能解决一些问题（不过我才疏学浅至今不怎么理解 Actor 模型），但并发带来的问题还远远没到解决的程度。</p><blockquote><p><em>革命尚未成功 同志任需努力</em></p></blockquote><p>所以说，<strong>软件工程没有银弹</strong>，路漫漫其修远兮，吾将上下而求索。</p>]]>
    </content>
    <id>https://lengzzz.com/2016/07/08/%E5%B9%B6%E5%8F%91%E9%9A%BE%7C%E6%B1%A0%E9%87%8C%E7%A9%B6%E7%AB%9F%E8%AF%A5%E6%94%BE%E5%A4%9A%E5%B0%91%E7%BA%BF%E7%A8%8B%EF%BC%9F/</id>
    <link href="https://lengzzz.com/2016/07/08/%E5%B9%B6%E5%8F%91%E9%9A%BE%7C%E6%B1%A0%E9%87%8C%E7%A9%B6%E7%AB%9F%E8%AF%A5%E6%94%BE%E5%A4%9A%E5%B0%91%E7%BA%BF%E7%A8%8B%EF%BC%9F/"/>
    <published>2016-07-08T06:29:46.000Z</published>
    <summary>
      <![CDATA[<p>最近在项目里做了一个数据同步的功能。需要把两个数据库里的数据做同步，在业务不繁忙的时候跑一次，核对库里每一条数据，主要是为了保证<strong>数据最终一致</strong>，做的一个保底工作。之前项目里同步的代码是单线程的，我改成了基于线程池的，但是在上线前的一些问题却引]]>
    </summary>
    <title>并发难 | 池里究竟该放多少线程？</title>
    <updated>2018-02-01T01:28:12.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="golang" scheme="https://lengzzz.com/tags/golang/"/>
    <category term="GC" scheme="https://lengzzz.com/tags/GC/"/>
    <category term="垃圾回收" scheme="https://lengzzz.com/tags/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/"/>
    <content>
      <![CDATA[<p>用任何带 GC 的语言最后都要直面 GC 问题。在以前学习 C# 的时候就被迫读了一大堆 .NET Garbage Collection 的文档。最近也学习了一番 golang 的垃圾回收机制，在这里记录一下。</p><p><a href="/notename/" title="gc in golang"></a></p><h2 id="常见-GC-算法"><a href="#常见-GC-算法" class="headerlink" title="常见 GC 算法"></a>常见 GC 算法</h2><p>趁着这个机会我总结了一下常见的 GC 算法。分别是：<strong>引用计数法</strong>、<strong>Mark-Sweep法</strong>、<strong>三色标记法</strong>、<strong>分代收集法</strong>。</p><h3 id="1-引用计数法"><a href="#1-引用计数法" class="headerlink" title="1. 引用计数法"></a>1. 引用计数法</h3><p>原理是在每个对象内部维护一个整数值，叫做这个对象的<strong>引用计数</strong>，当对象被引用时引用计数加一，当对象不被引用时引用计数减一。当引用计数为 0 时，自动销毁对象。</p><p>目前引用计数法主要用在 c++ 标准库的 std::shared_ptr 、微软的 COM 、Objective-C 和 PHP 中。</p><p>但是引用计数法有个缺陷就是不能解决循环引用的问题。循环引用是指对象 A 和对象 B 互相持有对方的引用。这样两个对象的引用计数都不是 0 ，因此永远不能被收集。</p><p>另外的缺陷是，每次对象的赋值都要将引用计数加一，增加了消耗。</p><h3 id="2-Mark-Sweep法（标记清除法）"><a href="#2-Mark-Sweep法（标记清除法）" class="headerlink" title="2. Mark-Sweep法（标记清除法）"></a>2. Mark-Sweep法（标记清除法）</h3><p>这个算法分为两步，标记和清除。</p><ul><li>标记：从程序的根节点开始， <strong>递归地</strong> 遍历所有对象，将能遍历到的对象打上标记。</li><li>清除：讲所有未标记的的对象当作垃圾销毁。</li></ul><center>![Animation_of_the_Naive_Mark_and_Sweep_Garbage_Collector_Algorithm.gif-143.9kB][1]<small>图片来自 https://en.wikipedia.org/wiki/Tracing_garbage_collection </small></center><p>如图所示。</p><p>但是这个算法也有一个缺陷，就是人们常常说的 STW 问题（Stop The World）。因为算法在标记时必须暂停整个程序，否则其他线程的代码可能会改变对象状态，从而可能把不应该回收的对象当做垃圾收集掉。</p><p>当程序中的对象逐渐增多时，递归遍历整个对象树会消耗很多的时间，在大型程序中这个时间可能会是毫秒级别的。让所有的用户等待几百毫秒的 GC 时间这是不能容忍的。</p><p>golang 1.5以前使用的这个算法。</p><h3 id="3-三色标记法"><a href="#3-三色标记法" class="headerlink" title="3. 三色标记法"></a>3. 三色标记法</h3><p>三色标记法是传统 Mark-Sweep 的一个改进，它是一个并发的 GC 算法。</p><p>原理如下，</p><ol><li>首先创建三个集合：白、灰、黑。</li><li>将所有对象放入白色集合中。</li><li>然后从根节点开始遍历所有对象（注意这里<strong>并不递归遍历</strong>），把遍历到的对象从白色集合放入灰色集合。</li><li>之后遍历灰色集合，将灰色对象引用的对象从白色集合放入灰色集合，之后将此灰色对象放入黑色集合</li><li>重复 4 直到灰色中无任何对象</li><li>通过write-barrier检测对象有变化，重复以上操作</li><li>收集所有白色对象（垃圾）</li></ol><center>![Animation_of_tri-color_garbage_collection.gif-94kB][2]<small>图片来自 https://en.wikipedia.org/wiki/Tracing_garbage_collection </small></center><p>过程如上图所示。</p><p>这个算法可以实现 “on-the-fly”，也就是在程序执行的同时进行收集，并不需要暂停整个程序。</p><p>但是也会有一个缺陷，可能程序中的垃圾产生的速度会大于垃圾收集的速度，这样会导致程序中的垃圾越来越多无法被收集掉。</p><p>使用这种算法的是 Go 1.5、Go 1.6。</p><h3 id="4-分代收集"><a href="#4-分代收集" class="headerlink" title="4. 分代收集"></a>4. 分代收集</h3><p>分代收集也是传统 Mark-Sweep 的一个改进。这个算法是基于一个经验：绝大多数对象的生命周期都很短。所以按照对象的生命周期长短来进行分代。</p><p>一般 GC 都会分三代，在 java 中称之为新生代（Young Generation）、年老代（Tenured Generation）和永久代（Permanent Generation）；在 .NET 中称之为第 0 代、第 1 代和第2代。</p><p>原理如下：</p><ul><li>新对象放入第 0 代</li><li>当内存用量超过一个较小的阈值时，触发 0 代收集</li><li>第 0 代幸存的对象（未被收集）放入第 1 代</li><li>只有当内存用量超过一个较高的阈值时，才会触发 1 代收集</li><li>2 代同理</li></ul><p>因为 0 代中的对象十分少，所以每次收集时遍历都会非常快（比 1 代收集快几个数量级）。只有内存消耗过于大的时候才会触发较慢的 1 代和 2 代收集。</p><p>因此，分代收集是目前比较好的垃圾回收方式。使用的语言（平台）有 jvm、.NET 。</p><h2 id="golang-的-GC"><a href="#golang-的-GC" class="headerlink" title="golang 的 GC"></a>golang 的 GC</h2><p>go 语言在 1.3 以前，使用的是比较蠢的传统 Mark-Sweep 算法。</p><p>1.3 版本进行了一下改进，把 Sweep 改为了并行操作。</p><p>1.5 版本进行了较大改进，使用了三色标记算法。go 1.5 在源码中的解释是“非分代的、非移动的、并发的、三色的标记清除垃圾收集器”</p><p>go 除了标准的三色收集以外，还有一个辅助回收功能，防止垃圾产生过快手机不过来的情况。这部分代码在 <a href="https://golang.org/src/runtime/mgcmark.go#L316"><code>runtime.gcAssistAlloc</code></a> 中。</p><p>但是 golang 并没有分代收集，所以对于巨量的小对象还是很苦手的，会导致整个 mark 过程十分长，在某些极端情况下，甚至会导致 GC 线程占据 50% 以上的 CPU。</p><p>因此，当程序由于高并发等原因造成大量小对象的gc问题时，最好可以使用 <a href="https://golang.org/pkg/sync/#Pool"><code>sync.Pool</code></a> 等对象池技术，避免大量小对象加大 GC 压力。</p>]]>
    </content>
    <id>https://lengzzz.com/2016/06/22/golang%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/</id>
    <link href="https://lengzzz.com/2016/06/22/golang%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/"/>
    <published>2016-06-22T07:22:16.000Z</published>
    <summary>
      <![CDATA[<p>用任何带 GC 的语言最后都要直面 GC 问题。在以前学习 C# 的时候就被迫读了一大堆 .NET Garbage Collection 的文档。最近也学习了一番 golang 的垃圾回收机制，在这里记录一下。</p>
<p><a href="/notename/" tit]]>
    </summary>
    <title>golang 垃圾回收机制</title>
    <updated>2019-07-20T09:14:32.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="架构" scheme="https://lengzzz.com/tags/%E6%9E%B6%E6%9E%84/"/>
    <category term="高可用" scheme="https://lengzzz.com/tags/%E9%AB%98%E5%8F%AF%E7%94%A8/"/>
    <category term="可伸缩" scheme="https://lengzzz.com/tags/%E5%8F%AF%E4%BC%B8%E7%BC%A9/"/>
    <content>
      <![CDATA[<p>最近在读一本书《大型网站技术架构》，收获颇多。这篇文章是对最近学习的一些总结，大多是一些结论性的内容，可以拿来就用的东西，对于大部分企业都比较适用。注意这些内容并不是我自己发明的，而是业界多年来的经验总结出的结论。</p><p><a href="/notename/" title="high availability scalable architecture"></a></p><h2 id="高性能"><a href="#高性能" class="headerlink" title="高性能"></a>高性能</h2><h3 id="1-性能测试"><a href="#1-性能测试" class="headerlink" title="1. 性能测试"></a>1. 性能测试</h3><p>保证网站高性能的前提是做性能测试，如果连网站的性能指标都不知道，怎么判断一个网站“慢不慢”和“高性能”呢。</p><p>对一个网站做性能测试时，主要需要测试一下几项数据：</p><ul><li>并发数</li><li>响应时间</li><li>吞吐量（TPS，每秒事物数）</li><li>资源消耗（主要指cpu、内存消耗）</li></ul><p>在做性能测试时，一般以并发数为自变量，逐步提高并发数，每次记录上述四个指标，制作出一张表格以方便后面进行分析。</p><p>例子如下：</p><p>| 并发数 | 响应时间 | TPS | 出错率 | CPU | 内存 | 备注 |<br>| :: | :: | :: | :: | :: | :: | :: |<br>| 100 | 10ms | 200 | 0% | 5% | 200MiB | 性能测试 |<br>| 200 | 15ms | 266 | 0% | 10% | 300MiB | 性能测试 |<br>| 300 | 20ms | 300 | 2% | 15% | 400MiB | 性能测试 |<br>| 400 | 50ms | 330 | 20% | 35% | 600MiB | 负载测试 |<br>| 500 | 100ms | 350 | 25% | 40% | 800MiB | 负载测试 |<br>| 600 | 500ms | 360 | 30% | 50% | 1.0GiB | 压力测试 |<br>| 700 | 1200ms | 350 | 50% | 60% | 1.5GiB | 压力测试 |<br>| 800 | timeout | 0 | 100% | N&#x2F;A | N&#x2F;A | 压力测试 |</p><p>看表格不是很直观，我简单解释一下。这个测试大致分为三个stage：<strong>性能测试</strong>、<strong>负载测试</strong>、<strong>压力测试</strong>。</p><ul><li>性能测试：网站正常运行的状态。随着并发数量的增大，响应时间不会有很大的变化；吞吐量和资源消耗基本上时线性正相关。</li><li>负载测试：网站某项或多项性能指标已达到临界值。此时增大并发数量响应时间已经有明显的影响；资源消耗增大而吞吐量不会有太大变化。</li><li>压力测试：已超出安全负载。此时响应延迟明显增大；资源消耗越来越多而系统吞吐量不增反降。继续增大负载，系统将崩溃，不能接受任何请求。</li></ul><p>分别以 “资源消耗” 为横轴、TPS 为纵轴画出函数图像可以更直观的观察两者之间的关系。</p><center>![QQ20160621-0@2x.png-76.3kB][1]</center><p>分别以 “并发数” 为横轴、“响应时间” 为纵轴，观察两者关系。</p><center>![QQ20160621-1@2x.png-84.2kB][2]</center><p>通过以上分析，判断一个网站是否高性能，可以通过并发数来做基本的判断。更准确的应当通过上图中的b点处（系统最佳运行点）的并发数量来判断网站性能。</p><h3 id="2-前端性能优化"><a href="#2-前端性能优化" class="headerlink" title="2. 前端性能优化"></a>2. 前端性能优化</h3><p>这部分和我们讨论的网站架构关系不大，对于后端开发者来说需要注意的点有：使用CDN、注意开启浏览器缓存、合并JS减少HTTP请求等等，主要还是靠前端工程师的努力啦。</p><h3 id="3-应用性能优化"><a href="#3-应用性能优化" class="headerlink" title="3. 应用性能优化"></a>3. 应用性能优化</h3><p>对于应用层的性能优化主要有三种方法：<strong>加缓存</strong>、<strong>使用异步操作</strong>、<strong>应用集群化</strong>。这三者越靠前者越应当优先使用，效果越好，对系统影响越小。</p><h4 id="分布式缓存"><a href="#分布式缓存" class="headerlink" title="分布式缓存"></a>分布式缓存</h4><p>有一位计算机科学家曾说“缓存是最伟大的发明”，在系统中使用缓存可以挡住大部分请求从而可以不访问数据库。但是并不是说缓存可以在任何地方使用（滥用）。使用缓存有以下原则。</p><ul><li>不应缓存频繁更改的数据。一般来说，缓存是被用来读的，如果一个缓存刚刚写入没过过久又被修改了，这样毫无意义。最好的情况是，缓存写入后被读了成千上万次。</li><li>应缓存热点数据。缓存使用内存存储，如果将不频繁访问的数据放入缓存的话太浪费内存空间。</li><li>注意数据不一致问题。缓存一般设置超时时间，当用户修改了数据后缓存不一定能及时更新。应根据业务合理设置超时时间。</li><li>注意缓存可用性问题。应妥善结果缓存挂了的情况，因为数据库已经习惯了有缓存的“舒适”的日子，一旦缓存挂了之后数据库压力山大而宕机导致网站整体不可用。</li><li>应进行缓存预热。当换上一台新缓存服务器后不应立即撤掉老的缓存服务器，因为新机器内部还没有任何数据，大部分访问还是会打到数据库。所以新的缓存服务器启动时最好将热点数据提前载入进行预热。</li></ul><p>具体在使用缓存时可以使用业界通用的开源缓存系统，如redis、memcached、jboss cache等。</p><h4 id="异步操作"><a href="#异步操作" class="headerlink" title="异步操作"></a>异步操作</h4><p>网站一些操作可以不直接访问数据库，而是将任务放入消息队列异步化。是用另一台 consumer 服务器从消息队列中取出任务进行数据库访问。</p><p>这样可以有效的起到“削峰”作用。经常用于应对突然增加的并发数，比如在抢购系统中，在前几分钟会接到很多请求，将这些请求放入消息队列中逐个处理，当处理结束后再使用 websocket 等通知方式告知用户是否抢购到商品。</p><center>![QQ20160621-2@2x.png-60.3kB][3]</center><h4 id="应用集群化"><a href="#应用集群化" class="headerlink" title="应用集群化"></a>应用集群化</h4><p>使用负载均衡技术，在网站入口搭建一台负载均衡服务器（如nginx），入口后搭建一个应用服务器集群，当接收到请求后，负载均衡服务器使用一定算法将请求平均的打到不同的应用服务器上。</p><p>这样，比如应用集群有10台机器，那么理想状态下每台机器的负载只有总负载的 1&#x2F;10 。</p><h3 id="4-存储性能优化"><a href="#4-存储性能优化" class="headerlink" title="4. 存储性能优化"></a>4. 存储性能优化</h3><p>先略过不表</p><h2 id="高可用"><a href="#高可用" class="headerlink" title="高可用"></a>高可用</h2><p>可用性指网站的故障情况。网站的故障时间越短，故障范围越小说明网站可用性越高。对于网站提高网站的可用性，需要从四方面入手：负载均衡（入口）、应用、缓存、数据库。</p><h3 id="1-入口层"><a href="#1-入口层" class="headerlink" title="1. 入口层"></a>1. 入口层</h3><p>需要在 nginx 挂了之后还有机器能顶上。可使用的技术有 VRRP，具体说来就是当一台机器挂了之后，另一台机器检测到了之后立即把自己的IP地址设置为原有机器的IP地址。通过这种抢地址的方式来接管新的请求。</p><h3 id="2-应用层"><a href="#2-应用层" class="headerlink" title="2. 应用层"></a>2. 应用层</h3><p>应用层做到高可用的方式就是“集群化”。部署多台应用服务器，对于负载均衡来说每台业务服务器都是一样的，当一台机器挂了之后将请求打到其他机器上就好了。</p><p>做应用集群化的核心就是“业务层不要有状态”。将状态保存到缓存层和数据库中。以下几点是大家常犯的错误：</p><ul><li>session数据，很多语言都自带了 session 功能（如php）。这样不利于集群化，所以在写代码时应注意不要使用语言自带的 session 系统，最好将 session 数据存储到缓存层、另外也可以使用 CookieSession ，将 session 加密存储在客户端。</li><li>缓存，很多程序员喜欢在业务层使用一个全局的 map 缓存一些数据，这样就不必频繁访问数据库了，但是在使用集群时很容易造成不同机器上的缓存数据不一致的问题。这样从缓存拿到的数据就是错误的。</li><li>全局变量。写代码时不要使用全局变量。</li></ul><h3 id="3-缓存层"><a href="#3-缓存层" class="headerlink" title="3. 缓存层"></a>3. 缓存层</h3><p>现在普遍使用缓存，因为缓存可以替 mysql 挡住大部分请求。所以这种情况下，整个系统都过于依赖缓存层。因为一旦缓存不可用之后，所有的请求都打到数据库上，导致数据库压力过大。</p><p>因此，保证缓存层高可用也是必要的了。达到缓存层高可用的方式也是集群化，将缓存分的细一些，不同的数据存储到不同的 cache 中。这样某一个 cache 宕机之后不至于太过严重。</p><h3 id="4-数据层"><a href="#4-数据层" class="headerlink" title="4. 数据层"></a>4. 数据层</h3><p>数据层有一个经典的原理，叫做 CAP 原理。CAP 分别指：一致性（ <strong>C</strong> onsistency)、可用性（<strong>A</strong> vailibility）、分区耐受性（<strong>P</strong> atition Tolerance）。</p><p>CAP 原理认为，一个数据服务通常无法同时做到以上三点。所以一般情况下网站会舍弃一致性，而达到可用性和分区耐受性（既可伸缩性）。</p><p>数据层做高可用的主要技术是：数据备份。</p><p>数据备份主要分为：热备、冷备。</p><p>冷备指定期对数据库进行备份，如果发现数据库出错了会滚到上一个备份。这种方式其实做不到数据最终一致。</p><p>热备主要分为同步热备和异步热备。</p><ul><li>同步热备指同时开两个数据库，应用在读写时同时对两个数据库进行操作，这样当一个数据库宕机之后另一个数据库中还保存完整数据。</li><li>异步热备一般指 Master-Slave 模式，应用只对 master 进行写入操作，master 会随后将数据同步到 slave 中。</li></ul><center>![QQ20160621-3@2x.png-116.2kB][4]</center><p>这两种功能 mysql 都有实现。</p><h2 id="可伸缩"><a href="#可伸缩" class="headerlink" title="可伸缩"></a>可伸缩</h2><p>可伸缩性指当业务达到负载上限时，可通过简单的增加机器的方式提高系统的负载能力。</p><h3 id="1-入口层-1"><a href="#1-入口层-1" class="headerlink" title="1. 入口层"></a>1. 入口层</h3><p>可以通过 DNS 来实现，目前 DNS 服务器会使用轮转算法返回 IP 地址，这样就可以把请求分配到不同的负载均衡上。</p><h3 id="2-应用层-1"><a href="#2-应用层-1" class="headerlink" title="2. 应用层"></a>2. 应用层</h3><p>其实应用层做到可伸缩性和高可用的解决方案一致，都是集群化，核心都是保证应用无状态。</p><h3 id="3-缓存层-1"><a href="#3-缓存层-1" class="headerlink" title="3. 缓存层"></a>3. 缓存层</h3><p>缓存要保证水平扩展的同时不增加额外的消耗需要一些技术。常见的技术是一致性 HASH。可以保证水平扩展后较少的数据失效。</p><center>![QQ20160621-4@2x.png-115.8kB][5]</center><h3 id="4-数据层-1"><a href="#4-数据层-1" class="headerlink" title="4. 数据层"></a>4. 数据层</h3><p>数据库层做可伸缩的话常用的技术就是分库、分表。</p><p>书中也介绍了一些开源的技术如 Cobar 之类的，不过我没有去深入研究。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>网上找了张图，总结的不错。</p><p><img src="/images/56d5977b5a4140ed3f59db7c5aed710c.png" alt="高可用可伸缩.png-27.5kB"></p>]]>
    </content>
    <id>https://lengzzz.com/2016/06/21/%E9%AB%98%E5%8F%AF%E7%94%A8%E5%8F%AF%E4%BC%B8%E7%BC%A9%E6%9E%B6%E6%9E%84/</id>
    <link href="https://lengzzz.com/2016/06/21/%E9%AB%98%E5%8F%AF%E7%94%A8%E5%8F%AF%E4%BC%B8%E7%BC%A9%E6%9E%B6%E6%9E%84/"/>
    <published>2016-06-21T05:39:33.000Z</published>
    <summary>
      <![CDATA[<p>最近在读一本书《大型网站技术架构》，收获颇多。这篇文章是对最近学习的一些总结，大多是一些结论性的内容，可以拿来就用的东西，对于大部分企业都比较适用。注意这些内容并不是我自己发明的，而是业界多年来的经验总结出的结论。</p>
<p><a href="/notename/" t]]>
    </summary>
    <title>高可用可伸缩架构</title>
    <updated>2016-06-21T10:19:49.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="HTTP" scheme="https://lengzzz.com/tags/HTTP/"/>
    <category term="CORS" scheme="https://lengzzz.com/tags/CORS/"/>
    <category term="跨域" scheme="https://lengzzz.com/tags/%E8%B7%A8%E5%9F%9F/"/>
    <content>
      <![CDATA[<p>之前一直对跨域问题一知半解，今天看了些资料<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS">^cors</a>总算把所有情况搞明白了。总的来说跨域请求分为两种：简单请求和复杂请求，下面来详细说明。</p><p><a href="/notename/" title="cross origin http request"></a></p><h2 id="简单请求"><a href="#简单请求" class="headerlink" title="简单请求"></a>简单请求</h2><p>简单请求是指：</p><ul><li>只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据，则数据类型 (Content-Type，<strong>注意是 request 的 Content-Type</strong>) 只能是 application&#x2F;x-www-form-urlencoded, multipart&#x2F;form-data 或 text&#x2F;plain中的一种。</li><li>不会使用自定义请求头（类似于 X-Modified 这种）。</li></ul><p>当浏览器遇到这种请求时，会直接向服务器发送请求，但是在把结果返回给JS代码前会做一次检查。浏览器会查看 response header 中是否有 <code>Access-Control-Allow-Origin</code> 这个 header 。如果有且允许当前 Origin 访问的话，才会真正将结果返回给 JS 程序。</p><h2 id="复杂请求和预请求"><a href="#复杂请求和预请求" class="headerlink" title="复杂请求和预请求"></a>复杂请求和预请求</h2><p>如果一个 Ajax 请求不是上面所说的那种。比如使用 POST 方法并且 Content-Type 是 application&#x2F;json。那么浏览器则不会直接发送这个 HTTP 请求，而是先发送一个预请求。</p><p>预请求使用 OPTION 方法发送，并且带上 <code>Access-Control-Request-Method</code>、<code>Access-Control-Request-Headers</code> 等 header。</p><p>举例说明：</p><p>在 a.com 下使用 POST 请求 b.com&#x2F;user ，数据类型是 application&#x2F;json ，并且使用了私有 header ：x-token</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs http"><span class="hljs-keyword">OPTIONS</span> <span class="hljs-string">/user</span> <span class="hljs-meta">HTTP/1.1</span><br><span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>b.com<br><span class="hljs-attribute">User-Agent</span><span class="hljs-punctuation">: </span>Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre<br><span class="hljs-attribute">Accept</span><span class="hljs-punctuation">: </span>text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8<br><span class="hljs-attribute">Accept-Language</span><span class="hljs-punctuation">: </span>en-us,en;q=0.5<br><span class="hljs-attribute">Accept-Encoding</span><span class="hljs-punctuation">: </span>gzip,deflate<br><span class="hljs-attribute">Accept-Charset</span><span class="hljs-punctuation">: </span>ISO-8859-1,utf-8;q=0.7,*;q=0.7<br><span class="hljs-attribute">Connection</span><span class="hljs-punctuation">: </span>keep-alive<br><span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>http://a.com<br><span class="hljs-attribute">Access-Control-Request-Method</span><span class="hljs-punctuation">: </span>POST<br><span class="hljs-attribute">Access-Control-Request-Headers</span><span class="hljs-punctuation">: </span>X-TOKEN<br><br><span class="language-dart">HTTP/<span class="hljs-number">1.1</span> <span class="hljs-number">200</span> OK</span><br><span class="language-dart"><span class="hljs-title class_">Date</span>: <span class="hljs-title class_">Mon</span>, <span class="hljs-number">01</span> <span class="hljs-title class_">Dec</span> <span class="hljs-number">2008</span> <span class="hljs-number">01</span>:<span class="hljs-number">15</span>:<span class="hljs-number">39</span> GMT</span><br><span class="language-dart"><span class="hljs-title class_">Server</span>: <span class="hljs-title class_">Apache</span>/<span class="hljs-number">2.0</span>.<span class="hljs-number">61</span> (<span class="hljs-title class_">Unix</span>)</span><br><span class="language-dart"><span class="hljs-title class_">Access</span>-<span class="hljs-title class_">Control</span>-<span class="hljs-title class_">Allow</span>-<span class="hljs-title class_">Origin</span>: http:<span class="hljs-comment">//foo.example</span></span><br><span class="language-dart"><span class="hljs-title class_">Access</span>-<span class="hljs-title class_">Control</span>-<span class="hljs-title class_">Allow</span>-<span class="hljs-title class_">Methods</span>: POST, GET, PUT, OPTIONS</span><br><span class="language-dart"><span class="hljs-title class_">Access</span>-<span class="hljs-title class_">Control</span>-<span class="hljs-title class_">Allow</span>-<span class="hljs-title class_">Headers</span>: X-TOKEN</span><br><span class="language-dart"><span class="hljs-title class_">Access</span>-<span class="hljs-title class_">Control</span>-<span class="hljs-title class_">Max</span>-<span class="hljs-title class_">Age</span>: <span class="hljs-number">1728000</span></span><br><span class="language-dart"><span class="hljs-title class_">Vary</span>: <span class="hljs-title class_">Accept</span>-<span class="hljs-title class_">Encoding</span>, <span class="hljs-title class_">Origin</span></span><br><span class="language-dart"><span class="hljs-title class_">Content</span>-<span class="hljs-title class_">Encoding</span>: gzip</span><br><span class="language-dart"><span class="hljs-title class_">Content</span>-<span class="hljs-title class_">Length</span>: <span class="hljs-number">0</span></span><br><span class="language-dart"><span class="hljs-title class_">Keep</span>-<span class="hljs-title class_">Alive</span>: timeout=<span class="hljs-number">2</span>, max=<span class="hljs-number">100</span></span><br><span class="language-dart"><span class="hljs-title class_">Connection</span>: <span class="hljs-title class_">Keep</span>-<span class="hljs-title class_">Alive</span></span><br><span class="language-dart"><span class="hljs-title class_">Content</span>-<span class="hljs-title class_">Type</span>: text/plain</span><br><span class="language-dart"></span><br></code></pre></td></tr></table></figure><p>这个 OPTION 请求类似一个询问，他会询问服务器是否支持用户的请求，服务器应当返回他所支持的请求。当进行了 OPTION 请求之后，浏览器进行判断此次请求是否合法，如果合法的话才会发起真正的 HTTP 请求。</p><h2 id="Cookies"><a href="#Cookies" class="headerlink" title="Cookies"></a>Cookies</h2><p>默认情况下跨域请求是不带 Cookies 的，如果需要 Cookies 的话，在客户端需要将 XMLHttpRequest 对象的 withCredentials 设置为 true 。同时，服务器也应当做相应调整。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">var</span> xhr = <span class="hljs-keyword">new</span> <span class="hljs-title class_">XMLHttpRequest</span>();<br>xhr.<span class="hljs-title function_">open</span>(<span class="hljs-string">&#x27;GET&#x27;</span>, url, <span class="hljs-literal">true</span>);<br>xhr.<span class="hljs-property">withCredentials</span> = <span class="hljs-literal">true</span>; <span class="hljs-comment">// 设置 withCredentials 为 true</span><br>xhr.<span class="hljs-title function_">send</span>();<br><br></code></pre></td></tr></table></figure><p>服务器应当返回 <code>Access-Control-Allow-Credentials: true</code> ，否则浏览器不会将结果返回给 JS 程序。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>总的来说，为了实现安全的跨域 Ajax 请求。会对 ajax 做一下检查。对于简单的请求，浏览器会直接发送请求，然后对结果进行检查；对于复杂请求，会首先发送一个预请求进行检查，检查通过之后才发送真正的请求。</p><p>为了实现检查的目的，在 HTTP 协议中新增了如下几个请求头和响应头。</p><h3 id="请求头（request-header）"><a href="#请求头（request-header）" class="headerlink" title="请求头（request header）"></a>请求头（request header）</h3><ul><li>Origin：用于表明此请求来自哪个源</li><li>Access-Control-Request-Method：用于表明此次请求需要使用哪个方法</li><li>Access-Control-Request-Headers：用于表明此次请求需要哪些自定义头</li></ul><h3 id="响应头（response-header）"><a href="#响应头（response-header）" class="headerlink" title="响应头（response header）"></a>响应头（response header）</h3><ul><li>Access-Control-Allow-Origin：用于表明此资源允许哪些源访问</li><li>Access-Control-Expose-Headers：用于表明此资源会返回哪些自定义响应头</li><li>Access-Control-Max-Age：用于表明此次预请求的有效期（秒）</li><li>Access-Control-Allow-Credentials：用于表明此资源允许使用 Cookies</li><li>Access-Control-Allow-Methods：用于表明此资源允许哪些方法</li><li>Access-Control-Allow-Headers：用于表明此资源允许哪些自定义请求头</li></ul>]]>
    </content>
    <id>https://lengzzz.com/2016/06/13/%E8%B7%A8%E6%BA%90HTTP%E8%AF%B7%E6%B1%82%EF%BC%88CORS%EF%BC%89/</id>
    <link href="https://lengzzz.com/2016/06/13/%E8%B7%A8%E6%BA%90HTTP%E8%AF%B7%E6%B1%82%EF%BC%88CORS%EF%BC%89/"/>
    <published>2016-06-13T06:37:05.000Z</published>
    <summary>
      <![CDATA[<p>之前一直对跨域问题一知半解，今天看了些资料<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS">^cors</a>总算把所有情况搞明白了。总的来说跨域请求分为两种：简单]]>
    </summary>
    <title>跨源 HTTP 请求（CORS）</title>
    <updated>2016-06-13T07:12:16.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="运维" scheme="https://lengzzz.com/tags/%E8%BF%90%E7%BB%B4/"/>
    <category term="linux" scheme="https://lengzzz.com/tags/linux/"/>
    <category term="ubuntu" scheme="https://lengzzz.com/tags/ubuntu/"/>
    <content>
      <![CDATA[<p>最近无痛升级了 ubuntu 16.04 ，软件包和数据一点没丢。比上次 12.04 升级 14.04 舒服多了，直接 ssh 就搞定升级了。不得不说 ubuntu 已经成熟很多了。升级后我才发现多了个 <code>apt</code> 命令，用起来很符合我的审美，不过这个东西好像也存在比较久的一段时间了，实在是后知后觉了。</p><p><a href="/notename/" title="it is time to replace apt-get with apt"></a></p><p>apt 这个命令行工具在功能上基本涵盖了以前 apt-get 和 apt-cache 的功能，在他们之上提供了一个 high-level 的命令行界面，而且也更有交互性。</p><p><img src="/images/78febf75a690a0a0b38c17a8c046defe.png" alt="image_1ajqh3qkm1kf81vb81kmtjldlipg.png-416.8kB"></p><p>在命令行下敲击 apt 后会打印出一些常见命令：</p><figure class="highlight dns"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs dns">zzz@ubuntu-server ~ $ apt<br>apt <span class="hljs-number">1</span>.<span class="hljs-number">2</span>.<span class="hljs-number">10</span> <span class="hljs-punctuation">(</span>amd64<span class="hljs-punctuation">)</span><br>用法： apt [选项] 命令<br><br>命令行软件包管理器 apt 提供软件包搜索，管理和信息查询等功能。<br>它提供的功能与其他 APT 工具相同（像 apt-get 和 apt-cache），<br>但是默认情况下被设置得更适合交互。<br><br>常用命令：<br>  list - 根据名称列出软件包<br>  search - 搜索软件包描述<br>  show - 显示软件包细节<br>  install - 安装软件包<br>  remove - 移除软件包<br>  autoremove - 卸载所有自动安装且不再使用的软件包<br>  update - 更新可用软件包列表<br>  upgrade - 通过 安装/升级 软件来更新系统<br>  full-upgrade - 通过 卸载/安装/升级 来更新系统<br>  edit-sources - 编辑软件源信息文件<br><br>参见 apt<span class="hljs-punctuation">(</span><span class="hljs-number">8</span><span class="hljs-punctuation">)</span> 以获取更多关于可用命令的信息。<br>程序配置选项及语法都已经在 apt.conf<span class="hljs-punctuation">(</span><span class="hljs-number">5</span><span class="hljs-punctuation">)</span> 中阐明。<br>欲知如何配置软件源，请参阅 sources.list<span class="hljs-punctuation">(</span><span class="hljs-number">5</span><span class="hljs-punctuation">)</span>。<br>软件包及其版本偏好可以通过 apt_preferences<span class="hljs-punctuation">(</span><span class="hljs-number">5</span><span class="hljs-punctuation">)</span> 来设置。<br>关于安全方面的细节可以参考 apt-secure<span class="hljs-punctuation">(</span><span class="hljs-number">8</span><span class="hljs-punctuation">)</span>.<br>                                         本 APT 具有超级牛力。<br></code></pre></td></tr></table></figure><p>虽然这个 <code>超级牛力</code> 是什么鬼我也不明白了。。。（莫非是 powered by GNU？）但是基本的使用介绍还是很清晰的。对于咱们普通用户来说，最明显的就是把 search 功能合并过来了。</p><p>另外，比较好用的一点是 list 命令。可以使用 <code>apt list --upgradable</code> 来查看需要升级的软件包，有点类似 <code>brew outdated</code> 。</p><p>还有一个 <code>apt show</code> 可以打印出软件包的基本信息。</p><figure class="highlight dart"><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><code class="hljs dart">zzz<span class="hljs-meta">@ubuntu</span>-server ~ $ apt <span class="hljs-keyword">show</span> zsh<br><span class="hljs-title class_">Package</span>: zsh<br><span class="hljs-title class_">Version</span>: <span class="hljs-number">5.1</span>.<span class="hljs-number">1</span>-1ubuntu2<br><span class="hljs-title class_">Priority</span>: optional<br><span class="hljs-title class_">Section</span>: shells<br><span class="hljs-title class_">Origin</span>: <span class="hljs-title class_">Ubuntu</span><br><span class="hljs-title class_">Maintainer</span>: <span class="hljs-title class_">Ubuntu</span> <span class="hljs-title class_">Developers</span> &lt;ubuntu-devel-discuss<span class="hljs-meta">@lists</span>.ubuntu.com&gt;<br><span class="hljs-title class_">Original</span>-<span class="hljs-title class_">Maintainer</span>: <span class="hljs-title class_">Debian</span> <span class="hljs-title class_">Zsh</span> <span class="hljs-title class_">Maintainers</span> &lt;pkg-zsh-devel<span class="hljs-meta">@lists</span>.alioth.debian.org&gt;<br><span class="hljs-title class_">Bugs</span>: https:<span class="hljs-comment">//bugs.launchpad.net/ubuntu/+filebug</span><br><span class="hljs-title class_">Installed</span>-<span class="hljs-title class_">Size</span>: <span class="hljs-number">2</span>,<span class="hljs-number">023</span> kB<br><span class="hljs-title class_">Pre</span>-<span class="hljs-title class_">Depends</span>: dpkg (&gt;= <span class="hljs-number">1.17</span>.<span class="hljs-number">14</span>)<br><span class="hljs-title class_">Depends</span>: zsh-common (= <span class="hljs-number">5.1</span>.<span class="hljs-number">1</span>-1ubuntu2), libc6 (&gt;= <span class="hljs-number">2.15</span>), libcap2 (&gt;= <span class="hljs-number">1</span>:<span class="hljs-number">2.10</span>), libtinfo5 (&gt;= <span class="hljs-number">6</span>)<br><span class="hljs-title class_">Recommends</span>: libncursesw5 (&gt;= <span class="hljs-number">6</span>), libpcre3<br><span class="hljs-title class_">Suggests</span>: zsh-doc<br><span class="hljs-title class_">Homepage</span>: http:<span class="hljs-comment">//www.zsh.org/</span><br><span class="hljs-title class_">Supported</span>: 5y<br><span class="hljs-title class_">Download</span>-<span class="hljs-title class_">Size</span>: <span class="hljs-number">651</span> kB<br>APT-<span class="hljs-title class_">Sources</span>: http:<span class="hljs-comment">//cn.archive.ubuntu.com/ubuntu xenial/main amd64 Packages</span><br><span class="hljs-title class_">Description</span>: 具有很多特性的 <span class="hljs-title class_">Shell</span><br> <span class="hljs-title class_">Zsh</span> 是一个既可用作登录 <span class="hljs-title class_">Shell</span>，又可用于执行脚本的命令解析器。 在标准 <span class="hljs-title class_">Shell</span> 中 <span class="hljs-title class_">Zsh</span> 与 <span class="hljs-title class_">Ksh</span><br> 最相近，同时又带有许多改进。<span class="hljs-title class_">Zsh</span> 支持命令行编辑，内置拼写纠正，支持可编程命令补全、带自动加载的 <span class="hljs-title class_">Shell</span> 函数、历史功能和大量其他特性。<br><br></code></pre></td></tr></table></figure><p>总之就是各种命令清晰漂亮了很多，个人用起来很舒服。</p>]]>
    </content>
    <id>https://lengzzz.com/2016/05/28/%E6%98%AF%E6%97%B6%E5%80%99%E7%94%A8apt%E4%BB%A3%E6%9B%BFapt-get%E4%BA%86%EF%BC%81/</id>
    <link href="https://lengzzz.com/2016/05/28/%E6%98%AF%E6%97%B6%E5%80%99%E7%94%A8apt%E4%BB%A3%E6%9B%BFapt-get%E4%BA%86%EF%BC%81/"/>
    <published>2016-05-28T01:51:48.000Z</published>
    <summary>
      <![CDATA[<p>最近无痛升级了 ubuntu 16.04 ，软件包和数据一点没丢。比上次 12.04 升级 14.04 舒服多了，直接 ssh 就搞定升级了。不得不说 ubuntu 已经成熟很多了。升级后我才发现多了个 <code>apt</code> 命令，用起来很符合我的审美，不过这个]]>
    </summary>
    <title>是时候用 apt 代替 apt-get 了！</title>
    <updated>2016-05-28T02:18:59.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="golang" scheme="https://lengzzz.com/tags/golang/"/>
    <category term="elasticsearch" scheme="https://lengzzz.com/tags/elasticsearch/"/>
    <category term="搜索引擎" scheme="https://lengzzz.com/tags/%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E/"/>
    <content>
      <![CDATA[<p>博客从 Wordpress 迁移过来之后一直缺少一个搜索功能，这个博客我是当做笔记性质的，有时候脑子里突然想不起某个东西的时候就上来查一下。没有搜索还是很不方便的，所以费了点时间研究了下大名鼎鼎的 elasticsearch 配合 golang 给博客加上了搜索功能。</p><p><a href="/notename/" title="add internal search to your blog using elasticsearch"></a></p><h2 id="elasticsearch-介绍"><a href="#elasticsearch-介绍" class="headerlink" title="elasticsearch 介绍"></a>elasticsearch 介绍</h2><p>elasticsearch 是一个 java 编写的搜索和分析引擎，功能十分强大。但是并不意味着你的程序必须使用 java 开发，elasticsearch 是一个独立运行的程序，它会开放一个 RESTful 的接口供人调用，所以使用起来十分方便，甚至使用 curl 就能对它进行访问。另外，elasticsearch 的可伸缩性也很吸引我，使用 elasticsearch 组建一个集群十分方便，只需要把几个 elasticsearch 放到同一个局域网内就可以了，不用做任何配置你就能跑起来一个集群。这样，当你的数据量或者并发量增大的时候，只需要简单的购买几台新服务器就能解决性能问题。</p><p>我是通过 <a href="http://es.xiaoleilu.com/">Elasticsearch 权威指南（中文版）</a> 这本书来学习的，也推荐大家看一看，比我讲的好。</p><h3 id="一些概念"><a href="#一些概念" class="headerlink" title="一些概念"></a>一些概念</h3><p>elasticsearch 中有几个基本概念，大概可以和数据库的这几个概念对应起来（如下表）。但是有一点需要注意，elasticsearch 中不会限制数据必须存在一个二维表中，你可以保存一个对象，一个数组，一个字符串，或者一个整数，就像一个 JSON 一样，十分灵活。事实上，elasticsearch 的通讯协议确实是使用 JSON 的。</p><p>| 数据库 | elasticsearch |<br>|  |  |<br>| Databases | 索引（Indices） |<br>| Tables | 类型（Types） |<br>| Rows | 文档（Documents） |<br>| Columns | 字段（Fields） |<br>| schema | Mapping |</p><p>在 elasticsearch 中保存的每条记录叫一个 <code>document</code> ，它可以是一个包含很多字段的对象，默认情况下每个字段都能被搜索。</p><h3 id="基本操作"><a href="#基本操作" class="headerlink" title="基本操作"></a>基本操作</h3><p>使用 curl 就可以对 elasticsearch 进行操作，但是我还是推荐一个 chrome 应用 <a href="https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop" title="postman 的下载链接"><code>postman</code></a> ，有 JSON 语法高亮和检测，还可以保存历史记录。</p><p><img src="/images/e1cb8a757d384d5cc83ac6945bdeef0e.jpg" alt="90.pic_hd.jpg-1070kB"></p><h4 id="索引一条记录"><a href="#索引一条记录" class="headerlink" title="索引一条记录"></a>索引一条记录</h4><p>在 elasticsearch 中存储数据的行为叫做 <code>索引（index）</code> 。使用 HTTP 协议的 PUT 动词可以存储数据。</p><figure class="highlight nix"><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><code class="hljs nix">PUT http:<span class="hljs-operator">//</span>localhost:<span class="hljs-number">9200</span><span class="hljs-symbol">/mdblog/note/23432</span><br>&#123;<br>    <span class="hljs-string">&quot;id&quot;</span>: <span class="hljs-number">23</span>,<br>    <span class="hljs-string">&quot;notename&quot;</span>: <span class="hljs-string">&quot;golang-china-download-mirror&quot;</span>,<br>    <span class="hljs-string">&quot;title&quot;</span>: <span class="hljs-string">&quot;做了个 golang 安装包的镜像&quot;</span>,<br>    <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;做了个 golang 安装包的镜像 闲扯 golang<span class="hljs-char escape_">\n</span>        <span class="hljs-char escape_">\n</span>        2016-05-25 16:04 PM<span class="hljs-char escape_">\n</span>    国内下载 golang 安装包还是挺蛋疼的。虽然现在 docker 镜像是个比较好的选择，但还是有很多场景需要原始的 golang 环境的。所以抽空做了个 mirror ，定时拉取 golang 官网的安装包到我的服务器上。包含了 golang 1.5 之后的所有版本，所有平台的安装包和源码包都放在里面，自行 control + f 搜一下吧。新版本的 golang release 之后，应该在一两天内可以拉取过来。&quot;</span>,<br>    <span class="hljs-string">&quot;timestamp&quot;</span>: <span class="hljs-string">&quot;2016-05-25T08:04:53Z&quot;</span>,<br>    <span class="hljs-string">&quot;lastModified&quot;</span>: <span class="hljs-string">&quot;2016-05-25T09:01:42.923822162Z&quot;</span>,<br>    <span class="hljs-string">&quot;tagList&quot;</span>: [<br>      <span class="hljs-string">&quot;golang&quot;</span>,<br>      <span class="hljs-string">&quot;闲扯&quot;</span><br>    ]<br>  &#125;<br><br></code></pre></td></tr></table></figure><p>如上，把要存储的数据写成一个 JSON 对象，放到 HTTP 的 Body 中传送给 elasticsearch 即可存储数据。</p><p>我们可以看到 url 中包含了 4 部分的信息。</p><p>| 名字 | 信息 |<br>|  |  |<br>| localhost:9200 | Elasticsearch 的 url |<br>| mdblog | 索引名（Index） |<br>| note | 类型名（Type） |<br>| 23432 | 文档ID（Document ID） |</p><p>很方便吧。</p><h4 id="获取一条记录"><a href="#获取一条记录" class="headerlink" title="获取一条记录"></a>获取一条记录</h4><p>大家应当已经想到了，使用 GET 动词。</p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">GET</span> http://localhost:<span class="hljs-number">9200</span>/mdblog/note/<span class="hljs-number">23432</span><br></code></pre></td></tr></table></figure><p>返回的信息会多一些 metadata 。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;_index&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;mdblog&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;_type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;note&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;389344&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;_version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;found&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;_source&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">23</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;notename&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;golang-china-download-mirror&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;title&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;做了个 golang 安装包的镜像&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;content&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;做了个 golang 安装包的镜像 闲扯 golang\n        \n        2016-05-25 16:04 PM\n    国内下载 golang 安装包还是挺蛋疼的。虽然现在 docker 镜像是个比较好的选择，但还是有很多场景需要原始的 golang 环境的。所以抽空做了个 mirror ，定时拉取 golang 官网的安装包到我的服务器上。包含了 golang 1.5 之后的所有版本，所有平台的安装包和源码包都放在里面，自行 control + f 搜一下吧。新版本的 golang release 之后，应该在一两天内可以拉取过来。&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;timestamp&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2016-05-25T08:04:53Z&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;lastModified&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2016-05-25T09:01:42.923822162Z&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;tagList&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>      <span class="hljs-string">&quot;golang&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-string">&quot;闲扯&quot;</span><br>    <span class="hljs-punctuation">]</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><h4 id="搜索"><a href="#搜索" class="headerlink" title="搜索"></a>搜索</h4><p>搜索的话可以使用 <code>查询 DSL</code> 进行，说是 DSL（领域特定语言） 听起来很吓人，实际上就是几个 JSON 对象的组合而已。</p><p>调用搜索接口需要在 url 后面加一个 <code>_search</code> 。</p><figure class="highlight ada"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs ada">GET http://localhost:<span class="hljs-number">9200</span>/mdblog/note/_search<br>&#123;<br>    <span class="hljs-string">&quot;query&quot;</span> : &#123;<br>        <span class="hljs-string">&quot;match&quot;</span> : &#123;<br>            <span class="hljs-string">&quot;title&quot;</span> : &quot;<span class="hljs-type">linux</span><span class="hljs-string">&quot;</span><br><span class="hljs-string">        &#125;</span><br><span class="hljs-string">    &#125;</span><br><span class="hljs-string">&#125;</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><p>这样，就可以使用 match 查询进行查询了。</p><p>结果：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><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></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;took&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">2</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;timed_out&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;_shards&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;total&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">5</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;successful&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">5</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;failed&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0</span><br>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;hits&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;total&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;max_score&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0.6609862</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hits&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>      <span class="hljs-punctuation">&#123;</span><br>        <span class="hljs-attr">&quot;_index&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;mdblog&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-attr">&quot;_type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;note&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-attr">&quot;_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;340165&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-attr">&quot;_score&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0.6609862</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-attr">&quot;_source&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>          <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">11</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-attr">&quot;notename&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;add-swap-on-linux&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-attr">&quot;title&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;在Linux下设置swap&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-attr">&quot;content&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;在Linux下设置swap linux\n        \n        2016-04-10 15:53 PM\n    今早起来发现博客的数据库挂了，赶紧用手机上的ConnectBot连上去把mysql启动。看了下日志大概是因为内存不够用且没设置swap，所以mysql进程申请不到内存挂了（小内存服务器桑不起）所以赶紧把swap搞上，这样至少能让服务不轻易挂掉。这里记录一下，以备遗忘。大概分三步\n生成一个空文件\n把文件格式化成swap格式\n挂载\n&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-attr">&quot;timestamp&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2016-04-10T07:53:58Z&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-attr">&quot;lastModified&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2016-05-24T05:27:01.435772194Z&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-attr">&quot;tagList&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>            <span class="hljs-string">&quot;linux&quot;</span><br>          <span class="hljs-punctuation">]</span><br>        <span class="hljs-punctuation">&#125;</span><br>      <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">]</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>另外，我们可以为搜索加上高亮：</p><figure class="highlight stylus"><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><code class="hljs stylus">GET http:<span class="hljs-comment">//localhost:9200/mdblog/note/_search</span><br>&#123;<br>    <span class="hljs-string">&quot;query&quot;</span> : &#123;<br>        <span class="hljs-string">&quot;match&quot;</span> : &#123;<br>            <span class="hljs-string">&quot;title&quot;</span> : <span class="hljs-string">&quot;linux&quot;</span><br>        &#125;<br>    &#125;,<br>    <span class="hljs-string">&quot;highlight&quot;</span>: &#123;<br>        <span class="hljs-string">&quot;pre_tags&quot;</span> : <span class="hljs-selector-attr">[<span class="hljs-string">&quot;&lt;b&gt;&quot;</span>]</span>,<br>        <span class="hljs-string">&quot;post_tags&quot;</span> : <span class="hljs-selector-attr">[<span class="hljs-string">&quot;&lt;/b&gt;&quot;</span>]</span>,<br>        <span class="hljs-string">&quot;fields&quot;</span> : &#123;<br>            <span class="hljs-string">&quot;title&quot;</span> : &#123;&#125;<br>        &#125;<br>    &#125;<br>&#125;<br><br></code></pre></td></tr></table></figure><p>这样的话，在 hit 中会有一个 <code>highlight</code> 字段，所有关键字会用 <code>&lt;b&gt;&lt;/b&gt;</code> 扩起来。</p><h4 id="创建-mapping"><a href="#创建-mapping" class="headerlink" title="创建 mapping"></a>创建 mapping</h4><p>默认情况下 elasticsearch 是不需要“建表”操作的。mapping（类似数据库的表结构）会在第一次 index 的时候建立。但是提前建立 mapping 有助于查询。建立 mapping 也是使用 PUT 动词。</p><figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><code class="hljs awk">PUT http:<span class="hljs-regexp">//</span>localhost:<span class="hljs-number">9200</span><span class="hljs-regexp">/mdblog/</span>note/_mapping<br>&#123;<br><span class="hljs-string">&quot;note&quot;</span>: &#123;<br><span class="hljs-string">&quot;properties&quot;</span>: &#123;<br><span class="hljs-string">&quot;id&quot;</span>: &#123;<br><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;long&quot;</span><br>&#125;,<br><span class="hljs-string">&quot;title&quot;</span>: &#123;<br><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>,<br><span class="hljs-string">&quot;term_vector&quot;</span>: <span class="hljs-string">&quot;with_positions_offsets&quot;</span>,<br><span class="hljs-string">&quot;analyzer&quot;</span>: <span class="hljs-string">&quot;ik_syno&quot;</span>,<br><span class="hljs-string">&quot;search_analyzer&quot;</span>: <span class="hljs-string">&quot;ik_syno&quot;</span><br>&#125;,<br><span class="hljs-string">&quot;content&quot;</span>: &#123;<br><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>,<br><span class="hljs-string">&quot;term_vector&quot;</span>: <span class="hljs-string">&quot;with_positions_offsets&quot;</span>,<br><span class="hljs-string">&quot;analyzer&quot;</span>: <span class="hljs-string">&quot;ik_syno&quot;</span>,<br><span class="hljs-string">&quot;search_analyzer&quot;</span>: <span class="hljs-string">&quot;ik_syno&quot;</span><br>&#125;,<br><span class="hljs-string">&quot;notename&quot;</span>: &#123;<br><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span><br>&#125;,<br><span class="hljs-string">&quot;tagList&quot;</span>: &#123;<br><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>,<br><span class="hljs-string">&quot;term_vector&quot;</span>: <span class="hljs-string">&quot;with_positions_offsets&quot;</span>,<br><span class="hljs-string">&quot;analyzer&quot;</span>: <span class="hljs-string">&quot;ik_syno&quot;</span>,<br><span class="hljs-string">&quot;search_analyzer&quot;</span>: <span class="hljs-string">&quot;ik_syno&quot;</span><br>&#125;,<br><span class="hljs-string">&quot;timestamp&quot;</span>: &#123;<br><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;date&quot;</span>,<br><span class="hljs-string">&quot;index&quot;</span>: <span class="hljs-string">&quot;not_analyzed&quot;</span><br>&#125;,<br><span class="hljs-string">&quot;lastModified&quot;</span>: &#123;<br><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;date&quot;</span>,<br><span class="hljs-string">&quot;index&quot;</span>: <span class="hljs-string">&quot;not_analyzed&quot;</span><br>&#125;<br>&#125;<br>&#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>如上，建立一个 mapping 。主要是设置一下数据类型和查询方式。</p><h2 id="在-golang-中使用"><a href="#在-golang-中使用" class="headerlink" title="在 golang 中使用"></a>在 golang 中使用</h2><p>在 golang 中有方便的 package 来操纵 elasticsearch。我使用的是 <a href="https://github.com/olivere/elastic"><code>gopkg.in/olivere/elastic.v3</code></a> 还不错的一个包，所有操作都是链式调用，很有 linq 的感觉。</p><p>使用 elastic 需要先创建一个客户端：</p><figure class="highlight golang"><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><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InitElasticSearch</span><span class="hljs-params">()</span></span> (err <span class="hljs-type">error</span>) &#123;<br>esClient, err = elastic.NewClient(<br>    elastic.SetURL(<span class="hljs-string">&quot;http://localhost:9200&quot;</span>))<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-keyword">return</span> err<br>&#125;<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span><br>&#125;<br></code></pre></td></tr></table></figure><p>然后，就可以用 client 进行操作了。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs golang">noteDetail := model.NoteDetail&#123;<br>Id:           note.Id,<br>Notename:     note.Notename,<br>Title:        note.Title,<br>Content:      note.ContentText(),<br>Timestamp:    note.Timestamp,<br>LastModified: note.LastModified,<br>TagList:      tagNameList,<br>&#125;<br><br><span class="hljs-comment">// 首先调用 Index 函数，代表这是一次索引（Index）操作</span><br><span class="hljs-comment">// 接着提供各种参数</span><br>_, err := esClient.Index().<br>Index(MdBlogIndexName).<br>Type(NoteTypeName).<br>Id(strconv.FormatInt(note.UniqueId, <span class="hljs-number">10</span>)).<br>BodyJson(noteDetail).<br>Do()<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-keyword">return</span> err<br>&#125;<br></code></pre></td></tr></table></figure><p>索引一条记录</p><figure class="highlight golang"><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><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">IsNoteDocumentExist</span><span class="hljs-params">(uniqueId <span class="hljs-type">int64</span>)</span></span> (<span class="hljs-type">bool</span>, <span class="hljs-type">error</span>) &#123;<br><span class="hljs-keyword">return</span> esClient.Exists().<br>Index(MdBlogIndexName).<br>Type(NoteTypeName).<br>Id(strconv.FormatInt(uniqueId, <span class="hljs-number">10</span>)).<br>Do()<br>&#125;<br></code></pre></td></tr></table></figure><p>判断是否存在</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SearchNoteByKeyword</span><span class="hljs-params">(keyword <span class="hljs-type">string</span>, </span></span><br><span class="hljs-params"><span class="hljs-function">    page, limit <span class="hljs-type">int64</span>)</span></span> ([]*model.SearchedNote, <span class="hljs-type">int64</span>, <span class="hljs-type">error</span>) &#123;<br>page-- <span class="hljs-comment">//数据库层的页数从0开始数</span><br>offset := page * limit<br><br>query := elastic.NewMultiMatchQuery(keyword).<br>FieldWithBoost(<span class="hljs-string">&quot;notename&quot;</span>, <span class="hljs-number">1</span>).<br>FieldWithBoost(<span class="hljs-string">&quot;tagList&quot;</span>, <span class="hljs-number">2</span>).<br>FieldWithBoost(<span class="hljs-string">&quot;content&quot;</span>, <span class="hljs-number">4</span>).<br>FieldWithBoost(<span class="hljs-string">&quot;title&quot;</span>, <span class="hljs-number">4</span>)<br>highlight := elastic.NewHighlight().<br>Field(<span class="hljs-string">&quot;content&quot;</span>).<br>Field(<span class="hljs-string">&quot;title&quot;</span>).<br>Field(<span class="hljs-string">&quot;tagList&quot;</span>)<br><br>result, err := esClient.Search().<br>Index(MdBlogIndexName).<br>Type(NoteTypeName).<br>Query(query).<br>Highlight(highlight).<br>From(<span class="hljs-type">int</span>(offset)).<br>Size(<span class="hljs-type">int</span>(limit)).<br>Do()<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-number">0</span>, err<br>&#125;<br><br><span class="hljs-keyword">if</span> result.Hits == <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-number">0</span>, <span class="hljs-literal">nil</span><br>&#125;<br>maxPage := (result.TotalHits()<span class="hljs-number">-1</span>)/limit + <span class="hljs-number">1</span><br><br>noteList := <span class="hljs-built_in">make</span>([]*model.SearchedNote, <span class="hljs-number">0</span>, <span class="hljs-built_in">len</span>(result.Hits.Hits))<br><span class="hljs-keyword">for</span> _, hit := <span class="hljs-keyword">range</span> result.Hits.Hits &#123;<br>note := model.NewSearchedNote()<br>err := json.Unmarshal(*hit.Source, note)<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-number">0</span>, err<br>&#125;<br>note.FillHighlight(hit.Highlight)<br><br>noteList = <span class="hljs-built_in">append</span>(noteList, note)<br>&#125;<br><br><span class="hljs-keyword">return</span> noteList, maxPage, <span class="hljs-literal">nil</span><br>&#125;<br></code></pre></td></tr></table></figure><p>搜索记录</p><h2 id="´-・ω・｀"><a href="#´-・ω・｀" class="headerlink" title="(´ ・ω・｀)"></a>(´ ・ω・｀)</h2><p>总的来说，elasticsearch 还是很方便强大的，好评。</p>]]>
    </content>
    <id>https://lengzzz.com/2016/05/25/%E7%94%A8elasticsearch%E7%BB%99%E5%8D%9A%E5%AE%A2%E5%8A%A0%E4%B8%8A%E4%BA%86%E6%90%9C%E7%B4%A2/</id>
    <link href="https://lengzzz.com/2016/05/25/%E7%94%A8elasticsearch%E7%BB%99%E5%8D%9A%E5%AE%A2%E5%8A%A0%E4%B8%8A%E4%BA%86%E6%90%9C%E7%B4%A2/"/>
    <published>2016-05-25T14:21:11.000Z</published>
    <summary>
      <![CDATA[<p>博客从 Wordpress 迁移过来之后一直缺少一个搜索功能，这个博客我是当做笔记性质的，有时候脑子里突然想不起某个东西的时候就上来查一下。没有搜索还是很不方便的，所以费了点时间研究了下大名鼎鼎的 elasticsearch 配合 golang 给博客加上了搜索功能。</p]]>
    </summary>
    <title>用 elasticsearch 给博客加上了搜索</title>
    <updated>2017-08-20T07:21:20.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="闲扯" scheme="https://lengzzz.com/tags/%E9%97%B2%E6%89%AF/"/>
    <category term="golang" scheme="https://lengzzz.com/tags/golang/"/>
    <content>
      <![CDATA[<p>国内下载 golang 安装包还是挺蛋疼的。虽然现在 docker 镜像是个比较好的选择，但还是有很多场景需要原始的 golang 环境的。所以抽空做了个 mirror ，定时拉取 golang 官网的安装包到我的服务器上。</p><p><a href="/notename/" title="golang china download mirror"></a></p><p>包含了 golang 1.5 之后的所有版本，所有平台的安装包和源码包都放在里面，自行 <code>control + f</code> 搜一下吧。新版本的 golang release 之后，应该在一两天内可以拉取过来。</p><p><img src="/images/1f5556f01de37fc838e02d1c4d7a6682.png" alt="屏幕快照 2016-05-25 下午4.06.56.png-311.7kB"></p>]]>
    </content>
    <id>https://lengzzz.com/2016/05/25/%E5%81%9A%E4%BA%86%E4%B8%AAgolang%E5%AE%89%E8%A3%85%E5%8C%85%E7%9A%84%E9%95%9C%E5%83%8F/</id>
    <link href="https://lengzzz.com/2016/05/25/%E5%81%9A%E4%BA%86%E4%B8%AAgolang%E5%AE%89%E8%A3%85%E5%8C%85%E7%9A%84%E9%95%9C%E5%83%8F/"/>
    <published>2016-05-25T07:57:16.000Z</published>
    <summary>
      <![CDATA[<p>国内下载 golang 安装包还是挺蛋疼的。虽然现在 docker 镜像是个比较好的选择，但还是有很多场景需要原始的 golang 环境的。所以抽空做了个 mirror ，定时拉取 golang 官网的安装包到我的服务器上。</p>
<p><a href="/notenam]]>
    </summary>
    <title>做了个 golang 安装包的镜像</title>
    <updated>2016-05-25T08:07:30.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="linux" scheme="https://lengzzz.com/tags/linux/"/>
    <category term="bash" scheme="https://lengzzz.com/tags/bash/"/>
    <category term="shell" scheme="https://lengzzz.com/tags/shell/"/>
    <category term="trap" scheme="https://lengzzz.com/tags/trap/"/>
    <content>
      <![CDATA[<p>在 Shell 编程中，为了脚本的健壮性，一般会用到 <code>trap</code> 这个 builtin command。trap 命令类似 c 语言中的 <code>signal</code> 函数，可以注册一个函数，当程序收到信号时执行函数。但是 trap 命令也有一些比较坑的小细节，比如 trap 的执行时机。</p><p><a href="/notename/" title="trap&#39;s trap in linux shell programing"></a></p><p>在 c 语言中，程序收到信号之后会立即执行 signal 注册的信号处理函数。那么在 shell 程序中呢，信号究竟什么时候被 trap 处理？是像 c 程序一样停下程序立即执行，还是等待当前程序执行之后再执行？</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-meta">#!/bin/bash</span><br><span class="hljs-built_in">trap</span> <span class="hljs-string">&#x27;echo &quot;signal received!&quot;; exit&#x27;</span> INT<br><span class="hljs-built_in">sleep</span> 100<br></code></pre></td></tr></table></figure><p>我们先写一段程序做个实验。在 bash 中执行它，然后按 <code>control + c</code> 发现程序立即停止了，然后打印了 signal received，似乎 trap 是类似 c 程序一样立即处理信号的。但其实是被表面现象蒙骗了。</p><p>我们再次执行这个 shell 程序，然后打开另一个 shell，在里面执行 <code>kill -SIGINT xxx</code> 发现 shell 程序并没有退出，等待了 100 秒之后才打印 signal received 退出。</p><p>因为第一次在键盘上按 <code>control + c</code> 后，sleep 程序和 shell 程序同属一个进程组，所以也接到了 int 信号退出了，而第二次只有 shell 程序收到信号，所以造成了两者的差异。我第一次也被蒙骗了，傻乎乎的以为 trap 能在 sleep 时也处理信号。</p><p>那么问题来了，我们如果需要在 sleep 时处理信号，并且及时退出怎么办呢？国外一篇文章给了例子<a href="http://mywiki.wooledge.org/SignalTrap">^sample</a>，我就负责搬运一下了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash">pid=<br><span class="hljs-built_in">trap</span> <span class="hljs-string">&#x27;[[ $pid ]] &amp;&amp; kill $pid&#x27;</span> EXIT<br><span class="hljs-built_in">sleep</span> 10000 &amp; pid=$!<br><span class="hljs-built_in">wait</span><br>pid=<br></code></pre></td></tr></table></figure><p>利用了 bash 的 builtin 命令 wait。wait 是一个 shell 内部的命令，而不是一个外部程序，所以它没有前面的限制。另外，要记得退出时 kill 掉 sleep 进程，擦好屁股。</p>]]>
    </content>
    <id>https://lengzzz.com/2016/05/24/LinuxShell%E7%BC%96%E7%A8%8B%E4%B8%AD%E7%9A%84trap%E7%9A%84%E5%B0%8F%E5%9D%91/</id>
    <link href="https://lengzzz.com/2016/05/24/LinuxShell%E7%BC%96%E7%A8%8B%E4%B8%AD%E7%9A%84trap%E7%9A%84%E5%B0%8F%E5%9D%91/"/>
    <published>2016-05-24T10:58:33.000Z</published>
    <summary>
      <![CDATA[<p>在 Shell 编程中，为了脚本的健壮性，一般会用到 <code>trap</code> 这个 builtin command。trap 命令类似 c 语言中的 <code>signal</code> 函数，可以注册一个函数，当程序收到信号时执行函数。但是 trap 命令也]]>
    </summary>
    <title>Linux Shell 编程中的 trap 的小坑</title>
    <updated>2016-05-24T11:25:19.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>wastecat</name>
    </author>
    <category term="CloudXNS" scheme="https://lengzzz.com/tags/CloudXNS/"/>
    <category term="DDNS" scheme="https://lengzzz.com/tags/DDNS/"/>
    <category term="docker" scheme="https://lengzzz.com/tags/docker/"/>
    <category term="动态DNS" scheme="https://lengzzz.com/tags/%E5%8A%A8%E6%80%81DNS/"/>
    <content>
      <![CDATA[<p>最近<a href="https://lengzzz.com/note/the-domain-hosting-to-cloudxns-perfect-supporting-the-let-s-encrypt">换上</a>了 CloudXNS 的域名服务。以前使用花生壳的时候比较方便，大多数路由器都支持，而且还提供了 Linux 下的客户端源码供定制。换上 CloudXNS 之后这些方便的东西当然没有了，不过 CloudXNS 也提供了 API，作为程序员当然要自己写一个了。这篇文章是这个 CloudXNS DDNS 客户端的使用介绍。</p><p><a href="/notename/" title="a docker image for CloudXNS DDNS"></a></p><p>客户端是使用 golang 开发的，放到了 <a href="https://github.com/zwh8800/cloudxns-ddns"><i class="icon-github"></i> github 上 <sub>https://github.com/zwh8800/cloudxns-ddns</sub></a>。需要的可以自己编译，不过我已经做好了 <a href="https://hub.docker.com/r/zwh8800/cloudxns-ddns">docker 镜像 <sub>https://hub.docker.com/r/zwh8800/cloudxns-ddns</sub></a> 了可以直接使用。</p><p>首先，拉取镜像：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker pull zwh8800/cloudxns-ddns<br></code></pre></td></tr></table></figure><p>然后，编写一个很简单的配置文件，放到某个文件夹中（如&#x2F;home&#x2F;zzz&#x2F;cloudxns-ddns&#x2F;config，下面以此为例子)</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs ini"><span class="hljs-section">[CloudXNS]</span><br><span class="hljs-attr">APIKey</span>=<span class="hljs-string">&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;</span><br><span class="hljs-attr">SecureKey</span>=<span class="hljs-string">&quot;xxxxxxxxxxxxxx&quot;</span><br><br><span class="hljs-section">[Domain]</span><br><span class="hljs-attr">Data</span>=<span class="hljs-string">&quot;home.lengzzz.com&quot;</span><br><span class="hljs-attr">Data</span>=<span class="hljs-string">&quot;haha.lengzzz.com&quot;</span><br><br></code></pre></td></tr></table></figure><p>上面 <code>APIKey</code> 是你在 <a href="https://www.cloudxns.net/AccountManage/apimanage.html">CloudXNS <sub>https://www.cloudxns.net/AccountManage/apimanage.html</sub></a> 申请的 key，填进去即可。下面是你想要动态的域名，可以写很多。</p><p>然后，启动镜像即可。</p><figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs awk">docker run --name cloudxns-ddns -d -v <span class="hljs-regexp">/home/</span>zzz<span class="hljs-regexp">/cloudxns-ddns/</span>log:<span class="hljs-regexp">/app/</span>log -v <span class="hljs-regexp">/home/</span>zzz<span class="hljs-regexp">/cloudxns-ddns/</span>config:<span class="hljs-regexp">/app/</span>config zwh8800/cloudxns-ddns<br></code></pre></td></tr></table></figure><p>注意一点，需要把刚写的配置文件当作 <code>volumn</code> 挂载到容器上，如上 <code>-v /home/zzz/cloudxns-ddns/config:/app/config</code> 。这样的话，你可以方便的修改配置文件然后 <code>docker restart cloud-ddns</code> 。</p>]]>
    </content>
    <id>https://lengzzz.com/2016/05/14/CloudXNS-DDNS%E5%8A%A8%E6%80%81%E5%9F%9F%E5%90%8D%E5%AE%A2%E6%88%B7%E7%AB%AFdocker%E9%95%9C%E5%83%8F/</id>
    <link href="https://lengzzz.com/2016/05/14/CloudXNS-DDNS%E5%8A%A8%E6%80%81%E5%9F%9F%E5%90%8D%E5%AE%A2%E6%88%B7%E7%AB%AFdocker%E9%95%9C%E5%83%8F/"/>
    <published>2016-05-14T07:15:34.000Z</published>
    <summary>
      <![CDATA[<p>最近<a href="https://lengzzz.com/note/the-domain-hosting-to-cloudxns-perfect-supporting-the-let-s-encrypt">换上</a>了 CloudXNS 的域名服务。以前使用花生壳的时]]>
    </summary>
    <title>CloudXNS-DDNS 动态域名客户端 docker 镜像</title>
    <updated>2016-05-14T07:50:23.000Z</updated>
  </entry>
</feed>
