如何使用PHP构建一个高性能的弹幕后端服务

        随着WEB2.0的流行,现在很多网站都流行使用“弹幕”这种形式来实现互动。 弹幕(barrage),中文流行词语,原意指用大量或少量火炮提供密集炮击。大量以字幕弹(dàn)出形式显示的评论同时在屏幕上飘过的现象也被称为弹幕。

        作为PHPer的我们,看到现在各种网站都有酷炫的弹幕飞过,我们是不是也想给自己的网站加入弹幕功能呢?

        首先弹幕的后端其实说白了和公共聊天室的后端原理十分相似,都是一个客户端发送消息给服务端,服务端再将收到的消息广播给其他的客户端。对于后端来说他们几乎没区别,区别就在于前端。

        好在我们有一个前端弹幕插件,这个插件是一个jquery插件,github地址:https://github.com/chiruom/jquery.danmu.js ,基本上会使用jquery语法,看看示例代码就可以傻瓜化使用。

        前端已经有了解决方案,但是后端呢?前端如何与后端通讯?用传统的ajax轮询吗?不行,这样效率太低,想想各大火爆的直播平台都是同一时间几万人在线,几千人同时发弹幕,如果靠ajax轮询一个php接口的话服务器会吃不消的。且弹幕消息存储方案略显复杂,有人问为什么要存储呢?因为ajax使用的HTTP协议是无状态协议,A客户端和B客户端之间对于服务器来说没有任何标志,如果服务器要确保A客户端和B客户端分别在两次请求的时候服务器只返回这两个客户端没有获取过的弹幕消息,那么服务器端就必须使用一个缓存来标识某某客户端看过哪条弹幕消息。综上所述ajax可以实现小规模的弹幕通信方案,但是很麻烦。


        好在最新的HTML5中加入了WebSocket协议,我们可以通过WebSocket这种基于HTTP协议之上的即时通信协议来替代ajax这种传统的我问你答的老旧通信模式。而我们是PHPer,对于我们这种只懂PHP的人该如何编写WebSocket服务端呢?好在我们又得知PHP有一个Swoole扩展,我们在PHP语言中使用它可以很方便的构建一个WebSocket服务端。

        尚未成功添加php-swoole扩展的,这里附上一个传送门:https://cksite.cn/article/86.html 

        

        我们可以先看一下效果图:

        冷暖自知一抹茶ck

    

    两个浏览器之前完全独立使用Websocket连接服务端,因此对于服务端来说这两个浏览器就相当于两个完全处在不同机器上的客户端。


冷暖自知一抹茶ck


        接下来就正式开始我们的编码旅程了,我们先看看官网的WebSocket服务端示例代码。

$serv = new Swoole\Websocket\Server("127.0.0.1", 9502);

$serv->on('Open', function($serv, $req) {
    echo "connection open: ".$req->fd;
});

$serv->on('Message', function($serv, $frame) {
    echo "message: ".$frame->data;
    $serv->push($frame->fd, json_encode(["hello", "world"]));
});

$serv->on('Close', function($serv, $fd) {
    echo "connection close: ".$fd;
});

$serv->start();

        我们看到这个代码的第一行先是new了一个WebSocket服务端对象,并且在构造方法中的第一个参数指定了服务端监听的IP,第二个参数指定了服务端监听的端口。然后使用on方法为每一个事件设置了回调函数,最后一行start方法正式开始运行服务端。

        这种写法非常像Javascript里面的异步调用,这也是Swoole中的事件驱动异步非阻塞特性,正因为是这种特性,每一个独立的事件(请求)会在服务端接收到之后分别异步处理,他们之间无需互相等待,这也是Swoole性能高的原因所在。

        我们来分别剖析一下每一个事件的含义。

$serv->on('Open', function($serv, $req) {
    echo "connection open: ".$req->fd;
});

        顾名思义,Open表示打开一个新的链接,并且在事件触发之后echo出连接上服务端的客户端id,该客户端唯一id为回调函数第二个参数中的fd字段。这也是服务端区分客户端的唯一id。

$serv->on('Message', function($serv, $frame) {
    echo "message: ".$frame->data;
    $server->push($frame->fd, json_encode(["hello", "world"]));
});

        同样顾名思义,Message表示消息到达服务端的事件,并且在事件触发之后echo出发送给服务端的数据,该数据为回调函数第二个参数的data字段。另外我们还看到它调用了$server->push,这是回调函数的第一个参数中的push方法,它是一个服务端给客户的发送数据的方法,第一个参数为要发送的客户端id,第二个为要发送的数据,这里的含义是向发给服务端消息的那个客户端发送["hello", "world"]这个数组(方括号写数组为PHP5.4的新特性,如果你是PHP5.3请使用传统的array工厂函数生成数组)经过json序列化之后的数据。

$serv->on('Close', function($serv, $fd) {
    echo "connection close: ".$fd;
});

        最后一个事件Close更加容易理解,就是关闭事件,当然关闭的不是服务端,而是客户端,可以理解为客户端与服务端断开连接的事件。回调函数中的代码含义为echo出与服务端断开连接的那个客户端id。


        基本的API都清楚了,下面就直接看代码吧,短短二十行而已。

        https://github.com/cw1997/danmu-demo/blob/master/server.php 

<?php
echo "author:cuikai \nstart at ".time()."\n";
//官网demo
$server = new swoole_websocket_server("0.0.0.0", 9501);
$server->on('open', function (swoole_websocket_server $server, $request) {
    echo "server: handshake success with fd{$request->fd}\n";//$request->fd 是客户端id
});
$server->on('message', function (swoole_websocket_server $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
    //$frame->fd 是客户端id,$frame->data是客户端发送的数据
    //服务端向客户端发送数据是用 $server->push( '客户端id' ,  '内容')
    $data = $frame->data;
    foreach($server->connections as $fd){
        $server->push($fd , $data);//循环广播
    }
});
$server->on('close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});
$server->start();

        这里最核心的广播代码其实还用到了一个之前没有提到过的成员,也就是swoole_websocket_server对象的connections成员,这个成员中保存了所有已连接上该WebSocket服务端的fd,也就是客户端id。因此我们只要在message事件中使用foreach遍历该成员,循环将所有服务端收到的弹幕消息都发送给其他已连接上该服务端的客户端即可。后端讲完了再讲讲前端吧。


前端代码也不是很多https://github.com/cw1997/danmu-demo/blob/master/index.html 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>swoole结合websocket实现弹幕</title>
<!-- <script src="dist/jquery-1.11.1.min.js"></script> -->
<script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="dist/jquery.danmu.js"></script>
<style type="text/css">
    #danmu {
        width: 640px;
        height: 360px;
        background-color: gray;
    }
    #send_box {
        position: absolute;
        top: 370px;
        left: 0px;
    }
</style>
</head>
<body>
<script type="text/javascript">
    console.log('code by cuikai 2018-04-13 21:04:48');
    var ws = new WebSocket("ws://192.168.31.134:9501");
    ws.onopen = function(){
       console.log("握手成功");
       ws.send('hello world!!!');
    };
    ws.onmessage = function(e){
       console.log("message:" + e.data);
       var time = jQuery('#danmu').data("nowtime") + 1;
       var text_obj = '{ "text":"' + e.data +  '" , "color":"green" ,"size":"1","position":"0","time":"' + time + '" ,
       "isnew":" "}';   //构造加上了innew属性的字符串danmu对象
       console.log(text_obj);
       var new_obj = eval('(' + text_obj + ')');       //转化为js对象
       jQuery('#danmu').danmu("add_danmu", new_obj);    //向插件中添加该danmu对象
    };
    ws.onerror = function(){
       console.log("error");
    };
    $(document).ready(function() {
        $("#danmu").danmu({
            left: 0,    //区域的起始位置x坐标
            top: 0 ,  //区域的起始位置y坐标
            height: 360, //区域的高度
            width: 640, //区域的宽度
            zindex :100, //div的css样式zindex
            speed:20000, //弹幕速度,飞过区域的毫秒数
            sumtime:50000 , //弹幕运行总时间
            danmuss:{}, //danmuss对象,运行时的弹幕内容
            default_font_color:"#FFFFFF", //弹幕默认字体颜色
            font_size_small:16, //小号弹幕的字体大小,注意此属性值只能是整数
            font_size_big:24, //大号弹幕的字体大小
            opacity:"0.9", //弹幕默认透明度
            top_botton_danmu_time:6000 //顶端底端弹幕持续时间
        } );
        $('#danmu').danmu('danmu_start');
    });
    function send() {+
        console.log(document.getElementById('content').value);
        ws.send(document.getElementById('content').value);
    }
</script>
<div id="danmu"></div>
<div id="send_box">
    <input id="content" type="text">
    <input type="submit" onclick="send()">
</div>
</body>
</html>

        核心代码都在这里,使用new WebSocket("ws://192.168.31.134:9501")创建一个WebSocket客户端连接对象,通过该对象的各种事件进行对应的操作,和服务端是不是很像?更多代码解释可以参考源代码中的注释,这里不做更多介绍。

        怎么跑起来呢?这里很重要的是要先启动服务器端,

        冷暖自知一抹茶ck

        然后,在客户端在访问 http://www.ck.com/index.html (修改成你的访问路径即可) 就可以看到属于自己的弹幕系统了。这里展示的只是一个最基础最原始的弹幕平台。我们也了解到了使用PHP开发一个弹幕平台需要涉及到的技术有WebSocket,Swoole扩展,甚至碰到了很多初级开发者平时不怎么接触的工具,比如说PECL,比如说Linux。


        其实PHP结合Swoole扩展还可以做很多事情,比如说对接各种家电,对接各种硬件接口实现在Web端实时控制家电,又比如说结合树莓派做智能小车,通过web端进行遥控等等,各种新奇的玩法等你发现。谁说PHP只能做Web开发?PHP拥有了Swoole扩展其实能做的事情还有很多,Swoole就像他的宣传标题一样:重新定义PHP。


        参考 微信公众号:PHP饭米粒

冷暖自知一抹茶ck
请先登录后发表评论
  • 最新评论
  • 总共0条评论