Hyperf 如何做到用两个端口 9501/9502 都能连接 Websocket 服务以及多 Worker 协作实现聊天室功能

为何 Hyperf 能够在两个端口上监听 WebSocket 连接?

源码角度来看,在配置了多个 Servers 时,实际上,只启动了一个 Server

注:我之前接触的代码都是启动一个服务绑定一个端口,之前也看过 swoole 扩展的文档,但是没留意服务和监听端口也是分离的,这启发了我一种思维,代码凡是能继续拆分的,就继续拆分,这样代码就会有更多的灵活,每个功能都能进行扩展,将服务和端口进行拆分之后,就可以在一个 Server 绑定多个 Port,每个 Port 又能有独立的事件。

/**
 * @param Port[] $servers
 * @return Port[]
 */
protected function sortServers(array $servers): array
{
    $sortServers = [];
    foreach ($servers as $server) {
        switch ($server->getType()) {
            case ServerInterface::SERVER_HTTP:
                $this->enableHttpServer = true;
                if (! $this->enableWebsocketServer) {
                    array_unshift($sortServers, $server);
                } else {
                    $sortServers[] = $server;
                }
                break;
            case ServerInterface::SERVER_WEBSOCKET:
                $this->enableWebsocketServer = true;
                array_unshift($sortServers, $server);
                break;
            default:
                $sortServers[] = $server;
                break;
        }
    }

    return $sortServers;
}

从源码看,排在第一个的服务配置,会被创建服务,之后都是增加监听

protected function initServers(ServerConfig $config)
{
    $servers = $this->sortServers($config->getServers());

    foreach ($servers as $server) {
        $name = $server->getName();
        $type = $server->getType();
        $host = $server->getHost();
        $port = $server->getPort();
        $sockType = $server->getSockType();
        $callbacks = $server->getCallbacks();

        if (! $this->server instanceof SwooleServer) {
            $this->server = $this->makeServer($type, $host, $port, $config->getMode(), $sockType);
            $callbacks = array_replace($this->defaultCallbacks(), $config->getCallbacks(), $callbacks);
            $this->registerSwooleEvents($this->server, $callbacks, $name);
            $this->server->set(array_replace($config->getSettings(), $server->getSettings()));
            ServerManager::add($name, [$type, current($this->server->ports)]);

            if (class_exists(BeforeMainServerStart::class)) {
                // Trigger BeforeMainServerStart event, this event only trigger once before main server start.
                $this->eventDispatcher->dispatch(new BeforeMainServerStart($this->server, $config->toArray()));
            }
        } else {
            /** @var bool|\Swoole\Server\Port $slaveServer */
            $slaveServer = $this->server->addlistener($host, $port, $sockType);
            if (! $slaveServer) {
                throw new \RuntimeException("Failed to listen server port [{$host}:{$port}]");
            }
            $server->getSettings() && $slaveServer->set(array_replace($config->getSettings(), $server->getSettings()));
            $this->registerSwooleEvents($slaveServer, $callbacks, $name);
            ServerManager::add($name, [$type, $slaveServer]);
        }

        // Trigger beforeStart event.
        if (isset($callbacks[Event::ON_BEFORE_START])) {
            [$class, $method] = $callbacks[Event::ON_BEFORE_START];
            if ($this->container->has($class)) {
                $this->container->get($class)->{$method}();
            }
        }

        if (class_exists(BeforeServerStart::class)) {
            // Trigger BeforeServerStart event.
            $this->eventDispatcher->dispatch(new BeforeServerStart($name));
        }
    }
}

从makeServer函数来看,如果服务中有SERVER_WEBSOCKET,则这个会被作为主服务启动,new SwooleWebSocketServer

protected function makeServer(int $type, string $host, int $port, int $mode, int $sockType): SwooleServer
{
    switch ($type) {
        case ServerInterface::SERVER_HTTP:
            return new SwooleHttpServer($host, $port, $mode, $sockType);
        case ServerInterface::SERVER_WEBSOCKET:
            return new SwooleWebSocketServer($host, $port, $mode, $sockType);
        case ServerInterface::SERVER_BASE:
            return new SwooleServer($host, $port, $mode, $sockType);
    }

    throw new RuntimeException('Server type is invalid.');
}

