<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>twocucao (twocucao)</title>
    <link>https://ruby-china.org/twocucao</link>
    <description>少说话，多做事</description>
    <language>en-us</language>
    <item>
      <title>斗鱼弹幕助手 (Ruby 版本)</title>
      <description>&lt;h2 id="斗鱼弹幕助手"&gt;斗鱼弹幕助手&lt;/h2&gt;&lt;h2 id="0.前言"&gt;0.前言&lt;/h2&gt;
&lt;p&gt;前几天 (寒假前咯) 闲着无聊，看到舍友们都在看斗鱼 TV，虽然我对那些网络游戏都不是非常感兴趣，但是我突然间想到，如果我可以获取上面的弹幕内容，不就有点意思了么？&lt;/p&gt;
&lt;h2 id="1.分析阶段"&gt;1.分析阶段&lt;/h2&gt;
&lt;p&gt;如果我想要抓取网页上面的东西，无非就是两种方法&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用浏览器，手工（自己点击）或者非手工（使用 JS 脚本），存取我想要的东西。&lt;/li&gt;
&lt;li&gt;编写 HTTP 客户端（斗鱼无 HTTPS 通讯）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;第一种方法是万能的，但显然是不行的，原因如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;手动保存实在是不可行，程序员不为也。&lt;/li&gt;
&lt;li&gt;浏览器与本地交互有限，换而言之，也就是即使我抓取了对应的弹幕，我也没有办法解决持久化的问题。&lt;/li&gt;
&lt;li&gt;假设你选择的是 Chrome 或者 firefox 浏览器，也不是不能实现持久化，但这需要写扩展，Chrome 扩展没有写过，也不是很感兴趣。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;第二种方法显然是一个正常的程序员的做法。&lt;/p&gt;

&lt;p&gt;语言选用 Ruby&lt;/p&gt;

&lt;p&gt;写一个客户端，也就是写一个小爬虫，使用的场景：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;用户在终端执行命令&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;danmu
danmu douyu &lt;span class="o"&gt;[&lt;/span&gt;room_id/url]
&lt;span class="c"&gt;#比如&lt;/span&gt;
danmu douyu qiuri
danmu douyu http://www.douyutv.com/13861
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以在终端欣赏弹幕咯。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/52890-d238a6bba1d5b822.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="Screen Shot 2016-02-09 at 12.23.15 PM.png"&gt;&lt;/p&gt;

&lt;p&gt;回想一下抓取网站的方法&lt;/p&gt;

&lt;p&gt;四步走：请求网页（原始数据） - 提取数据（提纯数据） - 保存数据 - 分析数据&lt;/p&gt;

&lt;p&gt;很显然，只要解决了请求网页，其他的也就无非解析和 SQL 语句什么的。&lt;/p&gt;
&lt;h3 id="1.1.斗鱼TV弹幕抓取的思路确定"&gt;1.1.斗鱼 TV 弹幕抓取的思路确定&lt;/h3&gt;
&lt;p&gt;如果是像我上面说的那么简单，也就不必再写一篇文章。毕竟，网页小爬虫没有什么技术含量。分布式爬虫才有。&lt;/p&gt;

&lt;p&gt;通常情况下的网页小爬虫无非要解决如下问题：&lt;/p&gt;

&lt;p&gt;请求，如果对方有一定策略的反爬虫，那需要反反爬虫。比如，&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;header 带上 host，带上 refer，带上其他&lt;/li&gt;
&lt;li&gt;需要验证，那就申请用户名和密码，然后登陆&lt;/li&gt;
&lt;li&gt;如果在登录时期有防跨站机制，那就先获取一次登录页面，然后解析出 token，带上对应的 token 然后登陆。&lt;/li&gt;
&lt;li&gt;在程序中加入 Log，并且存到本地。防止出现各种各样的反爬虫机制 ban 掉了程序，从而方便进行下一步防反爬虫对策。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;并且，由于请求响应机制的存在，通常情况下，每一个请求对应一个响应，如果出错了，要么超时，要么有状态码，所以普通的 web 爬虫也相对而言比较容易些。&lt;/p&gt;

&lt;p&gt;那么，斗鱼 TV 的站点是不是这样子的就能够容易爬取呢？&lt;/p&gt;

&lt;p&gt;你猜到了，答案是“不是”。&lt;/p&gt;

&lt;p&gt;由于弹幕具有实时性，就决定了斗鱼 TV 的弹幕无法通过保存完整指定时间端弹幕的 XML（比如 BILIBILI 的一个视频弹幕是存在一段 xml 中的）或者 Json 数据来显示弹幕。要不然的话，那主播操作很出色的时候，观众的弹幕岂不是无法实时显示了么？&lt;/p&gt;

&lt;p&gt;那么，肯定就是 WebSocket 了，于是，我一如既往的打开 F12，查看网络流量。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;正如你想到的那样，没有任何的弹幕流量来往。一个 WebSocket 的消息都没有。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;那么，消息肯定是有的，但是消息并不是通过 HTTP 协议或者 WebSocket 协议传输的，那么问题会出在哪呢？&lt;/p&gt;

&lt;p&gt;分析前端的代码，找出获取弹幕的 JS 代码，苦于代码太多，找了很久没有找到。那也就是执行逻辑可能在 flash 里面。&lt;/p&gt;

&lt;p&gt;于是祭出大杀器 WireShark，抓一下流量。终于看到弹幕的样子了。&lt;/p&gt;

&lt;p&gt;是这样的。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/52890-6b49060675906004.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="douyutveachmsg.png"&gt;&lt;/p&gt;

&lt;p&gt;原来使用的是 Flash 的 Socket 功能。&lt;/p&gt;

&lt;p&gt;那么，我们只需要模拟 Socket 的每一条消息就好了。&lt;/p&gt;

&lt;p&gt;多分析几组数据，但还是对发送消息内容缺乏把握，特别是在用户认证，用户接收弹幕这一块。在搜索引擎上搜索了一阵，发现知乎上有个帖子，读完终于解了我的疑惑。&lt;/p&gt;

&lt;p&gt;地址为： &lt;a href="https://www.zhihu.com/question/29027665" rel="nofollow" target="_blank"&gt;https://www.zhihu.com/question/29027665&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在此基础上，省略若干消息分析过程。&lt;/p&gt;

&lt;p&gt;总结后得出斗鱼 TV 网站的服务器分布。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/52890-7a8ba097bf0b29fe.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="douyutvinfo.jpg"&gt;&lt;/p&gt;
&lt;h3 id="1.2.房间信息和弹幕认证服务器获取"&gt;1.2.房间信息和弹幕认证服务器获取&lt;/h3&gt;
&lt;p&gt;首先我们拿随便一个主播房间来说，比如，qiuri&lt;/p&gt;

&lt;p&gt;Ta 的房间链接分为两种&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://www.douyutv.com/qiuri" rel="nofollow" target="_blank"&gt;http://www.douyutv.com/qiuri&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.douyutv.com/id]" rel="nofollow" target="_blank"&gt;http://www.douyutv.com/id]&lt;/a&gt;[房间&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对这个主播房间页面请求，正常，所有的有用信息都不是放在 HTML 中渲染出来，而是有一条放在 HTML 中内置的 JS 脚本中，这是为了减少服务器渲染 HTML 的压力？可是渲染放在 JS 里面不也一样需要渲染？（不明白）总之，就是程序先加载没有具体数据填充页面，然后 JS 更新数据。&lt;/p&gt;

&lt;p&gt;内置的两段 JS 脚本，JS 脚本中有两个变量，该变量很容易转换成 JSON 数据，也就是两段 JSON 数据，一个是关于主播的个人信息，另一个是关于弹幕认证服务器的列表（该列表中的任意一个服务器均可以认证，但每一次请求主播页面得到的认证服务器列表都不一样）&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/52890-be4ccef08eeb929d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="Sc"&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/52890-dbc0407b43fc8408.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="Screen Shot 2016-02-09 at 1.01.51 PM.png"&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/52890-b1055eec1c804fa1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="Screen Shot 2016-02-09 at 12.44.01 PM.png"&gt;&lt;/p&gt;

