Swoole和Websocket实现简易聊天室

  • Swoole和Websocket实现简易聊天室已关闭评论
  • 324 views
  • A+
所属分类:JavaScript
在这个全民直播的时代,在线视频直播已经成为我们饭后必看的内容了。视频直播中有个弹幕功能,相信大家也玩过其实这个类似一个聊天室。今天要讲的内容就是使用SwooleWebsocket怎么实现一个简易聊天室,下面图片就是最终实现出来的效果。
Swoole和Websocket实现简易聊天室

什么是Websocket

WebSocket是一种计算机通信协议,通过单个TCP连接提供全双工通信信道。 WebSocket协议在2011年被IETF标准化为RFC6455,WebSocket旨在在Web浏览器和Web服务器中实现,但可由任何客户端或服务器应用程序使用。 WebSocket协议是独立的基于TCP的协议。它与HTTP的唯一关系是它的握手被HTTP服务器解释为升级请求。WebSocket协议允许浏览器和Web服务器之间的交互具有较低的开销,从而实现从服务器的实时数据传输。大多数主要浏览器(包括Google Chrome,Microsoft Edge,Internet Explorer,Firefox,Safari和Opera)都支持WebSocket协议,大部分程序语言都可实现Websocket服务PHP的Swoole就是其中一个。

环境准备

  1. Chrome
  2. PHP 7.1.* swoole2.0.*
  3. Nginx
  4. Node.js Npm Webpack2
上面Nginx可选,我用的环境是VagrantPHP(v7.1.4)Chrome(v60)(Node.js(v6.10)Webpack2

开始工作

使用Swoole绑定事件实现消息接收和广播消息。广播消息使所有连上服务的socket都能收到其它socket的消息,从而达到主动推送到客户端。

绑定事件

 \'0.0.0.0\',
        \'port\' => 9527,
    ];

    public function __construct(array $config = [])
    {
        foreach ($config as $key => $value) {
            if (array_key_exists($key, $this->config)) {
                $this->config[$key] = $value;
            }
        }
        $this->initialize();
    }

    /**
     * 初始化socket,绑定open, message, close回调事件
     * @see https://wiki.swoole.com/wiki/page/397.html
     */
    private function initialize()
    {
        $this->socket = new WebSocket($this->config[\'host\'], $this->config[\'port\']);

        foreach ([\'open\', \'message\', \'close\'] as $callback) {
            # code...
            $this->socket->on($callback, [$this, $callback]);
        }
    }

    //返回所有socket
    public function getConnections()
    {
        return $this->socket->connections;
    }

    public function open(WebSocket $server, Request $request)
    {
        echo $request->fd . \'--open\';        
    }

    public function close(WebSocket $server, $fd)
    {
        echo "$fd--close";
    }

    //开启服务
    public function run()
    {
        $this->socket->start();
    }
}

接收推送消息

data == "new user") {
            $this->online  ;
        } else {
            $this->message .= $frame->data;
            if ($frame->finish) {
                $message = $this->message;
                $this->message = \'\';
                //遍历所有连接,将当前消息推送给其它的连接(客户端)
                foreach ($this->getConnections() as $fd) {
                    if ($frame->fd === $fd) continue;
                    $server->push($fd, $message);
                }
            }       
        }
    }

    //重写close, 在连接断开之后人数自减
    public function close(WebSocket $server, $fd)
    {
        $this->online--;
        echo "$fd--close";
    }
}
注意: 定义private $message 是因为数据帧不完整,一个WebSocket请求可能会分成多个数据帧进行发送,所有我们必须使用$frame->finish来检测数据帧的完整性。在不完整的情况我们使用类属性$message来保存帧数据。
通过以上两个类我们完成了推送接收消息,接下来我们要完成Html页面的内容制作和Websocket(JavaScript)的脚本编写。Html页面我们使用Bootstrap构建的模板

静态页面

  1. 创建一个html5标准的index.html, chat.js, app.css三个文件。
  2. 打开 https://bootsnipp.com/snippets/WaEvr地址。
  3. 将上地址的HTML、CSS、JS页签的内容拷贝到index.html, app.css, chat.js对应的文件
  4. 添加依赖到index.html
     	
     	
     	

    
上面这个开源的模板是没有聊天室昵称功能,我们要为它加上昵称功能。
  • 修改HTML将以下内容添加到body之内


  • 修改CSS将以下内容添加到app.css
  .popup {
        position:absolute;
        width:100%;
        height:100%;
        background-color:#f4645f
  }
  .popup .form {
      height: 100px;
      margin-top: -100px;
      position: absolute;
      text-align: center;
      top: 50%;
      width: 100%;
  }
  .form .title, .form .usernameInput {
      color: #fff;
      font-weight: 100;
      font-size: 200%;
  }
  .form .usernameInput {
    background-color: transparent;
    border: none;
    border-bottom: 2px solid #fff;
    outline: none;
    padding-bottom: 15px;
    text-align: center;
    width: 400px;
  }
Swoole和Websocket实现简易聊天室
  • 编写websocket代码
function Socket() {
    if (!(this instanceof Socket)) return new Socket();
    var _this = this;
    //socket连接状态
    this.isConnection = false;
    this.message = null;
    //这个是我本地websocket的服务
    this.socket = new WebSocket("ws://192.168.56.101:9527");
    this.socket.onopen = function(event) {
        _this.isConnection = true;
        //连接成功,向服务端发送一个new user的信息, 表示一个新的用户连接上了
        _this.socket.send("new user");
    }
    //接收服务端推送的消息
    this.socket.onmessage = function(event) {
        if (_this.message) _this.message(event.data);
        else console.log(event);
    };
    this.socket.onclose = function() {
        _this.isConnection = false;
    }
};
Socket.prototype.send = function(message) {
    if (this.isConnection) {
        this.socket.send(message);
    }   
}
Socket.prototype.bind = function(callback) {
    this.message = callback;
}
Socket.prototype.close = function() {
    this.socket.close();
}
  • chat.js最终的完整代码
require("css/app.css");
global.$ = window.$ = require(\'jquery\');

(function () {
    var Message;
    Message = function (arg) {
        this.text = arg.text, this.message_side = arg.message_side, this.user = arg.user;
        this.draw = function (_this) {
            return function () {
                var $message;
                $message = $($(\'.message_template\').clone().html());
                $message.addClass(_this.message_side).find(\'.text\').html(_this.text);
                $message.find(".avatar").html(_this.user);
                $(\'.messages\').append($message);
                return setTimeout(function () {
                    return $message.addClass(\'appeared\');
                }, 0);
            };
        }(this);
        return this;
    };
    function Socket() {
        if (!(this instanceof Socket)) return new Socket();
        var _this = this;
        this.isConnection = false;
        this.message = null;
        this.socket = new WebSocket("ws://192.168.56.101:9527");
        this.socket.onopen = function(event) {
            _this.isConnection = true;
            _this.socket.send("new user");
        }
        this.socket.onmessage = function(event) {
            if (_this.message) _this.message(event.data);
            else console.log(event);
        };
        this.socket.onclose = function() {
            _this.isConnection = false;
        }
    };
    Socket.prototype.send = function(message) {
        if (this.isConnection) {
            this.socket.send(message);
        }   
    }
    Socket.prototype.bind = function(callback) {
        this.message = callback;
    }
    Socket.prototype.close = function() {
        this.socket.close();
    }
    $(function () {
        var getMessageText, message_side, sendMessage, userName, chat;
        message_side = \'right\';
        getMessageText = function () {
            var $message_input;
            $message_input = $(\'.message_input\');
            return $message_input.val();
        };
        sendMessage = function (text) {
            var $messages, message, messageStruct = JSON.parse(text);
            if (text.trim() === \'\') {
                return;
            }
            $(\'.message_input\').val(\'\');
            $messages = $(\'.messages\');
            message_side = userName == messageStruct.user ? \'right\' : \'left\';
            message = new Message({
                text: messageStruct.message,
                message_side: message_side,
                user: messageStruct.user
            });
            message.draw();
            return $messages.animate({ scrollTop: $messages.prop(\'scrollHeight\') }, 300);
        };
        $(\'.send_message\').click(function (e) {
            var text = getMessageText();
            if (text.trim() === \'\') return ;
            var message = JSON.stringify({user: userName, message: text});
            chat.send(message);
            return sendMessage(message);
        });
        $(\'.message_input\').keyup(function (e) {
            if (e.which === 13) {
                var text = getMessageText();
                if (text.trim() === \'\') return ;
                var message = JSON.stringify({user: userName, message: text});
                chat.send(message);
                return sendMessage(message);
            }
        });
        $(".usernameInput").on("keyup", function(e) {
            var val = $(this).val();
            if (val != "" && e.keyCode == 13) {
                userName = val;
                $(".popup").remove();
                chat = new Socket();
                chat.bind(sendMessage);
            }
        });
        $(window).on("unload", function(e) {
            if (chat) {
                chat.close(); 
                chat = null;
            }
        });
        $(window).on("beforeunload", function(e) {
            if (chat) {
                chat.close(); 
                chat = null;
            }
        });

        /**
        sendMessage(\'Hello Philip! :)\');
        setTimeout(function () {
            return sendMessage(\'Hi Sandy! How are you?\');
        }, 1000);
        return setTimeout(function () {
            return sendMessage(\'I\\'m fine, thank you!\');
        }, 2000);
        */
    });
}.call(this));
  • 配置webpack
const path = require("path");
const webpack = require(\'webpack\')
// importing plugins that do not come by default in webpack
const ExtractTextPlugin = require(\'extract-text-webpack-plugin\');
const HtmlWebpackPlugin = require(\'html-webpack-plugin\');

const css = new ExtractTextPlugin(\'app.css\');

const plugins = [


];

const sourcePath = path.join(__dirname, "./src");

const buildPath = path.join(__dirname, "./public/dist");
module.exports = {
    context: sourcePath,
    //预编译入口
    entry: "./chat.js",
    //预编译输出
    output: {
        // options related to how webpack emits results

        path: buildPath, // string
        // the target directory for all output files
        // must be an absolute path (use the Node.js path module)

        filename: "bundle.js", // string
        // the filename template for entry chunks

        publicPath: "./public", // string
        // the url to the output directory resolved relative to the HTML page

        library: "", // string,
        // the name of the exported library

        libraryTarget: "umd", // universal module definition
        // the type of the exported library

        /* Advanced output configuration (click to show) */
    },

    module: {

        rules: [
            {
                test: /\.css$/,
                use: css.extract([ \'css-loader\'])
            },
            {
                test: /\.(html|svg|jpe?g|png|ttf|woff2?)$/,
                exclude: /node_modules/,
                use: {
                    loader: \'file-loader\',
                    options: {
                        name: \'static/[name]-[hash:8].[ext]\',
                    },
                },
            }
        ]
    },
    resolve: {
        extensions: [\'.webpack-loader.js\', \'.web-loader.js\', \'.loader.js\', \'.js\', \'.jsx\'],
        modules: [path.resolve(__dirname, \'node_modules\'), sourcePath],
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            async: true,
            children: true,
            minChunks: 2,
        }),

        // setting production environment will strip out
        // some of the development code from the app
        // and libraries
        new webpack.DefinePlugin({
            \'process.env\': { NODE_ENV: JSON.stringify(process.env.NODE_ENV) }
        }),

        // create css bundle
        css
    ]
}
  • 执行webpack生成文件
整个前端流程到此结束

最终工作

  • 创建websocket服务
run();
  • 启动服务
[[email protected] chat]# php bin/chat.php
  • 绑定域名,查看效果
Swoole和Websocket实现简易聊天室

项目地址

https://github.com/TianLiangZhou/loocode-example

推荐阅读

  1. https://www.w3.org/TR/2009/WD-websockets-20091222/
  2. https://developer.mozilla.org/en/docs/Web/API/WebSocket
  3. https://tools.ietf.org/html/rfc6455
  4. https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin