手写简化版workerman,剖析其原理

前言

workerman的最大特色就是基础功能不依赖额外扩展,使得这个框架可以在windowslinux系统中完美运行。其次就是workerman的学习成本极低,只需要会用php写回调就能灵活操控这个框架,对进程和网络相关的知识捆绑不是很高。

项目

https://github.com/d2gin/socketman

原理

<?php
$host   = 'tcp://0.0.0.0:996';
$socket = stream_socket_server($host);
// 非阻塞模式
stream_set_blocking($socket, false);
while (1) {
    // 获取客户端资源链接
    $clientSocket = @stream_socket_accept($socket, 0);
    if (!$clientSocket) continue;
    // 读取客户端数据
    $revBuffer = '';
    while ($raw = fread($clientSocket, 65535)) {
        $revBuffer .= $raw;
    }
    // http响应头
    $respBuffer = "HTTP/1.1 200 OK\r\n";
    $respBuffer .= "Content-Type: text/html;charset=utf-8\r\n";
    $respBuffer .= "\r\n";
    $respBuffer .= 'hello';//通过浏览器访问127.0.0.1:996可以看到一个hello
    fwrite($clientSocket, $respBuffer);
    fclose($clientSocket);
}

解读workerman

  1. 首先是他的执行流程

    • Worker::runAll() 程序入口。
    • Worker::init() 初始化一些基本信息,比如进程号、进程名、定时器、日志文件等。
    • Worker::lock() 以当前执行文件路径为进程标识,对进程文件进行上锁。
    • Worker::parseCommand() 解析命令行参数,如-d-gstartstoprestart等。
    • Worker::daemonize() 守护进程 只在linux下有效。
    • Worker::initWorkers() 初始化所有子服务,只在linux下执行,没有太多重要操作。
    • Worker::installSignal() 安装进程信号监听 只在linux下有效。
    • Worker::saveMasterPid() 保存进程id到进程文件,只在linux有效。
    • Worker::lock(\LOCK_UN) 解锁当前进程文件。
    • Worker::forkWorkers() 这里是新建进程的操作,分为linux和windows部分,流程基本类似(如果多进程环境,那么这一步是运行在子进程的):
      • Worker::listen() 监听端口,并向全局事件调度器注册一个接听客户端连接的任务。
      • Worker::run() 获取事件轮询器,并向定时器(Timer)注册事件轮询(EventLoop)的机制。开始轮询。
    • Worker::resetStd() 重定向标准输入输出,守护进程需要用到,所以只在linux有效。
    • Worker::monitorWorkers() 监视子进程,只在主进程执行。
  2. 关于事件轮询

    • 默认情况下workerman使用的是while(true);做的轮询,即Workerman\Events\Select,理论上这种做法依然是堵塞运行的,只要有一个事件卡住那么后面的事件也会得不到运行。
    • 定时器也是使用时间轮询实现的,如果轮询不支持异步的话,事件执行过程中也会出现堵塞的可能。
    • 要解决这个堵塞问题需要使用libevent或者event扩展,这样可以为workerman带来不小的性能提升。
    • 好消息是workerman已经预设了多个轮询机制,如果安装了libevent或者event会自动选择对应的轮询类,优先级:event/libevent。
    • 当然也可以将事件轮询交给swoole处理:
      <?php
      Worker::$eventLoopClass = '\Swoole\Event';
  3. 关于进程监视

    • linux中收到子进程退出信号时,父进程会对运行过程中退出的子进程重新复刻(fork),即重启。如果你不希望某个子进程被重启:
      $worker->reloadable = false;//告诉父进程这个进程不需要自动重启
    • windows中是重新执行一遍当前的入口文件,没有太多复杂操作。
  4. fread的8192问题

    • PHP官方文档if the stream is read buffered and it does not represent a plain file, at most one read of up to a number of bytes equal to the chunk size (usually 8192) is made; depending on the previously buffered data, the size of the returned data may be larger than the chunk size.,大概意思是如果fread读取的是网络资源,那么数据会按8192字节大小进行分片。所以这个时候int $length参数就形同虚设了。
    • 如何解决:使用stream_set_read_buffer($socket, 0)来设置资源数据不再缓冲。

手写简化版workerman,剖析其原理
http://blog.icy8.cn/posts/419/
作者
icy8
发布于
2022年10月20日
许可协议