共计 692 个字符,预计需要花费 2 分钟才能阅读完成。
在这个全民直播的时代,在线视频直播已经成为我们饭后必看的内容了。视频直播中有个弹幕功能,相信大家也玩过其实这个类似一个聊天室。今天要讲的内容就是使用 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 的消息,从而达到主动推送到客户端。
绑定事件
\'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 构建的模板
静态页面
- 创建一个 html5 标准的 index.html, chat.js, app.css 三个文件。
- 打开 https://bootsnipp.com/snippets/WaEvr地址。
- 将上地址的 HTML、CSS、JS 页签的内容拷贝到 index.html, app.css, chat.js 对应的文件
- 添加依赖到 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;
}
- 编写 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();
- 启动服务
[root@meshell chat]# php bin/chat.php
- 绑定域名,查看效果
项目地址
推荐阅读
- https://www.w3.org/TR/2009/WD-websockets-20091222/
- https://developer.mozilla.org/en/docs/Web/API/WebSocket
- https://tools.ietf.org/html/rfc6455
- https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API