当前位置: 首页 > 工具软件 > yii2-swoole > 使用案例 >

swoole与yii2的结合

缪远航
2023-12-01

前言

本篇文章将介绍怎么使用swoole运行yii2 advanced框架。在swoole中正常使用web服务器所支持的一般功能,同时运行了http服务和socket服务。在实践此文章前请先在自己的机器上安装上下面介绍的项目环境。该项目和一般的yii2 advanced一样,只是有3个地方不同。这里只介绍3个重要地方:swoole脚本文件、swoole服务配置文件、yii2 advanced中源码request.php修改

项目环境

centos7+php7+swoole2.2+yii2 advanced

swoole服务脚本文件

这是一个运行swoole服务的php脚本(YiiWebsocket.php),您可以放在任意位置,使用php运行他

php YiiWebsockrt.php

源码如下

<?php
/**
 * websocket服务器swoole入口脚本(兼容websocket和http响应)
 * @author      老虎还是猫
 * @property    THIS_APP        运行模块名称
 * @property    ROOT_DIR        根据实际情况,定义站点根目录。用于清晰脚本存放位置(比如本项目的站点根目录为/root/advanced/frontend/web)
 * @property    YII_ROOT        定义yii2 advanced的根目录,便于引用
 * @date        2018/06/21
 */ 
 
define('THIS_APP','frontend');
//根据实际情况,定义站点根目录。
define('ROOT_DIR', '/root/advanced/'.THIS_APP.'/web');
//定义yii2 advanced的根目录,便于引用
define('YII_ROOT','/root/advanced');


defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');


require YII_ROOT . '/vendor/autoload.php';
require YII_ROOT . '/vendor/yiisoft/yii2/Yii.php';
require YII_ROOT . '/common/config/bootstrap.php';
require YII_ROOT . '/'.THIS_APP.'/config/bootstrap.php';


class YiiWebSocket{
    
    //声明配置信息
    private static $config = null;
    
    //yii2配置信息
    private static $yiiconfig = null;
    
    //swoole的web soccket server对象
    private static $web_socket = null;
    
    //入口方法(外部唯一调用方法)
    public static function run(){
        
        //获取配置信息。
        if(empty(self::$config)){
            self::$config = include(YII_ROOT . '/'.THIS_APP.'/config/ServerConfig.php');
        }
        
        //获取yii2配置信息。
        if(empty(self::$yiiconfig)){
            self::$yiiconfig = yii\helpers\ArrayHelper::merge(
                                require YII_ROOT . '/common/config/main.php',
                                require YII_ROOT . '/common/config/main-local.php',
                                require YII_ROOT . '/'.THIS_APP.'/config/main.php',
                                require YII_ROOT . '/'.THIS_APP.'/config/main-local.php'
                            );
        }
        
        if(empty(self::$web_socket)){
            self::$web_socket = new swoole_websocket_server(self::$config['Server']['swoole_server_ip'], self::$config['Server']['swoole_http_server_port']);
        }
        
        self::$web_socket->set(self::$config['SwooleConfig']);
        self::$web_socket->listen(self::$config['Server']['swoole_server_ip'], self::$config['Server']['swoole_socket_server_port'], SWOOLE_SOCK_TCP);
        
        //websocket通信建立时触发
        self::$web_socket->on('open', function (swoole_websocket_server $server, $request) {
            echo "server: handshake success with fd{$request->fd}\n";
        });
        
        //websocket client发送请求时触发
        self::$web_socket->on('message', function (swoole_websocket_server $server, $frame) {
            self::onMessage($server, $frame);
        });
        
        //websocket通信关闭时触发
        self::$web_socket->on('close', function ($ser, $fd) {
            //echo "client {$fd} closed\n";
        });
        
        //websocket继承httpserver,注册onRequest方法后可以响应http请求。否则报404
        self::$web_socket->on('request', function($request, $response){
            $return = self::onRequest($request, $response);
        });
        
        self::$web_socket->start();
    }
    
    
    //request方法,响应http请求
    private static function onRequest($request, $response){
        //阻止favicon.ico 进入
        if ($request->server['request_uri']=='/favicon.ico'){
            $response->header("Content-Type", "text/html; charset=utf-8");
            $response->status(404);
            $response->end();
            return ;
        }
        
        //动态设置$response的header
        $path_arr = pathinfo($request->server['request_uri']);
        if(!empty($path_arr['extension'])){
            
            //判断是否为脚本
            if(array_key_exists($path_arr['extension'],self::$config['ContentType'])){
                try{
                    $response->header("Content-Type", self::$config['ContentType'][$path_arr['extension']]);
                    $response->status(200);
                    $response->end(file_get_contents(ROOT_DIR . $request->server['request_uri']));
                }catch(\Exception $e){
                    $response->header("Content-Type", "text/html; charset=utf-8");
                    $response->status(404);
                    $response->end();
                    throw $e;
                }
                return;
            }
            //判断是否为下载文件
            if(array_key_exists($path_arr['extension'],self::$config['DownloadType'])){
                try{
                    $response->header("Content-Type", self::$config['DownloadType'][$path_arr['extension']]);
                    $response->sendfile(urldecode(ROOT_DIR . $request->server['request_uri']));
                }catch(\Exception $e){
                    $response->header("Content-Type", "text/html; charset=utf-8");
                    $response->status(404);
                    $response->end();
                    throw $e;
                }
                return;
            }
        }
        
        //设置跨域请求
        if(!empty($request->header['origin']) and in_array($request->header['origin'],self::$config['AccessArr'])){
            header('Access-Control-Allow-Origin:'.$request->server['remote_addr']);  
            header('Access-Control-Allow-Credentials: true');
            header('Access-Control-Allow-Headers: TOKEN,Content-Type');
            header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
        }
        
        //即时更新$_SERVER
        foreach ($request->server as $k => $v) {
            $_SERVER[strtoupper($k)] = $v;
        }
        foreach ($request->header as $k => $v) {
            $_SERVER[strtoupper($k)] = $v;
        }
        $_SERVER['SCRIPT_FILENAME'] = ROOT_DIR;
        
        //获取$_GET
        $_GET = [];
        if (isset($request->get)) {
            foreach ($request->get as $k => $v) {
                $_GET[$k] = $v;
            }
        }
        //获取$_POST
        $_POST = [];
        if (isset($request->post)) {
            foreach ($request->post as $k => $v) {
                $_POST[$k] = $v;
            }
        }
        
        //捕获异常,防止服务器持续等待
        try{              
            $res = self::YiiLogick();
            $response->header("Content-Type", "text/html; charset=utf-8");
            $response->end($res);
        }catch(Exception $e){
            throw $e;
            $response->header("Content-Type", "text/html; charset=utf-8");
            $response->status(500);
            $response->end();
        }
        return;
    }
    
    
    //websocket请求返回,使用第三方类库处理(\extend\)。上传信息使用json格式,其中route为路由
    private static function onMessage(swoole_websocket_server $server, $frame){
        try{
            //解析数据
            $received_data = json_decode($frame->data,1);
    
            //获取路由信息
            $route = $received_data['route'];
            if(empty($received_data)){
                $server->push($frame->fd, "Routing information must be required !!");
                return;
            }
            $routeArr = explode('/',$received_data['route']);
            $functionid = sizeof($routeArr)-1;
            $functionname = $routeArr[$functionid];
            unset($routeArr[$functionid]);
            $classname = implode('\\',$routeArr);
            
            $class = new $classname;
            $class->init($frame->fd,$received_data);
            $res = $class->$functionname();
            $server->push($frame->fd, $res);
        }catch(Exception $e){
            echo $e->getTraceAsString();
            $server->push($frame->fd, 'Websocket server error !!');
        }
        return;
    } 
    
    //使用yii,运行系统业务逻辑
    private static function YiiLogick(){
        // 执行应用
        ob_start();
        try {
            (new yii\web\Application(self::$yiiconfig))->run();
        } catch (\Exception $exception) {
            throw $exception;
            return;
        }
        $res = ob_get_contents();
        ob_end_clean();       
        
        return $res;
    }
}


YiiWebSocket::run();




下面讲解下重要代码,其余代码都有详细注释

1. 目录结构定义
define('THIS_APP','frontend');
//根据实际情况,定义站点根目录。
define('ROOT_DIR', '/root/advanced/'.THIS_APP.'/web');
//定义yii2 advanced的根目录,便于引用
define('YII_ROOT','/root/advanced');

为了便于使用,我定义了3个常量

THIS_APP :默认的yii框架为我们提供了2个站点(frontend和backend),我们也能自己扩展新的站点。所以在这里进行定义我们 需要使用的站点。

ROOT_DIR :和一般的web服务器不同,swoole没有站点根目录这样的定义。他不会通过一个url访问到服务器上的资源,必须自己对url进行解析返回对应的服务器资源。为了便于使用资源,清晰资源的存放位置,所以在这里定义了一个站点根目录。所有的服务器的资源都应当放到该目录或者其子目录下。

YII_ROOT :因为要使用yii的框架逻辑所以我们需要引用其资源。所以应当在这里清晰的定义yii框架的根目录。

2. 应用swoole服务所需要的配置信息
//获取配置信息。
if(empty(self::$config)){
    self::$config = include(YII_ROOT . '/'.THIS_APP.'/config/ServerConfig.php');
}

为了编译管理swoole服务,我将所有需要用到的配置信息放到了一个文件中。在这里对其进行引用,若果您需要修改该文件的存放位置或者名称,同时需要修改这个地方。

3. 重写$_SERVER
$_SERVER['SCRIPT_FILENAME'] = ROOT_DIR;

yii对资源的引用是同过$_SERVER的部分参数来实现的,所以我们需要重写这部分。

swoole服务配置文件

这是一个存放所有swoole用到的配置参数的php文件(ServerConfig.php),我把他放到了/frontend/config/目录下。一个站点一个配置,我觉得这样比较合理,所以把他放到了fronted的配置文件夹里。当然您可以把他放到任何地方,只要保证能在swoole服务脚本文件中对其正常引用。下面是源码:

<?php

return [
    //swoole服务配置信息
    'Server'        => [
        // swoole服务ip
        'swoole_server_ip'          => '192.168.1.113',
        // swoole服务http监听端口
        'swoole_http_server_port'   => '9501',
        // swoole服务socket监听端口
        'swoole_socket_server_port' => '9502',
        //redis服务使用ip
        'redis_ip'                  => '127.0.0.1',
        //redis服务监听端口
        'redis_port'                => '6379',
    ],
    //swoole配置参数
    'SwooleConfig'  => [
        'log_file' => './log/swoole.log',   //指定swoole错误日志文件
        //'daemonize' => 1    //设置进程为守护进程,当使用systemd管理的时候,一定不要设置此参数,或者设置为0
    ],
    //定义文件类型和Content-Type对应关系
    'ContentType'   => [
        'xml'   => 'application/xml,text/xml,application/x-xml',
        'json'  => 'application/json,text/x-json,application/jsonrequest,text/json',
        'js'    => 'text/javascript,application/javascript,application/x-javascript',
        'css'   => 'text/css',
        'rss'   => 'application/rss+xml',
        'yaml'  => 'application/x-yaml,text/yaml',
        'atom'  => 'application/atom+xml',
        'pdf'   => 'application/pdf',
        'text'  => 'text/plain',
        //'image' => 'image/png,image/jpg,image/jpeg,image/pjpeg,image/gif,image/webp,image/*',
        'png'   => 'image/png',
        'jpg'   => 'image/jpg',
        'jpeg'  => 'image/jpeg',
        'pjpeg' => 'image/pjpeg',
        'gif'   => 'image/gif',
        'webp'  => 'image/webp',
        'csv'   => 'text/csv',
        'html'  => 'text/html,application/xhtml+xml,*/*',
    ],
    //定义可下载文件
    'DownloadType'  => [
        'xls'   => 'application/x-xls,application/vnd.ms-excel',
        'tgz'   => '',
        'zip'   => '',
    ],
    //定义允许跨域请求的域名
    'AccessArr'     => [
        'http://localhost:8080',
    ]
];

源码里面有详细的注释,我就不再解释了。

修改yii源码的request类

\vendor\yiisoft\yii2\web目录下面找到Request.php,找到其中的getScriptUrl方法,修改如下

public function getScriptUrl()
    {
        if ($this->_scriptUrl === null) {
            $this->_scriptUrl = '/';
            /*
            $scriptFile = $this->getScriptFile();
            $scriptName = basename($scriptFile);file_put_contents(
            if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $scriptName) {
                $this->_scriptUrl = $_SERVER['SCRIPT_NAME'];
            } elseif (isset($_SERVER['PHP_SELF']) && basename($_SERVER['PHP_SELF']) === $scriptName) {
                $this->_scriptUrl = $_SERVER['PHP_SELF'];
            } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $scriptName) {
                $this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME'];
            } elseif (isset($_SERVER['PHP_SELF']) && ($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName)) !== false) {
                $this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName;
            } elseif (!empty($_SERVER['DOCUMENT_ROOT']) && strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) {
                $this->_scriptUrl = str_replace([$_SERVER['DOCUMENT_ROOT'], '\\'], ['', '/'], $scriptFile);
            } else {
                throw new InvalidConfigException('Unable to determine the entry script URL.');
            }*/
        }

        return $this->_scriptUrl;
    }
这样才能保证对资源的正常引用


我把我测试使用的项目上传到了github Yii2-swoole,供大家参考

 类似资料: