写了一个 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
,在表单随便写点内容,然后提交
PHP 后缀的请求正确被转发到 FastCGI 处理了
再看下 Golang 程序的输出,截图省去了中间 FCGI_PARAMS 的大部分字段,这些字段都会储存在 PHP 的 $_SERVER
全局变量里
备注
代码参考了 github.com/zhyee/fastcgi-demo 的 FastCGI C 语言版本
本人也在学习 Golang 和 PHP 过程中,代码和解释难免出现问题,如果发现请不吝赐教
本作品采用《CC 协议》,转载必须注明作者和本文链接
优秀 :+1: