PHP+Node.js+Redis+Socket.io实现Web端即时通讯(一)

0.102字数 1574阅读 1977

Git Repository

如果你不想读本文,只想看一代码,那么你就可以打开这个东西。
👉👉👉项目位置

伏笔

  1. 在二维码横行的时代,各大网站都实现了扫描二维码登录的用户体验度极高的功能,我常常在如何实现这样的效果呢?于是,为本篇文章埋下伏笔。
  2. 本人为PHPer,常年在开发企业级的ERP系统,在不安于现状的情况下,我想尽可能让用户体验达到最好,于是用Vue开发了ERP其中的一个子系统,反响不错。但本人不安于现状,想着去玩点新鲜的。这时,即时的消息通知和数据更新便出现在了我的脑海了。于是,为此文又埋下伏笔。
  3. 在之前有同学问我<广播事件>的相关知识点,但鉴于才疏学浅,不曾涉猎此领域,于是就敷衍了事,但在我心里埋下了一颗种子。
  4. 正处于双11时间点,本人又是一个11,又没有什么朋友,于是一个人在家又读起Laravel文档来,当读到Laravel<广播事件>时,感觉有必要研究一下了,于是此文诞生。

准备

广播事件在Laravel的实现驱动有两种,其一为Pusher,其二为Socket.io + Redis
Pusher在文档里有阐释如何使用,包括demo等。使用驱动需要注册账号、安装组件等(好像还是要花钱的,毕竟人家是第三方服务),于是本人就没去体验。
Socket.io + Redis在官方文档里面有阐释其原理,但是没有一个demo,这就让上手难度增加了不少。不过在Laravel社区中有一个配合驱动的一个WebSocket服务器项目,大家可以下载来尝试,本人尝试了,但确实笔者比较愚钝,依然没整明白。
但当我读到的时候,我才真正的明白了其中原理。

Redis 广播器会使用 Redis 的「生产者/消费者」特性来广播消息;尽管如此,你仍需将它与 WebSocket 服务器一起使用。WebSocket 服务器会从 Redis 接收消息,然后再将消息广播到你的 WebSocket 频道上去。
当 Redis 广播器发布一个事件时,该事件会被发布到它指定的频道上去,传输的数据是一个采用 JSON 编码的字符串。该字符串包含了事件名、 data 数据和生成该事件套接字 ID 的用户(如果可用的话)。

环境

  1. Mac 2016 <MACOS 10.13.1>
  2. Node v7.1.0
  3. NPM v4.2.0
  4. Redis server v=3.2.8
  5. socket.io v2.0.4(NPM 安装)
  6. ioredis v3.2.1(NPM 安装)
  7. PHP 7.1
  8. Laravel 5.5

整理所用知识

  1. Redis发布与订阅
  2. 使用Node.js搭建Socket服务器
  3. 使用Node.js操作Redis
  4. HTML页面中使用socket.io组件实现信息的接收与发布

开始干活之搭建HTTP Server服务器

  1. 安装node环境(其它系统请自查)
$ brew install node
$ node -v
$ npm -v 

第一步安装node,二三步检查版本
npm如果下载不下来组件,请更换为cnpm淘宝镜像

  1. 初始化项目
$ mkdir ~/node-socket
$ cd ~/node-socket
$ npm init

第一步创建项目目录,二步进入目录,三步初始化项目(回车大法解决一切)

  1. 在项目根目录下创建index.jsindex.html文件
  • index.html
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <h1>Hello World</h1>
</body>
</html>
  • index.js
var app = require('http').createServer(handler)
var fs = require('fs');

function handler(req, res) {
    fs.readFile(__dirname + '/index.html',
        function (err, data) {
            if (err) {
                res.writeHead(500);
                return res.end('Error loading index.html');
            }
            
            res.writeHead(200);
            res.end(data);
        });
}

app.listen(3000);
console.log('Lisening http://localhost:3000');

解读:
第一行:使用node核心模块http创建Http服务器。
第二行:引用node核心模块文件模块fs
函数handler:服务器接受到请求的回调函数。接收两个参数requestresponse,请求与响应。其函数体具体含义为,读取__dirname绝对路径下的index.html文件数据,如果读取失败,那么抛出500的异常;如果读取成功,则响应具体读取到的数据。
最后:监听3000端口

  1. 开启服务
    在项目根目录执行第一行,并随之出现二行的命令行提示信息,即服务器已启动。
$ node index.js
$ Lisening http://localhost:3000
  1. 打开http://localhost:3000,出现如下画面你就成功了。
    image.png

开始干活之配置WebSocket服务器

  1. 安装socket.io
cnpm install socket.io --save

--save是保存到package.json文件,用于阐述项目依赖,与composer.json异曲同工

  1. 文件
  • index.html
<html>
<head>
    <meta charset="UTF-8">
    <script src="/socket.io/socket.io.js"></script>
