读懂 Workerman 框架和 GatewayWorker 框架

首先去 workerman 的官网查看相关的介绍,再次不在赘述。

我们通过一个 workerman 官网的例子来简单讲述一下workerman 的工作过程。

windows版本聊天室 点击下载workerman-chat-for-win

此版本跟 linux 版本的不同在与其中 workerman 的核心框架,其使用的是 windows 的版本,具体 windows 版本和 linux 版本框架有何不同,主要是在启动一个新进程上边,后边会介绍,windows 代码不支持多个子进程(当前版本 3.2.2)。当然亦可以下载此聊天室的 linux 版本,但是运行时候需要替换掉其中的 workerman 框架为windows 版本。

windows 上安装 PHP 环境,我这里用的是 PHP 5.5.12,然后按照官网上 PHP 的环境变量设置设置好 PHP,设置完成在 cmd 下运行 php -v,看是否有php 的版本信息。有的话设置成功。

一、启动示例,确保代码正常运行:

准备工作结束,下面我们来看示例:

点击代码中的 start_for_win.bat,会启动示例的 debug 模式

二、分析入口文件

现在示例正常运行没问题。我们看这个启动脚本到底做了些什么,文件内容如下:

php Applications\Chat\start_register.php Applications\Chat\start_web.php Applications\Chat\start_gateway.php Applications\Chat\start_businessworker.php
pause

可以看到,此 bat 文件是用 php 执行了相应的四个 php 文件,也就是各个服务的启动脚本。我们一个一个来:

三、查看 web 服务脚本入口 start_web.php

代码截图如上图,要建立一个 worker 的实例,或者说使用 workerman 框架,首先需要包含框架中的 Autoloader.php 文件,此文件为框架的自动类加载。看下代码:


 * @copyright walkor
 * @link http://www.workerman.net/
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Workerman;

// 包含常量定义文件
require_once __DIR__.'/Lib/Constants.php';

/**
 * 自动加载类
 * @author walkor
 */
class Autoloader
{
    // 应用的初始化目录,作为加载类文件的参考目录
    protected static $_appInitPath = '';
    
    /**
     * 设置应用初始化目录
     * @param string $root_path
     * @return void
     */
    public static function setRootPath($root_path)
    {
          self::$_appInitPath = $root_path;
    }

    /**
     * 根据命名空间加载文件
     * @param string $name
     * @return boolean
     */
    public static function loadByNamespace($name)
    {
        // 相对路径
        $class_path = str_replace('\\', DIRECTORY_SEPARATOR ,$name);
        // 如果是Workerman命名空间,则在当前目录寻找类文件
        if(strpos($name, 'Workerman\\') === 0)
        {
            $class_file = __DIR__.substr($class_path, strlen('Workerman')).'.php';
        }
        else 
        {
            // 先尝试在应用目录寻找文件
            if(self::$_appInitPath)
            {
                $class_file = self::$_appInitPath . DIRECTORY_SEPARATOR . $class_path.'.php';
            }
            // 文件不存在,则在上一层目录寻找
            if(empty($class_file) || !is_file($class_file))
            {
                $class_file = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR . "$class_path.php";
            }
        }
       
        // 找到文件
        if(is_file($class_file))
        {
            // 加载
            require_once($class_file);
            if(class_exists($name, false))
            {
                return true;
            }
        }
        return false;
    }
}
// 设置类自动加载回调函数
spl_autoload_register('\Workerman\Autoloader::loadByNamespace');

引入了一个 Constants.php ,此文件中主要是一些基本的常量设置,定义了时区,设置错误显示,定义两个宏定义。不在多说。

然后看 文件中包含一个 Autoloader 类,还有最后一句代码:

// 设置类自动加载回调函数
spl_autoload_register('\Workerman\Autoloader::loadByNamespace');
此函数定义了php 在找不到类的时候调用此方法中的 loadByNamespace 函数。此函数主要功能就是设置实例化类对应的源文件的地址,并且包含要实例化类的源文件。

继续往下看,简单几句代码,初始化一个 webserver,地址为 http://0.0.0.0:55151,此地址为,服务启动后,web 页面可以访问的地址,本机  http://127.0.0.1:55151

然后设置 web 目录,设置启动的进程数量,此参数在windows 环境下没有用。最后如果不是用的统一的启动脚本,可以单个启动服务,执行 Worker::runAll(); 即可。

好,我们来看webserver 的实例化方法:

我们可以看到,webserver 类继承的也是 Worker 父类,初始化中没有执行代码,继续跟踪 parent::__construct() 这一句:到wokerman 的worker类中的构造方法:


此代码中,关键看标红部分,把当前 webserver 的实例存储到了 Worker 类的静态变量中,其他地方没有任何往下执行的代码,所以 new WebServer 代码跟踪完毕,那么执行代码只能在最后一句中: Worker::ranall(),跟踪进去:

以上代码做了几部工作:

1、初始化方法 init:注意红色部分,这两句代码是要记录当前的启动脚本文件名到 $_startFile 变量中;

2、解析命令:不多说,自己看代码,这个函数主要用来区别你的启动命令的,我们刚才的启动方式,一个PHP 进程执行了三个启动脚本,在此命令中会记录所有的启动脚本文件名到 $_startFiles ,此变量 区别于上边的 $_startFile,单复数的区别。

3、初始化 workers,initWorkers:这个函数中的代码记录了woker名字长度,地址字符长度、进程数长度、状态长度,主要用来下一步输出启动界(displayUI 函数)面时候对齐数据用,相关代码如下:


4、展示启动界面:用上一步的数据来对齐显示。不多说。

5、ranAllWorkers ,启动所有的worker。

$process= proc_open("php $start_file -q", $descriptorspec, $pipes);
此句代码相当于开启了新的php进程,来执行对应的四个脚本,即相当于执行了四个命令

php start_web.php -q
php start_businessworker.php -q
php start_gateway.php -q
php start_register.php -q

然后以上每一进程,重新开始执行四个单个脚本的进程,相当于挨个启动。
当单个脚本执行到此步骤时候,不在进入第一次位置,具体看下边红色标注,$worker->lister() 和 $worker->run() 为此次最终的执行;此方法跟踪进入 

WebServer 类中的 run 方法(因为 worker 实例为初始化时候放进去的 WebServer 实例)。

    public function run()
    {
        $this->_onWorkerStart = $this->onWorkerStart;
        $this->onWorkerStart = array($this, 'onWorkerStart');
        $this->onMessage = array($this, 'onMessage');
        parent::run();
    }
run 方法中,初始化了一下此实例的回调函数 onWorkerStart、onMessage 然后执行父类的run 方法。父类run 方法如下:
   /**
     * 运行worker实例
     */
    public function run()
    {
        // 设置自动加载根目录
        Autoloader::setRootPath($this->_appInitPath);
        
        // 则创建一个全局事件轮询
        if(extension_loaded('libevent'))
        {
            self::$globalEvent = new Libevent();
        }
        else
        {
            self::$globalEvent = new Select();
        }
        // 监听_mainSocket上的可读事件(客户端连接事件)
        if($this->_socketName)
        {
            if($this->transport !== 'udp')
            {
                self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptConnection'));
            }
            else
            {
                self::$globalEvent->add($this->_mainSocket,  EventInterface::EV_READ, array($this, 'acceptUdpConnection'));
            }
        }
        
        // 用全局事件轮询初始化定时器
        Timer::init(self::$globalEvent);
        
        // 如果有设置进程启动回调,则执行
        if($this->onWorkerStart)
        {
            call_user_func($this->onWorkerStart, $this);
        }
        
        // 子进程主循环
        self::$globalEvent->loop();
    }

里边设置了全局的轮询事件,然后调用了 onWorkerStart 回调函数,然后全局事件进行循环,loop代码如下:
   /**
     * 主循环
     * @see Events\EventInterface::loop()
     */
    public function loop()
    {
        $e = null;
        while (1)
        {
            $read = $this->_readFds;
            $write = $this->_writeFds;
            // 等待可读或者可写事件
            stream_select($read, $write, $e, 0, $this->_selectTimeout);
            
            // 尝试执行定时任务
            if(!$this->_scheduler->isEmpty())
            {
                $this->tick();
            }
            
            // 这些描述符可读,执行对应描述符的读回调函数
            if($read)
            {
                foreach($read as $fd)
                {
                    $fd_key = (int) $fd;
                    if(isset($this->_allEvents[$fd_key][self::EV_READ]))
                    {
                        call_user_func_array($this->_allEvents[$fd_key][self::EV_READ][0], array($this->_allEvents[$fd_key][self::EV_READ][1]));
                    }
                }
            }
            
            // 这些描述符可写,执行对应描述符的写回调函数
            if($write)
            {
                foreach($write as $fd)
                {
                    $fd_key = (int) $fd;
                    if(isset($this->_allEvents[$fd_key][self::EV_WRITE]))
                    {
                        call_user_func_array($this->_allEvents[$fd_key][self::EV_WRITE][0], array($this->_allEvents[$fd_key][self::EV_WRITE][1]));
                    }
                }
            }
        }
    }
此函数中来执行定时任务,和相关的链接读写事件进入后的回调函数,这里WebServer 主要重写了 onMessage 回调函数,用来处理 http 协议,具体代码请见
WebServer::onMessage 函数。


/**
     * 运行所有的worker
     */
    public static function runAllWorkers()
    {
        // 只有一个start文件时执行run 单个脚本启动,进入此
        if(count(self::$_startFiles) === 1)
        {
            // win不支持同一个页面执初始化多个worker
            if(count(self::$_workers) > 1)
            {
                echo "@@@ Error: multi workers init in one php file are not support @@@\r\n";
                echo "@@@ Please visit http://wiki.workerman.net/Multi_woker_for_win @@@\r\n";
            }
            elseif(count(self::$_workers) <= 0)
            {
                exit("@@@no worker inited@@@\r\n\r\n");
            }
            
            // 执行worker的run方法
            reset(self::$_workers);
            $worker = current(self::$_workers);
            $worker->listen();
            // 子进程阻塞在这里
            $worker->run();
            exit("@@@child exit@@@\r\n");
        }
        // 多个start文件则多进程打开
        elseif(count(self::$_startFiles) > 1)// 多个脚本共同启动,进入此
        {
            foreach(self::$_startFiles as $start_file)
            {
                self::openProcess($start_file);
            }
        }
        // 没有start文件提示错误
        else
        {
            echo "@@@no worker inited@@@\r\n";
        }
    }

    /**
     * 打开一个子进程
     * @param string $start_file
     */
    public static function openProcess($start_file)
    {
        // 保存子进程的输出
        $start_file = realpath($start_file);
        $std_file = sys_get_temp_dir() . '/'.str_replace(array('/', "\\", ':'), '_', $start_file).'.out.txt';
        // 将stdou stderr 重定向到文件
        $descriptorspec = array(
                0 => array('pipe', 'a'), // stdin
                1 => array('file', $std_file, 'w'), // stdout
                2 => array('file', $std_file, 'w') // stderr
        );
        
        // 保存stdin句柄,用来探测子进程是否关闭
        $pipes = array();
       
        // 打开子进程 这里在创建新的进程,相当于在命令行中执行 php start_web.php -q ,用此种方式创建了一个新进程
        $process= proc_open("php $start_file -q", $descriptorspec, $pipes);
        
        // 打开stdout stderr 文件句柄
        $std_handler = fopen($std_file, 'a+');
        // 非阻塞
        stream_set_blocking($std_handler, 0);
        // 定时读取子进程的stdout stderr
        $timer_id = Timer::add(0.1, function()use($std_handler)
        {
            echo fread($std_handler, 65535);
        });
        
        // 保存子进程句柄
        self::$_process[$start_file] = array($process, $start_file, $timer_id);

    } // 此函数结束时候,也就是 Worker::ranAll() 主进程执行完的时候,


至于启动,其他的 gateway 和 businessworker 的过程与此类似,主线与此类似。

四、Workerman chat 的分布式部署:

左侧的三个为客户端,gateway 为workerman 的网关服务器,所有用户的链接都链接到此,在此,gateway 服务器将所有的链接 hold 住保存。

register 服务器的作用主要是用来通知 woker 和 gateway 各自的详细的通讯地址。当gateway 启动后,启动的 7272端口 作为服务客户端链接的soket地址,

并且在启动成功后会尝试建立一个异步的tpc 链接,链接register 1236 端口,register 在gateway 链接成功后会将gateway通讯录地址广播给所有的worker ,

同理,在worker链接到 register 服务器同时,register 服务会将所有的gateway 通讯录发送给当前worker。即:每个worker 都知道所有的 gateway 地址。

每个worker 上在启动时候首先会启动一个socket 请求链接register 服务器来获取gateway,然后它会去尝试链接每一个 gateway;同时 gateway 在接收到 worker 的链接后(在 gateway 启动时还会建立一个内网监听端口,用来监听 worker 链接),会 hold 住 worker 的所有链接,接收所有worker 传过来的数据,

根据不同的地址 connect_id 存储到 gateway 内存,用来读取或者写入数据。



最后:

webserver 开启了一个socket 监听 http 请求;

register 开启了一个socket 监听来自worker、gateway 的上线注册的服务;

gateway 开启了一个soket 用来监听客户端请求,开启了一个soket 用来监听 worker 请求,发起了一个请求 register 的链接;

worker 开启了一个请求 register 的链接,开启了 n 个请求 gateway 的链接。


最最后:windows 和 linux workerman 内核的主要不同点,见下图:

此中初始化的过程要比windows 下的多,在一个启动新进程的时候,windows 用的命令行的方式,linux 用的 $pid = pcntl_fork(); 来完成的新进程的启动;同时,启动的进程数与 $worker->count 相关;具体代码大家自行分析一下吧。


你可能感兴趣的:(PHP,基础,Workerman,框架)