最外层start.php,设置全局启动模式,加载Application里的个子服务目录下应用的启动文件(start开头,这些文件都是workmanwork类的子类,在载入文件的同时,这些子服务会生成对象,work类的构造方法会把生成的work对象都存入work类的静态变量$_workers,方便主文件后续设置以及启动子服务,全局启动模式子服务是不会启动的),设置时区,注册自动加载,调用workmanWorker:runall启动所有服务。
主文件:
// 标记是全局启动 define('GLOBAL_START', 1); require_once __DIR__ . '/Workerman/Autoloader.php'; // 加载所有Applications/*/start.php,以便启动所有服务 foreach(glob(__DIR__.'/Applications/*/start*.php') as $start_file) { require_once $start_file; } // 运行所有服务 Worker::runAll();
子服务文件start_gateway,提供接入请求的服务(class Gateway extends Worker):
// gateway 进程,这里使用Text协议,可以用telnet测试 $gateway = new Gateway("Text://0.0.0.0:8282"); // gateway名称,status方便查看 $gateway->name = 'YourAppGateway'; // gateway进程数 $gateway->count = 4; // 本机ip,分布式部署时使用内网ip $gateway->lanIp = '127.0.0.1'; // 内部通讯起始端口,假如$gateway->count=4,起始端口为4000 // 则一般会使用4001 4002 4003 4004 4个端口作为内部通讯端口 $gateway->startPort = 2300; // 心跳间隔 //$gateway->pingInterval = 10; // 心跳数据 //$gateway->pingData = '{"type":"ping"}'; /* // 当客户端连接上来时,设置连接的onWebSocketConnect,即在websocket握手时的回调 $gateway->onConnect = function($connection) { $connection->onWebSocketConnect = function($connection , $http_header) { // 可以在这里判断连接来源是否合法,不合法就关掉连接 // $_SERVER['HTTP_ORIGIN']标识来自哪个站点的页面发起的websocket链接 if($_SERVER['HTTP_ORIGIN'] != 'http://kedou.workerman.net') { $connection->close(); } // onWebSocketConnect 里面$_GET $_SERVER是可用的 // var_dump($_GET, $_SERVER); }; }; */ // 如果不是在根目录启动,则运行runAll方法 if(!defined('GLOBAL_START')) { Worker::runAll(); }
接入服务除了注册到全局静态$_workers变量,还设置了路由:
public function __construct($socket_name, $context_option = array()) { parent::__construct($socket_name, $context_option); //随机返回一个bussness的连接 $this->router = array("\GatewayWorker\Gateway", 'routerRand'); $backrace = debug_backtrace(); $this->_appInitPath = dirname($backrace[0]['file']); }
子服务文件start_bussinessworker,提供实际的业务处理,和gateway服务内部通讯类似nginx与php(class BusinessWorker extends Worker):
// bussinessWorker 进程 $worker = new BusinessWorker(); // worker名称 $worker->name = 'YourAppBusinessWorker'; // bussinessWorker进程数量 $worker->count = 4; // 如果不是在根目录启动,则运行runAll方法 if(!defined('GLOBAL_START')) { Worker::runAll(); }
附带基类work的构造函数:
/** * worker构造函数 * * @param string $socket_name * @param array $context_option */ public function __construct($socket_name = '', $context_option = array()) { // 保存worker实例 $this->workerId = spl_object_hash($this); self::$_workers[$this->workerId] = $this; self::$_pidMap[$this->workerId] = array(); // 获得实例化文件路径,用于自动加载设置根目录 $backrace = debug_backtrace(); $this->_appInitPath = dirname($backrace[0]['file']); // 设置socket上下文 if($socket_name) { $this->_socketName = $socket_name; if(!isset($context_option['socket']['backlog'])) { $context_option['socket']['backlog'] = self::DEFAUL_BACKLOG; } $this->_context = stream_context_create($context_option); } }
这里可以看到self::$_workers[$this->workerId] = $this;记录全局worker实例,
self::$_pidMap这个用来记录各个子服务开始fork后的所有子进程id
$context_option['socket']['backlog'] = self::DEFAUL_BACKLOG;设置嵌套字上下文里的未accept队列长度
在后面运行实例的listen方法监听的时候会传递到stream_socket_server方法里。
runAll的流程如下:
/** * 运行所有worker实例 * @return void */ public static function runAll() { // 初始化环境变量(pid文件,log文件,status文件,定时器信号回调) self::init(); // 解析命令(运行,重启,停止,重载,状态,从命令判断主进程是否以守护进程启动) // 启动之后通过php start.php XXX命令会到这里!因为第一步设置了这个文件的pid(这里可以看到pid对应到文件位置的重要性),所以后面的命令会对应为发送信号 self::parseCommand(); // 尝试以守护进程模式运行(fork两次进程,重置进程sid) self::daemonize(); // 初始化所有worker实例,主要是监听端口(记录所有子服务worker实例的最长名称name长度,最长嵌套字名socket_name长度,最长运行用户名user长度; // 所有定义了协议的子服务(Gate)开始监听也就是启动服务,子服务实例监听嵌套字对象mainSoctke,还没有注册accept回调) // 到这里所有Gate子服务启动监听,但是没有accept self::initWorkers(); // 初始化所有信号处理函数(为主进程注册stop,stats,reload的信号回调signalHandler) self::installSignal(); // 保存主进程pid(将获取daemonize方法后的新的主进程sid,存入init方法后的pid文件) self::saveMasterPid(); // 创建子进程(worker进程)并运行(主进程通过self::$_pidMap用来记录子服务进程创建的各自进程号,方便后面发送信号,
// 生成的子进程置空主进程的全局变量,self::$_pidMap,self::$_workers,如果在子服务文件里定义了self::$stdoutFile文件地址,
// 会重定向子服务子进程的stdout和stderr,直接运行work实例的run方法)
// 到这里子服务已经在子进程中运行,后面的代码就只有主服务执行
// 子进程的run方法会通过libevent绑定子服务mainSocket的accept回调,在accept回调方法里才有定义后面怎么处理请求socket
// 子进程的run方法会通过libevent绑定重新绑定信号量,以及用libevent来注入定时器
// 子进程的run方法会回调用户在子服务文件里的onWorkStar方法
// 子进程进入事件监听轮询
// 上面是基类中的run方法,基于gateway的子服务,会实现自己的onworkStar方法,然后在调用基类的run,这样可以在onWorkStar里实现gate与worker的连接
// 这里不知道怎么处理子进程对accpet时候的惊群 self::forkWorkers(); // 展示启动界面(打印所有启动的子服务的信息,由于initWorkers获取了各个子服务实例的名称等信息长度可以很好的格式化展示) self::displayUI(); // 尝试重定向标准输入输出(重定向主服务进程) self::resetStd(); // 监控所有子进程(worker进程)(处理主进程的信号量;通过pcntl_wait循环监听子进程状态,保持子进程的运行) /* 什么是平滑重启? 平滑重启不同于普通的重启,平滑重启可以做到在不影响用户的情况下重启服务,以便重新载入PHP程序,完成业务代码更新。 平滑重启一般应用于业务更新或者版本发布过程中,能够避免因为代码发布重启服务导致的暂时性服务不可用的影响。 注意:只有在on{...}回调中载入的文件平滑重启后才会自动更新,启动脚本中直接载入的文件或者写死的代码运行reload不会自动更新。 平滑重启原理 WorkerMan分为主进程和子进程,主进程负责监控子进程,子进程负责接收客户端的连接和连接上发来的请求数据, 做相应的处理并返回数据给客户端。当业务代码更新时,其实我们只要更新子进程,便可以达到更新代码的目的。 当WorkerMan主进程收到平滑重启信号时,主进程会向其中一个子进程发送安全退出(让对应进程处理完毕当前请求后才退出)信号, 当这个进程退出后,主进程会重新创建一个新的子进程(这个子进程载入了新的PHP代码),然后主进程再次向另外一个旧的进程发送停止 命令,这样一个进程一个进程的重启,直到所有旧的进程全部被置换为止。 我们看到平滑重启实际上是让旧的业务进程逐个退出然后并逐个创建新的进程做到的。为了在平滑重启时不影响客用户,这就要求进程中不 要保存用户相关的状态信息,即业务进程最好是无状态的,避免由于进程退出导致信息丢失。 */ //上面是官网对平滑启动的说明,设计的代码就是reload方法的 $one_worker_pid = current(self::$_pidsToRestart );这里是处理主进程的平滑启动信号的 // 在主进程里获取所有设置为可以平滑启动的子进程的pid,然后取一个发送平滑启动信号信号,这个信号到子进程,其实子进程会通过stopAll方法停止运行 // exit(0); // 主进程监听到子进程退出,然后重新生成一个新的子进程,然后把这个子进程的id从self::$_pidsToRestart里删除,然后再次调用reload方法去杀掉下一个子进程 // self::monitorWorkers(); }
主要的过程已经描述清楚了,主服务在主进程里,子服务开启监听之后,主服务开始fork,然后记录子服务进程的对应的pid,然后通过信号量来处理用户命令以及管理子服务进程。子服务在子进程里实现accpet监听回调。
work基类的主要代码片段:
子服务listen: // 获得应用层通讯协议以及监听的地址,udp会转换为传输协议 list($scheme, $address) = explode(':', $this->_socketName, 2); // 如果有指定应用层协议,则检查对应的协议类是否存在 if($scheme != 'tcp' && $scheme != 'udp') { $scheme = ucfirst($scheme); $this->_protocol = '\Protocols\'.$scheme; if(!class_exists($this->_protocol)) { $this->_protocol = "\Workerman\Protocols\$scheme"; if(!class_exists($this->_protocol)) { throw new Exception("class \Protocols\$scheme not exist"); } } } elseif($scheme === 'udp') { $this->transport = 'udp'; } // flag $flags = $this->transport === 'udp' ? STREAM_SERVER_BIND : STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; $errno = 0; $errmsg = ''; $this->_mainSocket = stream_socket_server($this->transport.":".$address, $errno, $errmsg, $flags, $this->_context); if(!$this->_mainSocket) { throw new Exception($errmsg); }
创建子进程: // 创建子进程 while(count(self::$_pidMap[$worker->workerId]) < $worker->count) { static::forkOneWorker($worker); } } } /** * 创建一个子进程 * @param Worker $worker * @throws Exception */ protected static function forkOneWorker($worker) { $pid = pcntl_fork(); // 主进程记录子进程pid if($pid > 0) { self::$_pidMap[$worker->workerId][$pid] = $pid; } // 子进程运行 elseif(0 === $pid) { // 启动过程中尝试重定向标准输出 if(self::$_status === self::STATUS_STARTING) { self::resetStd(); } self::$_pidMap = array(); self::$_workers = array($worker->workerId => $worker); Timer::delAll(); self::setProcessTitle('WorkerMan: worker process ' . $worker->name . ' ' . $worker->getSocketName()); self::setProcessUser($worker->user); $worker->run(); exit(250); } else { throw new Exception("forkOneWorker fail"); } }
子进程执行的基类run方法: /** * 运行worker实例 */ public function run() { // 注册进程退出回调,用来检查是否有错误 register_shutdown_function(array("\Workerman\Worker", 'checkErrors')); // 设置自动加载根目录 Autoloader::setRootPath($this->_appInitPath); // 如果没有全局事件轮询,则创建一个 if(!self::$globalEvent) { if(extension_loaded('libevent')) { self::$globalEvent = new Libevent(); } else { self::$globalEvent = new Select(); } // 监听_mainSocket上的可读事件(客户端连接事件)也只有Gate才有这个事件 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')); } } } // 重新安装事件处理函数,使用全局事件轮询监听信号事件 self::reinstallSignal(); // 用全局事件轮询初始化定时器 Timer::init(self::$globalEvent); // 如果有设置进程启动回调,则执行 if($this->onWorkerStart) { call_user_func($this->onWorkerStart, $this); } // 子进程主循环 self::$globalEvent->loop(); }
主服务监听子服务进程: pcntl_signal_dispatch(); // 挂起进程,直到有子进程退出或者被信号打断 $status = 0; $pid = pcntl_wait($status, WUNTRACED); // 如果有信号到来,尝试触发信号处理函数 pcntl_signal_dispatch(); // 有子进程退出 if($pid > 0) { // 查找是哪个进程组的,然后再启动新的进程补上 foreach(self::$_pidMap as $worker_id => $worker_pid_array) { if(isset($worker_pid_array[$pid])) { $worker = self::$_workers[$worker_id]; // 检查退出状态 if($status !== 0) { self::log("worker[".$worker->name.":$pid] exit with status $status"); } // 统计,运行status命令时使用 if(!isset(self::$_globalStatistics['worker_exit_info'][$worker_id][$status])) { self::$_globalStatistics['worker_exit_info'][$worker_id][$status] = 0; } self::$_globalStatistics['worker_exit_info'][$worker_id][$status]++; // 清除子进程信息 unset(self::$_pidMap[$worker_id][$pid]); break; } } // 如果不是关闭状态,则补充新的进程 if(self::$_status !== self::STATUS_SHUTDOWN) { self::forkWorkers(); // 如果该进程是因为运行reload命令退出,则继续执行reload流程 if(isset(self::$_pidsToRestart[$pid])) { unset(self::$_pidsToRestart[$pid]); self::reload(); } }
平滑启动过程: /** * 执行平滑重启流程 * @return void */ protected static function reload() { // 主进程部分 if(self::$_masterPid === posix_getpid()) { // 设置为平滑重启状态 if(self::$_status !== self::STATUS_RELOADING && self::$_status !== self::STATUS_SHUTDOWN) { self::log("Workerman[".basename(self::$_startFile)."] reloading"); self::$_status = self::STATUS_RELOADING; } // 如果有worker设置了reloadable=false,则过滤掉 $reloadable_pid_array = array(); foreach(self::$_pidMap as $worker_id =>$worker_pid_array) { $worker = self::$_workers[$worker_id]; if($worker->reloadable) { foreach($worker_pid_array as $pid) { $reloadable_pid_array[$pid] = $pid; } } } // 得到所有可以重启的进程 self::$_pidsToRestart = array_intersect(self::$_pidsToRestart , $reloadable_pid_array); // 平滑重启完毕 if(empty(self::$_pidsToRestart)) { if(self::$_status !== self::STATUS_SHUTDOWN) { self::$_status = self::STATUS_RUNNING; } return; } // 继续执行平滑重启流程 $one_worker_pid = current(self::$_pidsToRestart ); // 给子进程发送平滑重启信号 posix_kill($one_worker_pid, SIGUSR1); // 定时器,如果子进程在KILL_WORKER_TIMER_TIME秒后没有退出,则强行杀死 Timer::add(self::KILL_WORKER_TIMER_TIME, 'posix_kill', array($one_worker_pid, SIGKILL), false); } // 子进程部分 else { // 如果当前worker的reloadable属性为真,则执行退出 $worker = current(self::$_workers); if($worker->reloadable) { self::stopAll(); } } }
到这里主进程已经准备好了,子进程(Gate)已经开始监听了(还未讲gate与worker的连接通信,以及gate怎么接受请求,然后worker怎么处理请求)。上面说了gate与worker的连接是在OnWorkStart里实现的。后面就来看看
gate的run方法里保存了用户自定义的方法,然后自己的onWorkStart,onConnect,onMessage,onClose,onWorkstop都已定义好
/** * 运行 * @see Workerman.Worker::run() */ public function run() { // 保存用户的回调,当对应的事件发生时触发 $this->_onWorkerStart = $this->onWorkerStart; $this->onWorkerStart = array($this, 'onWorkerStart'); // 保存用户的回调,当对应的事件发生时触发 $this->_onConnect = $this->onConnect; $this->onConnect = array($this, 'onClientConnect'); // onMessage禁止用户设置回调 $this->onMessage = array($this, 'onClientMessage'); // 保存用户的回调,当对应的事件发生时触发 $this->_onClose = $this->onClose; $this->onClose = array($this, 'onClientClose'); // 保存用户的回调,当对应的事件发生时触发 $this->_onWorkerStop = $this->onWorkerStop; $this->onWorkerStop = array($this, 'onWorkerStop'); // 记录进程启动的时间 $this->_startTime = time(); // 运行父方法 parent::run(); }
在看看他的OnworkStart方法,也就是子进程运行后执行的方法
/** * 当Gateway启动的时候触发的回调函数 * @return void */ public function onWorkerStart() { // 分配一个内部通讯端口 // 主进程pid-子进程pid+startPort保证每个子进程的内部端口不同 $this->lanPort = function_exists('posix_getppid') ? $this->startPort - posix_getppid() + posix_getpid() : $this->startPort; if($this->lanPort<0 || $this->lanPort >=65535) { $this->lanPort = rand($this->startPort, 65535); } // 如果有设置心跳,则定时执行 if($this->pingInterval > 0) { $timer_interval = $this->pingNotResponseLimit > 0 ? $this->pingInterval/2 : $this->pingInterval; Timer::add($timer_interval, array($this, 'ping')); } //别名内部通讯协议 if(!class_exists('ProtocolsGatewayProtocol')) { class_alias('GatewayWorkerProtocolsGatewayProtocol', 'ProtocolsGatewayProtocol'); } // 初始化gateway内部的监听,用于监听worker的连接已经连接上发来的数据
// 这里内部链接在同一个ip+端口的情况下有两个服务
// 这个时候listen由于全局的事件self::$globalEvent在子进程的run方法里在回调OnWorkStart之前已经定义,所以不像主进程一样在listen的不监听accept事件 $this->_innerTcpWorker = new Worker("GatewayProtocol://{$this->lanIp}:{$this->lanPort}"); $this->_innerTcpWorker->listen(); $this->_innerUdpWorker = new Worker("GatewayProtocol://{$this->lanIp}:{$this->lanPort}"); $this->_innerUdpWorker->transport = 'udp'; $this->_innerUdpWorker->listen(); // 重新设置自动加载根目录 Autoloader::setRootPath($this->_appInitPath); // 设置内部监听的相关回调 $this->_innerTcpWorker->onMessage = array($this, 'onWorkerMessage'); $this->_innerUdpWorker->onMessage = array($this, 'onWorkerMessage'); $this->_innerTcpWorker->onConnect = array($this, 'onWorkerConnect'); $this->_innerTcpWorker->onClose = array($this, 'onWorkerClose'); // 注册gateway的内部通讯地址,worker去连这个地址,以便gateway与worker之间建立起TCP长连接 if(!$this->registerAddress()) { $this->log('registerAddress fail and exit'); Worker::stopAll(); } if($this->_onWorkerStart) { call_user_func($this->_onWorkerStart, $this); } }
可以看到他有新建了两个个work实例,innerTcpWorker,innerUdpWorker,并且在同一个地址{$this->lanIp}:{$this->lanPort}下开启了两个服务,并且注册了与worker通信的回调事件onWorkerConnect,onWorkerMessage,onWorkerClose。使用的协议是内部通讯协议GatewayProtocol。registerAddress方法通过文件锁把这个子进程的内部通讯服务地址{$this->lanIp}:{$this->lanPort}记录到一个公共地方,可能是文件,可能是memcache,可能是redis,后两种支持分布式部署gate与work,不然就要走同一台机器上。用后面两种可以部署多个gate,然后其他机器部署work。这时候定义的内部服务通过listen方法已经有accept监听事件了,如果work跟与gate连接就会进入到设置的Worker的回调方法里,客户端与Gate连接就会进入到Client方法里,因为他们是两种不同的work实例,监听的不同的端口。
到这里Gate子服务已经准备好了,除了自己是work实例提供给客户端的连接服务,被主进程管理之外;每个子Gate进程都会在新建一个work实例来提供对worker子进程的访问服务;对客户端的服务有new Gate的时候指定协议,对子worker进程的服务是默认协议,并且tcp与udp都监听了。后面的步奏应该是子worker进程在workstart方法里从子Gate服务建立内部服务是注册全局的内部通讯服务地址,连接到Gate,这样gate的内部服务监听就把子worker服务的地址记录下来。
子worker进程服务通过tcp与gate进程服务通信,通过在连接上的监听,实现消息的传递,worker进程在通过与Event文件的回调来通知客户端的请求,Event处理完毕之后,通过lib/gateway文件,直接udp到Gate来实现信息的传递。
分布式中的每台机器有主进程管理子进程,子Gate进程处理监听客户度与内部Work进程,Gate记录全局的客户端id与Gate的对应关系到全局储存器,也记录自己的内部服务地址到全局存储器。每个子Gate进程记录连接自己的内部worker地址。然后子worker启动时候从全局内部服务地址取地址进行tcp连接,记录与自己连接的Gate地址,client,gate,work直接的通信就打通了,如果一个客户端要与另一个客户端通信,在Event处理时从全局的client与gate的对以关系里得到要发送的client连接的gate,然后给这个gate发送udp信息,再由gate转发到对的client。
下面看看基类的accept方法:
/** * 接收一个客户端连接 * @param resource $socket * @return void */ public function acceptConnection($socket) { // 获得客户端连接 $new_socket = @stream_socket_accept($socket, 0); // 惊群现象,忽略 if(false === $new_socket) { return; } // 初始化连接对象 $connection = new TcpConnection($new_socket); $this->connections[$connection->id] = $connection; $connection->worker = $this; $connection->protocol = $this->_protocol; $connection->onMessage = $this->onMessage; $connection->onClose = $this->onClose; $connection->onError = $this->onError; $connection->onBufferDrain = $this->onBufferDrain; $connection->onBufferFull = $this->onBufferFull; // 如果有设置连接回调,则执行 if($this->onConnect) { try { call_user_func($this->onConnect, $connection); } catch(Exception $e) { ConnectionInterface::$statistics['throw_exception']++; self::log($e); } } } /** * 处理udp连接(udp其实是无连接的,这里为保证和tcp连接接口一致) * * @param resource $socket * @return bool */ public function acceptUdpConnection($socket) { $recv_buffer = stream_socket_recvfrom($socket , self::MAX_UDP_PACKEG_SIZE, 0, $remote_address); if(false === $recv_buffer || empty($remote_address)) { return false; } // 模拟一个连接对象 $connection = new UdpConnection($socket, $remote_address); if($this->onMessage) { if($this->_protocol) { /** @var WorkermanProtocolsProtocolInterface $parser */ $parser = $this->_protocol; $recv_buffer = $parser::decode($recv_buffer, $connection); } ConnectionInterface::$statistics['total_request']++; try { call_user_func($this->onMessage, $connection, $recv_buffer); } catch(Exception $e) { ConnectionInterface::$statistics['throw_exception']++; } } }
整体上来说就是tcp就是新建一个客户端连接,然后使用tcpConnecttion类封装,包括通信协议,然后回调onConnect事件;udp直接从连接获取数据,然后通过协议解析数据,回调onMessage方法。
gate的内部服务建立好了,再看看business子服务的run方法:
/** * 运行 * @see Workerman.Worker::run() */ public function run() { $this->_onWorkerStart = $this->onWorkerStart; $this->onWorkerStart = array($this, 'onWorkerStart'); parent::run(); } /** * 当进程启动时一些初始化工作 * @return void */ protected function onWorkerStart() { if(!class_exists('ProtocolsGatewayProtocol')) { class_alias('GatewayWorkerProtocolsGatewayProtocol', 'ProtocolsGatewayProtocol'); } Timer::add(1, array($this, 'checkGatewayConnections')); $this->checkGatewayConnections(); GatewayWorkerLibGateway::setBusinessWorker($this); if($this->_onWorkerStart) { call_user_func($this->_onWorkerStart, $this); } }
这里business子服务直接就是循环连接所有的Gate了。
连接打通之后,怎么通信呢,这就要看事件驱动管理以及协议了。