在新浪云使用PHP构建websocket服务

非常高兴的宣布,新浪云的七层协议已经支持原生的websocket服务,比之前我们推荐使用的channel服务要更灵活一点,channel是新浪云推出的类似websocket的产品,下行链路使用websocket协议,上行使用http协议。

但考虑到新浪云默认的PHP运行环境本身不支持常驻进程的任务,不能直接bind端口启动websocket服务,因此本篇文章会介绍如何从新浪云近期推出的“自定义运行环境”启动websocket服务,管理自定义运行环境的介绍可以参考我们的文档 http://www.sinacloud.com/doc/sae/docker/vm-getting-started.html

创建一个自定义运行环境

  • 注册并登陆SAE https://sae.sina.com.cn/
  • 点击控制台“创建新应用”按钮。
  • 部署环境选择“自定义”
  • 部署方式选择“手工部署”
  • 操作系统选择centos的7版本 (你可以选择其他熟悉的环境,本示例以centos 7环境示例)

管理容器,部署PHP、swoole运行环境

创建完应用后,进入容器的管理页面,可以看到当前的容器已经在运行,如图所示:

点击终端即可进入web版本的terminal:

也可以使用ssh客户端对容器进行管理,请参考官方的说明文档,这里不再详细介绍。

安装PHP

这里我们直接使用yum install php安装源里的PHP,直接登录容器后(这里我以ssh客户端演示)执行

yum install php  

一路回车安装可以发现PHP已经安装完成:

执行

php -v  

到此PHP已经安装完成,接下来我们将会安装swoole扩展,swoole一个优秀的PHP扩展,内部集成了websocket server的功能,github的地址在 https://github.com/swoole/swoole-src,本示例以这个项目提供的websocket样例为准。

安装swoole扩展

PHP扩展的安装方式可以多种,常见的为:

  • 从pecl直接安装
  • 自己编译源码安装

为了完整演示,本示例从源码安装代码。 为了编译swoole扩展,必须先安装php-devel包,直接执行命令

yum install php-devel  

安装完成如图:

安装wget下载文件

yum install wget  

下载swoole代码

cd /  
mkdir swoole  
cd swoole  
wget https://github.com/swoole/swoole-src/archive/v1.9.0-stable.zip  

安装unzip用于解压zip文件

yum install unzip  

解压

unzip v1.9.0-stable.zip  

进入swoole的代码目录

cd swoole-src-master/  

执行phpize (php-devel包提供)

phpize  

安装gcc用于编译

yum install gcc  

执行configure链接安装的PHP

./configure --with-php-config=/usr/bin/php-config

执行完成如图所示:

安装make进行编译

yum install make  

执行make编译

make  

耐心等待一下即可编译完成,编译完成如下:

安装PHP模块 将生成的模板安装到PHP下,执行

make install  

修改php.ini配置文件加载swoole模块,直接执行

php --ini  

可以查看默认的php.ini的位置,例如我们从yum仓库直接安装的PHP的配置文件php.ini在/etc/php.ini,还会去/etc/php.d这个路径下寻找*.ini文件,如图所示:

我们可以把swoole.ini放到/etc/php.d/路径下,执行

echo "extension=swoole.so" >swoole.ini  

查看模块是否已经加载,执行命令查看所有已经安装的PHP模块如下:

php -m  

到此我们看到swoole已经安装完成。

基于swoole模块的websocket server

我们从swoole官方的示例中找到了websocket的example代码 https://github.com/swoole/swoole-src/blob/master/examples/websocket/server.php

考虑到新浪云容器环境的特殊性,我们需要将端口默认监听在5050端口,并修改默认首页的ws连接地址为我们的二级域名地址

注意,因为我们的前端机默认端口是80,因此这个链接地址不能带5050端口。

可以直接复制以下代码:

<?php  
$server = new swoole_websocket_server("0.0.0.0", 5050, SWOOLE_BASE);
//$server->addlistener('0.0.0.0', 9502, SWOOLE_SOCK_UDP);
//$server->set(['worker_num' => 4,
//    'task_worker_num' => 4,
//]);

function user_handshake(swoole_http_request $request, swoole_http_response $response)  
{
    //自定定握手规则,没有设置则用系统内置的(只支持version:13的)
    if (!isset($request->header['sec-websocket-key']))
    {
        //'Bad protocol implementation: it is not RFC6455.'
        $response->end();
        return false;
    }
    if (0 === preg_match('#^[+/0-9A-Za-z]{21}[AQgw]==$#', $request->header['sec-websocket-key'])
        || 16 !== strlen(base64_decode($request->header['sec-websocket-key']))
    )
    {
        //Header Sec-WebSocket-Key is illegal;
        $response->end();
        return false;
    }

    $key = base64_encode(sha1($request->header['sec-websocket-key']
        . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
        true));
    $headers = array(
        'Upgrade'               => 'websocket',
        'Connection'            => 'Upgrade',
        'Sec-WebSocket-Accept'  => $key,
        'Sec-WebSocket-Version' => '13',
        'KeepAlive'             => 'off',
    );
    foreach ($headers as $key => $val)
    {
        $response->header($key, $val);
    }
    $response->status(101);
    $response->end();
    global $server;
    $fd = $request->fd;
    $server->defer(function () use ($fd, $server)
    {
        $server->push($fd, "hello, welcome\n");
    });
    return true;
}

$server->on('handshake', 'user_handshake');
$server->on('open', function (swoole_websocket_server $_server, swoole_http_request $request) {
    echo "server#{$_server->worker_pid}: handshake success with fd#{$request->fd}\n";
    var_dump($_server->exist($request->fd), $_server->getClientInfo($request->fd));
//    var_dump($request);
});

$server->on('message', function (swoole_websocket_server $_server, $frame) {
    var_dump($frame->data);
    echo "received ".strlen($frame->data)." bytes\n";
    if ($frame->data == "close")
    {
        $_server->close($frame->fd);
    }
    elseif($frame->data == "task")
    {
        $_server->task(['go' => 'die']);
    }
    else
    {
        //echo "receive from {$frame->fd}:{$frame->data}, opcode:{$frame->opcode}, finish:{$frame->finish}\n";
       // for ($i = 0; $i < 100; $i++)
        {
            $_send = str_repeat('B', rand(100, 800));
            $_server->push($frame->fd, $_send);
           // echo "#$i\tserver sent " . strlen($_send) . " byte \n";
        }
        $fd = $frame->fd;
        $_server->tick(2000, function($id) use ($fd, $_server) {
            $_send = str_repeat('B', rand(100, 5000));
            $ret = $_server->push($fd, $_send);
            if (!$ret)
            {
                var_dump($id);
                var_dump($_server->clearTimer($id));
            }
        });
    }
});

$server->on('close', function ($_server, $fd) {
    echo "client {$fd} closed\n";
});

$server->on('task', function ($_server, $worker_id, $task_id, $data)
{
    var_dump($worker_id, $task_id, $data);
    return "hello world\n";
});

$server->on('finish', function ($_server, $task_id, $result)
{
    var_dump($task_id, $result);
});

$server->on('packet', function ($_server, $data, $client) {
    echo "#".posix_getpid()."\tPacket {$data}\n";
    var_dump($client);
});

$server->on('request', function (swoole_http_request $request, swoole_http_response $response) {
    $response->end(<<<HTML
    <h1>Swoole WebSocket Server</h1>
    <script>
// 这里改为你的应用链接地址
var wsServer = 'ws://websocketdemo.applinzi.com';  
var websocket = new WebSocket(wsServer);  
websocket.onopen = function (evt) {  
    console.log("Connected to WebSocket server.");
};

websocket.onclose = function (evt) {  
    console.log("Disconnected");
};

websocket.onmessage = function (evt) {  
    console.log('Retrieved data from server: ' + evt.data);
};

websocket.onerror = function (evt, e) {  
    console.log('Error occured: ' + evt.data);
};
</script>  
HTML  
    );
});

$server->start();

将以上代码保存到服务器上的任意一个PHP文件,比如 websocket.php 。 在根目录/下创建一个example目录,并进入目录:

cd /;mkdir example && cd example  

创建websocket.php文件并插入内容(vi 打开文件后按I键插入),ctrl + c -> :wq保存。

vi websocket.php  

直接执行php websocket.php 启动websocket server

php websocket.php  

从浏览器直接打开二级域名,比如当前示例的是 http://websocketdemo.applinzi.com/

兴奋的看到websocket已经开始工作了。

保存辛苦工作的成果

新浪云的容器运行环境可以将我们辛苦工作的战场保留下来,如果需要启动新的容器可以直接从当前做好的环境启动,如何打包呢?

进入容器的管理页面,点击“打包镜像”,如图所示:

给镜像起一个名字,并输入一个版本(默认都是latest,如果需要多版本,可以起不同的名字)再点击确认即可。

确认一下打包成果,再次进入创建应用页面,看看我们能不能基于这个镜像创建新的容器: 我们惊喜的发现,从创建应用处已经可以选择这个镜像了,我们基于这个镜像再启动一个容器看看swoole扩展还有我们的websocket.php文件还在不在:)

创建一个新应用名字就叫websocketdemo2,创建完成后进入到“容器管理”页面,再次点开我们的web终端看看:

发现swoole扩展已经在了,我们的websocket.php也还在,是不是很有趣:)

总结

  • 本文讲述了如何基于新浪云的自定义运行环境搭建一个基于PHP的websocket服务程序
  • 同时阐述了如何打包镜像方便快速的启动和扩容
  • 如果你不满足我们标准PHP运行环境的扩展等,一样可以使用以上的思路从这里构建属于你自己的php运行环境