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 協議》,轉載必須註明作者和本文連結