reverb扩展-laravel-swoole-reverb

开篇

最近做了这个扩展包,学习了pusher协议标准。因此也做了一个合集,有兴趣可以看下Laravel11 从0开发 Swoole-Reverb 扩展包

关于laravel-swoole-reverb

laravel-swoole-reverb是一个将 Swoole 与 Laravel Reverb 集成的包,采用php8.2版本编写,可用于高性能的 WebSocket 和 Pub/Sub 消息传递。这个包依赖 Laravel Reverb ,通过提供swoole:reverb start 操作来替代 reverb:start.同时所说的替代,指的是可以使用swoole:reverb start来运行swoole的服务,但同时也保留了reverb的所有服务。开发者选择一个进行。
swoole reverb 将reactPHP换成swoole并且swoole reverb高度复用reverb组件,因此,大家可以放心使用,目前经我测试,public private channel 正常,立即发送和队列发送广播正常,echojs 监听正常。欢迎大家使用哈,有问题提 Issue。

知识点分享

在实现多进程的过程中,我也加深学习了一些基础知识,这里还是要说:一定要看完swoole 基础知识说的内容,现在有AI后,也更加让你能够理解。话不多说,赶紧分享:

fork 子进程后,子进程到底复制父进程的哪些资源,同时哪些资源又不能复制?

我觉得这个问题很有味道,了解这个后,我们在面对多进程、协程编程遇到的心智问题,会对我们有所帮助。然后在放出ai的总结内容前,我建议 朋友们可以去看看c++教程网出的socket编程系列视频(我也正在看,b站可以找到)。对于php go 等开发者,会有很大帮助,而且,我保证你不会c/c++ 也能看懂的。 为啥我觉得有用哈,因为下面会讲到fork 的子进程复制文件描述符是引用计数的策略,这个点在socket编程视频里面讲多进程回射服务器的时候也提到了:fork的子进程负责处理accept 后连接套接字,同时父进程使用close(conn)关闭连接套接字,就会说到引用计数-1

在 Unix/Linux 系统中,使用 fork() 创建子进程时,子进程会继承父进程的大部分资源,但某些资源不能直接复制。以下是具体的分类:

子进程会复制的资源

  1. 代码段(Text Segment)

    • 子进程与父进程共享相同的程序代码,但拥有独立的执行上下文。
  2. 数据段(Data Segment)

    • 包括全局变量、静态变量的副本。子进程对这些变量的修改不会影响父进程。
  3. 堆(Heap)

    • 复制父进程的堆数据,但现代操作系统通常使用写时复制(Copy-On-Write, COW)技术,在子进程修改数据之前,共享相同的物理内存。
  4. 栈(Stack)

    • 复制父进程的栈,包括局部变量、函数调用帧等,但与堆一样,通常使用写时复制
  5. 文件描述符表(File Descriptors)

    • 继承父进程的打开文件描述符(包括文件、管道、套接字),文件偏移量是共享的。
  6. 信号处理方式

    • 继承父进程的信号处理配置(如 SIG_IGNSIG_DFL),但某些信号(如 SIGCHLD)的状态可能会有所不同。
  7. 当前工作目录

    • 继承父进程的当前工作目录(cwd)。
  8. 环境变量(Environment Variables)

    • 复制父进程的环境变量。
  9. 资源限制(Resource Limits, ulimit

    • 继承父进程的资源限制,如 CPU 时间、文件大小等。
  10. 进程组和会话(Process Group and Session)

    • 子进程继承父进程的进程组 ID(PGID)和会话 ID(SID)。

子进程不会复制或有特殊行为的资源

  1. 进程 ID(PID)

    • 子进程会获得一个新的 PID,而不是继承父进程的 PID。
  2. 父进程 ID(PPID)

    • 子进程的 PPID 被设置为创建它的父进程的 PID。
  3. 文件锁(File Locks)

    • fcntl() 文件锁不会继承,需要子进程重新加锁。
  4. 挂起的信号(Pending Signals)

    • 父进程的挂起信号不会传递给子进程。
  5. 线程(Threads)

    • fork() 仅复制调用它的线程,其他线程不会复制。推荐使用 fork() 时避免多线程,否则可能导致同步问题。
  6. IPC 机制(Inter-Process Communication)

    • 共享内存(Shared Memory, shm:子进程仍可访问,但由于共享性质,数据不会独立。
    • 消息队列(Message Queues)和信号量(Semaphores):保持不变,但某些同步行为可能需要重新初始化。
  7. 打开的 socket 的连接状态

    • 继承 socket,但对于 TCP 连接,某些协议级别的状态可能需要重新初始化。
  8. 计时器(Timers)

    • setitimer()alarm() 设定的定时器在子进程中不会继承。

特殊情况

  • 写时复制(Copy-On-Write, COW)

    • 现代操作系统在 fork() 时通常不会真正复制堆和栈,而是采用 COW 机制,直到子进程修改数据时才进行真正的拷贝。
  • vfork()

    • vfork() 直接共享父进程的地址空间,子进程执行 exec()exit() 之前,父进程会被挂起。相比 fork()vfork() 在某些情况下可以提高性能,但需要谨慎使用。

总结

资源 继承/复制 备注
代码段 共享 只读共享
数据段 复制(COW) 修改时才真正复制
堆(Heap) 复制(COW) 修改时才真正复制
栈(Stack) 复制(COW) 修改时才真正复制
文件描述符 继承 共享文件偏移量
信号处理方式 继承 SIG_IGNSIG_DFL 继承
环境变量 复制 独立
进程 ID(PID) 新分配
父进程 ID(PPID) 继承
线程 不继承 仅复制 fork() 调用的线程
挂起信号 不继承
IPC(共享内存等) 共享 共享内存仍可访问
进程组 & 会话 继承
文件锁 不继承 fcntl() 需要重新加锁

因此,在 fork() 之后,父子进程是独立的进程,但由于 COW 和共享的文件描述符、信号处理等机制,它们仍然在某些方面有联系。

是的,关于 文件描述符(File Descriptors),除了继承父进程的打开文件描述符之外,还有一个很重要的细节——引用计数

文件描述符的继承机制

  1. 子进程继承父进程的文件描述符表

    • fork() 之后,父进程和子进程的文件描述符表是独立的拷贝,但它们的文件描述符指向同一个文件描述符表项(File Table Entry)
    • 这个文件表项记录了文件偏移量(File Offset)、打开标志(Flags)和文件引用计数(Reference Count)
  2. 文件引用计数(Reference Count)

    • 文件引用计数会增加,因为 fork() 之后,子进程的文件描述符仍然指向相同的文件结构。
    • 只有当所有进程(父进程和子进程)都关闭(close(fd))这个文件描述符后,文件才会真正关闭。
  3. 共享文件偏移量

    • 由于父子进程的文件描述符指向相同的文件表项,它们会共享文件的读写偏移量lseek() 影响彼此)。
    • 如果父进程 read()write(),会影响子进程的文件读写位置,反之亦然。
  4. 文件状态标志的继承

    • O_APPENDO_NONBLOCK 等文件标志(通过 fcntl() 设置)也是共享的

示例:文件描述符共享偏移量

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        perror("open");
        exit(1);
    }

    write(fd, "Hello", 5); // 写入 5 字节

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) { // 子进程
        write(fd, " Child", 6); // 继续写入
        close(fd);
        exit(0);
    } else { // 父进程
        wait(NULL); // 等待子进程结束
        write(fd, " Parent", 7); // 继续写入
        close(fd);
    }
    return 0;
}

预期输出 (test.txt 内容)

Hello Child Parent

说明

  • 父进程 write(fd, "Hello", 5) 之后,文件偏移量变成 5
  • 子进程 write(fd, " Child", 6),偏移量变成 11
  • 父进程等待子进程完成,再 write(fd, " Parent", 7),最终文件内容是 "Hello Child Parent"
  • 由于共享偏移量,读写操作会影响对方!

避免共享影响的方法

  • 使用 dup()dup2()

    • 这样可以创建一个新的文件表项,避免共享偏移量。
  • 重新打开文件

    int fd = open("test.txt", O_RDWR);
    • 这样父子进程拥有不同的 fd,文件偏移量互不影响。

总结

继承属性 共享情况
文件描述符(fd) 继承
文件引用计数 增加(直到 close() 释放)
文件偏移量 共享(读写影响对方)
文件状态标志 共享(O_APPEND, O_NONBLOCK)

所以,fork() 后子进程并不拥有文件的独立副本,而是共享同一个文件表项,只是它的文件描述符表是独立的。因此,如果父子进程都需要独立操作文件,应该使用 dup() 或者 open() 重新获取 fd

总结

这一篇就到这,后续我会继续写具体实现过程的分享,同时,如果大家觉得框架太绕了的话,可以在我的laravel-swoole-reverb里面找到swoole-reverb.php这个单文件,文件基本实现了整个过程,目前还差http pusher 接口的具体逻辑没写,后面也会继续完善(看下面第二张图)

image
image

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 1

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!