精华内容
下载资源
问答
  • 实时弹幕系统设计与实现

    千次阅读 2018-12-13 14:44:27
    看新年晚会的时候,发现最大的乐趣就是微信上墙了,但是量大了要等好久才能看见自己发的,为什么不能是弹幕的形式呢? 发现在GitHub上开源了一个JS弹幕模块核心CommentCoreLibrary,慢慢开始学习Node.js的一套。原来...

    前言:原文2014年发布在CNode社区,现在同步一下

    看新年晚会的时候,发现最大的乐趣就是微信上墙了,但是量大了要等好久才能看见自己发的,为什么不能是弹幕的形式呢?
    发现在GitHub上开源了一个JS弹幕模块核心CommentCoreLibrary,慢慢开始学习Node.js的一套。原来是比较做后台开发的,也是第一次做这样的分享,请大家多多指教啦……

    一、Express

    Express是Node.js最流行的一款web框架,小而灵活。Node.js和npm的安装配置可以参考这里

    可以通过npm安装Express(参考),也可以使用Express application generator快速产生一个Express样例(参考)

    对于Express初学者,用Express application generator生成样例更有利于快速上手。因此就以此为例:

    # install Express application generator 
    $ npm install express-generator -g
    
    # create an Express app named danmaku
    $ express danmaku
    
    # install dependencies
    $ cd danmaku
    $ npm install
    
    # run the app on Windows
    $ set DEBUG=danmaku & node .\bin\www
    # or
    $ npm start
    

    关于set DEBUG=danmaku可以见此文
    可以用npm start启动服务器是因为在packege.json中有了这么一段:

      "scripts": {
        "start": "node ./bin/www"
      }
    

    Express的4比之3,把服务器配置和服务器启动做了分离,原来都在app.js里,现在将启动代码放到了www中。
    现在,浏览一遍这个Express样例,对这框架就可以知道个大概了。

    • bin:存放启动项目的脚本文件
    • node_modules:存放所有的项目依赖库
    • public:静态文件(css、js、img等)
    • routes:路由文件(MVC中的C,controller)
    • views:页面文件(jade或ejs模板)
    • package.json:项目依赖配置及开发者信息
    • app.js:应用核心配置文件

    更多参考:

    1. Node.js开发框架Express3.0开发手记–从零开始
    2. Node.js开发框架Express4.x
    3. Express实例

    二、路由

    将实时弹幕系统实际上是分为三个角色:

    • 服务端:监听客户端连接、弹幕事件等并响应。
    • 发射客户端:由用户发射弹幕。以emitCtrl.js作为emit页面的controller。
    • 屏幕客户端:接收弹幕并显示。以indexCtrl.js作为index页面的controller。
      在这里插入图片描述

    添加 routes/indexCtrl.js

    var express = require('express');
    var router = express.Router();
    
    /* GET home page. */
    router.get('/', function(req, res, next) {
      res.render('index',{title:"danmaku"});
    });
    
    module.exports = router;
    
    

    添加 routes/emitCtrl.js

    var express = require('express');
    var router = express.Router();
    
    /* GET emit page. */
    router.get('/', function(req, res, next) {
      res.render('emit');
    });
    
    module.exports = router;
    

    修改 app.js

    var indexCtrl = require('./routes/indexCtrl');
    var emitCtrl = require('./routes/emitCtrl');
    ...
    app.use('/', indexCtrl);
    app.use('/emit', emitCtrl);
    

    启动后可查看到index页面。
    在这里插入图片描述

    在后面还会对emitCtrl.js增加弹幕配置的文件config.json的读取。


    三、屏幕客户端

    1. 静态

    CommentCoreLibrary是GitHub上开源的JS弹幕模块核心,提供从基本骨架到高级弹幕的支持。

    考虑到实际,感觉并不应该引入外部库。如果作为外部库用,需要

    $ npm install comment-core-library --save
    

    使用时(去除public)

    <link rel="stylesheet" href="/node_modules/comment-core-library/build/style.css" />
    <script src="/node_modules/comment-core-library/build/CommentCoreLibrary.js"></script>
    

    另外CommentCoreLibrary模块也有点笨重。

    所以换种方式,将CommentCoreLibrary.js放入public/javascripts,style.css放入public/stylesheets中

    添加views/index.jade

    doctype html
    html
        head
            title= title
            link(rel='stylesheet', href='/stylesheets/style.css')
            link(rel='stylesheet', href='/stylesheets/index.css')
            script(src='/javascripts/CommentCoreLibrary.js')
        body
            #my-player.abp(style='width:100%; height:600px; background:#000;')
                #my-comment-stage.container
            ul#messages
            script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.js')
            script(src='/javascripts/index.js')
    

    添加public/stylesheets/index.css

    * { margin: 0; padding: 0; box-sizing: border-box; }
    #messages { list-style-type: none; margin: 0; padding: 0; }
    #messages li { padding: 5px 10px; }
    #messages li:nth-child(odd) { background: #eee; }
    body {
        margin:0px;
        padding:0px;
        font-family: "Segoe UI", "Microsoft Yahei", sans-serif;
    }
    

    添加public/javascripts/index.js

    window.addEventListener('load', function () {
        // 在窗体载入完毕后再绑定
        var CM = new CommentManager($('#my-comment-stage'));
        CM.init();
        // 先启用弹幕播放(之后可以停止)
        CM.start();
        // 开放 CM 对象到全局这样就可以在 console 终端里操控
        window.CM = CM;
    });
    

    然后在Console里怒射一弹:

    var danmaku = {
        "mode": 1,
        "text": "hello world",
        "stime": 0,
        "size": 25,
        "color": 0xff00ff,
        "dur": 10000
    };
    CM.send(danmaku);
    

    不过这其实根本没用上服务器,也就是静态网页一样的效果。

    在这里插入图片描述

    2. 动态(服务端)

    动态是实现一个真正的“屏幕客户端”,监听等待“显示弹幕”的事件,并实时显示。

    CommentCoreLibrary的Doc中有一段:

    实时弹幕也需要后端服务器的支持。实时弹幕可以采取Polling(定时读取)或者 Push Notify(监听等待)两个主动和被动模式实现。

    WebSocketHTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。既然是双向通信,就意味着服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。知乎上关于WebSocket的科普

    Socket.IO是一个开源的WebSocket库,它通过Node.js实现WebSocket服务端,同时也提供客户端JS库。Socket.IO支持以事件为基础的实时双向通讯,它可以工作在任何平台、浏览器或移动设备。
    Socket.IO支持4种协议:WebSocket、htmlfile、xhr-polling、jsonp-polling,它会自动根据浏览器选择适合的通讯方式,从而让开发者可以聚焦到功能的实现而不是平台的兼容性,同时具有不错的稳定性和性能。
    p.s. 实际过程中踩到了phpwebsocket的坑。

    用npm导入Socket.IO

    npm install socket.io --save
    

    修改www(Express4从app.js里把启动分出来了)

    // Create socket.io
    var io = require('socket.io')(server);
    ...
    // Wait for socket event
    io.on('connection', function(socket){
        console.log('a user connected');
        socket.on('disconnect', function(){
            console.log('user disconnected');
        });
        socket.on('danmaku send', function(msg){
            console.log('message: ' + msg);
            io.emit('danmaku show', msg);
        });
    });
    

    修改index.jade

    script(src='http://cdn.bootcss.com/socket.io/1.3.2/socket.io.js')
    script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.js')
    script(src='/javascripts/index.js')
    

    修改index.js

    window.addEventListener('load', function () {
        // 在窗体载入完毕后再绑定
        var CM = new CommentManager($('#my-comment-stage'));
        CM.init();
        // 先启用弹幕播放(之后可以停止)
        CM.start();
        // 开放 CM 对象到全局这样就可以在 console 终端里操控
        window.CM = CM;
    	
        var socket = io();
        socket.on('danmaku show', function (msg) {
            console.log(msg);
            $('#messages').append($('<li>').text(msg));
            var danmaku = JSON.parse(msg);
            CM.send(danmaku);
        });
    });
    

    这样就由服务端监听了“connection”、“disconnect”和“danmaku send”三个事件,特别是在收到“danmaku send”时会发送“danmaku show”事件。而屏幕客户端监听“danmaku show”事件,并把传递来的弹幕显示出来。

    启动后打开index,确实能看到"connection"事件执行的提示。
    在这里插入图片描述

    四、发射客户端

    发射客户端发送“danmaku send”事件及弹幕给服务端。

    除此之外,在CommentCoreLibrary里可以对弹幕属性进行设置,比如文字大小、模式、颜色,将它们的可选值写成配置文件,并设定默认值。

    添加public/jsons/config.json

    {"sizes":[{"size":12,"title":"非常小"},{"size":16,"title":"较小"},{"size":18,"title":"小"},{"size":25,"title":"中"},{"size":36,"title":"大"},{"size":45,"title":"较大"},{"size":64,"title":"非常大"}],
    
    "modes":[{"mode":1,"title":"顶端滚动"},{"mode":2,"title":"底端滚动"},{"mode":5,"title":"顶端渐隐"},{"mode":4,"title":"底端渐隐"},{"mode":6,"title":"逆向滚动"}],
    
    "colors":[{"color":"000000","title":"黑色"},{"color":"C0C0C0","title":"灰色"},{"color":"ffffff","title":"白色"},{"color":"ff0000","title":"红色"},{"color":"00ff00","title":"绿色"},{"color":"0000ff","title":"蓝色"},{"color":"ffff00","title":"黄色"},{"color":"00ffff","title":"墨绿"},{"color":"ff00ff","title":"洋红"}],
    
    "inits":{"size":3,"mode":0,"color":4}}
    

    修改emitCtrl.js,读取配置

    var fs = require('fs');
    ...
    /* GET emit page. */
    router.get('/', function (req, res, next) {
        var config = JSON.parse(fs.readFileSync(__dirname + './../public/jsons/config.json'));
        res.render('emit', { title: 'Emitter', sizes: config.sizes, modes: config.modes, colors: config.colors, inits: config.inits});
    });
    

    添加views/emit.jade

    doctype html
    html
        head
            title= title
            meta(name='viewport', content='width=device-width, initial-scale=1,maximum-scale=1')
            link(rel='stylesheet',href='http://cdn.bootcss.com/jquery-mobile/1.4.3/jquery.mobile.css')
            script(src='http://cdn.bootcss.com/socket.io/1.3.2/socket.io.js')
            script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.min.js')
            script(src='http://cdn.bootcss.com/jquery-mobile/1.4.3/jquery.mobile.js')
        body
            div(data-role='page')
                div(data-role='content')
                    div.ui-grid-b
                        a#size.ui-btn.ui-btn-inline.ui-block-a(href='#popupMenu_font', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-size= sizes[inits.size].size )= sizes[inits.size].title
                        #popupMenu_font(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;')
                            ul(data-role='listview')
                                each val, index in sizes
                                    li
                                        a(data-rel='back',danmaku-size=val.size)= val.title
                        a#mode.ui-btn.ui-btn-inline.ui-block-b(href='#popupMenu_mode', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-mode= modes[inits.mode].mode  )= modes[inits.mode].title
                        #popupMenu_mode(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;')
    
                            ul(data-role='listview')
                                each val, index in modes
                                    li
                                        a(data-rel='back',danmaku-mode=val.mode)= val.title
                        a#color.ui-btn.ui-btn-inline.ui-block-c(href='#popupMenu_color', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-color= colors[inits.color].color  )= colors[inits.color].title
                        #popupMenu_color(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;')
                            .ui-grid-b
                                - var i=0;
                                each val, index in colors
                                    case i++%3
                                        when 0: a.ui-block-a(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title
                                        when 1: a.ui-block-b(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title
                                        when 2: a.ui-block-c(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title 
    
                    textarea#msg(placeholder='来一发弹幕~')
                    button#btnSend 发射
            script(src='/javascripts/emit.js')
    

    添加public/javascripts/emit.js

    var socket = io();
    
    $('#popupMenu_font a').click(function(e){
        $('#size').text($(e.target).text()).attr("danmaku-size",$(e.target).attr("danmaku-size"));
    });
    
    $('#popupMenu_mode a').click(function(e){
        $('#mode').text($(e.target).text()).attr("danmaku-mode",$(e.target).attr("danmaku-mode"));
    });
    
    $('#popupMenu_color a').click(function(e){
        $('#color').text($(e.target).text()).attr("danmaku-color",$(e.target).attr("danmaku-color"));
    });
    
    $('#btnSend').click(function(e){
        e.preventDefault();
        var danmaku = {
            "mode": Number($("#mode").attr("danmaku-mode")),
            "text": $('#msg').val(),
            "stime":0,
            "size": Number($("#size").attr("danmaku-size")),
            "color":parseInt($("#color").attr("danmaku-color"),16),
            "dur":10000
        };
        var msg=JSON.stringify(danmaku);
        console.log(msg);
        socket.emit('danmaku send',msg);
        $('#msg').val("");
    });
    

    最后整个效果就是这样啦~ 源码在此https://github.com/cstackess/danmaku
    在这里插入图片描述

    展开全文
  • 千万级弹幕系统的架构设计

    千次阅读 2019-04-07 19:44:23
    千万级弹幕系统的架构设计 技术难点 内核瓶颈 推送量大:100W在线 * 10条/每秒 = 1000W条/秒 内核瓶颈:linux内核发送TCP的极限包频 ≈ 100W/秒 锁瓶颈 需要维护在线用户集合(100W用户在线),通常是一个字典结构 ...

    千万级弹幕系统的架构设计

    技术难点

    内核瓶颈
    推送量大:100W在线 * 10条/每秒 = 1000W条/秒
    内核瓶颈:linux内核发送TCP的极限包频 ≈ 100W/秒

    锁瓶颈
    需要维护在线用户集合(100W用户在线),通常是一个字典结构
    推送消息即遍历整个集合,顺序发送消息,耗时极长
    推送期间,客户端仍旧正常的上下线,集合面临不停的修改,修改需要遍历,所以集合需要上锁

    CPU瓶颈
    浏览器与服务端之间一般采用的是JSon格式去通讯
    Json编码非常耗费CPU资源
    向100W在线推送一次,则需100W次Json Encode

    优化方案

    内核瓶颈
    减少网络小包的发送,我们将网络上几百字节定义成网络的小包了,小包的问题是对内核和网络的中间设备造成处理的压力。方案是将一秒内N条消息合并成1条消息,合并后,每秒推送数等于在线连接数。

    锁瓶颈
    大锁拆小锁,将长连接打散到多个集合中去,每个集合都有自己的锁,多线程并发推送集合,线程之间推送的集合不同,所以没有锁的竞争关系,避免锁竞争。
    读写锁取代互斥锁,多个推送任务可以并发遍历相同集合

    CPU瓶颈
    减少重复计算,Json编码前置,1次消息编码+100W次推送,消息合并前置,N条消息合并后,只需要编码一次。

    单机架构
    在这里插入图片描述
    最外层是在线的长连接,连接到服务端后,打散到多个集合里面存储,我们要发送的消息呢,通过打包后,经过json编码,被多个线程或协程分发到多个集合中去,最终推给了所有的在线连接。

    单机瓶颈
    维护海量长连接,会花费不少内存
    消息推送的瞬时,消耗大量的CPU
    消息推送的瞬时带宽高达400-600Mb(4-6Gbits),需要用到万兆网卡,是主要瓶颈

    集群
    部署多个节点,通过负载均衡,把连接打散到多个 服务器上,但推送消息的时候,不知道哪个直播间在哪个节点上,最常用的方式是将消息广播给所有的网关节点,此时就需要做一个逻辑集群。

    逻辑集群
    基于Http2协议向gateway集群分发消息(Http2支持连接复用,用作RPC性能更佳,即在单个连接上可以做高吞吐的请求应答处理)
    基于Http1协议对外提供推送API(Http1更加普及,对业务方更加友好)
    整体分布式架构图如下:
    在这里插入图片描述

    任何业务方通过Http接口调用到逻辑集群,逻辑集群把消息广播给所有网关,各个网关各自将消息推送给在线的连接即可。

    本文讲解了开发消息推送服务的难点与解决方案的大体思路,按照整个理论流程下来,基本能实现一套弹幕消息推送的服务。

    拉模式和推模式区别

    拉模式(定时轮询访问接口获取数据)

    数据更新频率低,则大多数的数据请求时无效的
    在线用户数量多,则服务端的查询负载很高
    定时轮询拉取,无法满足时效性要求

    推模式(向客户端进行数据的推送)

    仅在数据更新时,才有推送
    需要维护大量的在线长连接
    数据更新后,可以立即推送

    基于WebSocket协议做推送

    浏览器支持的socket编程,轻松维持服务端的长连接
    基于TCP协议之上的高层协议,无需开发者关心通讯细节
    提供了高度抽象的编程接口,业务开发成本较低

    WebSocket协议的交互流程
    在这里插入图片描述

    客户端首先发起一个Http请求到服务端,请求的特殊之处,在于在请求里面带了一个upgrade的字段,告诉服务端,我想生成一个websocket的协议,服务端收到请求后,会给客户端一个握手的确认,返回一个switching, 意思允许客户端向websocket协议转换,完成这个协商之后,客户端与服务端之间的底层TCP协议是没有中断的,接下来,客户端可以向服务端发起一个基于websocket协议的消息,服务端也可以主动向客户端发起websocket协议的消息,websocket协议里面通讯的单位就叫message。

    传输协议原理
    协议升级后,继续复用Http协议的底层socket完成后续通讯
    message底层会被切分成多个frame帧进行传输,从协议层面不能传输一个大包,只能切成一个个小包传输
    编程时,只需操作message,无需关心frame(属于协议和类库自身去操作的)
    框架底层完成TCP网络I/O,WebSocket协议的解析,开发者无需关心

    服务端技术选型与考虑

    NodeJs

    单线程模型(尽管可以多进程),推送性能有限

    C/C++

    TCP通讯、WebSocket协议实现成本高

    Go

    多线程,基于协程模型并发
    Go语言属于编译型语言,运行速度并不慢
    成熟的WebSocket标准库,无需造轮子

    基于Go实现WebSocket服务端
    用Go语言对WebSocket做一个简单的服务端实现,以及HTML页面进行调试,并对WebSocket封装,这里就直接给出代码了。

    参考博文

    慕课网地址:https://www.imooc.com/learn/1025

    原文地址:https://blog.csdn.net/Wing_93/article/details/81587809

    GO实现千万级WebSocket消息推送服务技术分析
    https://blog.csdn.net/dodod2012/article/details/81744526

    展开全文
  • 基于Netty实现高性能弹幕系统

    千次阅读 2019-08-07 18:24:26
    一、弹幕系统概要设计 二、Netty对Http协议解析实现 三、WebScoket协议解析实现 一、弹幕系统概要设计 什么是弹幕系统弹幕系统特点: 实时性高:你发我收, 毫秒之差 并发量大:一人吐槽,万人观看 ...

    一、弹幕系统概要设计

    二、Netty对Http协议解析实现

    三、WebScoket协议解析实现

     

    一、弹幕系统概要设计

    什么是弹幕系统?

     

    幕系统特点:

    1. 实时性高:你发我收, 毫秒之差
    1. 并发量大:一人吐槽,万人观看

    弹幕系统架构设计:

    业务架构:

     

    实现方案一:

    实现方案二:

    二、NettyHttp协议解析实现

     

     

    在上述方案中 浏览器不能直接能和Netty 建立连接 其必须借助http 请求 进行协议升级才能实现服务端与客户端基于Web Socket通信,其过程如下图:

     

    也就是说如果我们想实现弹幕就必须先实现Http服务,那么Netty如何实现Http服务呢?

    Http协议交互过程

    协议交互本质是指协议两端(客户端、服务端)如何传输数据?如何交换数据?

    传输数据一般基于TCP/IP 实现,体现到开发语言上就是我们所熟悉的Socket 编程。

    交换数据本质是指,两端(客户端、服务端)能各自识别对方所发送的数据。那么这就需要制定一套报文编码格式,双方以该格式编码数据发送给对方。Http 对应的Request 与Response报文格式如下图:

     

     

    request 报文:

     

    response 报文:

     

    http报文解析方案:

    1:请求行的边界是CRLF(回车),如果读取到CRLF(回车),则意味着请求行的信息已经读取完成。

    2:Header的边界是CRLF,如果连续读取两个CRLF,则意味着header的信息读取完成。

    3:body的长度是有Content-Length 来进行确定。

    netty关于http 的解决方案:

    // 解析请求

    很多http server的实现都是基于servlet标准,但是netty对http实现并没有基于servlet。所以在使用上比Servlet复杂很多。比如在servlet 中直接可以通过 HttpServletRequest 获取 请求方法、请求头、请求参数。而netty 确需要通过如下对象自行解析获取。

    HttpMethod:主要是对method的封装,包含method序列化的操作

    HttpVersion: 对version的封装,netty包含1.0和1.1的版本

    QueryStringDecoder: 主要是对urI进行解析,解析path和url上面的参数。

    HttpPostRequestDecoder:对post 中body 内容进行解析获取 form 参数。

    HttpHeaders:包含对header的内容进行封装及操作

    HttpContent:是对body进行封装,本质上就是一个ByteBuf。如果ByteBuf的长度是固定的,则请求的body过大,可能包含多个HttpContent,其中最后一个为LastHttpContent(空的HttpContent),用来说明body的结束。

    HttpRequest:主要包含对Request Line和Header的组合

    FullHttpRequest: 主要包含对HttpRequest和httpContent的组合

     

    Netty Http的请求处理流程:

     

    从图中可以看出做为服务端的Netty 就是在做 编码和解码操作。其分别通过以下两个ChannelHandler对象实现:

    HttpRequestDecoder :用于从byteBuf 获取数据并解析封装成HttpRequest 对象

    HttpResponseEncoder:用于将业务返回数据编码成 Response报文并发送到ByteBuf。

    将以上两个对象添加进 Netty 的 pipeline 即可实现最简单的http 服务。

     

    通过一个示例演示Http实现:

    编写serverBootstrap

    初始pipeline

    编写ServletHandler

     

    Http完整流程介绍:

     

    Decoder 流程:

     

    encode 流程:

     

    三、WebScoket协议实现

     

     

     

     

     

    webSocket 协议简介:

    webSocket 是html5 开始提供的一种浏览器与服务器间进行全双工二进制通信协议,其基于TCP双向全双工作进行消息传递,同一时刻即可以发又可以接收消息,相比Http的半双工协议性能有很大的提升,

    webSocket特点如下:

    1. 单一TCP长连接,采用全双工通信模式
    1. 对代理、防火墙透明
    1. 无头部信息、消息更精简
    1. 通过ping/pong 来保活
    1. 服务器可以主动推送消息给客户端,不在需要客户轮询

    WebSocket 协议报文格式:

    我们知道,任何应用协议都有其特有的报文格式,比如Http协议通过 空格 换行组成其报文。如http 协议不同在于WebSocket属于二进制协议,通过规范进二进位来组成其报文。具体组成如下图:

     

    Http协议报文:

     

    报文说明:

    FIN

    标识是否为此消息的最后一个数据包,占 1 bit

    RSV1, RSV2, RSV3: 用于扩展协议,一般为0,各占1bit

    Opcode

    数据包类型(frame type),占4bits

    0x0:标识一个中间数据包

    0x1:标识一个text类型数据包

    0x2:标识一个binary类型数据包

    0x3-7:保留

    0x8:标识一个断开连接类型数据包

    0x9:标识一个ping类型数据包

    0xA:表示一个pong类型数据包

    0xB-F:保留

    MASK:占1bits

    用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。

    Payload length

    Payload data的长度,占7bits,7+16bits,7+64bits:

    如果其值在0-125,则是payload的真实长度。

    如果值是126,则后面2个字节形成的16bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。

    如果值是127,则后面8个字节形成的64bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。

    Payload data

    应用层数据

    WebSocket 在浏览当中的使用

    Http 连接与webSocket 连接建立示意图:

     

    通过javaScript 中的API可以直接操作WebSocket 对象,其示例如下:

    var ws = new WebSocket(“ws://localhost:8080”);

    ws.onopen = function()// 建立成功之后触发的事件 {

    console.log(“打开连接”); ws.send("ddd"); // 发送消息

    };

    ws.onmessage = function(evt) { // 接收服务器消息

    console.log(evt.data);

    };

    ws.onclose = function(evt) {

    console.log(“WebSocketClosed!”); // 关闭连接 };

    ws.onerror = function(evt) {

    console.log(“WebSocketError!”); // 连接异常

    };

    弹幕系统实现讲解

    Http 协议后台实现:

    webSocket 协议后台实现

    弹幕系统前台实现

    弹幕系统实时演示

     

    #启动服务

     

    #查看当前端口连接数

    netstat -nat|grep -i "8880"|wc -l

     

    #查看指定进程线程数

    pstree -p 3000 | wc -l

     

    源码地址:https://gitee.com/h455952294_admin/netty-demo.git

     

    展开全文
  • websocket实现斗鱼弹幕系统

    千人学习 2019-08-05 19:16:59
    本课程主要讲解了1.服务器的消息如何发给客户端;2.通过实现方式利用websocket;3.websocket实现方案概述等内容,希望学完之后对你有所收获。
  • Android弹幕实现:基于B站弹幕开源系统(4)-重构弹幕在视频播放的APP中比较常见,但是逻辑比较复杂,现在在附录1,2,3的基础上,我再次对弹幕进行抽象和重构,把弹幕从底向上抽象成不同的层,便于复用。第一步,抽象...
    

    Android弹幕实现:基于B站弹幕开源系统(4)-重构

    弹幕在视频播放的APP中比较常见,但是逻辑比较复杂,现在在附录1,2,3的基础上,我再次对弹幕进行抽象和重构,把弹幕从底向上抽象成不同的层,便于复用。

    第一步,抽象数据层。
    通常弹幕的来源是来源于后台的数据接口请求,在实时直播时候,是通过网络的轮询机制获取数据,那么,我把这部分代码抽出来设计成一个MGDanmakuHttpController,该类专注于数据的获取与分发:

    package zhangphil.danmaku;
    
    import android.os.Handler;
    import android.os.Message;
    import android.support.annotation.NonNull;
    import android.util.Log;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.UUID;
    import java.util.concurrent.Callable;
    
    import io.reactivex.Observable;
    import io.reactivex.android.schedulers.AndroidSchedulers;
    import io.reactivex.observers.DisposableObserver;
    import io.reactivex.schedulers.Schedulers;
    import okhttp3.OkHttpClient;
    import okhttp3.Request;
    import okhttp3.Response;
    
    /**
     * Created by Phil on 2017/3/31.
     */
    
    public class MGDanmakuHttpController {
    
        //private final String TAG = getClass().getName() + String.valueOf(UUID.randomUUID());
    
        private int msgId = 0;
    
        private DataMessageListener mDataMessageListener = null;
        private OkHttpClient mOkHttpClient;
    
        public MGDanmakuHttpController() {
            mOkHttpClient = new OkHttpClient();
        }
    
        private final int WHAT_START = 0xff0a;
        //private final int WHAT_STOP = WHAT_START + 1;
    
        private boolean promise = false;
    
        private int interval = 0;
    
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
    
                if (msg.what == WHAT_START) {
                    handler.removeMessages(WHAT_START);
    
                    try {
                        if (promise)
                            startRequestDanmaku();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        };
    
    
        public void startRequestDanmaku() throws Exception {
            promise = true;
    
            Observable mObservable = Observable.fromCallable(new Callable<List<DanmakuMsg>>() {
                @Override
                public List<DanmakuMsg> call() throws Exception {
                    //同步方法返回观察者需要的数据结果
                    //在这里处理线程化的操作
                    return fetchData();
                }
            }).subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread());
    
            mObservable.subscribe(new DisposableObserver<List<DanmakuMsg>>() {
    
                @Override
                public void onNext(@NonNull List<DanmakuMsg> lists) {
                    if (mDataMessageListener != null && promise) {
                        mDataMessageListener.onDataMessageListener(lists);
                    }
                }
    
                @Override
                public void onComplete() {
                    fireRequest();
                }
    
                @Override
                public void onError(Throwable e) {
                    fireRequest();
                }
            });
        }
    
        public void stopRequestDanmaku() {
            promise = false;
        }
    
        /**
         * 设置轮询的间隔时间
         *
         * @param interval 单位毫秒 默认是0
         */
        public void setHttpRequestInterval(int interval) {
            this.interval = interval;
        }
    
        private void fireRequest() {
            //这里将触发重启数据请求,在这里可以调节重启数据请求的节奏。
            //比如可以设置一定的时延
            handler.sendEmptyMessageDelayed(WHAT_START, interval);
        }
    
        private List<DanmakuMsg> fetchData() {
            //同步方法返回观察者需要的数据结果
            //在这里处理线程化的操作
    //        String url = "http://blog.csdn.net/zhangphil";
    //        try {
    //            Request request = new Request.Builder().url(url).build();
    //            Response response = mOkHttpClient.newCall(request).execute();
    //            if (response.isSuccessful()) {
    //                byte[] bytes = response.body().bytes();
    //                String data = new String(bytes, 0, bytes.length);
    
    
            try {
                Thread.sleep((int) (Math.random() * 500));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            int count = (int) (Math.random() * 10);
    
            //装配模拟数据
            List<DanmakuMsg> danmakuMsgs = new ArrayList<>();
            for (int i = 0; i < count; i++) {
                DanmakuMsg danmakuMsg = new DanmakuMsg();
                danmakuMsg.msg = String.valueOf(msgId++);
                danmakuMsgs.add(danmakuMsg);
            }
    
            return danmakuMsgs;
    //            }
    //        } catch (Exception e) {
    //            e.printStackTrace();
    //        }
    //
    //        return null;
        }
    
    
        public interface DataMessageListener {
            void onDataMessageListener(@NonNull List<DanmakuMsg> lists);
        }
    
        public void setDataMessageListener(DataMessageListener listener) {
            mDataMessageListener = listener;
        }
    }


    第二步,通过一个模型把弹幕的view和数据用胶水粘合在一起,我写了一个MGDanmaku:

    package zhangphil.danmaku;
    
    import android.graphics.Color;
    import android.os.Handler;
    import android.os.Message;
    import android.support.annotation.NonNull;
    import android.text.TextUtils;
    import android.util.Log;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.UUID;
    import java.util.concurrent.ConcurrentLinkedQueue;
    
    import master.flame.danmaku.danmaku.model.BaseDanmaku;
    import master.flame.danmaku.danmaku.model.DanmakuTimer;
    import master.flame.danmaku.danmaku.model.IDisplayer;
    import master.flame.danmaku.danmaku.model.android.DanmakuContext;
    import master.flame.danmaku.ui.widget.DanmakuView;
    
    /**
     * Created by Phil on 2017/4/1.
     */
    
    public class MGDanmaku {
        private final String TAG = getClass().getName() + UUID.randomUUID();
    
        private MGDanmakuHttpController mMGDanmakuHttpController;
        private DanmakuView mDanmakuView;
        private AcFunDanmakuParser mParser;
    
        private DanmakuContext mDanmakuContext;
    
        private final int MAX_DANMAKU_LINES = 8; //弹幕在屏幕显示的最大行数
    
        private ConcurrentLinkedQueue<DanmakuMsg> mQueue = null; //所有的弹幕数据存取队列,在这里做线程的弹幕取和存
        private ArrayList<DanmakuMsg> danmakuLists = null;//每次请求最新的弹幕数据后缓存list
    
        private final int WHAT_GET_LIST_DATA = 0xffab01;
        private final int WHAT_DISPLAY_SINGLE_DANMAKU = 0xffab02;
    
        /**
         * 每次弹幕的各种颜色从这里面随机的选一个
         */
        private final int[] colors = {
                Color.RED,
                Color.YELLOW,
                Color.BLUE,
                Color.GREEN,
                Color.CYAN,
                Color.DKGRAY};
    
        //弹幕开关总控制
        // true正常显示和请求
        // false则取消
        private boolean isDanmukuEnable = false;
    
        public MGDanmaku(@NonNull DanmakuView view, @NonNull MGDanmakuHttpController controller) {
            this.mDanmakuView = view;
            this.mMGDanmakuHttpController = controller;
    
            initDanmaku();
    
            danmakuLists = new ArrayList<>();
            mQueue = new ConcurrentLinkedQueue<>();
    
            mMGDanmakuHttpController.setDataMessageListener(new MGDanmakuHttpController.DataMessageListener() {
                @Override
                public void onDataMessageListener(@NonNull List<DanmakuMsg> lists) {
                    danmakuLists = (ArrayList<DanmakuMsg>) lists;
                    //for (int i = 0; i < danmakuLists.size(); i++) {
                        //Log.d("获得数据", danmakuLists.get(i).msg);
                    //}
    
                    addListData();
                }
            });
    
            Log.d(getClass().getName(), TAG);
        }
    
        private Handler mDanmakuHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
    
                switch (msg.what) {
                    case WHAT_GET_LIST_DATA:
                        addListData();
                        break;
    
                    case WHAT_DISPLAY_SINGLE_DANMAKU:
                        mDanmakuHandler.removeMessages(WHAT_DISPLAY_SINGLE_DANMAKU);
                        displayDanmaku();
                        break;
                }
            }
        };
    
        private void addListData() {
            if (danmakuLists != null && !danmakuLists.isEmpty()) {
                mDanmakuHandler.removeMessages(WHAT_GET_LIST_DATA);
    
                mQueue.addAll(danmakuLists);
                danmakuLists.clear();
    
                mDanmakuHandler.sendEmptyMessage(WHAT_DISPLAY_SINGLE_DANMAKU);
            }
        }
    
    
        private void initDanmaku() {
            // 设置最大显示行数
            HashMap<Integer, Integer> maxLinesPair = new HashMap<>();
            maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, MAX_DANMAKU_LINES); // 滚动弹幕最大显示5行
    
            // 设置是否禁止重叠
            HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<>();
            overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
            overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);
    
            mDanmakuContext = DanmakuContext.create();
    
            //普通文本弹幕也描边设置样式
            //如果是图文混合编排编排,最后不要描边
            mDanmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 10) //描边的厚度
                    .setDuplicateMergingEnabled(false)
                    .setScrollSpeedFactor(1.2f) //弹幕的速度。注意!此值越小,速度越快!值越大,速度越慢。// by phil
                    .setScaleTextSize(1.2f)  //缩放的值
    //        .setCacheStuffer(new BackgroundCacheStuffer())  // 绘制背景使用BackgroundCacheStuffer
                    .setMaximumLines(maxLinesPair)
                    .preventOverlapping(overlappingEnablePair);
    
            mParser = new AcFunDanmakuParser();
            mDanmakuView.prepare(mParser, mDanmakuContext);
    
            //mDanmakuView.showFPS(true);
            mDanmakuView.enableDanmakuDrawingCache(true);
    
            if (mDanmakuView != null) {
                mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() {
                    @Override
                    public void updateTimer(DanmakuTimer timer) {
                    }
    
                    @Override
                    public void drawingFinished() {
    
                    }
    
                    @Override
                    public void danmakuShown(BaseDanmaku danmaku) {
                        Log.d("弹幕文本", "显示 text=" + danmaku.text);
                    }
    
                    @Override
                    public void prepared() {
                        mDanmakuView.start();
                    }
                });
            }
        }
    
        /**
         * 驱动弹幕显示机制重新运作起来
         */
        private void startDanmaku() {
            mDanmakuView.show();
            //mDanmakuView.start();
    
            mDanmakuHandler.sendEmptyMessage(WHAT_GET_LIST_DATA);
            mDanmakuHandler.sendEmptyMessage(WHAT_DISPLAY_SINGLE_DANMAKU);
        }
    
        private void stopDanmaku() {
            if (mDanmakuView != null) {
                mDanmakuView.hide();
                mDanmakuView.clearDanmakusOnScreen();
                mDanmakuView.clear();
            }
    
            mDanmakuHandler.removeMessages(WHAT_GET_LIST_DATA);
            mDanmakuHandler.removeMessages(WHAT_DISPLAY_SINGLE_DANMAKU);
    
            danmakuLists.clear();
            mQueue.clear();
        }
    
        public void setDanmakuRunning(boolean enable) {
            //如果是重复设置,则跳过
            if (isDanmukuEnable == enable) {
                return;
            }
    
            this.isDanmukuEnable = enable;
    
            //Log.d("isDanmukuEnable", String.valueOf(isDanmukuEnable));
    
            if (isDanmukuEnable) {
                startDanmaku();
    
                try {
                    mMGDanmakuHttpController.startRequestDanmaku();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                stopDanmaku();
                mMGDanmakuHttpController.stopRequestDanmaku();
            }
        }
    
    
        public boolean getDanmakuRunning() {
            return isDanmukuEnable;
        }
    
        public void sendMsg(@NonNull DanmakuMsg danmakuMsg) {
            displayDanmaku(danmakuMsg);
        }
    
        public void onResume() {
            if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
                mDanmakuView.resume();
            }
        }
    
        public void onPause() {
            if (mDanmakuView != null && mDanmakuView.isPrepared()) {
                mDanmakuView.pause();
            }
        }
    
        public void onDestroy() {
            if (mDanmakuView != null) {
                // dont forget release!
                mDanmakuView.release();
                mDanmakuView = null;
            }
    
            stopDanmaku();
        }
    
        private void displayDanmaku(@NonNull DanmakuMsg dm) {
            //如果当前的弹幕由于Android生命周期的原因进入暂停状态,那么不应该不停的消耗弹幕数据
            //要知道,在这里发出一个handler消息,那么将会消费(删掉)ConcurrentLinkedQueue头部的数据
            if (isDanmukuEnable) {
                if (!TextUtils.isEmpty(dm.msg)) {
                    addDanmaku(dm.msg, dm.islive);
                }
            }
        }
    
        private void displayDanmaku() {
            //如果当前的弹幕由于Android生命周期的原因进入暂停状态,那么不应该不停的消耗弹幕数据
            //要知道,在这里发出一个handler消息,那么将会消费(删掉)ConcurrentLinkedQueue头部的数据
            boolean b = !mQueue.isEmpty() && getDanmakuRunning();
            if (b) {
                DanmakuMsg dm = mQueue.poll();
                if (!TextUtils.isEmpty(dm.msg)) {
                    addDanmaku(dm.msg, dm.islive);//可以在此之后再加一行代码,驱动弹幕继续显示单个弹幕 mDanmakuHandler.sendEmptyMessageDelayed(WHAT_DISPLAY_SINGLE_DANMAKU, (long) (Math.random() * 400) + 100);
                }
            }
        }
    
        private void addDanmaku(CharSequence cs, boolean islive) {
            BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
            if (danmaku == null || mDanmakuView == null) {
                return;
            }
    
            danmaku.text = cs;
            danmaku.padding = 5;
            danmaku.priority = 0;  // 可能会被各种过滤器过滤并隐藏显示
            danmaku.isLive = islive;
            danmaku.setTime(mDanmakuView.getCurrentTime());
            danmaku.textSize = 20f * (mParser.getDisplayer().getDensity() - 0.6f); //文本弹幕字体大小
            danmaku.textColor = getRandomColor(); //文本的颜色
            danmaku.textShadowColor = getRandomColor(); //文本弹幕描边的颜色
            //danmaku.underlineColor = Color.DKGRAY; //文本弹幕下划线的颜色
            danmaku.borderColor = getRandomColor(); //边框的颜色
    
            mDanmakuView.addDanmaku(danmaku);
        }
    
        /**
         * 从一系列颜色中随机选择一种颜色
         *
         * @return
         */
        private int getRandomColor() {
            int i = ((int) (Math.random() * 10)) % colors.length;
            return colors[i];
        }
    }



    第三步,直接拿来在上层的activity用:

    package zhangphil.danmaku;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.CheckBox;
    import android.widget.CompoundButton;
    
    import master.flame.danmaku.ui.widget.DanmakuView;
    
    public class MainActivity extends Activity {
        private MGDanmaku mMGDanmaku;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Log.d(getClass().getName(),"onCreate");
    
            DanmakuView mDanmakuView = (DanmakuView) findViewById(R.id.danmakuView);
    
            MGDanmakuHttpController mMGDanmakuHttpController = new MGDanmakuHttpController();
            mMGDanmakuHttpController.setHttpRequestInterval(0);
            mMGDanmaku = new MGDanmaku(mDanmakuView, mMGDanmakuHttpController);
    
            CheckBox checkBox = (CheckBox) findViewById(R.id.checkBox);
            checkBox.setChecked(mMGDanmaku.getDanmakuRunning());
            checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    mMGDanmaku.setDanmakuRunning(isChecked);
                }
            });
    
            Button sendText = (Button) findViewById(R.id.sendText);
            sendText.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    DanmakuMsg msg = new DanmakuMsg();
                    msg.msg = "zhangphil: " + System.currentTimeMillis();
    
                    mMGDanmaku.sendMsg(msg);
                }
            });
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            mMGDanmaku.onResume();
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            mMGDanmaku.onPause();
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mMGDanmaku.onDestroy();
        }
    }



    一个简单的弹幕数据消息封装包:

    package zhangphil.danmaku;
    
    /**
     * Created by Phil on 2017/3/31.
     */
    
    import java.io.Serializable;
    
    /**
     * 弹幕数据封装的类(bean)
     */
    public class DanmakuMsg implements Serializable {
        public String id = "";
        public String msg = null;
        public boolean islive = true;
        public String point = "";
    }



    测试的MainActivity布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <CheckBox
            android:id="@+id/checkBox"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="弹幕开关" />
    
        <Button
            android:id="@+id/sendText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发送文本弹幕" />
    
        <master.flame.danmaku.ui.widget.DanmakuView
            android:id="@+id/danmakuView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </LinearLayout>
    


    注意!需要配置Activity在AndroidManifest.xml的属性configChanges和launchMode,以适应弹幕在横竖屏切换时的状态正确,配置如:

     <activity android:name=".MainActivity"
                android:configChanges="orientation|keyboardHidden|screenSize|fontScale"
                android:launchMode="singleTask">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>


    代码运行结果如图:


    附录:
    1,《Android弹幕实现:基于B站弹幕开源系统(1)》链接:http://blog.csdn.net/zhangphil/article/details/68067100 
    2,《Android弹幕实现:基于B站弹幕开源系统(2)》链接:http://blog.csdn.net/zhangphil/article/details/68114226 
    3,《Android弹幕实现:基于B站弹幕开源系统(3)-文本弹幕的完善和细节调整》链接:http://blog.csdn.net/zhangphil/article/details/68485505
    4,《Java ConcurrentLinkedQueue队列线程安全操作》链接:http://blog.csdn.net/zhangphil/article/details/65936066

    展开全文
  • [Unity]制作一个弹幕系统

    千次阅读 2017-05-28 10:34:49
    利用OnGUI,显示一系列会动的字,这一系列的弹幕可以用一个队列维护,每一帧都更新队列中弹幕的位置,并做一次检测,如果队列中弹幕位置已经不在屏幕内,可将该弹幕移除队列。 2.弹幕可以有的属性? 1.速度(矢量) 2...
  • 前端算法之弹幕设计

    2020-05-04 16:47:00
    本文愿从弹幕设计这个场景来描述算法在前端中的应用,我们先来看下实现效果: 图1.1 弹幕效果 开场之前我们先来描述弹幕开发的难度,再集中精力描述算法设计的思路。 如何保证不同字号的弹幕不碰撞 弹幕的...
  • Bilibili高并发实时弹幕系统的实现

    千次阅读 2018-06-07 17:00:06
    原文地址:https://blog.csdn.net/zhiboshequ/article/details/68489960点此打开原文站点高并发实时弹幕是一种互动的体验。对于互动来说,考虑最多的地方就是:高稳定性、高可用性以及低延迟这三个方面。高稳定性,...
  • 视频直播app源码直播弹幕系统如何实现 直播弹幕指直播间的用户,...美拍直播弹幕系统设计初期的核心要求是:快速上线,并能支撑百万用户同时在线。基于这两点,我们策略是前中期 HTTP 轮询方案,中后期替换为长连..
  • 今天简单记录一下弹幕服务器的设计思路,希望对大家有所帮助。业务特点弹幕典型的进少出多场景,一个房间如果有10W观众,每秒提交的弹幕也许只有1000次,但是广播弹幕给所有观众需要1000 * 10W次。单机模型为了推送...
  • 直播弹幕指直播间的用户,礼物,评论,点赞等消息,是直播间交互的重要手段。...一、快速上线消息模型美拍直播弹幕系统设计初期的核心要求是:快速上线,并能支撑百万用户同时在线。基于这两点,我们策略...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 8,029
精华内容 3,211
关键字:

弹幕系统设计