用C寫一個web服務器(一) 基礎功能

前言

C 語言是一門很基礎的語言,程序員們對它推崇備至,雖然它是個人入門語言,但大學的 C 語言知道早已經還給了老師,C 的使用能夠說是從頭學起。javascript

以前一直在讀書,看了《C Primer Plus》、《APUE》、《UNP》,第一本看完以後雖然對 C 的語法有了大概的瞭解,但是要說應用,還差得很遠;後兩本算是咬着牙翻完的,應用更不敢說,只是對概念有了基本的認識。php

咱們都知道,學一門語言,只看不寫,很容易出現眼高手低,寫代碼無處下手的狀況,因而終於在下班和週末擠出時間,準備寫一個小項目。正好最近在看 nginx 服務器與 php sapi 相關的知識,因而考慮以 nginx 的思想,寫一個相似的簡化版 web 服務器。css

項目最終的成果不敢保證,像上次寫的 PHP 框架,在原理通透,技術要點掌握以後只剩下功能完善和代碼堆疊,也就沒有繼續下去的慾望了,因而太監了。。。 可是跟着學習和理解一遍必定會有很大收穫,這點是能保證的。 另一直寫同一系列的東西會讓我有一種負擔感,並且偏底層的東西也須要不少時間去學習,這一系列可能會間隔更新,歡迎關注。html

最後附上項目 GitHub 地址:請點我java


服務器架構

目標架構

以 nginx 的思想來考慮本服務器架構,初步考慮以下圖:python

固然 php 進程也能夠替換爲其餘的腳本語言,能夠更改源碼中的 command 變量實現。nginx

服務器有一個 master 進程,其有多個子進程爲 worker 進程,master 進程受理客戶端的請求,而後分發給 worker 進程,worker 進程處理 http 頭信息後將參數傳遞給 php 進程處理後,將結果返回到上層,再響應給客戶端。git

也考慮過使用 php-fpm 的 worker 進程池方式,那樣的話 php-fpm 進程也要仿寫了,目前還不熟悉其內部構造,若是能夠簡單化,天然向其靠攏。目前對 PHP 的 SAPI 接口不熟,瞭解一下再考慮。程序員

當前狀態

當前狀態的服務器還極其簡單,總結下來有如下地方待優化:github

  • 當前仍是單進程,須要改爲多進程,最終爲 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 的用法還不太熟悉,沒用指針、結構等華麗操做,光簡單的實現就花了我很久。可能代碼路子也會有點野,但願有路過的大神能隨手提點一二;

服務器相關的知識很深,每個優化點須要紮實的基礎知識來鞏固,可能我學到的也只是皮毛,文章不免有錯漏處,若是發現,煩請指出。

若是您以爲本文對您有幫助,能夠點擊下面的 推薦 支持一下我。博客一直在更新,歡迎 關注

相關文章
相關標籤/搜索