准备
Nginx 配置
user root admin;
worker_processes 2;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type text/html;
gzip on;
gzip_types text/css text/x-component application/x-javascript application/javascript text/javascript text/x-js text/richtext image/svg+xml text/plain text/xsd text/xsl text/xml image/x-icon;
sendfile on;
server {
listen 80;
server_name localhost;
autoindex on;
root /var/www;
index index.html;
location / {
try_files $uri =404;
}
# 以 .go 结尾的 url 都通过 fastcgi 转发到 localhost:10000 处
location ~ \.go$ {
try_files $uri =404;
fastcgi_pass localhost:10000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
try_files $uri $uri.html =404;
}
}
Go FastCGI 代码
// main.go
package main
import (
"fmt"
"io"
"net"
"net/http"
"net/http/fcgi"
"os"
)
type FastCGIServer struct {
}
func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
for header, values := range req.Header {
fmt.Println(header, "=>", values)
}
defer req.Body.Close()
io.Copy(os.Stdout, req.Body)
resp.Write([]byte("<h1>Hello world</h1><p>Welcome to my Go web app.</p>"))
}
func main() {
listener, _ := net.Listen("tcp", "127.0.0.1:10000")
srv := new(FastCGIServer)
fcgi.Serve(listener, srv)
}
创建将要访问的 Go 文件
$ sudo touch /var/www/test.go
说明
Nginx 配置中,主要的配置是:
location ~ \.go$ {
try_files $uri =404;
fastcgi_pass localhost:10000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
其中以上配置又以fastcgi_pass localhost:10000;
为主,localhost:10000
就是 Go FastCGI 代码中监听的地址,配置的作用就是在 Nginx 接收客户端以.go
结尾的 URL 的 HTTP 请求时,将 HTTP 请求封装到 FastCGI 协议,再发送到localhost:10000
对应的 socket 中;Go 代码接收到 FastCGI 请求,从中解析出 HTTP 请求报文进行相应的业务处理,然后生成 FastCGI 响应报文给 Nginx;Nginx 解析出 FastCGI 响应中的 HTTP 报文,最后将 HTTP 响应报文发送给客户端。
Go 的代码没什么好说的,就是生成 HTML 内容。
由于 Nginx 配置了root /var/www;
,所以在/var/www
中创建一个test.go
文件,否则会报 404。只有存在请求文件时才能转发到localhost:10000
。
验证
先启动 Nginx 服务器,再运行 Go 程序:
$ go run main.go
访问不存在的文件
访问存在的文件
访问tes.go
时,由于文件不存在,Nginx 并不会进行报文转发;访问test.go
时,文件存在,符合重定向的规则,于是将 HTTP 报文封装到 FastCGI 报文中,转发到localhost:10000
中,如 Go 的 FastCGI 后端代码所示,接收到请求后,Go 程序会将一段 HTML 代码发送回 Nginx 中,Nginx 再将 HTTP 报文发到浏览器,效果如上所示。
FastCGI 请求报文初探
由于技术还不过关,只能用笨方法分析报文了。用命令strace -p pid
看了下 PHP-FPM 的系统调用,发现poll
之后调用了五次read
,前面三个固定长度的大概就是元信息吧,第四个就是跟转发的内容相关的,最后一个应该就是结束元信息吧?
root@7f16318cabb2:/usr/share/php# strace -p 6
strace: Process 6 attached
accept(9, {sa_family=AF_INET6, sin6_port=htons(41140), inet_pton(AF_INET6, "::ffff:172.17.0.4", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28]) = 3
poll([{fd=3, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 4297319044
read(3, "\1\1\0\1\0\10\0\0", 8) = 8
read(3, "\0\1\0\0\0\0\0\0", 8) = 8
read(3, "\1\4\0\1\3\326\2\0", 8) = 8
read(3, "\17:SCRIPT_FILENAME/usr/share/php/"..., 984) = 984
read(3, "\1\4\0\1\0\0\0\0", 8) = 8
下图是用 Wireshark 捕获的 FastCGI 报文中的一部分:
看到上图的内容应该感觉很熟悉吧?可见的 ASCII 字符都跟服务器以及 HTTP 请求报文相关的内容。
也可以使用以下程序简单感受一下。
package main
import (
"net"
"log"
"fmt"
)
func main() {
l, err := net.Listen("tcp", ":10001")
if err != nil {
log.Println("error listen:", err)
return
}
defer l.Close()
log.Println("listen ok", l.Addr())
for {
conn, err := l.Accept()
if err != nil {
log.Println("accept error:", err)
break
}
buf := make([]byte, 8)
buf2 := make([]byte, 8192)
n, _ := conn.Read(buf)
fmt.Println(buf[:n])
n, _ = conn.Read(buf)
fmt.Println(buf[:n])
n, _ = conn.Read(buf)
fmt.Println(buf[:n])
n, _ = conn.Read(buf2)
for _, v := range buf2[:n] {
fmt.Printf("%c", v)
}
fmt.Printf("END\n")
conn.Close()
}
}
只需要将 Nginx 的转发地址修改成localhost:10001
,现在请求任何 go 文件即可在终端看到报文输出。至于更详细的报文格式,等有机会啃完协议 RFC 之后再补充。
与 CGI 的区别
CGI(Common Gateway Interface)
工作原理
Web 服务器接收到动态请求时,会启动对应的 CGI 程序;CGI 程序处理完请求后,将数据返回给 Web 服务器;Web 服务器再把结果返回给客户端。
CGI 特点
-
高并发时性能差:由于 CGI 程序每次接收到请求时都会启动一个进程,会有较大的开销。
-
安全性较差。
FastCGI(Fast Common Gateway Interface)
FastCGI 是 CGI 的增强版本,由 CGI 发展改进而来,主要用于提高 CGI 程序性能,类似于 CGI。
工作原理
Web 服务器启动的同时,加载 FastCGI 进程管理器。FastCGI 初始化之后,启动多个 CGI 解析器进程等待 Web 服务器的连接;Web 服务器接收到动态请求时,FastCGI 进程管理器选择并连接一个 CGI 解析器;Web 服务器会将相关环境变量和标准输入发送到 FastCGI 子进程进行处理;FastCGI 子进程完成处理后将数据按照 CGI 规定的格式返回给 Web 服务器,然后等待下一次请求。
FastCGI 特点
-
FastCGI 程序接口采用 C/S 结构,可以将 Web 服务器和脚本解析服务器分开,独立于 Web 服务器运行,提高 Web 服务器的并发性能和安全性。
-
FastCGI 不依赖于任何 Web 服务器。
-
FastCGI 程序在修改配置时可以进行平滑重启加载新配置。
-
常驻内存,不必每次创建新进程,速度较快。
-
内存消耗较大,注意进程池的数量。