&lt;p&gt;通过这步，我们就拿到了主播的信息以及弹幕服务器的认证地址，端口。&lt;/p&gt;
&lt;h3 id="1.3.发送Socket消息的流程简介"&gt;1.3.发送 Socket 消息的流程简介&lt;/h3&gt;
&lt;p&gt;我们通过抓包，分析那一大坨数据包，可以确定以下通过以下的流程便可以获取弹幕消息。（分析过程比较繁琐）&lt;/p&gt;

&lt;p&gt;首先建立两个 Socket。一个用于认证 (&lt;a href="/danmu_auth_socket" class="user-mention" title="@danmu_auth_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_auth_socket&lt;/a&gt;)，另一个用户获取弹幕 (&lt;a href="/danmu_client" class="user-mention" title="@danmu_client"&gt;&lt;i&gt;@&lt;/i&gt;danmu_client&lt;/a&gt;)。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;步骤 1:&lt;/strong&gt; &lt;a href="/danmu_auth_socket" class="user-mention" title="@danmu_auth_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_auth_socket&lt;/a&gt; 发送消息登陆，获取消息 1 解析出匿名用户的用户名，再获取消息 2 解析出 gid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;步骤 2:&lt;/strong&gt; &lt;a href="/danmu_auth_socket" class="user-mention" title="@danmu_auth_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_auth_socket&lt;/a&gt; 发送 qrl 消息，获取两个没有什么用的消息&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;步骤 3:&lt;/strong&gt; &lt;a href="/danmu_auth_socket" class="user-mention" title="@danmu_auth_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_auth_socket&lt;/a&gt; 发送 keeplive 消息&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;步骤 4:&lt;/strong&gt; &lt;a href="/danmu_socket" class="user-mention" title="@danmu_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_socket&lt;/a&gt; 发送伪登陆消息（所有匿名用户都一样只需要输入步骤一中用户名就行了，因为认证已经在上面做过了）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;步骤 5:&lt;/strong&gt; &lt;a href="/danmu_socket" class="user-mention" title="@danmu_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_socket&lt;/a&gt; 发送 join_group 消息需要步骤一中国的 gid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;步骤 6:&lt;/strong&gt; &lt;a href="/danmu_socket" class="user-mention" title="@danmu_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_socket&lt;/a&gt; 不断的 recv 消息就可以获取弹幕消息了&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;后面会详细解释&lt;/p&gt;
&lt;h3 id="2.1.消息Socket消息格式以及发送一条消息"&gt;2.1.消息 Socket 消息格式以及发送一条消息&lt;/h3&gt;
&lt;p&gt;既然是发消息，那么每条消息总是有些格式的。&lt;/p&gt;

&lt;p&gt;斗鱼的消息格式大致如下：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/52890-621b3027cea6e0e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="douyutveachmsg.png"&gt;&lt;/p&gt;

&lt;p&gt;每一条消息并遵循下面的格式：&lt;/p&gt;

&lt;p&gt;1.通信协议长度，后四个部分的长度，四个字节
2.第二部分与第一部分一样
3.请求代码，发送给斗鱼的话，内容为 0xb1,0x02, 斗鱼返回的代码为 0xb2,0x02
4.发送内容
5.末尾字节&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# -*- encoding : utf-8 -*-&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;
  &lt;span class="c1"&gt;# 向斗鱼发送的消息&lt;/span&gt;
  &lt;span class="c1"&gt;# 1.通信协议长度,后四个部分的长度,四个字节&lt;/span&gt;
  &lt;span class="c1"&gt;# 2.第二部分与第一部分一样&lt;/span&gt;
  &lt;span class="c1"&gt;# 3.请求代码,发送给斗鱼的话,内容为0xb1,0x02, 斗鱼返回的代码为0xb2,0x02&lt;/span&gt;
  &lt;span class="c1"&gt;# 4.发送内容&lt;/span&gt;
  &lt;span class="c1"&gt;# 5.末尾字节&lt;/span&gt;
  &lt;span class="c1"&gt;#pack('c*')是字节数组转字符串的一种诡异的转化方式&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'c*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@length.dup&lt;/span&gt;
    &lt;span class="vi"&gt;@magic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0xb1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'c*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@content&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
    &lt;span class="vi"&gt;@end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'c*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_s&lt;/span&gt;
    &lt;span class="vi"&gt;@length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@code&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@magic&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@content&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过封装，我们仅仅关注那些可见的字符串，也就是 Content 部分就可以了。
content 部分，也就是发送消息的内容，在文章后面将会详解。&lt;/p&gt;

&lt;p&gt;开启两个 Socket，一个用户认证，另一个用于弹幕的获取。&lt;/p&gt;

&lt;p&gt;用于用户弹幕认证的，是 2.1 中所说的认证服务器列表中任意一个。挑选出来一组 ip 和端口&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="/danmu_auth_socket" class="user-mention" title="@danmu_auth_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_auth_socket&lt;/a&gt; = TCPSocket.new &lt;a href="/auth_dst_ip" class="user-mention" title="@auth_dst_ip"&gt;&lt;i&gt;@&lt;/i&gt;auth_dst_ip&lt;/a&gt;,&lt;a href="/auth_dst_port" class="user-mention" title="@auth_dst_port"&gt;&lt;i&gt;@&lt;/i&gt;auth_dst_port&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;用户获取弹幕的只要为&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;danmu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;douyutv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8601&lt;/span&gt;
&lt;span class="n"&gt;danmu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;douyutv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8602&lt;/span&gt;
&lt;span class="n"&gt;danmu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;douyutv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12601&lt;/span&gt;
&lt;span class="n"&gt;danmu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;douyutv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12602&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四组域名：端口均可以作为如下的 DANMU_SERVER 和 PORT&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="/danmu_socket" class="user-mention" title="@danmu_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_socket&lt;/a&gt; = TCPSocket.new DANMU_SERVER,DANMU_PORT&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;发送一条消息只需如此&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"type@=loginreq/username@="&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="vi"&gt;@username&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s2"&gt;"/password@=1234567890123456/roomid@="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@room_id.to_s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
&lt;span class="n"&gt;all_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@danmu_socket.write&lt;/span&gt; &lt;span class="n"&gt;all_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把需要传输的字符串放进去就好了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;接下来，我们需处理上面说的六个步骤&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="2.2.发送消息详细流程之步骤一"&gt;2.2.发送消息详细流程之步骤一&lt;/h3&gt;
&lt;p&gt;发送消息内容为：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;type@=loginreq/username@=/ct@=0/password@=/roomid@=156277/devid@=DF9E4515E0EE766B39F8D8A2E928BB7C/rt@=1453795822/vk@=4fc6e613fc650a058757331ed6c8a619/ver@=20150929/&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我们需要注意的内容如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;表示消息的类型登陆消息为loginreq&lt;/span&gt;
&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="n"&gt;不需要&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;请求登陆以后系统会自动的返回对应的游客账号&lt;/span&gt;&lt;span class="err"&gt;。&lt;/span&gt;
&lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="n"&gt;不清楚什么意思&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;默认为0并无影响&lt;/span&gt;
&lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="n"&gt;不需要&lt;/span&gt;
&lt;span class="n"&gt;roomid&lt;/span&gt; &lt;span class="n"&gt;房间的id&lt;/span&gt;
&lt;span class="n"&gt;devid&lt;/span&gt; &lt;span class="n"&gt;为设备标识&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;无所谓&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;所以我们使用随机的UUID生成&lt;/span&gt;
&lt;span class="n"&gt;rt&lt;/span&gt; &lt;span class="n"&gt;应该是runtime吧&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;时间戳&lt;/span&gt;
&lt;span class="n"&gt;vk&lt;/span&gt; &lt;span class="n"&gt;为时间戳&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s2"&gt;"7oE9nPEG9xXV69phU31FYCLUagKeYtsF"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;devid的字符串拼接结果的MD5值&lt;/span&gt;&lt;span class="err"&gt;（&lt;/span&gt;&lt;span class="n"&gt;这个是参考了一篇文章&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;关于这一处我也不大明白怎么探究出来的&lt;/span&gt;&lt;span class="err"&gt;）&lt;/span&gt;
&lt;span class="n"&gt;ver&lt;/span&gt; &lt;span class="n"&gt;默认&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这一步，我们可以获取两条消息，并从消息中使用正则表达式获取对应的用户名以及 gid&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@danmu_auth_socket.recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/\/username@=(.+)\/nickname/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@danmu_auth_socket.recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@gid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/\/gid@=(\d+)\//&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.3.发送消息详细流程之步骤二"&gt;2.3.发送消息详细流程之步骤二&lt;/h3&gt;
&lt;p&gt;发送的消息内容为&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"type@=qrl/rid@=" + &lt;a href="/room_id.to_s" class="user-mention" title="@room_id.to_s"&gt;&lt;i&gt;@&lt;/i&gt;room_id.to_s&lt;/a&gt; + "/"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;无需多说，类型为 qrl，rid 为 roomid，直接发送这条消息就好。返回的两条消息也没有什么价值。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"type@=qrl/rid@="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@room_id.to_s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@danmu_auth_socket.write&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
&lt;span class="n"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@danmu_auth_socket.recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@danmu_auth_socket.recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.4.发送消息详细流程之步骤三"&gt;2.4.发送消息详细流程之步骤三&lt;/h3&gt;
&lt;p&gt;发送的消息内容为&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"type@=keeplive/tick@=" + timestamp + "/vbw@=0/k@=19beba41da8ac2b4c7895a66cab81e23/"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;直接发送。无太大意义。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"type@=keeplive/tick@="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"/vbw@=0/k@=19beba41da8ac2b4c7895a66cab81e23/"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@danmu_auth_socket.write&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
&lt;span class="n"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@danmu_auth_socket.recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;前三步，也就是 2.2-2.3-2.4 三步骤，也就是使用&lt;a href="/danmu_auth_socket" class="user-mention" title="@danmu_auth_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_auth_socket&lt;/a&gt; 完成获取 username 和 gid 的重要步骤。获取这两个字段以后，也就完成了它存在的使命。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;接下来的就是&lt;a href="/danmu_socket" class="user-mention" title="@danmu_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_socket&lt;/a&gt;获取弹幕的时候了！&lt;/p&gt;
&lt;h3 id="2.5.发送消息详细流程之步骤四"&gt;2.5.发送消息详细流程之步骤四&lt;/h3&gt;
&lt;p&gt;消息内容为:"type@=loginreq/username@="+&lt;a href="/username" class="user-mention" title="@username"&gt;&lt;i&gt;@&lt;/i&gt;username&lt;/a&gt;+"/password@=1234567890123456/roomid@=" + &lt;a href="/room_id.to_s" class="user-mention" title="@room_id.to_s"&gt;&lt;i&gt;@&lt;/i&gt;room_id.to_s&lt;/a&gt; + "/"&lt;/p&gt;

&lt;p&gt;和上面 2.2 中略有不同。但是，需要注意的是&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="n"&gt;为2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;中所得到的username&lt;/span&gt;
&lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="n"&gt;的值得变化&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"type@=loginreq/username@="&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="vi"&gt;@username&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s2"&gt;"/password@=1234567890123456/roomid@="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@room_id.to_s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
&lt;span class="n"&gt;all_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@danmu_socket.write&lt;/span&gt; &lt;span class="n"&gt;all_data&lt;/span&gt;
&lt;span class="n"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@danmu_socket.recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.6.发送消息详细流程之步骤五"&gt;2.6.发送消息详细流程之步骤五&lt;/h3&gt;
&lt;p&gt;接下来就是完成认证的最后一步了，join_group 的消息内容为&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"type@=joingroup/rid@=" + &lt;a href="/room_id.to_s" class="user-mention" title="@room_id.to_s"&gt;&lt;i&gt;@&lt;/i&gt;room_id.to_s&lt;/a&gt; + "/gid@="+&lt;a href="/gid" class="user-mention" title="@gid"&gt;&lt;i&gt;@&lt;/i&gt;gid&lt;/a&gt;+"/"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;gid 为 2.2 中所得到的 gid。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;data&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"type@=joingroup/rid@="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@room_id.to_s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"/gid@="&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="vi"&gt;@gid&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@danmu_socket.write&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.7.发送消息详细流程之步骤六"&gt;2.7.发送消息详细流程之步骤六&lt;/h3&gt;
&lt;p&gt;获取弹幕，并且打印出来。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;danmu_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@danmu_socket.recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;danmu_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;danmu_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"type@="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;..-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sui'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@S'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@A='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;后三步，则是&lt;a href="/danmu_socket" class="user-mention" title="@danmu_socket"&gt;&lt;i&gt;@&lt;/i&gt;danmu_socket&lt;/a&gt;&lt;/strong&gt; 获取弹幕的步骤。&lt;/p&gt;

&lt;p&gt;于是，通过这些步骤，就可以完成了简单的 danmu 的核心代码，接下来的步骤就是完善，重构这些代码了。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;&lt;h3 id="痛点一，至今还没有解决rtmp地址的获取"&gt;痛点一，至今还没有解决 rtmp 地址的获取&lt;/h3&gt;
&lt;p&gt;找了很久没有办法解决 rtmp 地址的自动获取：&lt;/p&gt;

&lt;p&gt;路径如下&lt;/p&gt;

&lt;p&gt;&lt;a href="http://www.douyutv.com/swf_api/room/301712?cdn=&amp;amp;nofan=yes&amp;amp;_t=24243097&amp;amp;sign=3b2efb130cb25a85e621f477f95c7341" rel="nofollow" target="_blank"&gt;http://www.douyutv.com/swf_api/room/301712?cdn=&amp;amp;nofan=yes&amp;amp;_t=24243097&amp;amp;sign=3b2efb130cb25a85e621f477f95c7341&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这一处的请求不是 XHR，也就是不是 JS 脚本通过 XMLHttpRequest 异步加载;那么，八成是 flash 通过 http 协议获取的。我估计八成执行逻辑应该是在 flash 之中。也就不方便获取其中的 sign 值。故，暂时无法解析 rtmp 视频流地址了&lt;/p&gt;
&lt;h2 id="效果图和代码"&gt;效果图和代码&lt;/h2&gt;
&lt;p&gt;效果图：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/52890-a526c747cff6ade7.gif?imageMogr2/auto-orient/strip" title="" alt="test.gif"&gt;&lt;/p&gt;

&lt;p&gt;代码的地址为：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/twocucao/danmu" rel="nofollow" target="_blank"&gt;https://github.com/twocucao/danmu&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;技术浅薄，还请轻拍。&lt;/p&gt;
&lt;h2 id="参考链接"&gt;参考链接&lt;/h2&gt;
&lt;p&gt;PS:如果有问题可以在下方留言或者发送 email 到 twocucao@gmail.com 给我。&lt;/p&gt;</description>
      <author>twocucao</author>
      <pubDate>Tue, 09 Feb 2016 14:06:32 +0800</pubDate>
      <link>https://ruby-china.org/topics/28968</link>
      <guid>https://ruby-china.org/topics/28968</guid>
    </item>
  </channel>
</rss>
