写了一个 FastCGI 实现

github 地址 github.com/moodrain/fastcgi-go

最近在学习 PHP 进阶相关,一直都知道 Nginx 和 PHP 的交互是靠 PHP-FPM,并且是通过 FastCGI 协议的。按道理来说 FastCGI 只是一个协议,用什么语言实现都可以,就用 Golang 来写一个吧

协议内容

原文 fastcgi-archives.github.io/FastCGI...
翻译 mp.weixin.qq.com/s/oRTd_2WYabAvM-Q...

简单来说就是每一个包分为头和内容,头定义了包的类型和长度,根据这这些参数去处理包的内容

主要结构

头 结构

type FcgiHeader struct {
    Version byte
    Type byte
    RequestId uint16
    ContentLength uint16
    // RequestIdB1 byte
    // RequestIdB0 byte
    // ContentLengthB1 byte
    // ContentLengthB0 byte
    PaddingLength byte
    Reserved byte
}

头的长度是 8 byte,如果每个字段都是 1 byte,那么 8 个字段就刚刚好(事实上规范也是如此)。不过 RequestIdB1 和 RequestIdB0 共同表示请求 ID(ContentLengthB 也是类似),这样请求 ID 最大长度可以到 2 byte。为了方便处理,定义 Golang 结构体的时候把这些分成两个字段的用一个 uint16 表示了

内容 请求开始 结构
Role 表示服务器希望 FastCGI 担任的角色,一般是 FCGI_RESPONDER

type FcgiBeginRequestBody struct {
    Role uint16
    // RoleB1 byte
    // RoleB0 byte
    Flags    byte
    Reserved [5]byte
}

内容 请求结束 结构
ProtocolStatus 表示返回的状态 正常的话是 FCGI_REQUEST_COMPLETE

type FcgiEndRequestBody struct {
    AppStatus uint32
    // AppStatusB3 byte
    // AppStatusB2 byte
    // AppStatusB1 byte
    // AppStatusB0 byte
    ProtocolStatus byte
    Reserved [3]byte
}

可以看到这些包长度都是 8 byte 的(字节对齐),协议建议在内存中处理记录时,尽量保持每个记录的起始指针地址为 8 字节的整数倍

主要流程

var id uint16
env := make(map[string]string)
var data string
process := make(map[string]bool)
for {
    buff := make([]byte, HEAD_LEN)
    // 请求完成后服务器可能会断开连接,捕捉错误防止程序退出
    if _, err := conn.Read(buff); err != nil {
        _ = conn.Close()
        if conn, err = server.Accept(); err != nil {
            log.Fatal("Network Error")
        } else {
            if _, err = conn.Read(buff); err != nil {
                log.Fatal("Network Error")
            }
        }
    }
    head := ReadHead(buff)
    // 目前只做一次只接受一个请求的方案,请求 ID 不一致就跳过
    if id == 0 && head.RequestId != 0 {
        id = head.RequestId
    }
    if id != head.RequestId {
        continue
    }
    switch head.Type {
    case FCGI_BEGIN_REQUEST: ReadBeginRequest(head, conn)
    case FCGI_PARAMS:
        if newEnv := ReadParamsRequest(head, conn); len(newEnv) > 0 {
            env = newEnv
        }
        process["params"] = true
    case FCGI_STDIN:
        if newData := ReadStdinRequest(head, conn); len(newData) > 0 {
            data = newData
        }
        process["stdin"] = true
    default:
        log.Fatal("Unknown Request Type", head.Type)
    }
    // 经过 Params 和 Stdin 两个阶段后开始调用 PHP 处理请求
    if process["params"] && process["stdin"] {
        rs := ExecPhp(env, data)
        SendResponse(id, rs, conn)
        // 完成请求后重置 process 状态
        id = 0
        process["params"] = false
        process["stdin"] = false
    }
}

PHP 测试代码

<?php
// 由于此时 PHP 不像 PHP-FPM 那样运行在 CGI 环境
// $_GET, $_POST 这些全局变量都是无法使用的,这里需要传参模拟来兼容
if (php_sapi_name() == 'cli') { // 在 PHP-FPM 会返回 cgi-fcgi
    $post = getopt('', ['post:'])['post'] ?? '';
    parse_str($post, $_POST);
}

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $id = $_POST['id'] ?? 1;
    $name = $_POST['name'] ?? 'user';
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown Agent';

    echo $id . '-' . $name . ' from ' . $ua;
} else {
    echo 'Hello World';
}

测试

访问 localhost:8080,在表单随便写点内容,然后提交

写了一个 FastCGI 实现

PHP 后缀的请求正确被转发到 FastCGI 处理了

写了一个 FastCGI 实现

再看下 Golang 程序的输出,截图省去了中间 FCGI_PARAMS 的大部分字段,这些字段都会储存在 PHP 的 $_SERVER 全局变量里

写了一个 FastCGI 实现

备注

代码参考了 github.com/zhyee/fastcgi-demo 的 FastCGI C 语言版本
本人也在学习 Golang 和 PHP 过程中,代码和解释难免出现问题,如果发现请不吝赐教

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 4周前 自动加精
讨论数量: 1

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