手写弹幕服务器—包看懂篇

socket.io

简介

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

大多数实时聊天系统通常基于 socket 来构建。 Socket 为客户端和服务器提供了双向通信机制。

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

web 框架

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

首先创建一个 package.json 来描述我们的项目。 推荐新建一个空目录 (这里使用 chat-example)。

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

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

app.get('/', function(req, res){
  res.send('

Hello world

'); }); 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 内容如下:




  
    Socket.IO chat
    
  
  
    

    集成 Socket.IO

    Socket.IO 由两部分组成:

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

    这个两部分都会运用到

    npm install --save socket.io

    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 的 标签中添加如下内容:

    
    
    

    这样就加载了 socket.io-client。 socket.io-client 暴露了一个 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 事件。index.html 文件中的 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);
      });
    });
    

    用法总结

    服务端

    io.on('connection',function(socket));
    
    监听客户端连接,回调函数会传递本次连接的socket
    
    io.sockets.emit('String',data);
    
    给所有客户端广播消息
    
    socket.broadcast.emit("msg",{data:"hello,everyone"}); 
    
    给除了自己以外的客户端广播消息
    
    io.sockets.socket(socketid).emit('String', data);
    
    给指定的客户端发送消息
    
    socket.on('String',function(data));
    
    监听客户端发送的信息
    
    socket.emit('String', data);
    
    给该socket的客户端发送消息
    
    io.of('/some').on('connection', function (socket) {
        socket.on('test', function (data) {
            socket.broadcast.emit('event_name',{});
        });
    });
    
    分组
    
    

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

    手写弹幕服务器—包看懂篇_第1张图片
    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);
                }
            });
        }
    });
    

    [图片上传失败...(image-354cf6-1519888571721)]

    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数据进行处理

    修改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($('
  • ').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

    你可能感兴趣的:(手写弹幕服务器—包看懂篇)