Go实现FastCgi Proxy Client 系列(四)
Go实现FastCgi Proxy Client 系列(三)
Go实现FastCgi Proxy Client 系列(二)
Go实现FastCgi Proxy Client 系列(一)
cgi
CGI
CGI全称是“公共网关接口”(Common Gateway Interface),HTTP服务器与你的或其它机器上的程序进行“交谈”的一种工具,其程序须运行在网络服务器上。
cgi的弊端
cgi会产生什么问题呢?
当每一个请求进入的时候,cgi都会fork一个新的进程,然后以php为例,每个请求都要耗费相当大的内存,这样一来,并发起来,完全就会GG。
为了解决这个问题,于是产生了fastCgi。
FastCGI
FastCGI像是一个常驻(long-live)型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次(这是CGI最为人诟病的fork-and-execute 模式)。
它还支持分布式的运算,即 FastCGI 程序可以在网站服务器以外的主机上执行并且接受来自其它网站服务器来的请求。
一次完整的http经cgi请求到后端的分析
http request:
1Request URL: http://demo.inke.cn/server.php 2Request Method: GET 3Status Code: 200 OK 4Remote Address: 127.0.0.1:80 5Referrer Policy: no-referrer-when-downgrade 6Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 7Accept-Encoding: gzip, deflate 8Accept-Language: zh-CN,zh;q=0.9 9Cache-Control: max-age=0 10Connection: keep-alive 11Cookie: aid=fa29caf3-5b18-401c-8abd-677eb1a1c45f; s-59804d897bc03a0036dc141c=2a126bda133d4feba4169fa70308f2c7 12Host: demo.inke.cn 13Upgrade-Insecure-Requests: 1 14User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36
server.php
1<?php 2 3 4var_dump($_SERVER); 5返回结果: 6array(33) { 7 ["USER"]=> 8 string(6) "limars" 9 ["HOME"]=> 10 string(13) "/Users/limars" 11 ["HTTP_COOKIE"]=> 12 string(101) "aid=d11e3484-fd7d-4c6e-a2a5-bfabe2271da2; s-59804d897bc03a0036dc141c=fcb6a8cf23644f50b66e405d173eced7" 13 ["HTTP_ACCEPT_LANGUAGE"]=> 14 string(14) "zh-CN,zh;q=0.9" 15 ["HTTP_ACCEPT_ENCODING"]=> 16 string(13) "gzip, deflate" 17 ["HTTP_ACCEPT"]=> 18 string(118) "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" 19 ["HTTP_USER_AGENT"]=> 20 string(121) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" 21 ["HTTP_UPGRADE_INSECURE_REQUESTS"]=> 22 string(1) "1" 23 ["HTTP_CONNECTION"]=> 24 string(10) "keep-alive" 25 ["HTTP_HOST"]=> 26 string(12) "demo.inke.cn" 27 ["REDIRECT_STATUS"]=> 28 string(3) "200" 29 ["SERVER_NAME"]=> 30 string(12) "demo.inke.cn" 31 ["SERVER_PORT"]=> 32 string(2) "80" 33 ["SERVER_ADDR"]=> 34 string(9) "127.0.0.1" 35 ["REMOTE_PORT"]=> 36 string(5) "51429" 37 ["REMOTE_ADDR"]=> 38 string(9) "127.0.0.1" 39 ["SERVER_SOFTWARE"]=> 40 string(12) "nginx/1.12.2" 41 ["GATEWAY_INTERFACE"]=> 42 string(7) "CGI/1.1" 43 ["REQUEST_SCHEME"]=> 44 string(4) "http" 45 ["SERVER_PROTOCOL"]=> 46 string(8) "HTTP/1.1" 47 ["DOCUMENT_ROOT"]=> 48 string(31) "/Users/limars/Desktop/inke/demo" 49 ["DOCUMENT_URI"]=> 50 string(11) "/server.php" 51 ["REQUEST_URI"]=> 52 string(11) "/server.php" 53 ["SCRIPT_NAME"]=> 54 string(11) "/server.php" 55 ["CONTENT_LENGTH"]=> 56 string(0) "" 57 ["CONTENT_TYPE"]=> 58 string(0) "" 59 ["REQUEST_METHOD"]=> 60 string(3) "GET" 61 ["QUERY_STRING"]=> 62 string(0) "" 63 ["SCRIPT_FILENAME"]=> 64 string(42) "/Users/limars/Desktop/inke/demo/server.php" 65 ["FCGI_ROLE"]=> 66 string(9) "RESPONDER" 67 ["PHP_SELF"]=> 68 string(11) "/server.php" 69 ["REQUEST_TIME_FLOAT"]=> 70 float(1565082651.4024) 71 ["REQUEST_TIME"]=> 72 int(1565082651) 73}
它是个怎样的转换过程呢?仔细观察,我们会发现有很多东西是http协议里没有的,那这些东西是怎么生成的呢?
观察nginx配置:
1listen 80; 2server_name demo.inke.cn; 3root /Users/limars/Desktop/inke/demo; 4 5 location ~ \.php$ { 6 try_files $uri =404; 7 fastcgi_pass 127.0.0.1:9000; 8 9 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 10 include fastcgi_params; 11}
我们会发现一个叫 fastcgi_param的可配置参数,而nginx一般提供了默认值。 打开nginx目录下的fastcgi_params文件可以看到:
1fastcgi_param QUERY_STRING $query_string; 2fastcgi_param REQUEST_METHOD $request_method; 3fastcgi_param CONTENT_TYPE $content_type; 4fastcgi_param CONTENT_LENGTH $content_length; 5fastcgi_param SCRIPT_NAME $fastcgi_script_name; 6fastcgi_param REQUEST_URI $request_uri; 7fastcgi_param DOCUMENT_URI $document_uri; 8fastcgi_param DOCUMENT_ROOT $document_root; 9fastcgi_param SERVER_PROTOCOL $server_protocol; 10fastcgi_param REQUEST_SCHEME $scheme; 11fastcgi_param HTTPS $https if_not_empty; 12fastcgi_param GATEWAY_INTERFACE CGI/1.1; 13fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 14fastcgi_param REMOTE_ADDR $remote_addr; 15fastcgi_param REMOTE_PORT $remote_port; 16fastcgi_param SERVER_ADDR $server_addr; 17fastcgi_param SERVER_PORT $server_port; 18fastcgi_param SERVER_NAME $server_name;
这时候,我们对比下$_SERVER中http_XXX_XXX的,这不是都是http自带的头部嘛?
综合下来,这就是一个http请求完整转发到php的过程,一次完整的cgi请求,而fastcgi呢,就是对cgi协议的常驻封装,主要解决的问题是:
优化性能(cgi不停fork新进程,每个进程都干很多同样的事【加载配置文件、加载扩展】) 进程池维护,异步模型,请求是分发到空闲子进程,方便创建/销毁进程。
fcgi协议组成(go语言描述)
fcgi共12种消息类型
1const ( 2 typeBeginRequest uint8 = 1 3 typeAbortRequest uint8 = 2 4 typeEndRequest uint8 = 3 5 typeParams uint8 = 4 6 typeStdin uint8 = 5 7 typeStdout uint8 = 6 8 typeStderr uint8 = 7 9 typeData uint8 = 8 10 typeGetValues uint8 = 9 11 typeGetValuesResult uint8 = 10 12 typeUnknownType uint8 = 11 13)
见名知意:
1~3是信号类型
4~10是不同的数据类型
11、default 是错误类型
数据传输
通常协议传输,为了考虑数据大小的原因,都会将数据进行分片处理,fcgi也同样。
fcgi协议的每个包都由包头和包体组成
1//It's fcgi header 2type header struct { 3 Version uint8 4 Type uint8 5 Id uint16 6 ContentLength uint16 7 PaddingLength uint8 8 Reserved uint8 9}
包的正文,就是由分割的二进制内容组成,一个包最多传输2^16次方减一,也就是65535个字节。
一次完整的请求过程
请求:解析http请求获取配置->发送cgi请求->typeBeginRequest -> write Params -> write Stdin(if exists) -> write Data (if exists)
1//写头部 2if request.KeepConn { 3 //if it's keep-alive 4 //set flags 1 5 err = cgi.writeBeginRequest(reqId, roleResponder, 1) 6} else { 7 err = cgi.writeBeginRequest(reqId, roleResponder, 0) 8} 9 10if err != nil { 11 return 12} 13 14//写http header 15err = cgi.writePairs(typeParams, reqId, request.Params) 16if err != nil { 17 return 18} 19 20 21//读取http body并写到cgi 22p := make([]byte, 1024) 23n, _ := request.Stdin.Read(p) 24err = cgi.writeRecord(typeStdin, reqId, p[:n]) 25//写其他信息 26 27//写最后一个换行 28err = cgi.writeRecord(typeStdin, reqId, nil)
接收:check response type-> 错误处理(stderr、unknownType、default)->正确类型->接受数据复用socket返回给请求方。 rec := &record{} //建立一个记录缓冲
1 var err1 error 2 3 // recive untill EOF or FCGI_END_REQUEST 4 for { 5 err1 = rec.read(cgi.rwc) //从cgi建立的链接读取数据 6 if err1 != nil { //判断是否有错误,判断错误是否为终止符 7 if err1 != io.EOF { // keepalive on的时候 不会有终止符 这个要注意,读完长度为content-length即当前请求返回 8 err = err1 9 } 10 break 11 } 12 switch { //根据返回的类型,将内容写到响应的字节数组中 13 case rec.h.Type == typeStdout: 14 retout = append(retout, rec.content()...) 15 case rec.h.Type == typeStderr: 16 reterr = append(reterr, rec.content()...) 17 case rec.h.Type == typeEndRequest: 18 fallthrough 19 default: 20 break 21 } 22 }