$this->registerSwooleEvents($this->server, $callbacks, $name); 这句代码会将 Websocket 的各种事件都注册进去,于是主服务器拥有 websocket 的各种事件,而后 http 服务器挂载 9501 端口上,绑定了 onrequest 事件,但是如果有 websocket 连接9501 端口上时,默认该服务器是自动开启 websocket 自动升级的,又因为监听 9501 端口绑定的主服务器是 WebSocketServer,因此,WebSocketServer 默认的 onmessage,onopen事件就会被拿来用。

推测,如果开启 9503 WebSocket服务器,那么理论上用 WebSocket连接 9501 端口,应该就是连接的 9503 的回调事件。如果不想让 http 监听端口自动开启 websocket 协议,则将open_websocket_protocol=false

<?php
return [
// 这里省略了该文件的其它配置
'servers' => [
        [
            'name' => 'http',
            'type' => Server::SERVER_HTTP,
            'host' => '0.0.0.0',
            'port' => 9501,
            'sock_type' => SWOOLE_SOCK_TCP,
            'callbacks' => [
                Event::ON_REQUEST => [Hyperf\HttpServer\Server::class,'onRequest'],
             ],
             'settings' => ['open_websocket_protocol' => false,]
         ],
     ]
 ];

关于很多SWOOLE中出现的常量来看,这些东西在执行脚本时,会被自动设置好,通过实际代码运行发现

define('SWOOLE_HTTP2_ERROR_COMPRESSION_ERROR', 9);
define('SWOOLE_HTTP2_ERROR_CONNECT_ERROR', 10);
define('SWOOLE_HTTP2_ERROR_ENHANCE_YOUR_CALM', 11);
define('SWOOLE_HTTP2_ERROR_INADEQUATE_SECURITY', 12);
define('SWOOLE_BASE', 1);
define('SWOOLE_PROCESS', 2);
define('SWOOLE_IPC_UNSOCK', 1);
define('SWOOLE_IPC_MSGQUEUE', 2);
define('SWOOLE_IPC_PREEMPTIVE', 3);

以下,随便设置的一个test.php中输出SWOOLE_BASE,都能输出1,我之前都以为这些常量是运行时设置的呢,看来这种理解是错误的。

<?php
echo SWOOLE_BASE."\n";
echo "hello\n";

// 输出
1
hello

Hyperf-skeleton 给的默认配置就是进程模式,这个竟然没有发现,这样就比较明确了,使用的都是PROCESS模式,那么在websocket连接时,所有的连接都是由Manager来控制

SWOOLE_PRECESS 和 SWOOLE_BASE 两种模式

Server 的两种运行模式介绍

在 Swoole\Server 构造函数的第三个参数,可以填 2 个常量值 -- SWOOLE_BASE或 SWOOLE_PROCESS,下面将分别介绍这两个模式的区别以及优缺点

SWOOLE_PROCESS

SWOOLE_PROCESS 模式的 Server 所有客户端的 TCP 连接都是和主进程建立的,内部实现比较复杂,用了大量的进程间通信、进程管理机制。适合业务逻辑非常复杂的场景。Swoole 提供了完善的进程管理、内存保护机制。 在业务逻辑非常复杂的情况下,也可以长期稳定运行。

Swoole 在 Reactor线程中提供了 Buffer 的功能,可以应对大量慢速连接和逐字节的恶意客户端。

进程模式的优点:

  • 连接与数据请求发送是分离的,不会因为某些连接数据量大某些连接数据量小导致 Worker 进程不均衡

  • Worker 进程发生致命错误时,连接并不会被切断

  • 可实现单连接并发,仅保持少量 TCP 连接,请求可以并发地在多个 Worker 进程中处理

进程模式的缺点:

  • 存在 2 次 IPC 的开销,master 进程与 worker 进程需要使用 unixSocket进行通信

  • SWOOLE_PROCESS 不支持 PHP ZTS,在这种情况下只能使用 SWOOLE_BASE 或者设置 single_thread为 true

SWOOLE_BASE

SWOOLE_BASE 这种模式就是传统的异步非阻塞 Server。与 Nginx 和 Node.js 等程序是完全一致的。

worker_num参数对于 BASE 模式仍然有效,会启动多个 Worker 进程。

当有 TCP 连接请求进来的时候,所有的 Worker 进程去争抢这一个连接,并最终会有一个 worker 进程成功直接和客户端建立 TCP 连接,之后这个连接的所有数据收发直接和这个 worker 通讯,不经过主进程的 Reactor 线程转发。

  • BASE 模式下没有 Master 进程的角色,只有 Manager进程的角色。

  • 每个 Worker 进程同时承担了 SWOOLE_PROCESS模式下 Reactor线程和 Worker 进程两部分职责。

  • BASE 模式下 Manager 进程是可选的,当设置了 worker_num=1,并且没有使用 Task 和 MaxRequest 特性时,底层将直接创建一个单独的 Worker 进程,不创建 Manager 进程

BASE 模式的优点:

  • BASE 模式没有 IPC 开销,性能更好

  • BASE 模式代码更简单,不容易出错

BASE 模式的缺点:

  • TCP 连接是在 Worker 进程中维持的,所以当某个 Worker 进程挂掉时,此 Worker 内的所有连接都将被关闭

  • 少量 TCP 长连接无法利用到所有 Worker 进程

  • TCP 连接与 Worker 是绑定的,长连接应用中某些连接的数据量大,这些连接所在的 Worker 进程负载会非常高。但某些连接数据量小,所以在 Worker 进程的负载会非常低,不同的 Worker 进程无法实现均衡。

  • 如果回调函数中有阻塞操作会导致 Server 退化为同步模式,此时容易导致 TCP 的 backlog队列塞满问题。

BASE 模式的适用场景:

如果客户端连接之间不需要交互,可以使用 BASE 模式。如 Memcache、HTTP 服务器等。

BASE 模式的限制:

在 BASE 模式下,Server 方法除了 send和 close以外,其他的方法都不支持跨进程执行。

Reactor 线程和 Worker 进程

Reactor 线程

  • Reactor 线程是在 Master 进程中创建的线程

  • 负责维护客户端 TCP 连接、处理网络 IO、处理协议、收发数据

  • 不执行任何 PHP 代码

  • 将 TCP 客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包

Worker 进程

  • 接受由 Reactor 线程投递的请求数据包,并执行 PHP 回调函数处理数据

  • 生成响应数据并发给 Reactor 线程,由 Reactor 线程发送给 TCP 客户端

  • 可以是异步非阻塞模式,也可以是同步阻塞模式

  • Worker 以多进程的方式运行

他们之间的关系可以理解为 Reactor 就是 nginx,Worker 就是 PHP-FPM。Reactor 线程异步并行地处理网络请求,然后再转发给 Worker 进程中去处理。Reactor 和 Worker 间通过 unixSocket进行通信。

在 PHP-FPM 的应用中,经常会将一个任务异步投递到 Redis 等队列中,并在后台启动一些 PHP 进程异步地处理这些任务。Swoole 提供的 TaskWorker 是一套更完整的方案,将任务的投递、队列、PHP 任务处理进程管理合为一体。通过底层提供的 API 可以非常简单地实现异步任务的处理。另外 TaskWorker 还可以在任务执行完成后,再返回一个结果反馈到 Worker。

Swoole 的 Reactor、Worker、TaskWorker 之间可以紧密的结合起来,提供更高级的使用方式。

一个更通俗的比喻,假设 Server 就是一个工厂,那 Reactor 就是销售,接受客户订单。而 Worker 就是工人,当销售接到订单后,Worker 去工作生产出客户要的东西。而 TaskWorker 可以理解为行政人员,可以帮助 Worker 干些杂事,让 Worker 专心工作。

结论

  1. SWOOLE_PROCESS 模式下,Websocket 的连接对象都是由 Server 来控制,创建一个 Reactor 后,将该连接交付给 Reactor 来管理,Reactor 会将请求分配给 Worker 处理,Worker 处理完后,再把消息发给Server,Server 将消息压入队列等具体的 Reactor 发送出去

  2. hyperf 中有一处进程间通信,目前还不清楚,这里为何要向其他进程求助,以及其他进程监听到消息后会不会多发

    1. 开发者说,如果是 SWOOLE_PROCESS模式下不会触发让其他 Worker 发送的机制,只有 SWOOLE_BASE 模式下,每个链接交给每个 Worker 单独处理时,才需要在多个 Worker 协作处理,因为每个 Worker 争抢到的连接都是隔离的,所以不会出现发送多个的情况

  3. 上面 2 给了一种分布式 websocket 构建方式,采用这种多进程的方式就能让多台服务器协作提供长连接服务,保证千级万级用户的接入量

 

 下面这段代码是来自onPipeMessage 监听器的,意味着其他 worker 收到后,判断是在自己进程上的连接就执行

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/92419.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构--树4.2.1(二叉树)

目录 一、二叉树的存储结构 二、二叉树的遍历 一、二叉树的存储结构 顺序存储结构&#xff1a;二叉树的顺序存储结构就是用一维数组存储二叉树中的各个结点&#xff0c;并且结点的存储位置能体现结点之间的逻辑关系。 链式存储结构&#xff1a;二叉树每个结点最多只有两个孩…

SciencePlots 基本语法及特点

文章目录 简介安装 LaTeXSciencePlots 绘图示例 简介 用户有时需要根据期刊的配图绘制要求进行诸如字体、刻度轴、轴脊、图例等图层属性的定制化修改&#xff0c;耗时的同时也会容易导致用户忽略一些图层细节要求。 SciencePlots 作为一个专门用于科研论文绘图的第三方拓展工…

QT基础教程之二 第一个Qt小程序

QT基础教程之二 第一个Qt小程序 按钮的创建 在Qt程序中&#xff0c;最常用的控件之一就是按钮了&#xff0c;首先我们来看下如何创建一个按钮 QPushButton * btn new QPushButton; 头文件 #include <QPushButton>//设置父亲btn->setParent(this);//设置文字btn-&g…

微服务架构2.0--云原生时代

云原生 云原生&#xff08;Cloud Native&#xff09;是一种关注于在云环境中构建、部署和管理应用程序的方法和理念。云原生应用能够最大程度地利用云计算基础设施的优势&#xff0c;如弹性、自动化、可伸缩性和高可用性。这个概念涵盖了许多方面&#xff0c;包括架构、开发、…

channel并发编程

不要通过共享内存通信&#xff0c;要通过通信共享内存。 channel是golang并发编程中一种重要的数据结构&#xff0c;用于多个goroutine之间进行通信。 我们通常可以把channel想象成一个传送带&#xff0c;将goroutine想象成传送带周边的人&#xff0c;一个传送带的上游放上物品…

【沐风老师】如何在3dMax中将3D物体转化为样条线构成的对象?

在3dMax中如何把三维物体转化为由样条线构成的对象&#xff1f;通常这样的场景会出现在科研绘图或一些艺术创作当中&#xff0c;下面给大家详细讲解一种3dmax三维物体转样条线的方法。 第一部分&#xff1a;用粒子填充3D对象&#xff1a; 1.创建一个三维对象&#xff08;本例…

【广州华锐互动】VR高校虚拟实验教学平台提供丰富的资源支持,提高教学效果

随着科技的不断进步&#xff0c;虚拟现实(VR)技术已经逐渐渗透到各个领域&#xff0c;其中包括教育。 广州华锐互动利用VR虚拟现实技术打造的VR高校虚拟实验教学平台&#xff0c;是一种新型的教学工具&#xff0c;它提供了一个在线的教学资源管理平台&#xff0c;包含教学平台、…

Linux 基金会宣布正式进驻中国

在 LinuxCon 2017 &#xff08;北京&#xff09;即将召开前夕&#xff0c;我们Linux 中国会同 51CTO、开源中国对 Linux 基金会执行董事 Jim Zemlin 进行了一场远跨大洋的视频专访。 在这次专访中&#xff0c;Jim 先生回答了几个开源界和互联网领域关注的问题&#xff0c;并披…

低代码已经发展到什么水平了

陈老老老板&#x1f9b8; &#x1f468;‍&#x1f4bb;本文专栏&#xff1a;生活&#xff08;主要讲一下自己生活相关的内容&#xff09; &#x1f468;‍&#x1f4bb;本文简述&#xff1a;生活就像海洋,只有意志坚强的人,才能到达彼岸。 &#x1f468;‍&#x1f4bb;上一篇…

4.6 TCP面向字节流

TCP 是面向字节流的协议&#xff0c;UDP 是面向报文的协议 操作系统对 TCP 和 UDP 协议的发送方的机制不同&#xff0c;也就是问题原因在发送方。 UDP面向报文协议&#xff1a; 操作系统不会对UDP协议传输的消息进行拆分&#xff0c;在组装好UDP头部后就交给网络层处理&…