</head>
<body>
    <h1>Hello World</h1>
    <script>
        // 监听地址、端口一定与自己开启的HTTP SERVER的地址与端口一致。
        var socket = io('http://localhost:3000');
        
        // Socket连接成功的事件,当连接成功后,执行回调,log输出socket.id及自定义信息
        socket.on('connect', () => {
            console.log(socket.id);
            console.log('Connecting...');
        });
        
        // 监听新闻事件,与后台的事件名称必须一致,否则不能实现效果。
        socket.on('news', function (data) {
        // 当监听到news事件后,输出data信息;然后再向服务器发送back事件。
        // 如果后台同时监听了此事件,那么后台在监听到此事件后,会进行相应逻辑处理。
        // 这样就实现了Web端与服务端的相互通讯。
            console.log(data);
            socket.emit('back', { my: 'data' });
        });
    </script>
</body>
</html>
  • index.js
var app = require('http').createServer(handler)
var fs = require('fs');
var io = require('socket.io')(app);

// Socket连接事件,监听到此事件后,执行相应的回调。
io.on('connection', function (socket) {
    // 向前台发送news事件,并携带json数据
    socket.emit('news', { hello: 'world' });
    // 监听前台发送的back事件
    socket.on('back', function (data) {
        console.log(data);
    });
});

function handler(req, res) {
    fs.readFile(__dirname + '/index.html',
        function (err, data) {
            if (err) {
                res.writeHead(500);
                return res.end('Error loading index.html');
            }
            
            res.writeHead(200);
            res.end(data);
        });
}

app.listen(3000);
console.log('Lisening http://localhost:3000');
  1. 测试
    开启服务$ node index.js
  • web端


    image.png
  • 服务器端


    image.png

开始干活之Node.js与Redis的关联使用

  1. 测试与Redis的连接(前提是你安装并开启了redis服务器,至于怎么安装请自行查阅google)
  • index.js
...省略
var Redis = require('ioredis');
var redis = new Redis();
redis.set('foo', 'bar');
redis.get('foo', function (err, result) {
    console.log(result);
});
...省略

发现如下画面,你就成功了。

  • 服务器端


    image.png
  • Redis客户端(Redis如果不会请自行查询文档)


    image.png
  1. 订阅与发布
  • index.js
var app = require('http').createServer(handler)
var fs = require('fs');
var io = require('socket.io')(app);

var Redis = require('ioredis');
var redis = new Redis();
var pub = new Redis();

// 订阅channel频道 system,可以订阅多个频道,err指的是错误,count指的订阅频道个数
redis.subscribe('system', function (err, count) {
    // 当订阅成功之后,在频道内部发布一条反馈信息
    pub.publish('system', 'Listening The System Channel');
    console.log('Listening System Channel');
});

// Socket连接事件,当连接成功后触发回调函数
io.on('connection', function (socket) {
    // 向前台发送news事件,并携带json数据
    socket.emit('news', { hello: 'world' });
    // 监听前台发送的back事件
    socket.on('back', function (data) {
        console.log(data);
    });
    // 监听已订阅频道发来的信息事件 message event
    redis.on('message', function (channel, message) {
        // channel指的是频道,message回传的信息
        console.log('Receive message %s from channel %s', message, channel);
        // 向前台发送news事件
        socket.emit('news', {message: message});
    });
});

function handler(req, res) {
    fs.readFile(__dirname + '/index.html',
        function (err, data) {
            if (err) {
                res.writeHead(500);
                return res.end('Error loading index.html');
            }
            
            res.writeHead(200);
            res.end(data);
        });
}

app.listen(3000);
console.log('Lisening http://localhost:3000');
  • index.html
<html>
<head>
    <meta charset="UTF-8">
    <script src="/socket.io/socket.io.js"></script>
</head>
<body>
    <h1>Hello World</h1>
    <script>
        // 监听地址、端口一定与自己开启的HTTP SERVER的地址与端口一致。
        var socket = io('http://localhost:3000');
        
        // Socket连接成功的事件
        socket.on('connect', () => {
            console.log(socket.id);
            console.log('Connecting...');
        });
        
        // 新闻事件,与后台的触发的事件必须一致,否则不能实现效果
        socket.on('news', function (data) {
            // 当监听到news事件后,向服务器发送back事件,实现前后台的相互通讯
            console.log(data);
            socket.emit('back', { my: 'data' });
        });
    </script>
</body>
</html>
  1. 测试及效果
    启动node index.js服务,然后使用redis-cli在 system channel频道中发布一条信息I am System Message6,然后效果如下:
  • 服务端


    image.png
  • Web端


    image.png
  • Redis端


    image.png

开始干活之在PHP中使用Redis发布信息,在Node服务端订阅信息,利用Socket.io实现Web的即时信息展示。

  1. 打开Laravel项目,路由文件中写入
Route::get('/socket', function () {
    \Redis::publish('system', '我是一个系统级别的信息,我来自PHP');
    return 'success';
});
  1. 访问对应路由
  • PHP部分


    image.png
  • Node服务端


    image.png
  • Web端


    image.png

大功告成

基本实现流程就这些。但是笔者暂时还没有深入研究其中奥义所在。
比如使用Laravel的事件广播、用户识别、不同频道的不同信息处理、以及Web端回传数据,PHP端写入数据库、触发响应的事件等等。这些都需要时间进行研究,不过大致的流程已经跑通,相信之后的路会越走越通。
本文暂时未完,还有二系列的发布,敬请期待。

推荐阅读更多精彩内容