<?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>Pack - 她和她的猫</title>
    <link>https://her-cat.com/tags/pack/</link>
    <description>Pack的文章列表 - 她和她的猫</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, 01 Apr 2021 21:34:20 +0800</lastBuildDate>
    <atom:link href="https://her-cat.com/tags/pack/index.xml" rel="self" type="application/rss+xml" />
    <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><follow_challenge>
      <feedId>58021783493571598</feedId>
      <userId>56882619875632128</userId>
    </follow_challenge>
  </channel>
</rss>
