Linux下Web伺服器開發

2puT發表於2016-09-07

程式碼下載地址:http://download.csdn.net/detail/u010959074/9572149

以下是專案介紹。

學習提示:

1. 桌面環境中動手練習,若環境不流暢可選擇WebIDE或字元介面。

2. 在教程下方課程問答中提出問題,或共享桌面尋求遠端幫助。

3. 在教程下方實驗報告中完成作業,記錄心得。公開報告可以獲得大家點評。

4. 我的程式碼庫中用GIT提交你的實驗程式碼。

Web伺服器

The way to learn a programming language is to write programs.

這門專案訓練營最大的價值是實驗樓和教師共同提供的教學服務,目的只有一個:讓你把專案做出來並完全理解。所以請對自己負責,提出你遇到的任何問題,完成專案,你所花費的時間才有價值!

一、實驗說明

歡迎參加C++語言經典專案實戰訓練營,在四周的時間裡我們將一起完成四個小型及中型C++語言專案,涉及到C++語言技術的多方面。為了你能夠有所收穫,學習過程中務必注意:

進度很重要:必須跟上每週的進度,專案,問答。我們會認真對待每一位參與訓練營的同學,請你不要因為困難半途而廢。

問答很重要:遇到知識難點請多多提問,這是你的權利更是對自己負責的義務。

實踐很重要:完成每週專案,解決專案中出現的一切問題。專案都提供完整的程式碼,但僅供學習中參考,應按照實驗文件的思路自己實現,請勿直接拷貝程式碼。

實驗報告很重要:詳細記錄你完成專案任務和解決問題的思路。

反饋很重要:請務必將你對課程的建議告訴我們,不同於視訊,我們的文件可以很快更新升級以滿足你的學習需求。

程式碼練習說明

實驗樓環境使用GCC/G++ 4.8.4 編譯環境。C++程式碼採用C++11標準。編寫可以使用vim或gedit,並使用Git進行程式碼管理,實驗報告需要以Markdown格式編寫並公開。

這些技術都是很多程式設計師工作中天天用到的,如果你對其中任何知識不熟悉,可以先學習課程:

Linux基礎入門

Vim編輯器

Git與實驗樓程式碼庫

Markdown與實驗報告

如果需要儲存程式碼到我的程式碼庫,則需要用到git,目前實驗樓的程式碼庫可以在實驗環境或者你自己的電腦上提交,請務必注意git push時輸入的使用者名稱是你登陸實驗樓的郵箱(不是你程式碼庫使用者名稱),如果你是第三方登陸的,則需要先更新郵箱及登入密碼後才可以在實驗樓外部使用。

程式編寫與編譯最基本的方法(#標誌後為註釋):

# 開啟桌面上的Xfce終端並執行後續命令# 進入到/home/shiyanlou目錄cd /home/shiyanlou# 建立並開啟原始檔hello.cpp

gedit hello.cpp# 在gedit(類似windows上的記事本)中輸入程式碼# 儲存退出# 編譯hello.cpp

g++ -std=c++11 hello.cpp -o hello# 執行生成的二進位制檔案

./hello

二、專案簡介

1. 介紹

本實驗使用 C++ 實現一個Web伺服器。

這個專案會學習C++網路開發,kqueue IO複用機制,熟悉Linux下的C++程式編譯方法,Makefile編寫。

本節實驗專案比較複雜,提供的示例程式碼接近3000行,但程式碼邏輯簡單,難點在kqueue IO複用機制的理解,儘量獨立實現,遇到難點可以參考示例程式碼或到實驗樓問答中提問。

2. 專案需求

編寫一個Web伺服器,程式具備的基本功能:

支援多個客戶端連線

支援HTTP協議中的GETHEADOPTION三個方法

支援通過瀏覽器訪問多種型別的檔案資源

支援多虛擬主機,每個虛擬主機可以配置獨立的資源池

3. 知識點

本實驗實踐的知識點包括:

C++ 物件導向程式設計

基本的Makefile

C++ 網路程式設計

kqueue IO複用機制

HTTP 協議基礎

4. 專案效果圖

程式執行的截圖如下所示:

5. 專案完整程式碼

專案完整程式碼可以通過wget下載獲得:

# 下載程式程式碼wget http://labfile.oss.aliyuncs.com/courses/454/webserver.zip

# 解壓程式碼

unzip webserver.zip

# 進入程式碼資料夾檢視

cd webserver

三、程式設計與實現

3.1 需求分析

大家平時上網時,瀏覽器中顯示的HTML網頁都是來源自Web伺服器。本專案通過一個例項介紹如何實現一個簡單的Web伺服器。

本專案中實現的Web伺服器需要支援下面幾個功能:

支援多個客戶端連線

支援HTTP協議中的GETHEADOPTION三個方法

支援通過瀏覽器訪問多種型別的檔案資源

支援多虛擬主機,每個虛擬主機可以配置獨立的資源池

實現具備這些基本功能的Web伺服器,我們首先需要了解HTTP協議以及IO複用機制,在上一節的專案中我們已經學習了一種IO複用方法epoll,本專案採用kqueue機制,與epoll非常類似。

3.2 HTTP協議

HTTP協議介紹的好文章非常多,這裡只列出專案中需要的知識點,推薦大家系統學習HTTP協議,請閱讀:

HTTP協議詳解

HTTP協議的一個主要特點就是客戶端伺服器模式,在上一個專案中我們有過介紹,基於socket的連線過程,如果大家沒有印象可以去複習下。過程如下:

模型如下:

解釋如下:

伺服器端:

socket()建立監聽Socket

bind()繫結伺服器埠

listen()監聽客戶端連線

accept()接受連線

recv/send接收及傳送資料

close()關閉socket

客戶端:

socket()建立監聽Socket

connect()連線伺服器

recv/send接收及傳送資料

close()關閉socket

其實實現的Web伺服器是一個支援HTTP協議的TCP伺服器。所以伺服器啟動過程仍然與上述過程相同,listen後等待客戶端的連線。連線後開始進行HTTP協議的通訊,通過請求與響應來建立Web訪問。

HTTP的請求

HTTP請求由客戶端發給伺服器端,分三部分組成,分別是:請求行、訊息報頭、請求正文。

請求方法(所有方法全為大寫)有多種,各個方法的解釋如下:

GET 請求獲取Request-URI所標識的資源

POST Request-URI所標識的資源後附加新的資料

HEAD 請求獲取由Request-URI所標識的資源的響應訊息報頭

PUT 請求伺服器儲存一個資源,並用Request-URI作為其標識

DELETE 請求伺服器刪除Request-URI所標識的資源

TRACE 請求伺服器回送收到的請求資訊,主要用於測試或診斷

CONNECT 保留將來使用

OPTIONS 請求查詢伺服器的效能,或者查詢與資源相關的選項和需求

專案裡暫時實現了GET/HEAD/OPTION的處理,有興趣的可以根據這三個處理函式的示例實現其他的。

HTTP的響應

在接收和解釋請求訊息後,伺服器返回一個HTTP響應訊息。HTTP響應也是由三個部分組成,分別是:狀態行、訊息報頭、響應正文。

狀態行格式:HTTP-Version Status-Code Reason-Phrase CRLF,其中,HTTP-Version表示伺服器HTTP協議的版本;Status-Code表示伺服器發回的響應狀態程式碼;Reason-Phrase表示狀態程式碼的文字描述。

狀態程式碼有三位數字組成,第一個數字定義了響應的類別,且有五種可能取值:

1xx:指示資訊--表示請求已接收,繼續處理

2xx:成功--表示請求已被成功接收、理解、接受

3xx:重定向--要完成請求必須進行更進一步的操作

4xx:客戶端錯誤--請求有語法錯誤或請求無法實現

5xx:伺服器端錯誤--伺服器未能實現合法的請求

當客戶端與Web伺服器建立連線後就會通過傳送請求接收伺服器響應來進行通訊。而伺服器端如果要處理多個客戶端的請求,可以通過epollkqueueIO機制來獲得讀寫的訊息通知,本專案採用kqueue實現。

3.4 kqueue 機制

kqueueepoll非常相似,最初是2000Jonathan LemonFreeBSD系統上開發的一個高效能的事件通知介面。註冊一批socket描述符到kqueue以後,當其中的描述符狀態發生變化時,kqueue將一次性通知應用程式哪些描述符可讀、可寫或出錯了。

如果你已經理解了聊天室專案中介紹的epoll,那kqueue就很好理解,kqueue提供kqueue()kevent()兩個系統呼叫和struct kevent結構。

kqueue機制詳細介紹,推薦大家閱讀下面的理論文章,雖然講的是FreeBSD上的開發但在Linux上完全適用:

使用kqueueFreeBSD上開發高效能應用伺服器

kqueue的介面包括 kqueue()kevent()兩個系統呼叫和struct kevent結構:

kqueue() 生成一個核心事件佇列,返回該佇列的檔案描述索。其它 API通過該描述符操作這個kqueue

kevent() 提供向核心註冊 /反註冊事件和返回就緒事件或錯誤事件。

struct kevent 就是kevent()操作的最基本的事件結構。

 struct kevent {

     uintptr_t ident;       /* 事件 ID */ 

     short     filter;       /* 事件過濾器 */ 

     u_short   flags;        /* 行為標識 */ 

     u_int     fflags;       /* 過濾器標識值 */ 

     intptr_t  data;         /* 過濾器資料 */ 

     void      *udata;       /* 應用透傳資料 */ 

 };

專案中我們會用到的內容包括事件過濾器,可以將 kqueue filter 看作事件。核心檢測 ident上註冊的filter的狀態,狀態發生了變化,就通知應用程式。kqueue定義了較多的filter,本文只介紹Socket讀寫相關的filter

EVFILT_READTCP監聽socket,如果在完成的連線佇列(已收三次握手最後一個ACK)中有資料,此事件將被通知。收到該通知的應用一般呼叫accept(),且可通過data獲得完成佇列的節點個數。 流或資料包socket,當協議棧的socket層接收緩衝區有資料時,該事件會被通知,並且data被設定成可讀資料的位元組數。

EVFILT_WRIT:當 socket層的寫入緩衝區可寫入時,該事件將被通知;data指示目前緩衝區有多少位元組空閒空間。

行為標誌flags

EV_ADD:指示加入事件到 kqueue

EV_DELETE:指示將傳入的事件從 kqueue中移除

過濾器標識值:

EV_ENABLE:過濾器事件可用,註冊一個事件時,預設是可用的。

EV_DISABLE:過濾器事件不可用,當內部描述可讀或可寫時,將不通知應用程式。

這些引數都會在Web伺服器實現中使用。當socket有連線或讀寫資料事件時,伺服器讀取客戶端請求並將響應寫回到客戶端。kqueue充當的是事件觸發的中間層。

為了我們可以使用kqueue,首先要在系統中安裝libkqueue

sudo apt-get update

sudo apt-get install libkqueue-dev

編譯連結時也要在g++後新增-lkqueue才可以正常連結。

3.5 程式設計

根據上面的需求分析,設計所需的類。

首先需要一個HTTPServer類,用來提供伺服器的啟動關閉主迴圈等支援。HTTPServer物件在程式中唯一。

其次需要Client客戶端類,這個類用來儲存客戶端的必要資訊。HTTPServer物件中需要包含多個當前連線的Client物件。

再次,對於ClientHTTPServer之間的通訊內容需要設定HTTPRequestHTTPResponse類,而這兩個類具備很多通用的資訊,設計二者的基類為HTTPMessage類。

最後HTTPServer需要能夠返回伺服器資源給客戶端,因此需要資源類Resource(檔案,圖片等檔案)以及包含資源的資源池ResourceHost。這裡可以為不同的虛擬主機設定不同的ResourceHost

上面的幾個類是Web伺服器構成的主體,其中HTTPRequestHTTPResponseHTTPMessage三個類都是用來儲存訊息, 而客戶端與伺服器之間的訊息都是以佇列的方式進行讀取和寫入的,因此在HTTPMessage的上一層,我們新增一個新的基類ByteBuffer來實現 基礎位元組流的儲存和讀寫處理。

綜上專案中實現下面的類:

ByteBuffer:定義一個快取佇列用來儲存資料

HTTPMessage:繼承ByteBuffer,增加HTTP協議特有的資訊。

HTTPRequestHTTP請求類,繼承HTTPMessage

HTTPResponseHTTP響應類,繼承HTTPMessage

Resource:資源類,HTTP響應訊息中的檔案資源資料

ResourceHost:資源池,用來將伺服器上的檔案載入到Resource物件

Client:客戶端類,用來儲存客戶端的必要資訊

HTTPServer:伺服器類,提供伺服器的啟動關閉主迴圈等支援

3.6 程式碼結構

根據上述細化需求,我們先建立必要的程式檔案。

首先為要實現的程式命名為webserver:

# 進入到程式碼庫自動生成的目錄# 為了方便git提交程式碼請先開通程式碼庫

cd /home/shiyanlou/Code/shiyanlou_cs454

# 建立程式碼目錄mkdir webserver

cd webserver

# 為每個需要實現的類建立一個.h和一個.cpp檔案# 並將原始檔都放在src/目錄下mkdir src# touch src/XXX.h src/XXX.cpp# 建立Makefile

touch Makefile

# 建立示例目錄,該目錄可以作為Web伺服器的資源池mkdir bin/mkdir bin/htdoc

touch bin/htdoc/test.html

本專案參考程式碼基於https://github.com/RamseyK/httpserver進行實現。每個標頭檔案中的函式都進行了標註,核心模組HTTPServer.cpp也會進行詳細介紹。其他cpp檔案由於內容相對簡單,直接使用原作者的英文註釋,如果有任何不清楚的問題可以隨時在實驗樓問答提問。

下面我們將開始實現需要的類,建議大家按照以下步驟先自己實現,最後再對專案參考程式碼進行對照學習。

3.7 ByteBuffer

ByteBuffer 定義一個快取佇列用來儲存HTTP訪問中的請求及返回資料,這個快取佇列的作用相當於CPU和硬碟之間的記憶體,用來存放Web伺服器和客戶端之間通訊的資料,需要具備下面的功能:

從佇列頭部讀取資料

向佇列尾部寫入新的資料

當佇列儲存區域滿的時候可以自動擴充

支援隨機讀取和寫入多種資料型別到指定位置(使用模板實現)

支援清空,克隆,對比,擴容

支援遍歷佇列查詢指定的資料

ByteBuffer的核心介面如下:

class ByteBuffer {

    // 讀寫位置索引

    unsigned int rpos, wpos;

 

    // 快取內容使用容器儲存

    std::vector<byte> buf;

 

    // 佇列中剩餘的元素數量

    unsigned int bytesRemaining();

 

    // 清空佇列並重置讀寫索引

    void clear();

 

    // 拷貝當前ByteBuffer物件

    ByteBuffer* clone();

 

    // 對比兩個ByteBuffer物件

    bool equals(ByteBuffer* other);

 

    // 設定儲存空間大小

    void resize(unsigned int newSize);

 

    // 返回儲存空間大小

    unsigned int size();

 

    // 讀取佇列頭部的資料但不移動rpos

    byte peek();

    // 讀取佇列頭部的資料同時移動rpos

    byte get();

    // 讀取指定位置的資料

    byte get(unsigned int index);

    // 讀取佇列頭部指定長度的資料到buf中

    void getBytes(byte* buf, unsigned int len);

    // 讀取指定資料型別的元素:char,double,float,int,long,short

    char getChar();

    char getChar(unsigned int index);

 

    // 將src寫入佇列

    void put(ByteBuffer* src);

    // 將b寫入佇列

    void put(byte b);

    // 將b寫入佇列指定位置

    void put(byte b, unsigned int index);

    // 將長度為len的快取b寫入佇列頭部

    void putBytes(byte* b, unsigned int len);

    // 將長度為len的快取b寫入佇列指定位置

    void putBytes(byte* b, unsigned int len, unsigned int index);

    // 寫入指定資料型別的元素:char,double,float,int,long,short

    void putChar(char value);

    void putChar(char value, unsigned int index);     

 

}

看上去非常繁瑣,但可以使用模板實現基礎函式,大部分函式都能夠通過呼叫函式模板的方式簡化實現內容。

3.8 HTTPMessage

繼承ByteBuffer實現HTTPMessage類。這個類需要包含:

HTTP頭部

HTTP版本號

訊息資料:包括ByteBuffer中的資料及額外的資料(響應訊息中返回的資源及請求訊息中額外的引數)

HTTP訊息解析函式,根據HTTP協議的規範對訊息頭部和內容進行解析

HTTP訊息建立函式,建立符合HTTP協議要求的HTTP訊息

HTTP頭部管理函式,新增HTTP頭部資訊

這個類比較簡單,只需要把上述幾個函式依次實現,核心函式包括:

class HTTPMessage : public ByteBuffer {private:

 

    // 訊息頭部資訊

    std::map<std::string, std::string> *headers;

protected:

    // HTTP版本號

    std::string version;

 

    // 訊息資料

    // 響應訊息中表示資源,請求訊息中表示額外的引數

    byte* data;

public:

 

    // 建立訊息

    virtual byte* create() = 0;

 

    // 解析訊息

    virtual bool parse() = 0;

 

};

3.9 HTTPRequest

HTTP請求類繼承HTTPMessage,需要實現以下資料項及函式功能:

請求的方法:GET/HEAD/OPTION

請求的URI

實現HTTPMessage的建立和解析虛擬函式

核心成員宣告:

// HTTPRequest:HTTP請求訊息,從客戶端發給伺服器的訊息class HTTPRequest : public HTTPMessage {private:

 

    // 請求的方法型別

    int method;

 

    // 請求的URL

    std::string requestUri;

protected:

 

    // 初始化訊息

    virtual void init();

public:

    virtual byte *create();

    virtual bool parse();

 

    // 輔助函式

    // 方法字串與數字相互轉換

    int methodStrToInt(std::string name);

    std::string methodIntToStr(unsigned int mid);

 

};

3.10 HTTPResponse

HTTP響應類繼承HTTPMessage,需要實現以下資料項及函式功能:

響應狀態碼和資訊

實現HTTPMessage的建立和解析虛擬函式

核心成員宣告:

// HTTPResponse:HTTP響應訊息,從伺服器發給客戶端的訊息class HTTPResponse : public HTTPMessage {private:    

    // 響應狀態碼和資訊

    int status;

    std::string reason;

 

protected:

    virtual void init();

public:

    virtual byte* create();

    virtual bool parse();

 

};

3.11 Resource

HTTP響應訊息中的Resource除了包含檔案資料以外,最重要的特點是具備mimeType標識,MIME (Multipurpose Internet Mail Extensions) 是描述訊息內容型別的因特網標準。MIME訊息能包含文字、影象、音訊、視訊以及其他應用程式專用的資料。

Resource類中需要的成員:

資源資料

資料大小

資料MIME型別

資源在伺服器上儲存的路徑

核心成員宣告如下:

// Resource:HTTP響應訊息中的檔案資源資料class Resource {

private:

 

    // 檔案資源

    byte* data;

    // 資源大小

    unsigned int size;

    // 資源型別

    std::string mimeType;

    // 資源在伺服器上儲存的路徑

    std::string location;

    // 是否是目錄型別

    bool directory;

 

};

3.11 ResourceHost

ResourceHost是一個Resource的管理元件,可以通過一個目錄來構建。需要支援的成員包括:

基礎目錄,其他的訪問路徑都是該目錄下的相對路徑

根據訪問地址URI獲取Resource資源物件

將資源寫入到檔案系統,用來支援上傳操作

Resource中的MimeType的獲取可以通過判斷副檔名來實現,需要構建副檔名和MimeType的對映表。

我們需要定義一個資料結構包含所有的MIME資訊,示例程式碼中採用巨集的方式從檔案MimeTypes.inc中讀取:

// 構建副檔名與MimeType對應的字典,從檔案MimeTypes.inc中獲取

std::unordered_map<std::string, std::string> mimeMap = {

    #define STR_PAIR(K,V) std::pair<std::string, std::string>(K,V)

    #include "MimeTypes.inc"

};

MimeTypes.inc中的內容是若干行:

STR_PAIR("bmp", "image/bmp"),

STR_PAIR("book", "application/book"),

...

核心成員宣告如下:

// ResourceHost:資源池,用來將伺服器上的檔案載入到Resource物件class ResourceHost {private:

    // 基礎目錄,其他的訪問路徑都是該目錄下的相對路徑

    std::string baseDiskPath;

 

    // 構建副檔名與MimeType對應的字典,從檔案MimeTypes.inc中獲取

    std::unordered_map<std::string, std::string> mimeMap = {

        #define STR_PAIR(K,V) std::pair<std::string, std::string>(K,V)

        #include "MimeTypes.inc"

    };

public:

    // 將資源寫入到檔案系統

    void putResource(Resource* res, bool writeToDisk);

 

    // 根據URI獲取Resource資源物件

    Resource* getResource(std::string uri);

};

3.12 Client

客戶端的資料會存放在HTTPServer中的一個對映表中,用客戶端連線的socketkey。每個客戶端物件都應該包含客戶端的socketsockaddr_in地址資訊,資料傳送佇列。

客戶端的操作需要支援新增新的資料到傳送佇列,對傳送佇列進行出隊,清空等操作。

傳送佇列使用std::queue,每個儲存單元需要單獨設計要包含傳送的資料及資料偏移,並要增加是否需要在傳送完成後斷開連線的標誌。傳送儲存單元命名為SendQueueItem類,在Client類宣告中需要使用。

核心成員宣告如下:

// HTTP客戶端// class Client {

 

    // 連線的socket

    int socketDesc;

 

    // 地址資訊

    sockaddr_in clientAddr;

 

    // 資料傳送佇列

    std::queue<SendQueueItem*> sendQueue;

public:

 

    // 傳送佇列操作

    // 新增新的資料到傳送佇列

    void addToSendQueue(SendQueueItem* item);

 

    // 傳送佇列長度

    unsigned int sendQueueSize();

 

    // 獲取傳送佇列中第一個元素

    SendQueueItem* nextInSendQueue();

 

    // 出隊操作,刪除第一個元素

    void dequeueFromSendQueue();

 

    // 清空傳送佇列

    void clearSendQueue();

};

3.13 HTTPServer

這是整個專案的核心,具有最複雜的類和實現過程。

首先分析HTTPServer需要具備的資料成員,剛才已經提到Client客戶端列表是必須的,此外還需要包含:

監聽的埠號

監聽的socket

伺服器地址資訊

kqueue 描述符

kevent 事件佇列

客戶端字典,對映客戶端的socket和客戶端物件

資源主機及檔案系統列表

虛擬主機,對映請求的地址到不同的ResourceHost

根據需求中HTTPServer需要支援下面幾大類功能:

處理客戶端連線

處理客戶端請求

傳送響應訊息給客戶端

伺服器管理

每個功能集合中又可以細分出不同的功能。

處理客戶端連線

接受連線:accept接受連線並將新的socket新增到kqueue中,建立客戶端物件新增到客戶端列表中

獲取客戶端物件:根據socket查詢客戶端列表返回客戶端物件

斷開連線:清理kqueue中的socket,關閉socket,清理客戶端列表中的物件

處理客戶端讀取事件:recv()接收客戶端資料並構造HTTPRequest物件,交給handleRequest()函式處理。

處理客戶端寫入事件:獲取傳送佇列中的SendQueueItemsend()傳送給客戶端

處理客戶端請求

處理請求:對請求方法進行分類分別交給不同的處理函式

處理GET/HEAD/OPTIONS請求:構造HTTPResponse,呼叫sendResponse傳送給客戶端

傳送響應訊息給客戶端

傳送響應狀態碼:構造HTTPResponse,呼叫sendResponse傳送給客戶端

傳送響應訊息:將HTTPResponse封裝後新增到客戶端的傳送佇列

伺服器管理

啟動:包含各種資料和資源的初始化,監聽socket的建立,繫結,kqueue的建立和初始化。

停止:釋放資源,清理客戶端列表,清理kqueue,清理socket

主迴圈:進入事件迴圈,等待kqueue事件觸發,有事件觸發後需要先判斷是新的客戶端連線還是客戶端讀寫事件

核心成員宣告如下:

// HTTPServer:Web伺服器類,提供伺服器的建立,啟動,停止等管理操作class HTTPServer {    

    // 監聽的埠號

    int listenPort;

 

    // 監聽的socket

    int listenSocket;

 

    // 伺服器地址資訊

    struct sockaddr_in serverAddr;

 

    // kqueue 描述符

    int kqfd;

 

    // kevent佇列

    struct kevent evList[QUEUE_SIZE];

 

    // 客戶端字典,對映客戶端的socket和客戶端物件

    std::unordered_map<int, Client*> clientMap;

 

    // 資源主機及檔案系統列表

    std::vector<ResourceHost*> hostList;

 

    // 虛擬主機,對映請求的地址到不同的ResourceHost

    std::unordered_map<std::string, ResourceHost*> vhosts;

 

    // 處理客戶端連線

    void acceptConnection();

    void disconnectClient(Client* cl, bool mapErase = true);

    void readClient(Client* cl, int data_len);

    bool writeClient(Client* cl, int avail_bytes);

 

    // 處理客戶端請求

    void handleRequest(Client* cl, HTTPRequest* req);

    void handleGet(Client* cl, HTTPRequest* req, ResourceHost* resHost);

    void handleOptions(Client* cl, HTTPRequest* req);

 

    // 傳送響應訊息給客戶端

    void sendStatusResponse(Client* cl, int status, std::string msg = "");

    void sendResponse(Client* cl, HTTPResponse* resp, bool disconnect);

 

    // 啟動及停止Web伺服器

    bool start(int port);

    void stop();

 

    // Web伺服器主迴圈

    void process();

};

HTTPServer啟動時我們預設設定當前路徑下的htdoc資料夾為ResourceHost,並新增到HTTPServer物件中。而htdoc下我們新增一個test.html檔案,作為可以訪問的Resourcetest.html中的內容可以很簡單,例如:

<html><head><title>shiyanlou demo site</title></head>

<body>

Hello Shiyanlou!<br /></body></html>

這個示例頁面可以在後面的測試中通過訪問localhost:8080/test.html訪問到。

3.14 主函式

主函式中實現包括兩部分內容:

註冊各種訊號處理函式,能夠讓Web服務中止時可正常釋放資源

建立HTTPServer物件,依次啟動,進入主迴圈

需要處理的包括中斷訊號SIGABRTSIGINTSIGTERM,這些訊號發生時要將HTTPServer的停止標誌置為True,下次迴圈時就可以退出。SIGPIPE訊號需要被忽略,避免"Broken pipe"出現。

示例程式碼片段:

// 忽視 SIGPIPE "Broken pipe" 訊號

signal(SIGPIPE, handleSigPipe);

// 註冊中斷處理

signal(SIGABRT, &handleTermSig);

signal(SIGINT, &handleTermSig);

signal(SIGTERM, &handleTermSig);

// 建立並啟動HTTPServer例項

svr = new HTTPServer();

svr->start(8080);

// 進入主迴圈

svr->process();

// 停止伺服器

svr->stop();

delete svr;

3. 編譯及執行

將你完成的檔案儲存為/home/shiyanlou/Code/shiyanlou_cs454/webserver,在同樣的目錄下我們編輯Makefile檔案:

cd /home/shiyanlou/Code/shiyanlou_cs454/webserver

vim Makefile

Makefile檔案裡的內容:

CC := g++SRCDIR := srcBINDIR := binBUILDDIR := buildTARGET := httpserverUNAME := $(shell uname)

# Debug FlagsDEBUGFLAGS := -g3 -O0 -WallRTCHECKS := -fmudflap -fstack-check -gnato

# Production FlagsPRODFLAGS := -Wall -O2

CFLAGS := -std=c++11 -Iinclude/ $(DEBUGFLAGS)LINK := -lpthread -lkqueue $(DEBUGFLAGS)

SRCEXT := cppSOURCES := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT))OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.o))

$(TARGET): $(OBJECTS)

    @echo " Linking..."$(LINK); $(CC) $^ -o $(BINDIR)/$(TARGET) $(LINK)

$(BUILDDIR)/%.o: $(SRCDIR)/%.$(SRCEXT)

    @mkdir -p $(BUILDDIR)

    @echo " CC $<"; $(CC) $(CFLAGS) -c -o $@ $<

clean:

    @echo " Cleaning..."; rm -r $(BUILDDIR) $(BINDIR)/$(TARGET)*

 

.PHONY: clean

Makefile內容很多,大部分都是變數定義,核心內容只有下面兩行:

@echo " CC $<"; $(CC) $(CFLAGS) -c -o $@ $< 編譯每個CPP檔案,生成.o目標檔案

$(CC) $^ -o $(BINDIR)/$(TARGET) $(LINK) 連結上一步驟生成的所有目標檔案,得到可執行的httpserver檔案

儲存Makefile後,我們只需要在目錄下執行make就可以生成可執行檔案httpserver。

編譯過程截圖:

Makefile會把可執行檔案放到了bin/目錄下,因為bin/目錄下的htdoc/已經寫在程式碼中作為預設ResourceHost了,所以測試啟動後的Web伺服器可以訪問htdoc下的檔案。

現在進入執行測試階段,首先啟動服務端:

cd bin/

./httpserver

執行截圖如下:

如果中間有任何問題,需要根據輸出的錯誤資訊查驗下程式碼是否有BUG。歡迎隨時到實驗樓問答提問。

3.8 完整程式碼參考

我們提供本專案完整的程式碼及詳細註釋供參考,由於程式碼比較多,文件中不再貼上,建議大家下載檢視。

# 下載程式程式碼wget http://labfile.oss.aliyuncs.com/courses/454/webserver.zip

# 解壓程式碼

unzip webserver.zip

# 進入程式碼資料夾檢視

cd webserver

本專案參考程式碼基於https://github.com/RamseyK/httpserver修改。程式碼License仍然遵循Apache License, Version 2.0

四、專案擴充套件

本實驗實現了一個Web伺服器程式。基於本課節學習,大家可以在此程式碼基礎上實現擴充套件:

支援更多的HTTP請求方法,例如POST

支援配置檔案,比如配置多個虛擬主機,資源池,埠號等

支援PHP頁面解析,可以加入單獨的模組

五、小結

通過本節實驗的學習,我們開發了Web伺服器程式,學習了C++語言的基本語法及物件導向開發,網路程式開發,HTTP協議及kqueue IO複用機制。

完成專案後可以公開你的實驗報告,並點選實驗報告下方的分享按鈕分享到微博,將會獲得教師點評,同時優秀的實驗報告官微將轉發推薦!

再次提醒,任何問題歡迎到實驗樓問答中提問,老師會及時回答你的困惑。

特別說明:

實驗作業與學習心得請寫在下方實驗報告(支援markdown格式)裡並公開,每週選取優秀報告獎勵IT書籍!

您已經完成本課程所有實驗

 

相關文章