PHP Swoole 实现 WebSocket 聊天室

实时的,在线的

一直以来我都在回避去做一些需要“实时”的项目。比如在线自动匹配、在线发送消息等。
究其原因还是没太接触过这方面的知识,并不知道用什么技术实现,总感觉会很麻烦。
但是最近学校有个微信小程序的项目,有用户实时聊天的需求。
抱着多学习一点东西的想法,我居然给接下来了!
刚开始是在百度上粗略地找了下聊天室的实现原理,发现了一个用 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,这也是要去学的。



喜欢这篇文章?为什么不打赏一下呢?

爱发电

2 条评论

 

昵称
  1. Accepted Doge

    学弟博客写得这么好,居然没人评论。那我第一个点赞啦!

    1. John

      嗯嗯,谢谢学长!!