<?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>Workerman - 她和她的猫</title>
    <link>https://her-cat.com/tags/workerman/</link>
    <description>Workerman的文章列表 - 她和她的猫</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>Thu, 22 Jul 2021 16:31:07 +0800</lastBuildDate>
    <atom:link href="https://her-cat.com/tags/workerman/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>什么是惊群问题</title>
      <link>https://her-cat.com/posts/2021/07/22/what-is-thundering-herd/</link>
      <pubDate>Thu, 22 Jul 2021 16:31:07 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/07/22/what-is-thundering-herd/</guid>
      <description>惊群问题又称惊群效应，当多个进程等待同一个事件，事件发生后内核会唤醒所有等待中的进程，但是只有一个进程能够获得 CPU 执行权对事件进行处理</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>我们知道，像 Nginx、Workerman 都是单 Master 多 Worker 的进程模型。</p>
<p>Master 进程用于创建监听套接字、创建 Worker 进程及管理 Worker 进程。</p>
<p>Worker 进程是由 Master 进程通过 fork 系统调用派生出来的，所以会自动继承 Master 进程的监听套接字，每个 Worker 进程都可以独立地接收并处理来自客户端的连接。</p>
<p>由于多个 Worker 进程都在等待同一个套接字上的事件，就会出现标题所说的惊群问题。</p>
<p><img decoding="async" height="662" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/07/22/what-is-thundering-herd/process-model.png" srcset="/posts/2021/07/22/what-is-thundering-herd/process-model_hu_5019476705f2d31a.png 384w" style="max-width: 100%; height: auto; aspect-ratio: 0.9532;" width="631"></p>
<h2 id="什么是惊群问题">什么是惊群问题</h2>
<p>惊群问题又称惊群效应，当多个进程等待同一个事件，事件发生后内核会唤醒所有等待中的进程，但是只有一个进程能够获得 CPU 执行权对事件进行处理，其他的进程都是被无效唤醒的，随后会再次陷入阻塞状态，等待下一次事件发生时被唤醒。</p>
<p>举个例子，你们寝室几个人都在一边睡觉一边等外卖，外卖到了的时候，快递小哥嗷一嗓子把你们几个人都叫醒了，但是他只送了一个人的外卖，其它人骂骂咧咧的又躺下了，下次外卖来的时候，又会把这几个人都吵醒。</p>
<p>这里的室友表示进程，外卖小哥表示操作系统，外卖就是等待的事件。</p>
<h3 id="惊群问题带来的问题">惊群问题带来的问题</h3>
<p>由于每次事件发生会唤醒所有进程，所以操作系统会对多个进程频繁地做无效的调度，让 CPU 大部分时间都浪费在了上下文切换上面，而不是让真正需要工作的进程运行，导致系统性能大打折扣。</p>
<h2 id="发生惊群问题的时机">发生惊群问题的时机</h2>
<p>通过上面的介绍可以知道，惊群问题主要发生在 socket_accept 和 socket_select 两个函数的调用上。</p>
<p>下面我们通过两个例子复现这两个系统调用的惊群。</p>
<h3 id="socket_accept-函数">socket_accept 函数</h3>
<p>PHP 中的 socket_accept 函数是 accept 系统调用的一层包装。函数原型如下：</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">socket_accept</span><span class="p">(</span><span class="nx">Socket</span> <span class="nv">$socket</span><span class="p">)</span><span class="o">:</span> <span class="nx">Socket</span><span class="o">|</span><span class="k">false</span>
</span></span></code></pre></div><p>该函数接收监听套接字上的新连接，一旦接收成功，就会返回一个新的套接字（连接套接字）用于与客户端进行通信。如果没有待处理的连接，socket_accept 函数将阻塞，直到有新的连接出现。</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 套接字
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nv">$server_socket</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">// 将套接字绑定到指定的主机地址和端口上
</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_socket</span><span class="p">,</span> <span class="s2">&#34;0.0.0.0&#34;</span><span class="p">,</span> <span class="mi">8080</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_listen</span><span class="p">(</span><span class="nv">$server_socket</span><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;master[%d] running</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</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="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="mi">5</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">$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="k">exit</span><span class="p">(</span><span class="s1">&#39;fork 失败&#39;</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">==</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">$pid</span> <span class="o">=</span> <span class="nx">posix_getpid</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;worker[%d] running</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$pid</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_socket</span> <span class="o">=</span> <span class="nx">socket_accept</span><span class="p">(</span><span class="nv">$server_socket</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">$conn_socket</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;worker[%d] 接收新连接失败，原因：%s</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$pid</span><span class="p">,</span> <span class="nx">socket_last_error</span><span class="p">(</span><span class="nv">$conn_socket</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="c1">// 获取客户端地址及端口号
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nx">socket_getpeername</span><span class="p">(</span><span class="nv">$conn_socket</span><span class="p">,</span> <span class="nv">$address</span><span class="p">,</span> <span class="nv">$port</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;worker[%d] 接收新连接成功：%s:%d</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$pid</span><span class="p">,</span> <span class="nv">$address</span><span class="p">,</span> <span class="nv">$port</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_close</span><span class="p">(</span><span class="nv">$conn_socket</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="c1">// 这里是父进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></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="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">// 为待处理的信号调用信号处理程序。
</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="c1">// 暂停当前进程的执行，直到一个子进程退出，或者直到一个信号被传递。
</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_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="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></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="nx">printf</span><span class="p">(</span><span class="s2">&#34;worker[%d] 退出</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$pid</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>上面的代码先创建了一个监听套接字 $server_socket，然后通过 pcntl_fork 函数派生出 5 个子进程。
在调用完 pcntl_fork 函数后，如果派生子进程成功，那么该函数会有两个返回值，在父进程中返回子进程的进程 ID，在子进程中返回 0；派生失败则返回 -1。</p>
<ul>
<li>父进程：调用 pcntl_wait 函数阻塞等待子进程退出，然后回收进程资源</li>
<li>子进程：调用 socket_accept 函数并阻塞，直到有新连接需要处理。</li>
</ul>
<p>将上面的代码保存为 accept.php，然后在 CLI 中执行 <code>php accept.php</code> 启动服务端程序，可以看到 1 个 master 进程和 5 个 worker 进程都已经处于运行状态：</p>
<p><img decoding="async" height="170" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/07/22/what-is-thundering-herd/accept-running.png" srcset="/posts/2021/07/22/what-is-thundering-herd/accept-running_hu_f372460f9dd14787.png 384w" style="max-width: 100%; height: auto; aspect-ratio: 2.7294;" width="464"></p>
<p>执行 <code>pstree -acp pid</code> 查看一下进程树：</p>
<p><img decoding="async" height="145" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/07/22/what-is-thundering-herd/accept-pstree.png" srcset="/posts/2021/07/22/what-is-thundering-herd/accept-pstree_hu_1532873346689313.png 384w" style="max-width: 100%; height: auto; aspect-ratio: 2.8759;" width="417"></p>
<p>进程树的结构与我们服务启动的日志是一致的。</p>
<p>接下来我们执行 <code>telnet 0.0.0.0 8080</code> 命令连接到服务端程序上，accept.php 输出：</p>
<p><img decoding="async" height="192" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/07/22/what-is-thundering-herd/accept-thundering-herd.png" srcset="/posts/2021/07/22/what-is-thundering-herd/accept-thundering-herd_hu_e3caba1353f1f6f9.png 384w" style="max-width: 100%; height: auto; aspect-ratio: 2.5000;" width="480"></p>
<p>咦，怎么回事，跟一开始说的不一样啊，这明明只有一个进程被唤醒然后处理了新连接！</p>
<p>莫慌，这是在预料之中的，因为在 Linux 2.6 后的版本中，Linux 已经修复了 accept 的惊群问题。</p>
<p>演示这一步主要是为后面的内容做铺垫。</p>
<h3 id="socket_select-函数">socket_select 函数</h3>
<p>跟 socket_accept 函数一样，socket_select 函数也是 select 系统调用的一层包装。</p>
<p>select 是最早的一种多路复用实现方式，性能相对于后面出现的 poll、epoll 要差很多，那么为什么这里要用 select 来做演示呢？</p>
<p>一是因为支持 select 的操作系统比较多，连 Windows 和 MacOS 也都支持 select 系统调用。
二是截止目前 Linux 内核版本 4.4.0 依然没有解决 select 的惊群问题。</p>
<p>socket_select 接受套接字数组并阻塞等待它们有事件发生。函数原型如下：</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">socket_select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="k">array</span><span class="o">|</span><span class="k">null</span> <span class="o">&amp;</span><span class="nv">$read</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="k">array</span><span class="o">|</span><span class="k">null</span> <span class="o">&amp;</span><span class="nv">$write</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="k">array</span><span class="o">|</span><span class="k">null</span> <span class="o">&amp;</span><span class="nv">$except</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">int</span><span class="o">|</span><span class="k">null</span> <span class="nv">$seconds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">int</span> <span class="nv">$microseconds</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span><span class="o">:</span> <span class="nx">int</span><span class="o">|</span><span class="k">false</span>
</span></span></code></pre></div><ul>
<li>$read 表示需要监听可读事件的套接字数组。</li>
<li>$write 表示需要监听可写事件的套接字数组。</li>
<li>$except 表示需要监听的异常事件套接字数组。</li>
<li>$seconds 和 $microseconds 组合起来表示 select 阻塞超时时间，$seconds 为 0 表示不等待，立即返回，设置为 null 表示一直阻塞等待，直到有事件发生。</li>
</ul>
<p>当在函数超时前有事件发生时，返回值为发生事件的套接字数量，如果是函数超时，返回值为 0 ，有错误发生时返回 false。</p>
<p>socket_select 函数的示例程序与上面 socket_accept 函数的差不多，只不过需要将监听套接字设置为非阻塞，然后在 socket_accept 函数之前调用 socket_select 进行阻塞等待事件。</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 套接字
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nv">$server_socket</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">// 将套接字绑定到指定的主机地址和端口上
</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_socket</span><span class="p">,</span> <span class="s2">&#34;0.0.0.0&#34;</span><span class="p">,</span> <span class="mi">8080</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_listen</span><span class="p">(</span><span class="nv">$server_socket</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_set_nonblock</span><span class="p">(</span><span class="nv">$server_socket</span><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;master[%d] running</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</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="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="mi">5</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">$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="k">exit</span><span class="p">(</span><span class="s1">&#39;fork 失败&#39;</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">==</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">$pid</span> <span class="o">=</span> <span class="nx">posix_getpid</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;worker[%d] running</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$pid</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">// 将监听套接字放入可读事件的套接字数组中，
</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="c1">// 监听套接字发生可读事件说明有客户端连接上来了。
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nv">$reads</span> <span class="o">=</span> <span class="p">[</span><span class="nv">$server_socket</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">$writes</span> <span class="o">=</span> <span class="nv">$excepts</span> <span class="o">=</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 超时时间设置为 NULL，表示一直阻塞等待，直到有事件发生。
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nv">$num</span> <span class="o">=</span> <span class="nx">socket_select</span><span class="p">(</span><span class="nv">$reads</span><span class="p">,</span> <span class="nv">$writes</span><span class="p">,</span> <span class="nv">$excepts</span><span class="p">,</span> <span class="k">NULL</span><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;worker[%d] wakeup，num：%d</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$pid</span><span class="p">,</span> <span class="nv">$num</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="nv">$conn_socket</span> <span class="o">=</span> <span class="nx">socket_accept</span><span class="p">(</span><span class="nv">$server_socket</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">$conn_socket</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;worker[%d] 接收新连接失败</span><span class="se">\n</span><span class="s2">&#34;</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">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="c1">// 获取客户端地址及端口号
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nx">socket_getpeername</span><span class="p">(</span><span class="nv">$conn_socket</span><span class="p">,</span> <span class="nv">$address</span><span class="p">,</span> <span class="nv">$port</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;worker[%d] 接收新连接成功：%s:%d</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$pid</span><span class="p">,</span> <span class="nv">$address</span><span class="p">,</span> <span class="nv">$port</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_close</span><span class="p">(</span><span class="nv">$conn_socket</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="c1">// 这里是父进程
</span></span></span><span class="line"><span class="cl"><span class="c1"></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="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">// 为待处理的信号调用信号处理程序。
</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="c1">// 暂停当前进程的执行，直到一个子进程退出，或者直到一个信号被传递。
</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_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="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></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="nx">printf</span><span class="p">(</span><span class="s2">&#34;worker[%d] 退出</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$pid</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>select.php</code> 并执行 <code>php select.php</code> 启动服务，然后使用 <code>telnet 127.0.0.1 8080</code> 连接上去就会发现 5 个子进程都输出了 wakeup，但是只有一个进程 accept 成功了。</p>
<p><img decoding="async" height="313" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 768px, 1024px" src="/posts/2021/07/22/what-is-thundering-herd/select-thundering-herd.png" srcset="/posts/2021/07/22/what-is-thundering-herd/select-thundering-herd_hu_792606c6ba64ac31.png 384w, /posts/2021/07/22/what-is-thundering-herd/select-thundering-herd_hu_b837e1031c8e82dd.png 768w, /posts/2021/07/22/what-is-thundering-herd/select-thundering-herd_hu_9da08aeef1f544a.png 1024w" style="max-width: 100%; height: auto; aspect-ratio: 4.6518;" width="1456"></p>
<h2 id="如何解决惊群问题">如何解决惊群问题</h2>
<p>因为惊群问题主要是出在系统调用上，但是内核系统更新肯定没那么及时，而且不能保证所有操作系统都会修复这个问题。</p>
<p>所以解决方案可以分为两类：用户程序层面和内核程序层面，用户程序层面就是通过加锁解决问题，内核程序层面就是让内核程序提供一些机制，一劳永逸地解决这个问题。</p>
<h3 id="用户程序加锁">用户程序：加锁</h3>
<p>通过上面我们可以知道，惊群问题发生的前提是多个进程监听同一个套接字上的事件，所以我们只让一个进程去处理监听套接字就可以了。</p>
<p>Nginx 采用了自己实现的 accept 加锁机制，避免多个进程同时调用 accept。Nginx 多进程的锁在底层默认是通过 CPU 自旋锁实现的，如果操作系统不支持，就会采用文件锁。</p>
<p>Nginx 事件处理的入口函数使 ngx_process_events_and_timers()，下面是简化后的加锁过程：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 是否开启 accept 锁，
</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">if</span> <span class="p">(</span><span class="n">ngx_use_accept_mutex</span><span class="p">)</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">ngx_accept_disabled</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">// ngx_accept_disabled 的值是经过算法计算出来的，
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="c1">// 当值大于 0 时，说明此进程负载过高，不再接收新连接。
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">ngx_accept_disabled</span><span class="o">--</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">// 尝试抢 accept 锁，发生错误直接返回
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">if</span> <span class="p">(</span><span class="nf">ngx_trylock_accept_mutex</span><span class="p">(</span><span class="n">cycle</span><span class="p">)</span> <span class="o">==</span> <span class="n">NGX_ERROR</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">if</span> <span class="p">(</span><span class="n">ngx_accept_mutex_held</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="n">flags</span> <span class="o">|=</span> <span class="n">NGX_POST_EVENTS</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</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="k">if</span> <span class="p">(</span><span class="n">timer</span> <span class="o">==</span> <span class="n">NGX_TIMER_INFINITE</span>
</span></span><span class="line"><span class="cl">                <span class="o">||</span> <span class="n">timer</span> <span class="o">&gt;</span> <span class="n">ngx_accept_mutex_delay</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="n">timer</span> <span class="o">=</span> <span class="n">ngx_accept_mutex_delay</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 class="p">}</span>
</span></span></code></pre></div><p>在 ngx_trylock_accept_mutex 函数中，如果抢到了锁，Nginx 会把监听套接字的可读事件放入事件循环中，该进程有新连接进来的时候就可以 accept 了。</p>
<h3 id="内核程序从根源解决问题">内核程序：从根源解决问题</h3>
<p>在高本版的 Nginx 中 accept 锁默认是关闭的，如果开启了 accept 锁，那么在多个 worker 进程并行的情况下，对于 accept 函数的调用是串行的，效率不高。</p>
<p>所以最好的方式还是让内核程序解决惊群的问题，从问题的根源上去解决。</p>
<p>Linux 内核 3.9 及后续版本提供了新的套接字参数 SO_REUSEPORT，该参数允许多个进程绑定到同一个套接字上，内核在收到新的连接时，只会唤醒其中一个进程进行处理，内核中也会做负载均衡，避免某个进程负载过高。</p>
<p>对于 epoll 多路复用机制，Linux 内核 4.5+ 新增 EPOLLEXCLUSIVE 标志，这个标志会保证一个事件只会有一个阻塞在 epoll_wait 函数的进程被唤醒，避免了惊群问题。</p>
<p>在 Nginx 的 ngx_event_process_init 函数中，可以看到 Nginx 是如何使用 SO_REUSEPORT 和 EPOLLEXCLUSIVE 的。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// Nginx 支持端口复用
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#if (NGX_HAVE_REUSEPORT)
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>    <span class="c1">// 配置 listen 80 resuseport 时，支持多进程共用一个端口，
</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="k">if</span> <span class="p">(</span><span class="n">ls</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">reuseport</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nf">ngx_add_event</span><span class="p">(</span><span class="n">rev</span><span class="p">,</span> <span class="n">NGX_READ_EVENT</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="n">NGX_ERROR</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">NGX_ERROR</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">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 class="cp">#endif
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 打开 accept_mutex 锁之后，
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// 每个 worker 进程不能直接处理监听套接字，
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// 需要在 worker 进程抢到锁之后才能将监听套接字放入自己的事件循环中。
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="n">ngx_use_accept_mutex</span><span class="p">)</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="c1">// Nginx 支持 EPOLLEXCLUSIVE 标志
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#if (NGX_HAVE_EPOLLEXCLUSIVE)
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>    <span class="c1">// 如果 nginx 使用的是 epoll 多路复用机制，并且 worker 进程大于 1，
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// 那么就将监听套接字加入自己的事件循环中，并且设置 EPOLLEXCLUSIVE 标志。
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">((</span><span class="n">ngx_event_flags</span> <span class="o">&amp;</span> <span class="n">NGX_USE_EPOLL_EVENT</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">&amp;&amp;</span> <span class="n">ccf</span><span class="o">-&gt;</span><span class="n">worker_processes</span> <span class="o">&gt;</span> <span class="mi">1</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="nf">ngx_add_event</span><span class="p">(</span><span class="n">rev</span><span class="p">,</span> <span class="n">NGX_READ_EVENT</span><span class="p">,</span> <span class="n">NGX_EXCLUSIVE_EVENT</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">==</span> <span class="n">NGX_ERROR</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">return</span> <span class="n">NGX_ERROR</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">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 class="cp">#endif
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 未开启 accept_mutex 锁，未启动 resuseport 端口复用，不支持 EPOLLEXCLUSIVE 标志，
</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="k">if</span> <span class="p">(</span><span class="nf">ngx_add_event</span><span class="p">(</span><span class="n">rev</span><span class="p">,</span> <span class="n">NGX_READ_EVENT</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="n">NGX_ERROR</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">NGX_ERROR</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>
]]></content:encoded>
    </item>
    <item>
      <title>Workerman 源码分析：文件上传</title>
      <link>https://her-cat.com/posts/2021/05/08/workerman-file-upload/</link>
      <pubDate>Sat, 08 May 2021 17:42:00 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/05/08/workerman-file-upload/</guid>
      <description>Workerman 将解析协议这一步进行后置，当程序需要用到 HTTP 协议携带的信息时才会解析相应的数据，并将解析结果</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>在 Nginx 中 HTTP 数据是一边接收一边进行解析的，如果解析过程中发现收到的数据有问题就会停止解析，并且停止接收数据。</p>
<p>而 Workerman 将解析协议这一步进行后置，当程序需要用到 HTTP 协议携带的信息时才会解析相应的数据，并把解析结果缓存起来，下次获取信息时就直接从缓存中读取即可，避免多次解析。</p>
<p>两种方式各有自己的优点，前者的优点就是可以及时的检查数据是否有问题，后者的优点是在接收数据的逻辑相对要简单一点。</p>
<p>解析上传文件的逻辑在 <code>Request::parseUploadFiles</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">source</span><span class="p">:</span> <span class="n">Protocols</span>\<span class="n">Http</span>\<span class="n">Request</span><span class="o">.</span><span class="n">php</span>
</span></span><span class="line"><span class="cl"><span class="n">post</span><span class="p">()</span><span class="o">/</span><span class="n">file</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="o">-&gt;</span> <span class="n">parsePost</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="o">-&gt;</span> <span class="n">parseUploadFiles</span><span class="p">()</span>
</span></span></code></pre></div><h2 id="解析-post-数据">解析 POST 数据</h2>
<p>当调用 post 或 file 方法获取 POST 参数或上传的文件时，程序会检查是否已经解析过了，如果已经解析过了则直接返回对应的结果，否则就会调用 parsePost 方法解析数据。</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">protected</span> <span class="n">function</span> <span class="n">parsePost</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">rawBody</span><span class="p">()</span> <span class="err">返回的就是请求体的内容</span>
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">body_buffer</span> <span class="o">=</span> <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">rawBody</span><span class="p">();</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="n">this</span><span class="o">-&gt;</span><span class="n">_data</span><span class="p">[</span><span class="s1">&#39;post&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">_data</span><span class="p">[</span><span class="s1">&#39;files&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">array</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">body_buffer</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="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 class="o">//</span> <span class="err">尝试从缓存中读取</span>
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">cacheable</span> <span class="o">=</span> <span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">_enableCache</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">isset</span><span class="p">(</span><span class="o">$</span><span class="n">body_buffer</span><span class="p">[</span><span class="mi">1024</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">cacheable</span> <span class="o">&amp;&amp;</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">_postCache</span><span class="p">[</span><span class="o">$</span><span class="n">body_buffer</span><span class="p">]))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">_data</span><span class="p">[</span><span class="s1">&#39;post&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">_postCache</span><span class="p">[</span><span class="o">$</span><span class="n">body_buffer</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 class="o">//</span> <span class="err">读取请求头中的</span> <span class="n">content</span><span class="o">-</span><span class="n">type</span> <span class="err">字段，如果请求头没有被解析过，那么也会进行解析并缓存</span>
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">content_type</span> <span class="o">=</span> <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">header</span><span class="p">(</span><span class="s1">&#39;content-type&#39;</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="o">//</span> <span class="err">尝试解析</span> <span class="n">boundary</span> <span class="err">字段，如果获取到了说明是</span> <span class="n">multipart</span><span class="o">/</span><span class="n">form</span><span class="o">-</span><span class="n">data</span> <span class="err">类型的，可能会上传文件</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span>\<span class="n">preg_match</span><span class="p">(</span><span class="s1">&#39;/boundary=&#34;?(\S+)&#34;?/&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">content_type</span><span class="p">,</span> <span class="o">$</span><span class="n">match</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="o">//</span> <span class="o">$</span><span class="n">match</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="err">中就是我们上面说的边界值</span>
</span></span><span class="line"><span class="cl">        <span class="o">//</span> <span class="err">加上</span> <span class="o">--</span> <span class="err">得到请求体中的边界值</span>
</span></span><span class="line"><span class="cl">        <span class="o">$</span><span class="n">http_post_boundary</span> <span class="o">=</span> <span class="s1">&#39;--&#39;</span> <span class="o">.</span> <span class="o">$</span><span class="n">match</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">parseUploadFiles</span><span class="p">(</span><span class="o">$</span><span class="n">http_post_boundary</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 class="o">//</span> <span class="err">如果从</span> <span class="n">content</span><span class="o">-</span><span class="n">type</span> <span class="err">中匹配到了</span> <span class="n">json</span><span class="err">，比如</span> <span class="n">application</span><span class="o">/</span><span class="n">json</span>
</span></span><span class="line"><span class="cl">    <span class="o">//</span> <span class="err">说明请求体的数据是</span> <span class="n">JSON</span> <span class="err">格式的</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span>\<span class="n">preg_match</span><span class="p">(</span><span class="s1">&#39;/</span><span class="se">\b</span><span class="s1">json</span><span class="se">\b</span><span class="s1">/i&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">content_type</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">_data</span><span class="p">[</span><span class="s1">&#39;post&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">array</span><span class="p">)</span> <span class="n">json_decode</span><span class="p">(</span><span class="o">$</span><span class="n">body_buffer</span><span class="p">,</span> <span class="bp">true</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="o">//</span> <span class="err">否则就是</span> <span class="n">application</span><span class="o">/</span><span class="n">x</span><span class="o">-</span><span class="n">www</span><span class="o">-</span><span class="n">form</span><span class="o">-</span><span class="n">urlencoded</span>
</span></span><span class="line"><span class="cl">            \<span class="n">parse_str</span><span class="p">(</span><span class="o">$</span><span class="n">body_buffer</span><span class="p">,</span> <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">_data</span><span class="p">[</span><span class="s1">&#39;post&#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="err">如果开启了缓存，就把解析结果缓存起来</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">cacheable</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">_postCache</span><span class="p">[</span><span class="o">$</span><span class="n">body_buffer</span><span class="p">]</span> <span class="o">=</span> <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">_data</span><span class="p">[</span><span class="s1">&#39;post&#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">count</span><span class="p">(</span><span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">_postCache</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">256</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">unset</span><span class="p">(</span><span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">_postCache</span><span class="p">[</span><span class="n">key</span><span class="p">(</span><span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">_postCache</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>接下来就是 parseUploadFiles 方法的内容了。</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">$http_body = $this-&gt;rawBody();
</span></span><span class="line"><span class="cl">//删除末尾的结束边界值
</span></span><span class="line"><span class="cl">$http_body = \substr($http_body, 0, \strlen($http_body) - (\strlen($http_post_boundary) + 4))
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 通过边界值 + \r\n 分割请求体，得到数据块数组
</span></span><span class="line"><span class="cl">$boundary_data_array = \explode($http_post_boundary . &#34;\r\n&#34;, $http_body);
</span></span><span class="line"><span class="cl">if ($boundary_data_array[0] === &#39;&#39;) {
</span></span><span class="line"><span class="cl">    unset($boundary_data_array[0]);
</span></span><span class="line"><span class="cl">};
</span></span></code></pre></div><p>为什么计算 substr 结束位置最后要 +4 呢？</p>
<p>因为 <code>结束边界值</code> = <code>边界值</code> + <code>--</code> + <code>\r\n</code>。</p>
<p>接下来用两个 foreach 和一个 switch case 来解析请求体的内容。</p>
<h3 id="遍历所有的数据块">遍历所有的数据块</h3>
<p>通过 <code>\r\n\r\n</code> 分割数据块，得到<strong>数据块的头部信息</strong>和<strong>数据块的值</strong>，然后去除数据块的值末尾的 <code>\r\n</code>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">foreach ($boundary_data_array as $boundary_data_buffer) {
</span></span><span class="line"><span class="cl">    list($boundary_header_buffer, $boundary_value) = \explode(&#34;\r\n\r\n&#34;, $boundary_data_buffer, 2);
</span></span><span class="line"><span class="cl">    // 去除 $boundary_value 末尾的 \r\n
</span></span><span class="line"><span class="cl">    $boundary_value = \substr($boundary_value, 0, -2);
</span></span><span class="line"><span class="cl">    $key++;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><h3 id="解析数据块的头部信息">解析数据块的头部信息</h3>
<p>数据块的头部信息可能存在多行，所以需要通过 <code>\r\n</code> 分割头部信息字符串得到头部信息的数组。</p>
<p>然后遍历该数组，在循环中通过 <code>: </code> 分割每行的头部信息，得到字段名 $header_key 和字段值 $header_value 。</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">foreach</span> <span class="p">(</span>\<span class="n">explode</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\r\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="o">$</span><span class="n">boundary_header_buffer</span><span class="p">)</span> <span class="n">as</span> <span class="o">$</span><span class="n">item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">list</span><span class="p">(</span><span class="o">$</span><span class="n">header_key</span><span class="p">,</span> <span class="o">$</span><span class="n">header_value</span><span class="p">)</span> <span class="o">=</span> \<span class="n">explode</span><span class="p">(</span><span class="s2">&#34;: &#34;</span><span class="p">,</span> <span class="o">$</span><span class="n">item</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="o">$</span><span class="n">header_key</span> <span class="o">=</span> \<span class="n">strtolower</span><span class="p">(</span><span class="o">$</span><span class="n">header_key</span><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">header_key</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="s2">&#34;content-disposition&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="o">//</span> <span class="err">匹配到了</span> <span class="n">filename</span> <span class="err">说明是文件数据</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span>\<span class="n">preg_match</span><span class="p">(</span><span class="s1">&#39;/name=&#34;(.*?)&#34;; filename=&#34;(.*?)&#34;/i&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">header_value</span><span class="p">,</span> <span class="o">$</span><span class="n">match</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="o">$</span><span class="n">error</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="o">$</span><span class="n">tmp_file</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="o">//</span> <span class="err">获取文件大小</span>
</span></span><span class="line"><span class="cl">                <span class="o">$</span><span class="n">size</span> <span class="o">=</span> \<span class="n">strlen</span><span class="p">(</span><span class="o">$</span><span class="n">boundary_value</span><span class="p">);</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="n">tmp_upload_dir</span> <span class="o">=</span> <span class="n">HTTP</span><span class="p">::</span><span class="n">uploadTmpDir</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">tmp_upload_dir</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="o">$</span><span class="n">error</span> <span class="o">=</span> <span class="n">UPLOAD_ERR_NO_TMP_DIR</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="o">//</span> <span class="err">使用</span> <span class="n">tempnam</span> <span class="err">函数在临时目录下创建一个唯一文件名的临时文件</span>
</span></span><span class="line"><span class="cl">                    <span class="o">$</span><span class="n">tmp_file</span> <span class="o">=</span> \<span class="n">tempnam</span><span class="p">(</span><span class="o">$</span><span class="n">tmp_upload_dir</span><span class="p">,</span> <span class="s1">&#39;workerman.upload.&#39;</span><span class="p">);</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="k">if</span> <span class="p">(</span><span class="o">$</span><span class="n">tmp_file</span> <span class="o">===</span> <span class="bp">false</span> <span class="o">||</span> <span class="bp">false</span> <span class="o">==</span> \<span class="n">file_put_contents</span><span class="p">(</span><span class="o">$</span><span class="n">tmp_file</span><span class="p">,</span> <span class="o">$</span><span class="n">boundary_value</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                        <span class="o">$</span><span class="n">error</span> <span class="o">=</span> <span class="n">UPLOAD_ERR_CANT_WRITE</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="o">//</span> <span class="err">格式化上传的文件信息</span>
</span></span><span class="line"><span class="cl">                <span class="o">$</span><span class="n">files</span><span class="p">[</span><span class="o">$</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">array</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="s1">&#39;key&#39;</span> <span class="o">=&gt;</span> <span class="o">$</span><span class="n">match</span><span class="p">[</span><span class="mi">1</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;name&#39;</span> <span class="o">=&gt;</span> <span class="o">$</span><span class="n">match</span><span class="p">[</span><span class="mi">2</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;tmp_name&#39;</span> <span class="o">=&gt;</span> <span class="o">$</span><span class="n">tmp_file</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;size&#39;</span> <span class="o">=&gt;</span> <span class="o">$</span><span class="n">size</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;error&#39;</span> <span class="o">=&gt;</span> <span class="o">$</span><span class="n">error</span> <span class="o">//</span> <span class="err">错误</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="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="o">//</span> <span class="err">未匹配到</span> <span class="n">filename</span> <span class="err">说明是</span> <span class="n">POST</span> <span class="err">字段，需要解析</span> <span class="o">$</span><span class="n">_POST</span><span class="o">.</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span>\<span class="n">preg_match</span><span class="p">(</span><span class="s1">&#39;/name=&#34;(.*?)&#34;$/&#39;</span><span class="p">,</span> <span class="o">$</span><span class="n">header_value</span><span class="p">,</span> <span class="o">$</span><span class="n">match</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="o">$</span><span class="n">this</span><span class="o">-&gt;</span><span class="n">_data</span><span class="p">[</span><span class="s1">&#39;post&#39;</span><span class="p">][</span><span class="o">$</span><span class="n">match</span><span class="p">[</span><span class="mi">1</span><span class="p">]]</span> <span class="o">=</span> <span class="o">$</span><span class="n">boundary_value</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="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="s2">&#34;content-type&#34;</span><span class="p">:</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="n">files</span><span class="p">[</span><span class="o">$</span><span class="n">key</span><span class="p">][</span><span class="s1">&#39;type&#39;</span><span class="p">]</span> <span class="o">=</span> \<span class="n">trim</span><span class="p">(</span><span class="o">$</span><span class="n">header_value</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><p>switch 中的逻辑就是判断 $header_key 的值，然后执行相应的操作。</p>
<ul>
<li>content-disposition: 通过正则判断是否为文件数据，如果是文件就将数据块的值写入临时文件中，否则将字段和值保存到存放 POST 数据的数组中。</li>
<li>content-type: 记录文件的类型。</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>浅入浅出 HTTP 协议</title>
      <link>https://her-cat.com/posts/2021/04/17/what-is-http-protocol/</link>
      <pubDate>Sat, 17 Apr 2021 23:31:38 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/04/17/what-is-http-protocol/</guid>
      <description>HTTP 消息是服务器和客户端之间交换数据的方式。有两种类型的消息︰ 请求（requests）--由客户端发送用来触发一个服务器上的动作</description>
      <content:encoded><![CDATA[<p>先粘一段来自 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Messages">MDN</a> 的解释：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">HTTP 消息是服务器和客户端之间交换数据的方式。有两种类型的消息︰ 请求（requests）--由客户端发送用来触发一个服务器上的动作；响应（responses）--来自服务器的应答。
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">HTTP消息由采用 ASCII 编码的多行文本构成。
</span></span></code></pre></div><p>说白了就是客户端与服务端通信的协议，就像人与人之间进行交流，大家都要用相同的语言（协议）才能进行沟通（通信）。</p>
<h2 id="get-请求">GET 请求</h2>
<p>来看一个简单的 GET 请求协议的数据：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">GET /index.html?name=her-cat HTTP/1.1\r\n
</span></span><span class="line"><span class="cl">Host: 127.0.0.1:8081\r\n
</span></span><span class="line"><span class="cl">Connection: keep-alive\r\n
</span></span><span class="line"><span class="cl">Accept: text/html\r\n
</span></span><span class="line"><span class="cl">\r\n
</span></span></code></pre></div><blockquote>
<p>这里通过多行展示只是为了看起来更清晰，<strong>实际上不会有视觉上的换行</strong>。</p></blockquote>
<p>第一行是 <strong>请求行（Request Line）</strong>，由请求方法、请求地址（包含请求参数）、HTTP 协议版本三部分组成，通过空格分隔；服务端只需要解析该行就可以知道客户端通过什么方式取哪里的数据。</p>
<p>第二、三行是 <strong>请求头（Request Header）</strong>，每行以 <code>字段: 值</code> 方式组成，用来携带一些请求信息。比如 Host 就是目标主机的地址和端口号，Connection 就是连接方式。</p>
<p>第四行是一个 <strong>CRLF</strong>（又称回车换行符：\r\n），用来表示请求头信息到此结束。</p>
<p>在传输过程中协议的数据实际是这样的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">GET /index.html?name=her-cat HTTP/1.1\r\nHost: 127.0.0.1:8081\r\nConnection: keep-alive\r\nAccept: text/html\r\n\r\n
</span></span></code></pre></div><p>可以看到最后有两个连续的 CRLF，第一个是上一行的结束标识，第二个是请求头结束标识。</p>
<p>通过判断数据中是否包含两个连续的 CRLF 来确定 HTTP 请求头是否读取完毕。</p>
<p>下面是 Workerman 的实现：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">// source: Workerman/Protocols/Http.php
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">/**
</span></span><span class="line"><span class="cl"> * 返回值为 0 有两种情况：
</span></span><span class="line"><span class="cl"> *   1) 如果断开了连接，表示不再接收数据
</span></span><span class="line"><span class="cl"> *   2) 如果未断开连接，表示需要继续接收数据
</span></span><span class="line"><span class="cl"> *
</span></span><span class="line"><span class="cl"> * 返回值大于 0 表示 HTTP 协议包的大小或请求体的大小
</span></span><span class="line"><span class="cl"> *
</span></span><span class="line"><span class="cl"> */
</span></span><span class="line"><span class="cl">public static function input($recv_buffer, TcpConnection $connection)
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    // 判断收到的数据中是否包含两个连续的 CRLF
</span></span><span class="line"><span class="cl">    $crlf_pos = \strpos($recv_buffer, &#34;\r\n\r\n&#34;);
</span></span><span class="line"><span class="cl">    // false 说明不包含，需要继续接收数据
</span></span><span class="line"><span class="cl">    if (false === $crlf_pos) {
</span></span><span class="line"><span class="cl">        // 判断请求头数据长度是否超限
</span></span><span class="line"><span class="cl">        if ($recv_len = \strlen($recv_buffer) &gt;= 16384) {
</span></span><span class="line"><span class="cl">            // 超限则断开连接并响应 413 状态码
</span></span><span class="line"><span class="cl">            $connection-&gt;close(&#34;HTTP/1.1 413 Request Entity Too Large\r\n\r\n&#34;);
</span></span><span class="line"><span class="cl">            return 0;
</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><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 请求头长度，+4 是将两个 CRLF 的长度算上
</span></span><span class="line"><span class="cl">    $head_len = $crlf_pos + 4;
</span></span><span class="line"><span class="cl">    // 请求方式
</span></span><span class="line"><span class="cl">    $method = \strstr($recv_buffer, &#39; &#39;, true);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    if ($method === &#39;GET&#39; || $method === &#39;OPTIONS&#39; || $method === &#39;HEAD&#39; || $method === &#39;DELETE&#39;) {
</span></span><span class="line"><span class="cl">        // 这几种方法不需要接收请求体，直接返回请求头长度
</span></span><span class="line"><span class="cl">        return $head_len;
</span></span><span class="line"><span class="cl">    } else if ($method !== &#39;POST&#39; &amp;&amp; $method !== &#39;PUT&#39;) {
</span></span><span class="line"><span class="cl">        // 非法的请求方式
</span></span><span class="line"><span class="cl">        $connection-&gt;close(&#34;HTTP/1.1 400 Bad Request\r\n\r\n&#34;, true);
</span></span><span class="line"><span class="cl">        return 0;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 到这里说明本次是 POST 或 PUT 请求，在后面的 POST 请求中讲解。
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><h2 id="post-请求">POST 请求</h2>
<p>POST 请求其实跟 GET 差不多，只不过请求方法是 POST，多了一个请求体。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">POST / HTTP/1.1\r\n
</span></span><span class="line"><span class="cl">Host: 127.0.0.1:8080\r\n
</span></span><span class="line"><span class="cl">Connection: keep-alive\r\n
</span></span><span class="line"><span class="cl">Content-Length: 28\r\n
</span></span><span class="line"><span class="cl">Accept: text/html\r\n
</span></span><span class="line"><span class="cl">Content-Type: application/x-www-form-urlencoded\r\n
</span></span><span class="line"><span class="cl">\r\n
</span></span><span class="line"><span class="cl">name=her-cat&amp;password=123456
</span></span></code></pre></div><p>name=her-cat&amp;password=123456 就是请求体的内容。</p>
<p><strong>Content-Length</strong> 字段用来表示请求体的大小，如果请求体为空，那么该值为 0。</p>
<p>在接收 HTTP 协议数据时，可以通过 Content-Length 字段判断请求数据是否接收完毕。</p>
<p>当 Content-Length 等于已接收的字节数时，说明已经读取完毕了。</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 input($recv_buffer, TcpConnection $connection)
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    ...上面解析 GET 请求的逻辑...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 获取请求头的内容
</span></span><span class="line"><span class="cl">    $header = \substr($recv_buffer, 0, $crlf_pos);
</span></span><span class="line"><span class="cl">    // 请求体的长度
</span></span><span class="line"><span class="cl">    $length = false;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 解析请求体的长度
</span></span><span class="line"><span class="cl">    if ($pos = \strpos($header, &#34;\r\nContent-Length: &#34;)) {
</span></span><span class="line"><span class="cl">        $length = $head_len + (int)\substr($header, $pos + 18, 10);
</span></span><span class="line"><span class="cl">    } else if (\preg_match(&#34;/\r\ncontent-length: ?(\d+)/i&#34;, $header, $match)) {
</span></span><span class="line"><span class="cl">        $length = $head_len + $match[1];
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    if ($length !== false) {
</span></span><span class="line"><span class="cl">        // 返回请求体的长度
</span></span><span class="line"><span class="cl">        return $length;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 未解析到 Content-Length 字段则断开连接并响应 400 状态码
</span></span><span class="line"><span class="cl">    $connection-&gt;close(&#34;HTTP/1.1 400 Bad Request\r\n\r\n&#34;, true);
</span></span><span class="line"><span class="cl">    return 0;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p><strong>Content-Type</strong> 字段表示请求体的类型，这个值可以通过 form 表单的 enctype 属性指定，有 <strong>application/x-www-form-urlencoded</strong>、 <strong>multipart/form-data</strong> 两种类型，下面分别介绍一下两种类型的区别。</p>
<h2 id="applicationx-www-form-urlencoded">application/x-www-form-urlencoded</h2>
<p>当 enctype 为 application/x-www-form-urlencoded 时，在发送到服务端之前，会先将表单的数据转为 <strong>名称=值</strong> 的格式，再通过 <strong>&amp;</strong> 符号将它们拼接起来。</p>
<p>如果名称或值有特殊字符，则会先将它们进行 urlencode（又叫百分号编码），编码方法很简单，在该字节 ASCII 码的 16 进制字符前面加 <strong>%</strong>。</p>
<p>比如 &amp; 字符，ASCII 码是 38，对应 16 进制是 26，那么 urlencode 编码结果是 %26。</p>
<p>用 PHP 实现该过程：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">$form_params = [
</span></span><span class="line"><span class="cl">    &#39;&amp;name&#39; =&gt; &#39;her-cat=&#39;,
</span></span><span class="line"><span class="cl">    &#39;password&#39; =&gt; &#39;123456&amp;&#39;,
</span></span><span class="line"><span class="cl">];
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">$data = [];
</span></span><span class="line"><span class="cl">foreach ($form_params as $name =&gt; $value) {
</span></span><span class="line"><span class="cl">    $data[] = urlencode($name).&#39;=&#39;.urlencode($value);
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">$body = implode(&#39;&amp;&#39;, $data);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">echo $body;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 输出：%26name=her-cat%3D&amp;password=123456%26
</span></span></code></pre></div><p>PHP 服务端接收到该数据后，可以使用 parse_str 函数得到表单数据。</p>
<blockquote>
<p>注意，application/x-www-form-urlencoded 类型下，请求体末尾没有 CRLF。</p></blockquote>
<h2 id="multipartform-data">multipart/form-data</h2>
<p>当 enctype 为 multipart/form-data 时，收到的 HTTP 数据：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">POST / HTTP/1.1\r\n
</span></span><span class="line"><span class="cl">Host: 127.0.0.1:8080\r\n
</span></span><span class="line"><span class="cl">Connection: keep-alive\r\n
</span></span><span class="line"><span class="cl">Content-Length: 243\r\n
</span></span><span class="line"><span class="cl">Content-Type: multipart/form-data; boundary=----WebKitFormBoundarySyX8l4XxjtjHAusG\r\n
</span></span><span class="line"><span class="cl">\r\n
</span></span><span class="line"><span class="cl">------WebKitFormBoundarySyX8l4XxjtjHAusG\r\n
</span></span><span class="line"><span class="cl">Content-Disposition: form-data; name=&#34;name&#34;\r\n
</span></span><span class="line"><span class="cl">\r\n
</span></span><span class="line"><span class="cl">her-cat\r\n
</span></span><span class="line"><span class="cl">------WebKitFormBoundarySyX8l4XxjtjHAusG\r\n
</span></span><span class="line"><span class="cl">Content-Disposition: form-data; name=&#34;file&#34;; filename=&#34;code.txt&#34;
</span></span><span class="line"><span class="cl">Content-Type: text/plain\r\n
</span></span><span class="line"><span class="cl">\r\n
</span></span><span class="line"><span class="cl">hello her-cat\r\n
</span></span><span class="line"><span class="cl">------WebKitFormBoundarySyX8l4XxjtjHAusG--\r\n
</span></span></code></pre></div><p>相对于上一种类型，multipart/form-data 要复杂一些，因为这种类型不仅仅可以用来提交表单数据，本文的重点“文件上传”也是用的该类型，只不过它们的格式有一点点不同。</p>
<p>可以看到 <strong>Content-Type</strong> 中除了 multipart/form-data，还多了 boundary=&mdash;-WebKitFormBoundarySyX8l4XxjtjHAusG。</p>
<p>boundary 翻译过来就是边界的意思，它表示了数据块在请求体中的边界，后面的值就是分割请求体中数据块的边界值。</p>
<p>在边界值的前面添加 <code>--</code> 才是在请求体中实际的边界值。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">  ----WebKitFormBoundarySyX8l4XxjtjHAusG // 请求头中的边界值
</span></span><span class="line"><span class="cl">------WebKitFormBoundarySyX8l4XxjtjHAusG // 请求体中的边界值
</span></span></code></pre></div><blockquote>
<p>boundary 的值并非是固定的，可以是 1 到 70 个字符组成的随机字符串，不同的浏览器中 boundary 的生成规则也不一样。</p></blockquote>
<h3 id="content-disposition">Content-Disposition</h3>
<p><strong>Content-Disposition</strong> 存储了数据块的内容信息，form-data 表示是表单数据，name 字段表示该数据块的名称，如果有 filename 字段说明该数据块是用来存储文件上传的数据，filename 字段存储的是文件名称。</p>
<h3 id="content-type">Content-Type</h3>
<p><strong>Content-Type</strong> 字段只会在文件上传时才会存在，用于表示数据块的内容类型。比如 text/plain 表示纯文本、image/jpeg 表示 jpg 图片、video/mp4 表示视频。</p>
<p>详见：<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types">MIME 类型
</a></p>
<p>紧接着一个空白行（实际上是 CRLF）后面的就是数据块的内容。如果数据块是普通的表单数据，这里就是它的值，如果是文件上传，那么这里就是文件的内容了。</p>
<p>最后，通过在边界值的后面添加 <code>--</code> 表示请求体结束。</p>
<h2 id="总结">总结</h2>
<p>这篇文章其实是 “Workerman 源码分析：基于 HTTP 协议实现文件上传” 的前半部分，写的时候发现大部分都是在讲 HTTP 协议，索性分成了两篇文章。</p>
<blockquote>
<p>本文对于 HTTP 协议的描述仅仅是一小部分，如果想要深入的了解，推荐阅读 MDN 的文档。</p></blockquote>
<p>参考链接：</p>
<ul>
<li><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP">HTTP | MDN</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/2021/03/23/the-way-for-phper-to-advance/</link>
      <pubDate>Tue, 23 Mar 2021 17:39:56 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/03/23/the-way-for-phper-to-advance/</guid>
      <description>已经 1368 年了，你扪胸自问，没有了 Nginx 的你，还能用 PHP 做什么。有一些高端的刁民会愤怒地说：“有 Swoole 啊，有 Workerman 啊！”，那么，有两个问题需要回答</description>
      <content:encoded><![CDATA[<p>原文：<a href="http://t.ti-node.com/thread/6494805245070147584">没有Nginx，你还能做什么？</a></p>
<p>PHP 程序员的未来不是 Java，Java 拯救不了你。</p>
<p>已经 1368 年了，你扪胸自问，没有了 Nginx 的你，还能用 PHP 做什么。有一些高端的刁民会愤怒地说：“有 Swoole 啊，有 Workerman 啊！”，那么，有两个问题需要回答：</p>
<p>你可不可以用 Swoole 或 Workerman 去逐渐实现并代替贵司现有 PHP 业务
如果可以更换，除了你之外的其他同事们需要花费多少精力和时间
认真思考一下，仿佛感觉 FPM 就是 PHP 的业界毒瘤，不过老话说得好：能用就行…</p>
<p>不说静态语言，就说脚本语言而言，拿同行 Python 相比，你看人家 Python，多么的均衡多么的全面，进程、线程、IO、Stream 什么都没有拉下，一句话总结一下就是：既没有明显缺点，也没有明显优点，什么都能做。</p>
<p>你们知道么，能做到&quot;既没有明显缺点，也没有明显优点，什么都能做”是多么的困难的一件事。</p>
<p>搞 Python 的同行们，除了能用 Flask 码 Web，也能用 Tornado 搞异步非阻塞，能够运用线程池来做一些 task；相比之下，作为同行的我们，似乎除了会在 FPM 或者 apache_mod 下搞一搞增删改查，似乎别的什么也做不了了，而且在接收新概念的时候，不太容易能接纳（后半句科班生以及优秀的非科班生忽略）。</p>
<p>除了 Python 外，大举入侵的 Nodejs，能够很快让原来的娱乐圈的同行们很快融入切换到后端圈来，而且天生的异步非阻塞优势能够让他们写出 QPS 很高的 Web 程序。</p>
<p>Java，恕我直言，实际上 PHP 压根就不具备和 Java 对比的资格，别玻璃，事实如此，PHP 被 Java 按在地上摩擦，那为啥文章开头你为啥说…我就是讨厌 Java，个人偏见，仅此而已…</p>
<p>回应文章标题的话，Nodejs 压根不需要 Nginx，而 Python 用 Tornado 也是完全 O jb K！当然了，PHP 也能这么做，然后请回到文章开头第五行和第六行。归根结底，都是生态问题导致的。我不能从从业者质量问题上去理解这个问题…</p>
<p>PHP7 似乎是 PHP 的奋力一击，性能猛地提升了一倍。然而，以我目前的认知水准，似乎总有强弩之末的赶脚。PHP 的强项在 Web，这门为 Web 而生的语言似乎还没有做好拥抱新时代的准备。</p>
<p>都1368年了，PHPer 该如何提升自己？</p>
<p>第一：还请继续深入研究 PHP，打好 PHP 自身的基础，PHP 的 SPL 库系列请仔细研究；PHP 的 socket 模块以及 pcntl 模块，一定要研究尝试一下，请尝试学习使用 PHP cli 模式去运行 PHP，上面这几点都是针对纯语言方向的研究；然后，最好尝试从工程代码组织角度去理解和学习设计模式和面向对象 OOP，因为干巴巴地背诵设计模式，压根理解不了。如果可以，请尝试使用 Swoole 或者 Workerman，推荐 Swoole，因为折腾 Swoole 的过程会让你知道很多你需要补充的知识点</p>
<p>第二：请接纳一门新的语言。首先推荐 Golang，然后是 Java，其次是 Nodejs，终极杀招是 C/C++。不太严格地讲，编程语言分静态编译或动态脚本语言，所以我不建议再搞新的脚本语言，比如 Nodejs 或 Python 甚至 Ruby 之类，你既然要花费时间和精力去补充新鲜血液，不妨尝试 Golang。作为终极大杀器，如果你对自己足够狠，请深入研究 C 语言</p>
<p>第三：请深入研究数据结构，了解了数据结构，很多东西就会理解了。然后基础算法，注意是基础算法，那些脑筋急转弯就省省得了。现有的这些基础算法已经是人类智慧的结晶了，能够熟练运用就非常不错了，推荐书籍：《大话数据结构》</p>
<p>上面三点如果研究了一段时间，已经有所积累了的话，准备下面的几个步骤：</p>
<p>第四：深入研究一下 MySQL 和 Redis。MySQL 请购买《MySQL 技术内幕：innodb 存储引擎》和《高性能 MySQL》两本书，Redis 请购买《Redis 设计与实现》。有了前面三点累计的成果，你会对以前面试前需要背诵的什么 Mysql 索引优化原则了然于胸，不用背诵了，因为他就是应该是那样的。</p>
<p>实际上，你第四步进行一个周期后，还是会有一些疑惑，确实理解不了，只能靠背诵和记忆，无妨。</p>
<p>第五：终究绕不开的还是学习 CLang 和使用 Linux 操作系统。你需要准备两本书，按照学习先后顺序，分别是《C Primer Plus》和《Unix 环境高级编程》。这地方有一个巨大的错觉，就是读完一遍《C Primer Plus》后就觉得自己会 CLang 了，有这种优越感的，请你尝试用 CLang 做个什么东西出来？然后你发现似乎真的什么也做不了，这会儿就可以步入到《Unix 环境高级编程》的节奏了，在这里你才能逐渐发现 CLang 可以做些什么。一个流程完毕后，你再回到第四步，试试？第一次看第四步的时候遗留的问题是不是可以搞定一部分了？</p>
<p>再往下，就没有了，到了这一步，实际上大多数人自己已经方向是什么了。说到底都是基础，一切基于基础之上的上层应用都是海市蜃楼，犹如过眼云烟，你今天背过这个，明天就会冒出来新的。今天他叫 Node，明天他就叫 Deno，然而不变的永远是基于事件监听的异步非阻塞 IO…</p>
]]></content:encoded>
    </item>
    <item>
      <title>优化 Workerman 检查主进程是否存活的逻辑</title>
      <link>https://her-cat.com/posts/2021/03/17/optimize-workerman-to-check-whether-the-main-process-is-alive/</link>
      <pubDate>Wed, 17 Mar 2021 11:40:49 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/03/17/optimize-workerman-to-check-whether-the-main-process-is-alive/</guid>
      <description>新增了判断进程是否为 Workerman 进程的逻辑，从而优化了确定主进程是否存活的准确性。</description>
      <content:encoded><![CDATA[<p>主要新增了判断进程是否为 Workerman 进程的逻辑，从而优化了确定主进程是否存活的准确性。</p>
<h2 id="发现问题">发现问题</h2>
<p>年前逛 GitHub 的时候，发现 Workerman 有一个 2017 年打开的 Issue：<a href="https://github.com/walkor/Workerman/issues/125">already running</a>，原文如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Where is the problem?! I reboot the server and it is the first time I want to run workerman
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">php index.php start -d
</span></span><span class="line"><span class="cl">The result is
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Workerman[index.php] start in DAEMON mode
</span></span><span class="line"><span class="cl">Workerman[index.php] already running
</span></span></code></pre></div><p>大概意思就是重启服务器之后，第一次启动 Workerman 会提示已经在运行了，但实际上并没有运行。</p>
<p>因为重启服务器之后，保存 Workerman 主进程 PID 的文件仍保留在磁盘上。</p>
<p>正常情况下，Workerman 退出时会清理掉这个文件，但是该用户重启服务器后文件并没有被清理，导致 Workerman 误认为已经在运行中。</p>
<p>作者给出了一个补救方法：手动删除记录主进程 PID 的文件。虽然临时解决了问题，但是每次出现都要去手动处理一下，感觉不太友好。</p>
<p>要想解决这个问题，首先得弄清楚两个问题：</p>
<ul>
<li>为什么 Workerman 没有清理 PID 文件？</li>
<li>为什么重启服务器后启动 Workerman 提示已经在运行中？</li>
</ul>
<h2 id="workerman-判断是否已运行的逻辑">Workerman 判断是否已运行的逻辑</h2>
<p>Workerman 在启动的时候会生成一个文件，用于记录主进程的 PID。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">// Start file.
</span></span><span class="line"><span class="cl">$backtrace        = \debug_backtrace();
</span></span><span class="line"><span class="cl">static::$_startFile = $backtrace[\count($backtrace) - 1][&#39;file&#39;];
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 生成文件名
</span></span><span class="line"><span class="cl">$unique_prefix = \str_replace(&#39;/&#39;, &#39;_&#39;, static::$_startFile);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 保存记录主进程 PID 的文件路径
</span></span><span class="line"><span class="cl">if (empty(static::$pidFile)) {
</span></span><span class="line"><span class="cl">    static::$pidFile = __DIR__ . &#34;/../$unique_prefix.pid&#34;;
</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">static::setProcessTitle(static::$processTitle . &#39;: master process  start_file=&#39; . static::$_startFile);
</span></span></code></pre></div><p>然后检查 Workerman 是否已经在运行中。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">// 获取主进程的 PID，如果文件不存在或者不是一个正常的文件则返回 0
</span></span><span class="line"><span class="cl">$master_pid      = \is_file(static::$pidFile) ? \file_get_contents(static::$pidFile) : 0;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 如果 PID 存在就给它发送一个信号 `0`，信号量 `0` 类似于 ping，用于检测进程是否存活
</span></span><span class="line"><span class="cl">// 然后判断当前进程 PID 是否不等于文件中记录的 PID（不相等说明 Workerman 已经在运行中，但是又再次执行命令了）
</span></span><span class="line"><span class="cl">$master_is_alive = $master_pid &amp;&amp; \posix_kill($master_pid, 0) &amp;&amp; \posix_getpid() !== $master_pid;
</span></span><span class="line"><span class="cl">if ($master_is_alive) {
</span></span><span class="line"><span class="cl">    // 如果主进程存活并且执行的命令为 start，提示 Workerman 正在运行中并退出
</span></span><span class="line"><span class="cl">    if ($command === &#39;start&#39;) {
</span></span><span class="line"><span class="cl">        static::log(&#34;Workerman[$start_file] already running&#34;);
</span></span><span class="line"><span class="cl">        exit;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">} elseif ($command !== &#39;start&#39; &amp;&amp; $command !== &#39;restart&#39;) {
</span></span><span class="line"><span class="cl">    // 如果主进程未存活且执行的命令不是 start 或 restart，则提示 Workerman 未运行并退出
</span></span><span class="line"><span class="cl">    static::log(&#34;Workerman[$start_file] not run&#34;);
</span></span><span class="line"><span class="cl">    exit;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>当一系列检查通过后，开始保存主进程的 PID。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">protected static function saveMasterPid()
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    // 非 Linux 系统不保存 PID
</span></span><span class="line"><span class="cl">    if (static::$_OS !== \OS_TYPE_LINUX) {
</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">    // 获取主进程的 PID
</span></span><span class="line"><span class="cl">    static::$_masterPid = \posix_getpid();
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 将主进程的 PID 写入到文件中
</span></span><span class="line"><span class="cl">    if (false === \file_put_contents(static::$pidFile, static::$_masterPid)) {
</span></span><span class="line"><span class="cl">        throw new Exception(&#39;can not save pid to &#39; . static::$pidFile);
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>当收到 <code>SIGINT</code>、<code>SIGTERM</code>、<code>SIGHUP</code> 等信号时，将进程状态设置为 <code>STATUS_SHUTDOWN</code> 并通知子进程退出。</p>
<p>如果主进程的状态为 <code>STATUS_SHUTDOWN</code> 并且所有子进程已经退出，就会去清除 PID 文件并退出。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">protected static function exitAndClearAll()
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    foreach (static::$_workers as $worker) {
</span></span><span class="line"><span class="cl">        $socket_name = $worker-&gt;getSocketName();
</span></span><span class="line"><span class="cl">        if ($worker-&gt;transport === &#39;unix&#39; &amp;&amp; $socket_name) {
</span></span><span class="line"><span class="cl">            list(, $address) = \explode(&#39;:&#39;, $socket_name, 2);
</span></span><span class="line"><span class="cl">            @\unlink($address);
</span></span><span class="line"><span class="cl">        }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    // 删除 PID 文件
</span></span><span class="line"><span class="cl">    @\unlink(static::$pidFile);
</span></span><span class="line"><span class="cl">    static::log(&#34;Workerman[&#34; . \basename(static::$_startFile) . &#34;] has been stopped&#34;);
</span></span><span class="line"><span class="cl">    if (static::$onMasterStop) {
</span></span><span class="line"><span class="cl">        \call_user_func(static::$onMasterStop);
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    // 退出进程
</span></span><span class="line"><span class="cl">    exit(0);
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><h2 id="复现问题">复现问题</h2>
<p>看到这有人肯定会问了，这不是有清理 PID 文件的机制吗？为什么还能从文件中获取到 PID？</p>
<p>我先在虚拟机中进行了测试，服务器在重启的时候会发送 <code>SIGTERM</code> 信号通知进程，Workerman 可以正常退出并且清理 PID 文件。</p>
<p>但是在云服务器中测试的时候，如果勾选了<code>强制重启</code>会导致 Workerman 收不到信号，也就不能够执行 exitAndClearAll() 里面的代码了。</p>
<blockquote>
<p>来自服务器厂商的提醒：强制重启会导致云服务器中未保存的数据丢失，请谨慎操作。</p></blockquote>
<p>为什么给 PID 文件中的进程发信号还会返回 true 呢？</p>
<p>服务器在重启后，另一个进程启动了，它的 PID 与 Workerman 的旧 PID 相同（没错，就是这么巧）。</p>
<p>所以在检查主进程是否存活时，还要判断该进程是否为 Workerman 的进程。</p>
<h2 id="解决问题">解决问题</h2>
<p>Issue 中 <a href="https://github.com/walkor/Workerman/issues/125#issuecomment-468289378">@detain</a> 给出了一个使用 shell 脚本的解决方法：</p>
<p>To check to see if its running and safely remove pid files can do something like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">if [ $(php start.php status 2&gt;/dev/null|grep &#34;PROCESS STATUS&#34;|wc -l) -eq 0 ]; then
</span></span><span class="line"><span class="cl">  # clean up old run, remove pid file or run a stop command?
</span></span><span class="line"><span class="cl">  php start.php stop
</span></span><span class="line"><span class="cl">  php start.php start -d
</span></span><span class="line"><span class="cl">fi
</span></span></code></pre></div><p>先通过 <code>php start.php status</code> 命令获取 Workerman 的状态，然后统计 <code>PROCESS STATUS</code> 出现的次数（每个进程都会有一个 PROCESS STATUS），如果次数为 0 说明没有运行中的进程，就可以执行停止命令，再启动 Workerman。</p>
<p>受到这个方法启发，然后基于它改造了 Workerman 检查主进程是否存的逻辑，一顿复制粘贴之后就有了第一版的代码：</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">Get</span> <span class="n">master</span> <span class="n">process</span> <span class="n">PID</span><span class="o">.</span>
</span></span><span class="line"><span class="cl"><span class="o">$</span><span class="n">master_pid</span>      <span class="o">=</span> \<span class="n">is_file</span><span class="p">(</span><span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">pidFile</span><span class="p">)</span> <span class="err">?</span> \<span class="n">file_get_contents</span><span class="p">(</span><span class="k">static</span><span class="p">::</span><span class="o">$</span><span class="n">pidFile</span><span class="p">)</span> <span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="o">//</span> <span class="n">Master</span> <span class="n">is</span> <span class="n">still</span> <span class="n">alive</span><span class="err">?</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="k">static</span><span class="p">::</span><span class="n">checkMasterIsAlive</span><span class="p">(</span><span class="o">$</span><span class="n">master_pid</span><span class="p">))</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">command</span> <span class="o">===</span> <span class="s1">&#39;start&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">static</span><span class="p">::</span><span class="nb">log</span><span class="p">(</span><span class="s2">&#34;Workerman[$start_file] already running&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">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><span class="line"><span class="cl">
</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="n">Check</span> <span class="n">master</span> <span class="n">process</span> <span class="n">is</span> <span class="n">alive</span>
</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="n">param</span> <span class="o">$</span><span class="n">master_pid</span>
</span></span><span class="line"><span class="cl"> <span class="o">*</span> <span class="err">@</span><span class="k">return</span> <span class="ne">bool</span>
</span></span><span class="line"><span class="cl"> <span class="o">*/</span>
</span></span><span class="line"><span class="cl"><span class="n">protected</span> <span class="k">static</span> <span class="n">function</span> <span class="n">checkMasterIsAlive</span><span class="p">(</span><span class="o">$</span><span class="n">master_pid</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">master_pid</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">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="o">$</span><span class="n">master_is_alive</span> <span class="o">=</span> <span class="o">$</span><span class="n">master_pid</span> <span class="o">&amp;&amp;</span> \<span class="n">posix_kill</span><span class="p">(</span><span class="o">$</span><span class="n">master_pid</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&amp;&amp;</span> \<span class="n">posix_getpid</span><span class="p">()</span> <span class="o">!==</span> <span class="o">$</span><span class="n">master_pid</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">master_is_alive</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">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="o">//</span> <span class="n">Master</span> <span class="n">process</span> <span class="n">will</span> <span class="n">send</span> <span class="n">SIGUSR2</span> <span class="k">signal</span> <span class="n">to</span> <span class="n">all</span> <span class="n">child</span> <span class="n">processes</span><span class="o">.</span>
</span></span><span class="line"><span class="cl">    \<span class="n">posix_kill</span><span class="p">(</span><span class="o">$</span><span class="n">master_pid</span><span class="p">,</span> <span class="n">SIGUSR2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="o">//</span> <span class="n">Sleep</span> <span class="mi">1</span> <span class="n">second</span><span class="o">.</span>
</span></span><span class="line"><span class="cl">    \<span class="n">sleep</span><span class="p">(</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">return</span> <span class="n">stripos</span><span class="p">(</span><span class="k">static</span><span class="p">::</span><span class="n">formatStatusData</span><span class="p">(),</span> <span class="s1">&#39;PROCESS STATUS&#39;</span><span class="p">)</span> <span class="o">!==</span> <span class="bp">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>逻辑跟 shell 脚本差不多，就不再解释了。这个解决方法也有两个小问题：</p>
<ul>
<li>执行命令时会延迟一秒钟，因为执行一些命令的时候需要 sleep 一秒钟等待子进程写入状态信息。</li>
<li>如果另一个进程的 PID 与 Workerman 的旧 PID 相同，它将接收 SIGUSR2 信号。</li>
</ul>
<p>感觉在启动的时候慢一秒应该还能接受，只要处理请求的时候不慢就行了，于是就提交了 PR，并描述了这一段代码的作用及带来的问题。</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">Fixed</span><span class="p">:</span> <span class="c1">#125</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">There</span> <span class="n">is</span> <span class="n">a</span> <span class="n">problem</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">another</span> <span class="n">process</span> <span class="n">starts</span> <span class="ow">and</span> <span class="n">the</span> <span class="n">pid</span> <span class="n">is</span> <span class="n">the</span> <span class="n">same</span> <span class="n">as</span> <span class="n">the</span> <span class="n">workerman</span><span class="s1">&#39;s old pid, it will receive the SIGUSR2 signal.</span>
</span></span></code></pre></div><p>没过多久作者便在 PR 下面回复了我：</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">Thank</span> <span class="n">you</span> <span class="k">for</span> <span class="n">your</span> <span class="n">pr</span><span class="o">.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">There</span> <span class="n">is</span> <span class="n">a</span> <span class="n">problem</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">another</span> <span class="n">process</span> <span class="n">starts</span> <span class="ow">and</span> <span class="n">the</span> <span class="n">pid</span> <span class="n">is</span> <span class="n">the</span> <span class="n">same</span> <span class="n">as</span> <span class="n">the</span> <span class="n">workerman</span><span class="s1">&#39;s old pid, it will receive the SIGUSR2 signal.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">If</span> <span class="n">the</span> <span class="n">PR</span> <span class="n">is</span> <span class="n">merged</span><span class="p">,</span> <span class="n">some</span> <span class="n">commands</span> <span class="n">will</span> <span class="n">be</span> <span class="n">delayed</span> <span class="n">by</span> <span class="n">one</span> <span class="n">second</span><span class="o">.</span>
</span></span><span class="line"><span class="cl"><span class="n">I</span> <span class="n">think</span> <span class="n">a</span> <span class="n">better</span> <span class="n">way</span> <span class="n">is</span> <span class="n">to</span> <span class="n">read</span> <span class="o">/</span><span class="n">proc</span><span class="o">/</span><span class="n">PID</span> <span class="n">information</span> <span class="n">to</span> <span class="n">determine</span> <span class="n">whether</span> <span class="n">it</span> <span class="n">is</span> <span class="n">a</span> <span class="n">PHP</span> <span class="n">process</span> <span class="ow">or</span> <span class="n">a</span> <span class="n">workerman</span> <span class="n">process</span><span class="o">.</span>
</span></span></code></pre></div><p>先说了延迟一秒钟的问题，接着又给出了更好的解决方法：读取 <code>/proc/PID</code> 信息来确定它是其它进程还是 Workerman 进程。</p>
<p>搜索资料之后发现可以读取 <code>/proc/PID/cmdline</code> 得到启动进程时的命令。</p>
<p>Workerman 在启动时会调用 <code>Worker：:setProcessTitle()</code> 方法覆盖 <code>cmdline</code> 的内容，所以实际上得到的是 Workerman 的进程名称。</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="n">Set</span> <span class="n">process</span> <span class="n">name</span><span class="o">.</span>
</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="n">param</span> <span class="n">string</span> <span class="o">$</span><span class="n">title</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">void</span>
</span></span><span class="line"><span class="cl"> <span class="o">*/</span>
</span></span><span class="line"><span class="cl"><span class="n">protected</span> <span class="k">static</span> <span class="n">function</span> <span class="n">setProcessTitle</span><span class="p">(</span><span class="o">$</span><span class="n">title</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="err">设置一个空的错误处理函数，避免提示</span> <span class="n">PHP</span> <span class="err">错误</span>
</span></span><span class="line"><span class="cl">    \<span class="n">set_error_handler</span><span class="p">(</span><span class="n">function</span><span class="p">(){});</span>
</span></span><span class="line"><span class="cl">    <span class="o">//</span> <span class="o">&gt;=</span><span class="n">php</span> <span class="mf">5.5</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span>\<span class="n">function_exists</span><span class="p">(</span><span class="s1">&#39;cli_set_process_title&#39;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        \<span class="n">cli_set_process_title</span><span class="p">(</span><span class="o">$</span><span class="n">title</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="o">//</span> <span class="n">Need</span> <span class="n">proctitle</span> <span class="n">when</span> <span class="n">php</span><span class="o">&lt;=</span><span class="mf">5.5</span> <span class="o">.</span>
</span></span><span class="line"><span class="cl">    <span class="n">elseif</span> <span class="p">(</span>\<span class="n">extension_loaded</span><span class="p">(</span><span class="s1">&#39;proctitle&#39;</span><span class="p">)</span> <span class="o">&amp;&amp;</span> \<span class="n">function_exists</span><span class="p">(</span><span class="s1">&#39;setproctitle&#39;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        \<span class="n">setproctitle</span><span class="p">(</span><span class="o">$</span><span class="n">title</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="err">还原错误处理函数</span>
</span></span><span class="line"><span class="cl">    \<span class="n">restore_error_handler</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>默认情况下，进程名称将被设置为 WorkerMan: master process  start_file=启动文件的完整路径。</p>
<p>所以只需要判断 <code>cmdline</code> 是否<strong>包含</strong> <code>Worker::$processTitle</code> 就可以知道该进程是否为 Workerman 进程。</p>
<blockquote>
<p>cmdline可以保存多少个字符跟启动命令有关，比如启动命令是 <strong>php index.php start -d</strong>，那么进程名称就有可能被截取为 <strong>WorkerMan: worker proce</strong>，所以这里用的是包含而不是等于。</p></blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">protected static function checkMasterIsAlive($master_pid)
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    if (empty($master_pid)) {
</span></span><span class="line"><span class="cl">        return false;
</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">    $master_is_alive = $master_pid &amp;&amp; \posix_kill($master_pid, 0) &amp;&amp; \posix_getpid() !== $master_pid;
</span></span><span class="line"><span class="cl">    if (!$master_is_alive) {
</span></span><span class="line"><span class="cl">        return false;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 到了这里说明进程是存活的，但是不能保证这个进程是 Workerman 进程
</span></span><span class="line"><span class="cl">    // 需要读取进程信息才能确定，有任何一个步骤导致不能获取进程信息都要返回 true
</span></span><span class="line"><span class="cl">    // 因为根据上面的检测结果，进程是存活的
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    $cmdline = &#34;/proc/{$master_pid}/cmdline&#34;;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 进程信息不可读或设置的进程名为空
</span></span><span class="line"><span class="cl">    if (!is_readable($cmdline) || empty(static::$processTitle)) {
</span></span><span class="line"><span class="cl">        return true;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    $content = file_get_contents($cmdline);
</span></span><span class="line"><span class="cl">    // 未读取到进程信息
</span></span><span class="line"><span class="cl">    if (empty($content)) {
</span></span><span class="line"><span class="cl">        return true;
</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">    return stripos($content, static::$processTitle) !== false;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>再次提交，没过多久就收到了代码被合并的邮件。</p>
<h2 id="总结">总结</h2>
<p>回答一下上面提出的两个问题：</p>
<p>Q：为什么 Workerman 没有清理 PID 文件？</p>
<p>A：因为 Workerman 没有正常退出（强制关机、重启、断电）</p>
<p>Q：为什么重启服务器后启动 Workerman 提示已经在运行中？</p>
<p>A：因为服务器重启后，其他进程的 PID 与 Workerman 的旧 PID 相同，误认为是 Workerman 进程。</p>
<h2 id="相关链接">相关链接</h2>
<ul>
<li><a href="https://github.com/walkor/Workerman/issues/125">already running</a></li>
<li><a href="https://github.com/walkor/Workerman/pull/595">Optimize the logic of checking whether the master is alive</a></li>
</ul>
]]></content:encoded>
    </item><follow_challenge>
      <feedId>58021783493571598</feedId>
      <userId>56882619875632128</userId>
    </follow_challenge>
  </channel>
</rss>
