实时的,在线的
一直以来我都在回避去做一些需要“实时”的项目。比如在线自动匹配、在线发送消息等。
究其原因还是没太接触过这方面的知识,并不知道用什么技术实现,总感觉会很麻烦。
但是最近学校有个微信小程序的项目,有用户实时聊天的需求。
抱着多学习一点东西的想法,我居然给接下来了!
刚开始是在百度上粗略地找了下聊天室的实现原理,发现了一个用 Java 写的在线 Web 聊天室。体验了一下,还不错,甚至还找出了几个 XSS 的洞。给作者提了 issue 后也是蛮开心的。这个项目使用 WebSocket 这个持久化的协议实现的。居然不是 HTTP 轮询!
我便天真的搜了下 PHP WebSocket,还真发现了一个叫 Swoole 的东西!
今天花了一上午的时间用 Swoole 做了一个十分简陋的聊天室 DEMO,效果居然还不错!
环境搭建
Swoole 是用 C / C++ 写的网络通信引擎。他以一个 PHP 扩展的形式整合进 PHP 中。Swoole 不仅可以用来写 WebSocket 服务器,还可以提供 TCP、UDP、HTTP 服务!同时,从官网的文档来看,调用 Swoole 十分的方便简单,感觉冗余的代码完全没有,十分的优雅。
我是在自己的腾讯云服务器上用 Docker 搭了一个 WebSocket Server。
首先,我们先把相关的 Docker 镜像拉下来。之所以使用镜像,是因为 Swoole 作为 PHP 的扩展,官网的安装方式是自己手动编译安装,这就有些麻烦了;不如用现成的镜像。
docker pull xlight/docker-php7-swoole
然后使用镜像创建一个容器:
docker run -d -t --name swoole -v /home/swoole:/home/swoole -p 5901:5901 xlight/docker-php7-swoole
这里我们是准备将 WebSocket 服务器开放在 5901 端口,因此需要先在腾讯云的控制台中配置安全组规则放出端口。
Server
这里我是写了一个十分简陋的 WebSocket 聊天室,毕竟只是个 DEMO 嘛。
$server = new Swoole\WebSocket\Server("0.0.0.0", 9501);
$server->start(); //开启服务器
首先创建一个$server
变量,这将是我们的 WebSocket 服务器,设置它的 IP 为本地,端口为 9501。
后面是三个十分常用的侦听器(这么叫应该没问题吧):
客户端连接
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
echo "server: handshake success with fd{$request->fd}\n";
});
当有客户端连接是,将会被触发。此时会在命令行中输出客户端的 ID。
客户端发送消息
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
});
当接收到客户端发向服务端的消息时会触发。fd
为客户端的 ID,data
为客户端发送的消息。
客户端断开连接
$server->on('close', function ($ser, $fd) {
echo "client {$fd} closed\n";
});
当客户端断开连接时触发,fd
为客户端 ID。
注意这里的客户端 ID 是不会被对应保存的,同一个客户端,第一次连上,ID 为 1;断开后再连上,ID 会向后顺延为 2。因此不能使用fd
来作为客户端的唯一标识符。
我的想法是,在实际的场景中,使用诸如 Redis 这类的数据库,存储当前用户的在线情况与fd
。若用户上线,则更新数据库中用户的状态并保存当次连接的fd
。因为用户之间发送消息,是用户A发送消息到服务器,服务器再发送给用户B。
服务器是通过fd
来确定发送的用户的,因此我们需要保存这个值。
服务端发送消息
$server->push($fd, "this is server");
这里$fd
即为客户端的 ID,第二个参数是发送的信息。
之后保存为ws.php
,在 Docker 容器中输入php ws.php
即可运行。在实际使用中,我们期望这个 PHP 服务器能一直在后台一直不断运行,这就需要使用:
nohup php ws.php &
同时会生成一个nohup.out
文件,里面是 PHP 的输出,我们可以将其当做日志文件使用。
Client
令我感到吃惊的是,客户端居然全部用 JavaScript 即可实现。(见识太短
var ws = new WebSocket("ws://233.233.233.233:9501");
首先是建立一个 WebSocket 连接。注意这里的协议是ws://
。
客户端接受消息
ws.onmessage= function(event){
console.log(event.data);
}
发送消息到服务端
ws.send('Hello Server.');
断开连接
ws.onclose = function(event){
console.log("连接已关闭");
};
完善一下出个 DEMO
现在,一个超级超级基本的 WebSocket 服务端 + 客户端就已经做好了。让我们再完善一下,做一个小的聊天室玩玩。
服务端代码:
<?php
$users = []; //存储用户名称与 fd 对于的数组
$server = new Swoole\WebSocket\Server("0.0.0.0", 9501); //WebSocket 服务器
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
echo "server: handshake success with fd{$request->fd}\n";
});
//收到发送的信息
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
global $users;
echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
$data = json_decode($frame->data, true); //发送过来的数据是 JSON 格式的
$users[$data['name']] = $frame->fd; //将用户名与对于的 fd 保存到数组中
switch($data['action']){ //判断发送过来的信息的类型
case 'connect': //初次连接的信息
$server->push($frame->fd, 'Server Connected!!'); //发送消息给客户端,告诉已经连接上了
break;
case 'to': //收到客户端发送的信息
$server->push($users[$data['to']], $data['name'] . ' : ' . $data['data']); //发送消息给对应的接收者
break;
}
});
$server->on('close', function ($ser, $fd) {
echo "client {$fd} closed\n";
});
$server->start(); //开启服务器
客户端代码:
var ws = new WebSocket("ws://233.233.233.233:9501");
var isConnect = false;
function btnclick(){
console.log(111)
if(!isConnect){
onConnect();
}else{
onSend();
}
}
function onConnect(){
var myName = document.getElementById('myName').value;
var data = {
'name': myName,
'action':'connect',
'data':'connect'
};
showmsg("初始化!!");
isConnect = true;
ws.send(JSON.stringify(data));
}
function onSend(){
var myName = document.getElementById('myName').value;
var toUser = document.getElementById('toUser').value;
var myData = document.getElementById('sendData').value;
var data = {
'name': myName,
'action':'to',
'to':toUser,
'data':myData
};
showmsg('我 : ' + myData);
ws.send(JSON.stringify(data));
}
ws.onmessage= function(event){
showmsg(event.data);
}
ws.onclose = function(event){
showmsg("连接已关闭");
};
function showmsg(msg){
var box = document.getElementById("msg").innerHTML;
document.getElementById("msg").innerHTML = box + '<br><br>' + msg;
}
我这里是模仿之前看到的那个 Java 项目里的数据结构。与服务器的发送的消息全部用 JSON。
然后服务器解析一下。
就是这样
大概就是这样啦~今天又接触了一个新的东西。能逐渐开始写这些实时通信的东西,自己也是挺开心的。
最近正忙着学 Vue.js,因为学校有一个项目我写前端。从零开始学,踩了巨多坑,快要自闭了。
日后若想完成那个完整的聊天室,估计得用到 Redis,这也是要去学的。
喜欢这篇文章?为什么不打赏一下呢?
赏
学弟博客写得这么好,居然没人评论。那我第一个点赞啦!
嗯嗯,谢谢学长!!