Swoole和Websocket实现简易聊天室
在这个全民直播的时代,在线视频直播已经成为我们饭后必看的内容了。视频直播中有个弹幕功能,相信大家也玩过其实这个类似一个聊天室。今天要讲的内容就是使用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就是其中一个。
环境准备
- Chrome
- PHP 7.1.* + swoole2.0.*
- Nginx
- Node.js + Npm + Webpack2
上面Nginx可选,我用的环境是Vagrant、PHP(v7.1.4)、Chrome(v60)、(Node.js(v6.10)、Webpack2
开始工作
使用Swoole绑定事件实现消息接收和广播消息。广播消息使所有连上服务的socket都能收到其它socket的消息,从而达到主动推送到客户端。
绑定事件
<?php
namespace Chat\Server;
use Swoole\Websocket\Server as WebSocket;
use Swoole\Websocket\Frame;
use Swoole\Http\Request;
class WebSocketServer
{
private $socket = null;
private $config = [
//host 地址为0.0.0.0开放所有地址连接
'host' => '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();
}
}
接收推送消息
<?php
namespace Chat\Server;
use Swoole\Websocket\Server as WebSocket;
use Swoole\Websocket\Frame;
class ChatServer extends WebSocketServer
{
private $message = '';
//聊天室的人数
private $online = 0;
/**
* 在收到客户发送消息后会回调此函数, 帧数据存在$frame变量中
* @param WebSocket $server
* @param Frame $frame
* @see https://wiki.swoole.com/wiki/page/402.html
*/
public function message(WebSocket $server, Frame $frame)
{
$message = '';
if ($frame->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构建的模板
静态页面
- 创建一个html5标准的index.html, chat.js, app.css三个文件。
- 打开 https://bootsnipp.com/snippets/WaEvr地址。
- 将上地址的HTML、CSS、JS页签的内容拷贝到index.html, app.css, chat.js对应的文件
- 添加依赖到index.html
<link href="//cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="//cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="./dist/app.css" rel="stylesheet">
<script type="text/javascript" src="./dist/bundle.js"></script>
上面这个开源的模板是没有聊天室昵称功能,我们要为它加上昵称功能。
- 修改HTML将以下内容添加到body之内
<div class="popup">
<div class="form">
<h3 class="title">输入您的聊天室名?</h3>
<input class="usernameInput" type="text" maxlength="14">
</div>
</div>
- 修改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;
}
- 编写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服务
<?php
include __DIR__ . '/../vendor/autoload.php';
use Chat\Server\ChatServer;
$chat = new ChatServer();
$chat->run();
- 启动服务
[root@meshell chat]# php bin/chat.php
- 绑定域名,查看效果
项目地址
https://github.com/TianLiangZhou/loocode-example