傳統(tǒng) CGI 工作原理分析
客戶端訪問某個 URL 地址之后讲婚,通過 GET/POST/PUT 等方式提交數據钦无,并通過 HTTP 協(xié)議向 Web 服務器發(fā)出請求,服務器端的 HTTP Daemon(守護進程)將 HTTP 請求里描述的信息通過標準輸入 stdin 和環(huán)境變量(environment variable)傳遞給主頁指定的 CGI 程序,并啟動此應用程序進行處理(包括對數據庫的處理),處理結果通過標準輸出 stdout 返回給 HTTP Daemon 守護進程,再由 HTTP Daemon 進程通過 HTTP 協(xié)議返回給客戶端斤斧。
上面的這段話理解可能還是比較抽象,下面我們就通過一次GET請求為例進行詳細說明霎烙。 ?
下面用代碼來實現(xiàn)圖中表述的功能撬讽。Web 服務器啟動一個 socket 監(jiān)聽服務,然后在本地執(zhí)行 CGI 程序悬垃。后面有比較詳細的代碼解讀游昼。
10年架構師領你架構-成長之路-(附面試題(含答案))
(騰訊T3-T4)打造互聯(lián)網PHP架構師教程目錄大全,只要你看完尝蠕,薪資立馬提升2倍(持續(xù)更新)
Web 服務器代碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define SERV_PORT 9003
char* str_join(char *str1, char *str2);
char* html_response(char *res, char *buf);
int main(void)
{
int lfd, cfd;
struct sockaddr_in serv_addr,clin_addr;
socklen_t clin_len;
char buf[1024],web_result[1024];
int len;
FILE *cin;
if((lfd = socket(AF_INET,SOCK_STREAM,0)) == -1){
perror("create socket failed");
exit(1);
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);
if(bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
{
perror("bind error");
exit(1);
}
if(listen(lfd, 128) == -1)
{
perror("listen error");
exit(1);
}
signal(SIGCLD,SIG_IGN);
while(1)
{
clin_len = sizeof(clin_addr);
if ((cfd = accept(lfd, (struct sockaddr *)&clin_addr, &clin_len)) == -1)
{
perror("接收錯誤\n");
continue;
}
cin = fdopen(cfd, "r");
setbuf(cin, (char *)0);
fgets(buf,1024,cin); //讀取第一行
printf("\n%s", buf);
//============================ cgi 環(huán)境變量設置演示 ============================
// 例如 "GET /user.cgi?id=1 HTTP/1.1";
char *delim = " ";
char *p;
char *method, *filename, *query_string;
char *query_string_pre = "QUERY_STRING=";
method = strtok(buf,delim); // GET
p = strtok(NULL,delim); // /user.cgi?id=1
filename = strtok(p,"?"); // /user.cgi
if (strcmp(filename,"/favicon.ico") == 0)
{
continue;
}
query_string = strtok(NULL,"?"); // id=1
putenv(str_join(query_string_pre,query_string));
//============================ cgi 環(huán)境變量設置演示 ============================
int pid = fork();
if (pid > 0)
{
close(cfd);
}
else if (pid == 0)
{
close(lfd);
FILE *stream = popen(str_join(".",filename),"r");
fread(buf,sizeof(char),sizeof(buf),stream);
html_response(web_result,buf);
write(cfd,web_result,sizeof(web_result));
pclose(stream);
close(cfd);
exit(0);
}
else
{
perror("fork error");
exit(1);
}
}
close(lfd);
return 0;
}
char* str_join(char *str1, char *str2)
{
char *result = malloc(strlen(str1)+strlen(str2)+1);
if (result == NULL) exit (1);
strcpy(result, str1);
strcat(result, str2);
return result;
}
char* html_response(char *res, char *buf)
{
char *html_response_template = "HTTP/1.1 200 OK\r\nContent-Type:text/html\r\nContent-Length: %d\r\nServer: mengkang\r\n\r\n%s";
sprintf(res,html_response_template,strlen(buf),buf);
return res;
}
如上代碼中的重點:
66~81行找到CGI程序的相對路徑(我們?yōu)榱撕唵谓创玻苯訉⑵涓夸浂x為Web程序的當前目錄),這樣就可以在子進程中執(zhí)行 CGI 程序了趟佃;同時設置環(huán)境變量扇谣,方便CGI程序運行時讀让两荨;
94~95行將 CGI 程序的標準輸出結果寫入 Web 服務器守護進程的緩存中罐寨;
97行則將包裝后的 html 結果寫入客戶端 socket 描述符靡挥,返回給連接Web服務器的客戶端。
-
感謝大家一直來支持鸯绿,這是我準備的1000粉絲福利
CGI 程序(user.c)
#include <stdio.h>
#include <stdlib.h>
// 通過獲取的 id 查詢用戶的信息
int main(void){
//============================ 模擬數據庫 ============================
typedef struct
{
int id;
char *username;
int age;
} user;
user users[] = {
{},
{
1,
"mengkang.zhou",
18
}
};
//============================ 模擬數據庫 ============================
char *query_string;
int id;
query_string = getenv("QUERY_STRING");
if (query_string == NULL)
{
printf("沒有輸入數據");
} else if (sscanf(query_string,"id=%d",&id) != 1)
{
printf("沒有輸入id");
} else
{
printf("用戶信息查詢<br>學號: %d<br>姓名: %s<br>年齡: %d",id,users[id].username,users[id].age);
}
return 0;
}
將上面的 CGI 程序編譯成gcc user.c -o user.cgi
,放在上面web程序的同級目錄瓶蝴。
代碼中的第28行毒返,從環(huán)境變量中讀取前面在Web服務器守護進程中設置的環(huán)境變量,是我們演示的重點舷手。
FastCGI 工作原理分析
相對于 CGI/1.1 規(guī)范在 Web 服務器在本地 fork 一個子進程執(zhí)行 CGI 程序拧簸,填充 CGI 預定義的環(huán)境變量,放入系統(tǒng)環(huán)境變量男窟,把 HTTP body 體的 content 通過標準輸入傳入子進程盆赤,處理完畢之后通過標準輸出返回給 Web 服務器。FastCGI 的核心則是取締傳統(tǒng)的 fork-and-execute 方式歉眷,減少每次啟動的巨大開銷(后面以 PHP 為例說明)牺六,以常駐的方式來處理請求。
FastCGI 工作流程如下:
- FastCGI 進程管理器自身初始化汗捡,啟動多個 CGI 解釋器進程淑际,并等待來自 Web Server 的連接。
- Web 服務器與 FastCGI 進程管理器進行 Socket 通信扇住,通過 FastCGI 協(xié)議發(fā)送 CGI 環(huán)境變量和標準輸入數據給 CGI 解釋器進程庸追。
- CGI 解釋器進程完成處理后將標準輸出和錯誤信息從同一連接返回 Web Server。
- CGI 解釋器進程接著等待并處理來自 Web Server 的下一個連接台囱。
?
FastCGI 與傳統(tǒng) CGI 模式的區(qū)別之一則是 Web 服務器不是直接執(zhí)行 CGI 程序了,而是通過 socket 與 FastCGI 響應器(FastCGI 進程管理器)進行交互读整,Web 服務器需要將 CGI 接口數據封裝在遵循 FastCGI 協(xié)議包中發(fā)送給 FastCGI 響應器程序簿训。正是由于 FastCGI 進程管理器是基于 socket 通信的,所以也是分布式的米间,Web服務器和CGI響應器服務器分開部署强品。
再啰嗦一句,F(xiàn)astCGI 是一種協(xié)議屈糊,它是建立在CGI/1.1基礎之上的的榛,把CGI/1.1里面的要傳遞的數據通過FastCGI協(xié)議定義的順序、格式進行傳遞逻锐。
準備工作
可能上面的內容理解起來還是很抽象夫晌,這是由于第一對FastCGI協(xié)議還沒有一個大概的認識雕薪,第二沒有實際代碼的學習。所以需要預先學習下 FastCGI 協(xié)議的內容晓淀,不一定需要完全看懂所袁,可大致了解之后,看完本篇再結合著學習理解消化凶掰。
http://www.fastcgi.com/devkit... (英文原版)
http://andylin02.iteye.com/bl... (中文版)
FastCGI 協(xié)議分析
下面結合 PHP 的 FastCGI 的代碼進行分析燥爷,不作特殊說明以下代碼均來自于 PHP 源碼。
FastCGI 消息類型
FastCGI 將傳輸的消息做了很多類型的劃分懦窘,其結構體定義如下:
typedef enum _fcgi_request_type {
FCGI_BEGIN_REQUEST = 1, /* [in] */
FCGI_ABORT_REQUEST = 2, /* [in] (not supported) */
FCGI_END_REQUEST = 3, /* [out] */
FCGI_PARAMS = 4, /* [in] environment variables */
FCGI_STDIN = 5, /* [in] post data */
FCGI_STDOUT = 6, /* [out] response */
FCGI_STDERR = 7, /* [out] errors */
FCGI_DATA = 8, /* [in] filter data (not supported) */
FCGI_GET_VALUES = 9, /* [in] */
FCGI_GET_VALUES_RESULT = 10 /* [out] */
} fcgi_request_type;
消息的發(fā)送順序
下圖是一個簡單的消息傳遞流程
?
最先發(fā)送的是FCGI_BEGIN_REQUEST
前翎,然后是FCGI_PARAMS
和FCGI_STDIN
,由于每個消息頭(下面將詳細說明)里面能夠承載的最大長度是65535畅涂,所以這兩種類型的消息不一定只發(fā)送一次港华,有可能連續(xù)發(fā)送多次。
FastCGI 響應體處理完畢之后毅戈,將發(fā)送FCGI_STDOUT
苹丸、FCGI_STDERR
,同理也可能多次連續(xù)發(fā)送苇经。最后以FCGI_END_REQUEST
表示請求的結束赘理。
需要注意的一點,FCGI_BEGIN_REQUEST
和FCGI_END_REQUEST
分別標識著請求的開始和結束扇单,與整個協(xié)議息息相關商模,所以他們的消息體的內容也是協(xié)議的一部分,因此也會有相應的結構體與之對應(后面會詳細說明)蜘澜。而環(huán)境變量施流、標準輸入、標準輸出鄙信、錯誤輸出瞪醋,這些都是業(yè)務相關,與協(xié)議無關装诡,所以他們的消息體的內容則無結構體對應银受。
由于整個消息是二進制連續(xù)傳遞的,所以必須定義一個統(tǒng)一的結構的消息頭鸦采,這樣以便讀取每個消息的消息體宾巍,方便消息的切割。這在網絡通訊中是非常常見的一種手段渔伯。
FastCGI 消息頭
如上顶霞,F(xiàn)astCGI 消息分10種消息類型,有的是輸入有的是輸出锣吼。而所有的消息都以一個消息頭開始选浑。其結構體定義如下:
typedef struct _fcgi_header {
unsigned char version;
unsigned char type;
unsigned char requestIdB1;
unsigned char requestIdB0;
unsigned char contentLengthB1;
unsigned char contentLengthB0;
unsigned char paddingLength;
unsigned char reserved;
} fcgi_header;
字段解釋下:
version
標識FastCGI協(xié)議版本蓝厌。
type
標識FastCGI記錄類型,也就是記錄執(zhí)行的一般職能鲜侥。
requestId
標識記錄所屬的FastCGI請求褂始。
contentLength
記錄的contentData組件的字節(jié)數。
關于上面的xxB1
和xxB0
的協(xié)議說明:當兩個相鄰的結構組件除了后綴“B1”和“B0”之外命名相同時描函,它表示這兩個組件可視為估值為B1<<8 + B0的單個數字崎苗。該單個數字的名字是這些組件減去后綴的名字。這個約定歸納了一個由超過兩個字節(jié)表示的數字的處理方式舀寓。
比如協(xié)議頭中requestId
和contentLength
表示的最大值就是65535
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
unsigned char requestIdB1 = UCHAR_MAX;
unsigned char requestIdB0 = UCHAR_MAX;
printf("%d\n", (requestIdB1 << 8) + requestIdB0); // 65535
}
你可能會想到如果一個消息體長度超過65535怎么辦胆数,則分割為多個相同類型的消息發(fā)送即可。
FCGI_BEGIN_REQUEST 的定義
typedef struct _fcgi_begin_request {
unsigned char roleB1;
unsigned char roleB0;
unsigned char flags;
unsigned char reserved[5];
} fcgi_begin_request;
字段解釋
role
表示Web服務器期望應用扮演的角色互墓。分為三個角色(而我們這里討論的情況一般都是響應器角色)
typedef enum _fcgi_role {
FCGI_RESPONDER = 1,
FCGI_AUTHORIZER = 2,
FCGI_FILTER = 3
} fcgi_role;
而FCGI_BEGIN_REQUEST
中的flags
組件包含一個控制線路關閉的位:flags & FCGI_KEEP_CONN
:如果為0必尼,則應用在對本次請求響應后關閉線路。如果非0篡撵,應用在對本次請求響應后不會關閉線路判莉;Web服務器為線路保持響應性。
FCGI_END_REQUEST 的定義
typedef struct _fcgi_end_request {
unsigned char appStatusB3;
unsigned char appStatusB2;
unsigned char appStatusB1;
unsigned char appStatusB0;
unsigned char protocolStatus;
unsigned char reserved[3];
} fcgi_end_request;
大廠2000道面試題(含答案)
PHP面試題匯總育谬,看完這些面試題助力你面試成功券盅,工資必有20-25K
字段解釋
appStatus
組件是應用級別的狀態(tài)碼。
protocolStatus
組件是協(xié)議級別的狀態(tài)碼膛檀;protocolStatus
的值可能是:
FCGI_REQUEST_COMPLETE:請求的正常結束锰镀。
FCGI_CANT_MPX_CONN:拒絕新請求。這發(fā)生在Web服務器通過一條線路向應用發(fā)送并發(fā)的請求時咖刃,后者被設計為每條線路每次處理一個請求泳炉。
FCGI_OVERLOADED:拒絕新請求。這發(fā)生在應用用完某些資源時嚎杨,例如數據庫連接花鹅。
FCGI_UNKNOWN_ROLE:拒絕新請求。這發(fā)生在Web服務器指定了一個應用不能識別的角色時枫浙。
protocolStatus
在 PHP 中的定義如下
typedef enum _fcgi_protocol_status {
FCGI_REQUEST_COMPLETE = 0,
FCGI_CANT_MPX_CONN = 1,
FCGI_OVERLOADED = 2,
FCGI_UNKNOWN_ROLE = 3
} dcgi_protocol_status;
需要注意dcgi_protocol_status
和fcgi_role
各個元素的值都是 FastCGI 協(xié)議里定義好的刨肃,而非 PHP 自定義的。
消息通訊樣例
為了簡單的表示自脯,消息頭只顯示消息的類型和消息的 id,其他字段都不予以顯示斤富。下面的例子來自于官網
{FCGI_BEGIN_REQUEST, 1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS, 1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_STDIN, 1, "quantity=100&item=3047936"}
{FCGI_STDOUT, 1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
{FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
配合上面各個結構體膏潮,則可以大致想到 FastCGI 響應器的解析和響應流程:
首先讀取消息頭,得到其類型為FCGI_BEGIN_REQUEST
满力,然后解析其消息體焕参,得知其需要的角色就是FCGI_RESPONDER
轻纪,flag
為0,表示請求結束后關閉線路叠纷。然后解析第二段消息刻帚,得知其消息類型為FCGI_PARAMS
,然后直接將消息體里的內容以回車符切割后存入環(huán)境變量涩嚣。與之類似崇众,處理完畢之后,則返回了FCGI_STDOUT
消息體和FCGI_END_REQUEST
消息體供 Web 服務器解析航厚。
PHP 中的 FastCGI 的實現(xiàn)
下面對代碼的解讀筆記只是我個人知識的一個梳理提煉顷歌,如有勘誤,請大家指出幔睬。對不熟悉該代碼的同學來說可能是一個引導眯漩,初步認識,如果覺得很模糊不清晰麻顶,那么還是需要自己逐行去閱讀赦抖。
以php-src/sapi/cgi/cgi_main.c
為例進行分析說明,假設開發(fā)環(huán)境為 unix 環(huán)境辅肾。main 函數中一些變量的定義队萤,以及 sapi 的初始化,我們就不討論在這里討論了宛瞄,只說明關于 FastCGI 相關的內容浮禾。
1.開啟一個 socket 監(jiān)聽服務
fcgi_fd = fcgi_listen(bindpath, 128);
從這里開始監(jiān)聽,而fcgi_listen
函數里面則完成 socket 服務前三步socket
,bind
,listen
份汗。
2.初始化請求對象
為fcgi_request
對象分配內存盈电,綁定監(jiān)聽的 socket 套接字。
fcgi_init_request(&request, fcgi_fd);
整個請求從輸入到返回杯活,都圍繞著fcgi_request
結構體對象在進行匆帚。
typedef struct _fcgi_request {
int listen_socket;
int fd;
int id;
int keep;
int closed;
int in_len;
int in_pad;
fcgi_header *out_hdr;
unsigned char *out_pos;
unsigned char out_buf[1024*8];
unsigned char reserved[sizeof(fcgi_end_request_rec)];
HashTable *env;
} fcgi_request;
3.創(chuàng)建多個 CGI 解析器子進程
這里子進程的個數默認是0,從配置文件中讀取設置到環(huán)境變量旁钧,然后在程序中讀取吸重,然后創(chuàng)建指定數目的子進程來等待處理 Web 服務器的請求。
if (getenv("PHP_FCGI_CHILDREN")) {
char * children_str = getenv("PHP_FCGI_CHILDREN");
children = atoi(children_str);
...
}
do {
pid = fork();
switch (pid) {
case 0:
parent = 0; // 將子進程中的父進程標識改為0歪今,防止循環(huán) fork
/* don't catch our signals */
sigaction(SIGTERM, &old_term, 0);
sigaction(SIGQUIT, &old_quit, 0);
sigaction(SIGINT, &old_int, 0);
break;
case -1:
perror("php (pre-forking)");
exit(1);
break;
default:
/* Fine */
running++;
break;
}
} while (parent && (running < children));
4.在子進程中接收請求
到這里一切都還是 socket 的服務的套路嚎幸。接受請求,然后調用了fcgi_read_request
寄猩。
fcgi_accept_request(&request)
int fcgi_accept_request(fcgi_request *req)
{
int listen_socket = req->listen_socket;
sa_t sa;
socklen_t len = sizeof(sa);
req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);
...
if (req->fd >= 0) {
// 采用多路復用的機制
struct pollfd fds;
int ret;
fds.fd = req->fd;
fds.events = POLLIN;
fds.revents = 0;
do {
errno = 0;
ret = poll(&fds, 1, 5000);
} while (ret < 0 && errno == EINTR);
if (ret > 0 && (fds.revents & POLLIN)) {
break;
}
// 僅僅是關閉 socket 連接嫉晶,不清空 req->env
fcgi_close(req, 1, 0);
}
...
if (fcgi_read_request(req)) {
return req->fd;
}
}
并且把request
放入全局變量sapi_globals.server_context
,這點很重要,方便了在其他地方對請求的調用替废。
SG(server_context) = (void *) &request;
5.讀取數據
下面的代碼刪除一些異常情況的處理箍铭,只顯示了正常情況下執(zhí)行順序。
在fcgi_read_request
中則完成我們在消息通訊樣例中的消息讀取椎镣,而其中很多的len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
操作诈火,已經在前面的FastCGI 消息頭中解釋過了。
這里是解析 FastCGI 協(xié)議的關鍵状答。
static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count)
{
int ret;
size_t n = 0;
do {
errno = 0;
ret = read(req->fd, ((char*)buf)+n, count-n);
n += ret;
} while (n != count);
return n;
}
static int fcgi_read_request(fcgi_request *req)
{
...
if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
return 0;
}
len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
padding = hdr.paddingLength;
req->id = (hdr.requestIdB1 << 8) + hdr.requestIdB0;
if (hdr.type == FCGI_BEGIN_REQUEST && len == sizeof(fcgi_begin_request)) {
char *val;
if (safe_read(req, buf, len+padding) != len+padding) {
return 0;
}
req->keep = (((fcgi_begin_request*)buf)->flags & FCGI_KEEP_CONN);
switch ((((fcgi_begin_request*)buf)->roleB1 << 8) + ((fcgi_begin_request*)buf)->roleB0) {
case FCGI_RESPONDER:
val = estrdup("RESPONDER");
zend_hash_update(req->env, "FCGI_ROLE", sizeof("FCGI_ROLE"), &val, sizeof(char*), NULL);
break;
...
default:
return 0;
}
if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
return 0;
}
len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
padding = hdr.paddingLength;
while (hdr.type == FCGI_PARAMS && len > 0) {
if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
req->keep = 0;
return 0;
}
len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
padding = hdr.paddingLength;
}
...
}
}
6.執(zhí)行腳本
假設此次請求為PHP_MODE_STANDARD
則會調用php_execute_script
執(zhí)行PHP文件冷守。這里就不展開了。
7.結束請求
fcgi_finish_request(&request, 1);
int fcgi_finish_request(fcgi_request *req, int force_close)
{
int ret = 1;
if (req->fd >= 0) {
if (!req->closed) {
ret = fcgi_flush(req, 1);
req->closed = 1;
}
fcgi_close(req, force_close, 1);
}
return ret;
}
在fcgi_finish_request
中調用fcgi_flush
剪况,fcgi_flush
中封裝一個FCGI_END_REQUEST
消息體教沾,再通過safe_write
寫入 socket 連接的客戶端描述符。
8.標準輸入標準輸出的處理
標準輸入和標準輸出在上面沒有一起討論译断,實際在cgi_sapi_module
結構體中有定義授翻,但是cgi_sapi_module
這個sapi_module_struct
結構體與其他代碼耦合太多,我自己也沒深入的理解孙咪,這里簡單做下比較堪唐,希望其他網友予以指點、補充翎蹈。
cgi_sapi_module
中定義了sapi_cgi_read_post
來處理POST數據的讀取.
while (read_bytes < count_bytes) {
fcgi_request *request = (fcgi_request*) SG(server_context);
tmp_read_bytes = fcgi_read(request, buffer + read_bytes, count_bytes - read_bytes);
read_bytes += tmp_read_bytes;
}
在fcgi_read
中則對FCGI_STDIN
的數據進行讀取淮菠。
同時cgi_sapi_module
中定義了sapi_cgibin_ub_write
來接管輸出處理,而其中又調用了sapi_cgibin_single_write
荤堪,最后實現(xiàn)了FCGI_STDOUT
FastCGI 數據包的封裝.
fcgi_write(request, FCGI_STDOUT, str, str_length);
寫在最后
把 FastCGI 的知識學習理解的過程做了這樣一篇筆記合陵,把自己理解的內容(自我認為)有條理地寫出來,能夠讓別人比較容易看明白也是一件不挺不容易的事澄阳。同時也讓自己對這個知識點的理解又深入了一層拥知。對 PHP 代碼學習理解中還有很多困惑的地方還需要我自己后期慢慢消化和理解。
本文都是自己的一些理解碎赢,水平有限低剔,如有勘誤,希望大家予以指正肮塞。
喜歡我的文章就關注我吧襟齿,持續(xù)更新中.....
以上內容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸枕赵,業(yè)務代碼寫多了沒有方向感猜欺,不知道該從那里入手去提升,對此我整理了一些資料拷窜,包括但不限于:分布式架構开皿、高可擴展钓试、高性能、高并發(fā)副瀑、服務器性能調優(yōu)、TP6恋谭,laravel糠睡,YII2,Redis疚颊,Swoole狈孔、Swoft、Kafka材义、Mysql優(yōu)化均抽、shell腳本、Docker其掂、微服務油挥、Nginx等多個知識點高級進階干貨需要的可以免費分享給大家,需要的可以點擊進入暗號:知乎款熬。