聊天系統很複雜?前端工程師也能完成!

圓兒圈圈發表於2018-06-13

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.iosocket.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

專案地址

連結點這裡

相關文章