春秋云镜:CVE-2019-9042(Sitemagic CMS v4.4 任意文件上传漏洞)

一、题目 靶标介绍&#xff1a; Sitemagic CMS v4.4 index.php?SMExtSMFiles 存在任意文件上传漏洞&#xff0c;攻击者可上传恶意代码执行系统命令。 进入题目&#xff1a; admin/admin /index.php?SMExtSMFiles&SMTemplateTypeBasic&SMExecModeDedicated&SMFil…

苍穹外卖总结

前言 1、软件开发流程 瀑布模型需求分析//需求规格说明书、产品原型↓ 设计 //UI设计、数据库设计、接口设计↓编码 //项目代码、单元测试↓ 测试 //测试用例、测试报告↓上线运维 //软件环境安装、配置第一阶段&#xff1a;需求分析需求规格说明书、产品原型一般来说…

再见 Xshell替代工具Tabby

替代Xshell 之前经常使用Xshell来操作Linux虚拟机&#xff0c;基本上是够用了。但是Xshell免费使用只供非商业用途&#xff0c;而且如果你想用FTP来进行文件传输的话&#xff0c;还需单独下载Xftp。 无意中发现了另一款开源的终端工具Tabby&#xff0c;它直接集成了SFTP功能&…

smartbi token回调获取登录凭证漏洞

前段时间&#xff0c;Smartbi官方修复了一处权限绕过漏洞。未经授权的攻击者可利用该漏洞&#xff0c;获取管理员token&#xff0c;完全接管管理员权限。于是研究了下相关补丁并进行分析。 0x01分析结果 依据补丁分析&#xff0c;得到如下漏洞复现步骤 第一步&#xff0c;设…

opencv的haarcascade_frontalface_default.xml等文件

文章目录 GitHub下载在安装好的OpenCV文件夹下寻找opencv-python中获取 GitHub下载 下载地址&#xff1a;https://github.com/opencv/opencv/tree/master/data/haarcascades 在安装好的OpenCV文件夹下寻找 路径如下&#xff1a; 你安装的opencv路径\OpenCV\opencv\build\et…

UE4/5Niagara粒子特效之Niagara_Particles官方案例:3.3->4.3

目录 3.3 Visibility Tag 左边的发射器&#xff1a; 发射器更新 粒子生成 粒子更新 右边的发射器 和左边发射器不同的地方 3.4 Texture Sampling 发射器更新 粒子生成 粒子更新 4.1Play Audio Per Particle 系统 第三个发射器 发射器更新 粒子生成 粒子更新 第二个…

线性代数的学习和整理7:各种特殊效果矩阵汇总

目录 1 矩阵 1.1 1维的矩阵 1.2 2维的矩阵 1.3 没有3维的矩阵---3维的是3阶张量 1.4 下面本文总结的都是各种特殊效果矩阵特例 2 方阵: 正方形矩阵 3 单位矩阵 3.1 单位矩阵的定义 3.2 单位矩阵的特性 3.3 为什么单位矩阵I是 [1,0;0,1] 而不是[0,1;1,0] 或[1,1;1,1]…

前端开发工具: VSCode

VSCode 安装使用教程&#xff08;图文版&#xff09; | arry老师的博客-艾编程 1. 下载 在官方网站&#xff1a;https://code.visualstudio.com/ 下载最新版本的 VSCode 即可 2. VSCode 常见插件安装 所有插件安装后,需要重启一下才生效 2.1 简体中文语言包 2.2 编辑器主…

Redis(缓存预热,缓存雪崩,缓存击穿,缓存穿透)

目录 一、缓存预热 二、缓存雪崩 三、缓存击穿 四、缓存穿透 一、缓存预热 开过车的都知道&#xff0c;冬天的时候启动我们的小汽车之后不要直接驾驶&#xff0c;先让车子发动机预热一段时间再启动。缓存预热是一样的道理。 缓存预热就是系统启动前&#xff0c;提前将相关的…

ELK中Logstash的基本配置和用法

文章目录 Logstash的条件判断Logstash的输入插件Stdin输入文件内容输入filter过滤器 Logstash的输出插件测试收集日志启动kibana在kibana中配置索引数据 在 《Elasticsearch搜索引擎系统入门》中简单描述了Logstah的安装&#xff0c;本篇文章将较为详细的讲解Logstash的基本配置…