如何用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 響應信息再將結果返回給客戶端。

下面介紹各個功能的實現:

功能實現

socket系列方法

在介紹函數之間先用一張圖來介紹一次 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(),可以自行選用。

另外函數參數釋義和要點,都被我註釋在代碼中瞭,感興趣的可以拉下來看一下,這些在網上也多有介紹,這裡不再贅述。

服務器與 PHP cli 交互

然後是 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服務器之基礎功能的詳細內容,更多關於用C寫一個web服務器之基礎功能的資料請關註WalkonNet其它相關文章!

推薦閱讀: