弹幕系统很复杂?前端工程师也能完成!

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介:

socket.io

简介

使用流行的 web 应用技术栈 —— 比如PHP —— 来编写聊天应用通常是很困难的。它包含了轮询服务器以检测变化,还要追踪时间戳,并且这种实现是比较慢的。

大多数实时聊天系统通常基于 WebSocket 来构建,具体来说就是socket.io。 WebSocket 为客户端和服务器提供了双向通信机制。

这意味着服务器可以 推送 消息给客户端。无论何时你发布一条消息,服务器都可以接收到消息并推送给其他连接到服务器的客户端。

web 框架

首先要制作一个 HTML 页面来提供表单和消息列表。我们使用了基于 Node.JS 的 web 框架 express 。 请确保安装了 Node.JS。

首先创建一个 package.json 来描述我们的项目。 推荐新建一个空目录。

express 已经安装好了。我们现在新建一个 index.js 文件来创建应用。

var app = require('express')();
var http = require('http').Server(app);

app.get('/', function(req, res){
 res.send('<h1>Hello world</h1>');
});

http.listen(3000, function(){
 console.log('listening on *:4000');
});
 
复制代码

这段代码作用如下:

Express 初始化 app 作为 HTTP 服务器的回调函数。

定义了一个路由 / 来处理首页访问。

使 http 服务器监听端口 4000。

HTML 服务器

目前在 index.js 中我们是通过 res.send 返回一个 HTML 字符串。 如果我们将整个应用的 HTML 代码都放到应用代码里,代码结构将变得很混乱。 替代的方法是新建一个 index.html 文件作为服务器响应。

现在我们用 sendFile 来重构之前的回调:

app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
复制代码

index.html 内容如下:


<!doctype html>
<html>
 <head>
 <title>Socket.IO chat</title>
 <style>
 * { margin: 0; padding: 0; box-sizing: border-box; }
 body { font: 13px Helvetica, Arial; }
 form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
 form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
 form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
 #messages { list-style-type: none; margin: 0; padding: 0; } #messages li { padding: 5px 10px; } #messages li:nth-child(odd) { background: #eee; }
 </style>
 </head>
 <body>
 <ul id="messages"></ul>
 <form action="">
 <input id="m" autocomplete="off" /><button>Send</button>
 </form>
 </body>
</html>
复制代码

集成 Socket.IO

Socket.IO 由两部分组成:

  • 一个服务端用于集成 (或挂载) 到 Node.JS HTTP 服务器: socket.io
  • 一个加载到浏览器中的客户端: socket.io-client

这个两部分都会运用到

npm install --save socket.io

npm install --save socket.io-client

var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/', function(req, res){
 res.sendFile(__dirname + '/index.html');
});

io.on('connection', function(socket){
 console.log('a user connected');
});

http.listen(3000, function(){
 console.log('listening on *:3000');
});
复制代码

我们通过传入 http (HTTP 服务器) 对象初始化了 socket.io 的一个实例。 然后监听 connection 事件来接收 sockets, 并将连接信息打印到控制台。

在 index.html 的 标签中添加如下内容:

<script src="/socket.io/socket.io.js"></script>
<script>
 var socket = io();
</script>
复制代码

这样就加载了 socket.io。 socket.io 暴露了一个 io 全局变量,然后连接服务器。

请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。

重新加载服务器和网站,你将看到控制台打印出 “a user connected”。

每个 socket 还会触发一个特殊的 disconnect 事件:

io.on('connection', function(socket){
 console.log('a user connected');
 socket.on('disconnect', function(){
 console.log('user disconnected');
 });
});
 
复制代码

触发事件

Socket.IO 的核心理念就是允许发送、接收任意事件和任意数据。任意能被编码为 JSON 的对象都可以用于传输。二进制数据 也是支持的。

这里的实现方案是,当用户输入消息时,客户端发送一个 chat message 事件,服务器接收一个 chat message 事件。index.html 文件中的 script 部分现在应该内容如下:

<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
 $(function () {
 var socket = io();
 $('form').submit(function(){
 socket.emit('chat message', $('#m').val());
 $('#m').val('');
 return false;
 });
 });
</script>
复制代码

广播

接下来的目标就是让服务器将消息发送给其他用户。

要将事件发送给每个用户,Socket.IO 提供了 io.emit 方法:

io.emit('some event', { for: 'everyone' });

为了简单起见,我们将消息发送给所有用户,包括发送者。

io.on('connection', function(socket){
 socket.on('chat message', function(msg){
 io.emit('chat message', msg);
 });
});
复制代码

用法总结

服务端

1.连接

监听客户端连接,回调函数会传递本次连接的socket

 io.on('connection',function(socket));
复制代码

2.广播

(1)给所有客户端广播消息

 io.sockets.emit('String',data);
复制代码

(2)给除了自己以外的客户端广播消息

 socket.broadcast.emit("msg",{data:"hello,everyone"});
复制代码

(3)给指定的客户端发送消息

 io.sockets.socket(socketid).emit('String', data);
复制代码

3.发送的消息

(1)监听客户端

 socket.on('String',function(data));
复制代码

(2)给该socket的客户端发送消息

 socket.emit('String', data);
复制代码

4.分组

io.of('/some').on('connection', function (socket) {
 socket.on('test', function (data) {
 socket.broadcast.emit('event_name',{});
 });
});
复制代码

进阶——处理用户发送的数据

image

一、redis

什么是Redis?

REmote DIctionary Server(Redis) 是一个由SalvatoreSanfilippo写的key-value(键值对)存储系统。

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

它通常被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Map), 列表(list), 集合(sets) 和有序集合(sorted sets)等类型。

Redis中的数据类型

哈希(Map hashmap):散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。

列表(list):列表是一种数据项构成的有限序列,即按照一定的线性顺序,排列而成的数据项的集合。(redis中使用双向链表实现)

集合(sets):和中学时学习的概念是相似的。特点是集合中元素不能重复是唯一的。切内部是无序的

有序集合(sorted sets):也是一种集合,但是内部数据是经过排序的。

redis安装

Redis 安装链接

npm redis

redis使用方法

0、建立node-redis的client端连接 npm i redis --save

// redis 链接
var redis = require('redis');
var client = redis.createClient('6379', '127.0.0.1');

// redis 链接错误
client.on("error", function(error) {
 console.log(error);
});
// redis 验证 (reids.conf未开启验证,此项可不需要)
// client.auth("foobared");
module.exports = {
 client:client
}

复制代码

1、set的存取

const {client} = require('./redis')

client.set('key001', 'AAA', function (err, response) {
 if (err) {
 console.log("err:", err);
 } else {
 console.log(response);
 client.get('key001', function (err, res) {
 if (err) {
 console.log("err:", err);
 } else {
 console.log(res);
 client.end(true);
 }
 });
 }
});
复制代码

2、hash存取

hash set的设值和抽取数据都有单个key和多个key两种方式:

const {client} = require('./redis')

client.hset('filed002', 'key001', 'wherethersisadoor', function (err, res) {
 if (err) {
 console.log(err);
 } else {
 console.log('res:', res);
 client.hget('filed002', 'key001', function (err, getRslt) {
 if (err) {
 console.log(err);
 } else {
 console.log('getRslt:', getRslt);
 client.end(true);
 }
 });
 }
});
复制代码

注意:当hget方法在指定field下找不到指定的key时,会传给回调函数null,而非空字符或undefined。

※ 设定多个key的值,取值时获取指定field下指定单个或多个key的值

const {client} = require('./redis')

var qe = {a: 2, b:3, c:4};
client.hmset('field003', qe, function(err, response) {
 console.log("err:", err);
 console.log("response:", response);
 client.hmget('field003', ['a', 'c'], function (err, res) {
 console.log(err);
 console.log(res);
 client.end(true);
 });
});
复制代码

hmset方法的设定值可以是JSON格式的数据,但是redis中key的值是以字符串形式存储的,如果JSON数据层数超过一层,会出现值是'[object Object]'的情况。

hmget方法的返回值是个数组,其中元素的顺序对应于参数的key数组中的顺序,如果参数数组中有在field内不存在的key,返回结果数组的对应位置会是null,也即无论是否能取到值,结果数组中的元素位置始终与参数的key数组中元素位置一一对应。

获取hash中所有key的方法是client.keys(fieldname, callback); 需要注意的是如果hash中key的数目很多,这个方法的可能耗费很长时间。

3.链表 适合存储社交网站的新鲜事 lpush key value [value ...] 向链表key左边添加元素 rpush key value [value...] 向链表key右边添加元素 lpop key 移除key链表左边第一个元素 rpop key 移除key链表右边第一元素

const {client} = require('./redis')

client.lpush('test', 12345, function(err, response) {
 if(err){
 console.log("err:", err);
 }else{
 console.log("response:", response);
 client.rpop('test',function (err, res){
 if(err){
 console.log(err);
 }else{
 console.log(res);
 client.end(true);
 }
 });
 }
});
复制代码

redis-cli

image
socket.io中接入redis 并创建多个命名空间

How to use

const io = require('socket.io')(3000);
const redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
复制代码

将index.js修改为

const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const redis = require('socket.io-redis');
const {client} = require('./test/redis')
const moment = require('moment')


app.get('/', function(req, res){
 res.sendFile(__dirname + '/index.html');
});

io.adapter(redis({host: 'localhost', port: 6379}));

var nameBox = ['/chatroom','/live','/vod','/wechat','/broadcast'];

for(var item in nameBox){
 var nsp = io.of(nameBox[item])
 socketMain(nsp,nameBox[item])
}

function socketMain(nsp,roomName) {
 nsp.on('connection',function (socket) {
 console.log('a user connected')
 socket.on('disconnect', function(){
 console.log('user disconnected');
 });
 socket.on('chat message', function(msg){
 var data = {"socketid":socket.id,"cid":roomName,"msg":msg,createTime:moment().unix()};
 client.lpush('message',JSON.stringify(data),redis.print)
 
 console.log('message: ' + msg);
 });
 })
}

http.listen(4000, function(){
 console.log('listening on *:4000');
});
复制代码

index.html

 var socket = io.connect("http://127.0.0.1:4000/live");

复制代码
接入redis
client.lpush('message',JSON.stringify(msg),redis.print)

复制代码

二、另起一个服务端拿redis数据进行处理

Question:两个服务怎么相互通信?
Answer:使用socket.io-client 具体步骤如下:
1.在数据处理程序中引入 socket.io-client 
var io = require('socket.io-client');

2.用socket.io-client 模拟了一个,连接到主程序io中的客户端
var socket = io.connect('ip+'/live'', {reconnect: true});

3.通过这个模拟的客户端,与主程序通信
socket.emit('redisCome', result);
复制代码

修改redis.js

module.exports = {
 client:client,
 ip:'http://127.0.0.1:4000'
}
复制代码

新建sclient.js

const io = require('socket.io-client');
const async = require('async');
const moment = require('moment');
const redis = require('redis');

const {client,ip} = require('./test/redis');
const domain = require('domain');
const debug = require('debug')('socket-client:main');

var origin = io.connect(ip+'/', {reconnect: true});
var chatroom = io.connect(ip+'/chatroom', {reconnect: true});
var live = io.connect(ip+'/live', {reconnect: true});
var vod = io.connect(ip+'/vod', {reconnect: true});
var wechat = io.connect(ip+'/wechat', {reconnect: true});
var broadcast = io.connect(ip+'/broadcast', {reconnect: true});

var namBox = {root:origin,chatroom:chatroom,live:live,vod:vod,wechat:wechat,broadcast:broadcast};

var reqDomain = domain.create();
reqDomain.on('error', function (err) {
 console.log(err);
 try {
 var killTimer = setTimeout(function () {
 process.exit(1);
 }, 100);
 killTimer.unref();
 } catch (e) {
 console.log('error when exit', e.stack);
 }
});

reqDomain.run(function () {
 compute();
});

process.on('uncaughtException', function (err) {
 console.log(err);
 try {
 var killTimer = setTimeout(function () {
 process.exit(1);
 }, 100);
 killTimer.unref();
 } catch (e) {
 console.log('error when exit', e.stack);
 }
});

function compute() {
 client.llen('message', function(error, count){
 if(error){
 console.log(error);
 }else{
 if(count){
 //console.log('-------------has count',time);
 popLogs();
 process.nextTick(compute);
 }else{
 //console.log('-------------empty',time);
 setTimeout(function(){
 compute();
 },100);
 }
 }
 });
}

function popLogs(){
 var time = moment().unix();
 console.log('-------------dealStart-------------',time);
 client.rpop('message',function(err,result){
 if(err){
 console.log(err);
 }else{
 var result = JSON.parse(result);
 try{
 var cid = result.cid;
 //console.log('place',result.place);
 }catch(e){
 console.log('empty data cid',result);
 return;
 }
 console.log(' start '+' nsp: '+cid +' time: '+time);
 if(namBox[cid]){
 console.log(result);
 namBox[cid].emit('redisCome',result);
 }
 }
 });
}
复制代码

修改index.js 增加redisCome监听事件

/*接收redis发来的消息*/
socket.on('redisCome',function (data) {
 console.log('-------------redisCome',data.msg);
 try{
 var msg = data.msg
 }catch(e){
 var msg = '';
 }
 console.log(data);
 nsp.emit('message.add',msg);
});
复制代码

修改index.html

socket.on('message.add',function (msg) {
 $('#messages').append($('<li>').text(msg));
})
复制代码

三、增加用户发送信息校验

增加信息的安全性,我们可以对用户发送的信息进行敏感词、sql注入攻击、xss攻击等进行过滤 使用async一步步操作流程

修改sclient.js

async.waterfall([
 function (done) {
 user.messageDirty({msg:result.msg},function(err,res){
 //console.log('sql done'/*,res*/);
 done(err,res);
 });
 },
 function (res,done) {
 user.messageValidate({msg:result.msg},function(err,res){
 //console.log('key done'/*,res*/);
 done(err,res);
 });
 }
],function (err,res) {
 if(err){
 console.log('err!!!!',err,result);
 namBox[cid].emit('messageError',err);
 }else{
 if(namBox[cid]) {
 console.log(result);
 namBox[cid].emit('redisCome', result);
 }
 }
})
复制代码

修改index.js

/*接收redis错误信息返回*/
socket.on('messageError',function(err){
 console.log('messageError');
 try{
 nsp.emit('message.error',err.msg);
 }catch(e){

 }
});
复制代码

修改index.html

mysql入库

1.在本地安装mysql数据库 2.下载node mysql包

npm install mysql --save
复制代码

3.连接数据库 建立连接池

var mysql = require('mysql');
var pool = mysql.createPool({
 host: 'localhost',
 user:'root',
 password:'123456',
 database : 'danmaku'
});

var query = function(sql,options,callback){
 pool.getConnection(function(err,conn){
 if(err){
 callback(err,null,null);
 }else{
 conn.query(sql,options,function(err,results,fields){
 //释放连接
 conn.release();
 //事件驱动回调
 callback(err,results,fields);
 });
 }
 });
};
复制代码

新建query.js

var {query} = require("./test/redis");

query("select * from demo", function(err,results,fields){
 //do something
 if(err){
 console.log(err)
 }else {
 console.log(results)
 }
});
复制代码

新建insert.js

var {query} = require("./test/redis");
const moment = require('moment')

query('insert into demo(message,createTime) values(?,?)',[123,moment().unix()],function(err,results,fields){
 //do something
 if(err){
 console.log(err)
 }else {
 console.log(results)
 }
});
复制代码

mysql -u root -p use danmaku; select * from demo;

4.在程序中添加入库步骤

弹幕播放器

ABPlayerHTML5

项目地址

链接点这里


原文发布时间:2018-06-13

原文作者:圆儿圈圈

本文来源掘金如需转载请紧急联系作者

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
移动开发 前端开发 JavaScript
web前端基础(一)——HTML+CSS
web前端基础(一)——HTML+CSS
139 0
|
移动开发 前端开发 JavaScript
Web前端基础
Web前端基础
|
前端开发 JavaScript
Web前端技术
介绍Web前端技术的知识结构、项目结构及该项目的反馈和进展。
Web前端技术
|
JavaScript 前端开发 Java
Web前端基础(13)
Web前端基础
147 0
Web前端基础(13)
|
前端开发
Web前端基础(14)
Web前端基础
131 0
Web前端基础(14)
|
前端开发
Web前端基础(02)
Web前端基础
135 0
Web前端基础(02)
|
移动开发 前端开发 BI
Web前端基础(07)
Web前端基础
211 0
Web前端基础(07)
|
前端开发
Web前端基础(04)
Web前端基础
104 0
Web前端基础(04)
|
前端开发
Web前端基础(16)
Web前端基础
165 0
Web前端基础(16)
|
前端开发
Web前端基础(09)
Web前端基础
137 0
Web前端基础(09)