Contents
  1. 1. Express
  2. 2. 路由
  3. 3. 屏幕客户端
    1. 3.1. 静态
    2. 3.2. 动态(服务端)
  4. 4. 发射客户端

看院新年晚会的时候,发现最大的乐趣就是微信上墙了,但是量大了要等好久才能看见自己发的,为什么不是弹幕的形式呢?当时是那么想的。然后我寒假有空的时候搜罗了一下如何实现,最后算是变成Node.js的Express4框架实例了,又名基于Socket.IO的实时弹幕系统的设计与实现。

Express

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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中有了这么一段:

1
2
3
"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。

structure.png

添加 routes/indexCtrl.js

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
var indexCtrl = require('./routes/indexCtrl');
var emitCtrl = require('./routes/emitCtrl');
...
app.use('/', indexCtrl);
app.use('/emit', emitCtrl);

启动后可查看到index页面。
index_0.png

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


屏幕客户端

静态

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

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

1
$ npm install comment-core-library --save

使用时(去除public)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<link rel="stylesheet" href="/node_modules/comment-core-library/build/style.css" />
<script src="/node_modules/comment-core-library/build/CommentCoreLibrary.js"></script>
```
另外[CommentCoreLibrary][11]模块也有点笨重。</s>

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


添加**views/index.jade**
``` 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

1
2
3
4
5
6
7
8
9
* { 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

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

然后在Console里怒射一弹:

1
2
3
4
5
6
7
8
9
var danmaku = {
"mode": 1,
"text": "hello world",
"stime": 0,
"size": 25,
"color": 0xff00ff,
"dur": 10000
};
CM.send(danmaku);

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

index_1.png

动态(服务端)

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

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:

1
npm install socket.io --save

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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

1
2
3
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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”事件执行的提示。
index_2.png


发射客户端

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

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

添加public/jsons/config.json

1
2
3
4
5
6
7
{"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,读取配置

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

emit.png


p.s. 写文章虽然好辛苦,但真的有助于理清思路……

Contents
  1. 1. Express
  2. 2. 路由
  3. 3. 屏幕客户端
    1. 3.1. 静态
    2. 3.2. 动态(服务端)
  4. 4. 发射客户端