您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關怎么用C寫一個web服務器之基礎功能的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
以 nginx 的思想來考慮本服務器架構,初步考慮如下圖:
當然 php 進程也可以替換為其他的腳本語言,可以更改源碼中的 command 變量實現。
服務器有一個 master 進程,其有多個子進程為 worker 進程,master 進程受理客戶端的請求,然后分發給 worker 進程,worker 進程處理 http 頭信息后將參數傳遞給 php 進程處理后,將結果返回到上層,再響應給客戶端。
也考慮過使用 php-fpm 的 worker 進程池方式,那樣的話 php-fpm 進程也要仿寫了,目前還不熟悉其內部構造,如果可以簡單化,自然向其靠攏。目前對 PHP 的 SAPI 接口不熟,了解一下再考慮。
當前狀態的服務器還極其簡單,總結下來有以下地方待優化:
當前還是單進程,需要改成多進程,最終為 worker 進程池方式;
優化 socket IO 模型,考慮 epoll、事件驅動方式;
只支持 HTTP GET 請求方法,未進行太多的異常處理來定義 http 狀態碼;
與 php 進程的交互方式,考慮如 nginx 使用 unix domain socket 方式。
協議目前只考慮了 http,后續會考慮一些基于 TCP 的協議;
雖然簡單,但服務器已經有基本的功能了:
它監聽本地地址的 8080 端口,將接收到的 http 頭中的 path 信息提出出來交給 php 進程,php 進程將參數信息處理后返回給服務器,服務器拼裝 http 響應信息再將結果返回給客戶端。
下面介紹各個功能的實現:
在介紹函數之間先用一張圖來介紹一次 http 請求中客戶端與服務器之間的交互:
如圖:服務器創建要進行:
1.調用 socket() 創建一個連接;int socket(int domain, int type, int protocol);
2.調用 bind() 給套接字命名,綁定端口;int bind( int socket, const struct sockaddr *address, size_t address_len);
3.調用 listen() 監聽此套接字;int listen(int socket, int backlog);
4.調用 accept() 接受客戶端的連接;int accept(int socket, struct sockaddr *address, size_t *address_len);
5.調用 recv() 接收客戶端的信息;int recv(int s, void *buf, int len, unsigned int flags);
6.調用 send() 將響應信息發送給客戶端;int send(int s, const void * msg, int len, unsigned int falgs);
socket 間的接收和發送信息在 C 中有幾個系列:write() / read() 、send() / recv() 、sendto() / recvfrom()、 sendmsg() / recvmsg(),可以自行選用。
另外函數參數釋義和要點,都被我注釋在代碼中了,感興趣的可以拉下來看一下,這些在網上也多有介紹,這里不再贅述。
然后是 C 進程和 php 進程的交互,考慮到簡單易用,目前在 C 進程中直接執行 php 腳本:
一開始使用 system() 函數: int system(const char *command);
system 函數會 fork 一個子進程,在子進程中以 cli 方式執行 php 腳本,并將錯誤碼或返回值返回。由于其結果類型不可控,編譯時會報一個 warning。而且它將結果返回給父進程時,還會在標準輸出中打印結果,在服務器執行時會拋出異常。
于是找到了另一個方法 popen, FILE * popen(const char * command, const char * type);:
popen 同樣會 fork 一個子進程來執行 command ,然后建立管道連到子進程的標準輸出設備或標準輸入設備,然后返回一個文件指針。隨后進程便可利用此文件指針來讀取子進程的輸出設備或是寫入到子進程的標準輸入設備中。
其 type 參數便是控制連接到子進程的標準輸入還是標準輸出。我們想要子進程的標準輸出,于是傳入 type參數為 字符 “r” (read)。同理,如果想寫入子進程標準輸入的話,可以傳值 “w”(write)。
另外在接收緩沖區內容的時候也出現了一點小意外:由于使用的 fgets() 方法會以換行符\n
為一段的結尾,在接收 php 進程輸出時遇到換行會結束,這里使用了一個中間字符串數組line
來接收每一行的信息,將每一行的信息拼裝到結果中。
代碼如下:
char * execPHP(char *args){ // 這里不能用變長數組,需要給command留下足夠長的空間,以存儲args參數,不然拼接參數時會棧溢出 char command[BUFF_SIZE] = "php /Users/mfhj-dz-001-441/CLionProjects/cproject/tinyServer/index.php "; FILE *fp; static char buff[BUFF_SIZE]; // 聲明靜態變量以返回變量指針地址 char line[BUFF_SIZE]; strcat(command, args); memset(buff, 0, BUFF_SIZE); // 靜態變量會一直保留,這里初始化一下 if((fp = popen(command, "r")) == NULL){ strcpy(buff, "服務器內部錯誤"); }else{ // fgets會在獲取到換行時停止,這里將每一行拼接起來 while (fgets(line, BUFF_SIZE, fp) != NULL){ strcat(buff, line); }; } return buff; }
socket 處于應用層和傳輸層之間的虛擬層,由于設置服務器 socket 協議類型為 TCP,那么 TCP 的握手揮手、數據讀取等步驟對于我們都是透明的。我們拿到的數據即 HTTP 報文,關于 HTTP 報文結構和其字段解釋的文章非常多,這里也不再多提。
首先使用 C 的 strtok() 方法,獲取到 HTTP 頭的第一行,獲取到其 http 方法和 path 信息,將這些信息處理后,再使用 sprintf() 方法拼合 HTTP 響應報文,主要替換了 響應內容長度和響應內容。
感謝各位的閱讀!關于“怎么用C寫一個web服務器之基礎功能”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。