深入HTTP序列化和反序列化
本篇介紹
在上一節已經完成了客戶端和服務端基本的HTTP通信,但是前面的傳遞并沒有完全體現出HTTP的序列化和反序列化,為了更好得理解其工作流程,在本節會以更加具體的方式分析到HTTP序列化和反序列化
本節會在介紹HTTP協議基本結構與基本實現HTTPServer的基礎之上繼續完善HTTP服務器,所以需要有對應的知識作為鋪墊才可以開始本節
基本實現思路
本次實現的HttpServer
類主要完成接收客戶端發送的HTTP請求,也就是說,服務器需要根據客戶端的HTTP請求回復一個HTTP響應,所以必須要有的方法就是處理請求方法,但是前面已經提到過,HTTP的請求屬于結構化的數據,并且這個數據在傳遞給服務器時就已經做了序列化,服務器需要處理的就是將結構化的數據進行反序列化;同樣,服務器處理完畢后還需要發給客戶端,所以此處就需要服務器對處理的結果填充到HTTP響應結構對象中再返回給客戶端,此處就需要進行序列化
基于上面的原因,與前面序列化和反序列化與網絡計算器一樣,需要實現一個協議,包含HttpRequest
和HttpResponse
類,用于處理序列化和反序列化
本次為了更好得理解序列化和反序列化,以HTTP請求為例,首先以請求行、請求報頭和請求體三個整體做序列化和反序列化,接著再深入請求行、請求報頭和請求體中的字段
根據這個兩個階段,需要實現的目標如下:
- 第一階段:打印出反序列化和序列化的結果
- 第二階段:向客戶端返回具體的靜態HTML文件
第一階段
創建HttpRequest
類
根據前面的基本思路,實現HttpRequest
類就需要實現對應的反序列化。因為HTTP請求中帶有三種數據:
- 請求行
- 請求報頭
- 請求體
所以需要定義三個成員分別存放從請求獲取到的內容,所以基本結構如下:
class HttpRequest
{
public:HttpRequest(){}// 反序列化bool deserialize(std::string& in_str){}~HttpRequest(){}
private:std::string _req_line; // 請求行std::vector<std::string> _req_head; // 請求報頭std::string _req_body; // 請求體
};
實現HttpRequest
反序列化接口
在HTTP中的反序列化本質就是根據基本的格式去除掉多余的部分,從而提取出有效的數據放在相應的字段中,所以根據這個思路依次進行提取
注意,前面提到過,HTTP是基于TCP的,而TCP是面向字節流的,這就導致可能服務器接收到的HTTP請求不完整,對此還需要對接收到的HTTP請求進行完整性判斷,但是本次不考慮這一步
截取請求行
首先提取請求行中的數據,根據前面對HTTP請求結構的描述可以知道HTTP請求的請求行以\r\n
結尾,所以只需要找到第一個\r\n
,就說明找到了請求行,這里定義一個成員函數用來處理這個邏輯:
// 獲取請求行
bool parseReqLineFromTotal(std::string& in_str)
{auto pos = in_str.find(default_sep);if(pos == std::string::npos){LOG(LogLevel::WARNING) << "未找到請求行";return false;}// 獲取到請求行_req_line = in_str.substr(0, pos);// 從原始字符串中移除請求頭和第一個分隔符in_str.erase(0, pos + default_sep.size());LOG(LogLevel::DEBUG) << "請求行處理后:" << in_str;return true;
}
這里考慮到后面的請求體也是以\r\n
結尾,所以考慮將該函數更改為更通用的版本:
// 截取以\r\n結尾的數據
bool parseOneLineFromTotal(std::string& in_str, std::string& out_str)
{auto pos = in_str.find(default_sep);if(pos == std::string::npos)return false;// 獲取到截取數據out_str = in_str.substr(0, pos);// 從原始字符串中移除截取的字符串和對應的分隔符in_str.erase(0, pos + default_sep.size());return true;
}
接著完善反序列化接口:
// 反序列化
bool deserialize(std::string& in_str)
{bool getReqLineFlag = parseOneLineFromTotal(in_str, _req_line);if(!getReqLineFlag){LOG(LogLevel::WARNING) << "反序列化獲取請求行失敗";return false;}LOG(LogLevel::INFO) << "截取的請求行為:" << _req_line;// 未完...
}
截取請求報頭
截取請求報頭的方式與請求行非常類似,無非就是需要多次截取,但是需要考慮截取何時結束。根據HTTP請求體的特點,最后一行就是一個空行,即\r\n
,所以可以考慮利用這個空行進行處理,具體思路如下:
因為每一次截取都會截取到分隔符之前的內容,所以可以考慮定義一個變量用于接收請求報頭的結果,那么根據截取一行的函數的邏輯,只有成功找到了\r\n
時才會進行截取,而截取的結果不會包含\r\n
,那么一旦截取的結果是空且找到了\r\n
,就說明找到了最后一行
根據這個思路,將獲取請求報頭數據的邏輯放在一個單獨的函數中,如下:
// 獲取請求報頭
bool getReqHeadFromTotal(std::string &in_str)
{// 保存以\r\n結尾的一行數據std::string oneLine;while (true){bool getOneLineFlag = parseOneLineFromTotal(in_str, oneLine);// 如果getOneLineFlag為true并且oneLine不為空,說明當前行有效,否則代表已經找到了結尾if(getOneLineFlag && !oneLine.empty()){_req_head.push_back(oneLine);}else if(getOneLineFlag && oneLine.empty()){break;}else{return false;}}return true;
}
繼續完善反序列化接口:
// 反序列化
bool deserialize(std::string &in_str)
{// ...bool getReqHeadLine = getReqHeadFromTotal(in_str);if (!getReqHeadLine){LOG(LogLevel::WARNING) << "反序列化獲取請求報頭失敗";return false;}LOG(LogLevel::INFO) << "獲取到的請求行為:";std::for_each(_req_head.begin(), _req_head.end(), [&](int i){std::cout << _req_head[i] << std::endl;});// 未完...
}
截取請求體
因為在前面截取請求行和截取請求報頭時已經修改了HTTP請求字符串,所以剩下的就是請求體,直接賦值即可:
// 反序列化
bool deserialize(std::string &in_str)
{// ...// 獲取請求體_req_body = in_str;return true;
}
創建HttpResponse
類
服務器需要給客戶端返回內容,所以在這之前必須對整個HttpResponse
結構進行序列化。同樣,HTTP響應也有對應的三種數據:
- 請求行
- 請求報頭
- 請求體
所以需要定義三個成員分別存放從請求獲取到的內容,所以基本結構如下:
// HTTP響應
class HttpResponse
{
public: HttpResponse(std::string &rl, std::vector<std::string> &rq, std::string &rb): _resp_line(rl), _resp_head(rq), _resp_body(rb){}HttpResponse(std::string &rl, std::string &rb): _resp_line(rl), _resp_body(rb){}// 序列化bool serialize(std::string &out_str){}~HttpResponse(){}
private:std::string _resp_line; // 響應頭std::vector<std::string> _resp_head; // 響應報頭std::string _resp_body; // 響應體
};
實現HttpResponse
序列化接口
實現serialize
就會比實現deserialize
接口簡單,只需要根據對應的字段加上\r\n
即可,所以基本代碼如下:
// 序列化
bool serialize(std::string &out_str)
{// 給請求行添加\r\n_resp_line += default_sep;out_str += _resp_line;// 給請求報頭的每一個字段加上\r\nstd::for_each(_resp_head.begin(), _resp_head.end(), [&](std::string &str){str += default_sep;out_str += str; });// 添加空行out_str += default_sep;out_str += _resp_body;return true;
}
修改HttpServer
類
只需要改變HttpServer
類中的請求處理函數,但是如果要打印反序列的結果就必須提供對應的接口或者在HttpRequest
類內提供打印函數,本次考慮后者:
void print()
{// 請求行LOG(LogLevel::INFO) << "請求行:" << _req_line;// 請求報頭std::for_each(_req_head.begin(), _req_head.end(), [&](std::string& str){ LOG(LogLevel::INFO) << "請求報頭:" << str; });// 請求體LOG(LogLevel::INFO) << "請求體:" << _req_body;
}
接著修改HttpServer
的handleHttpRequest
函數:
void handleHttpRequest(SockAddrIn sock_addr_in, int ac_socketfd)
{LOG(LogLevel::INFO) << "收到來自:" << sock_addr_in.getIp() << ":" << sock_addr_in.getPort() << "的連接";// 獲取客戶端傳來的HTTP請求base_socket_ptr bs = _tp->getSocketPtr();std::string in_str;bs->recvData(in_str, ac_socketfd);// 反序列化HttpRequest req;req.deserialize(in_str);// 打印反序列結果req.print();// 構建HttpResponse返回std::string line = "HTTP 1.1 200 OK";std::string body = "<h1>Build HttpResponse success</h1>";HttpResponse resp(line, body);std::string out_str;// 序列化resp.serialize(out_str);LOG(LogLevel::INFO) << out_str;bs->sendData(out_str, ac_socketfd);
}
測試
主函數與上一節一樣,測試結果如下:
從上圖可以看到可以成功獲取到HTTP請求結果并且正常回復HTTP響應,第一階段目標完成
第二階段
在第一階段的基礎之上,現在需要對HTTP請求和HTTP響應的每一個字段進行細化,本次不考慮某個字段或者屬性是什么含義,只需要將其進行提取即可
修改HttpRequest
類
提取HTTP請求行中的字段
因為HTTP請求行中的每個字段是根據空格進行分隔的,回憶C/C++的輸入和輸出,默認也是以空白字符進行分隔,所以就可以利用這一點,可以使用C語言的sscanf()
進行讀取,也可以考慮使用C++的stringstream進行
因為需要讀取到每個字段,所以需要對應的成員進行接收,這里就使用三個成員_req_method
、_req_uri
和_req_ver
作為補充成員:
class HttpRequest
{
public:// ...private:std::string _req_method; // 請求方法std::string _req_uri; // 請求資源路徑std::string _req_ver; // HTTP請求版本// ...
};
接著就是實現一個函數用于從req_line
中提取對應的字段填充_req_method
、_req_uri
和_req_ver
三個成員:
=== “sscanf
版本”
// sscanf版本
bool getContentFromReqLine()
{char method[1024] = {0};char uri[1024] = {0};char ver[1024] = {0};sscanf(_req_line.c_str(), "%s%s%s", method, uri, ver);LOG(LogLevel::INFO) << "請求行:" << method << "-" << uri << "-" << ver;_req_method = method;_req_uri = uri;_req_ver = ver;return true;
}
=== “stringstream
版本”
// stringstream版本bool getContentFromReqLine(){std::stringstream ss;// 讀取到stringstream中ss << _req_line;// 輸出到成員中ss >> _req_method >> _req_uri >> _req_ver;LOG(LogLevel::INFO) << "請求行:" << _req_method << "-" << _req_uri << "-" << _req_ver;return true;}
接下來修改deserialize
的邏輯:
bool deserialize(std::string &in_str)
{// 截取請求行bool getReqLineFlag = parseOneLineFromTotal(in_str, _req_line);if (!getReqLineFlag){LOG(LogLevel::WARNING) << "反序列化獲取請求行失敗";return false;}LOG(LogLevel::INFO) << "截取的請求頭為:" << _req_line;// 填充請求行的字段getContentFromReqLine();// ...return true;
}
提取HTTP請求報頭中的字段
前面完成了獲取到HTTP請求報頭中的每一條數據,但是請求報頭實際上是key-value
結構的數據,服務器需要拿到其中的key
以及value
進行后續的處理,所以這里就需要分別取出key
和對應的value
為了存儲對應的key
和value
,可以考慮使用一個哈希表。這里,因為處理每一個每一條報頭數據不需要經過_req_head
過渡,所以可以考慮直接將分割出的字符串傳遞給處理分隔的函數,在該函數中直接將對應的鍵值對添加到哈希表即可
每一個鍵值對字符串以
:
分隔,而不是:
首先完成分割邏輯:
bool getPairFromReqHead(std::string& oneLine)
{// 找到分隔符auto pos = oneLine.find(default_head_sep);// 左側即為keystd::string key = oneLine.substr(0, pos);// 右側即為valuestd::string value = oneLine.substr(pos + default_head_sep.size());// 插入到哈希表中_kv.insert({key, value});return true;
}
接下來修改deserialize
和getReqHeadFromTotal
的邏輯:
=== “getReqHeadFromTotal
”
// 獲取請求報頭
bool getReqHeadFromTotal(std::string &in_str)
{// 保存以\r\n結尾的一行數據std::string oneLine;while (true){bool getOneLineFlag = parseOneLineFromTotal(in_str, oneLine);// 如果getOneLineFlag為true并且oneLine不為空,說明當前行有效,否則代表已經找到了結尾if (getOneLineFlag && !oneLine.empty()){getPairFromReqHead(oneLine);}// ...}return true;
}
=== “deserialize
”
// 反序列化
bool deserialize(std::string &in_str)
{// ...// 截取請求報頭bool getReqHeadLine = getReqHeadFromTotal(in_str);if (!getReqHeadLine){LOG(LogLevel::WARNING) << "反序列化獲取請求報頭失敗";return false;}LOG(LogLevel::INFO) << "獲取到的請求報頭為:";std::for_each(_kv.begin(), _kv.end(), [&](std::pair<std::string, std::string> kv){LOG(LogLevel::INFO) << kv.first << "-" << kv.second;});// ...return true;
}
提取HTTP請求體中的字段
保持和第一階段的處理方式一樣
修改HttpResponse
類
第一階段的HttpResponse
類只是使用一個固定的字符串進行序列化再發送給客戶端,這個做法明顯是不妥的。實際上,對于HTTP響應來說,應該處理下面幾點:
- 允許根據請求內容是否合法自動構建響應狀態碼,并且根據狀態碼自動生成狀態碼描述
- 允許外部添加響應報頭中的屬性
- 根據客戶端請求的內容讀取服務端存在的對應文件并返回給客戶端,沒有時返回404頁面
HTTP狀態碼
根據上面的思路,首先需要處理的就是狀態碼,在介紹HTTP協議基本結構與基本實現HTTPServer一節提到,HTTP協議規定任何客戶端的請求都必須得到響應,而區分響應情況就是通過狀態碼
在HTTP中,狀態碼有以下幾種:
類別 | 原因短語 |
---|---|
1XX | Informational(信息性狀態碼)接收的請求正在處理 |
2XX | Success(成功狀態碼)請求正常處理完畢 |
3XX | Redirection(重定向狀態碼)需要進行附加操作以完成請求 |
4XX | Client Error(客戶端錯誤狀態碼)服務器無法處理請求 |
5XX | Server Error(服務器錯誤狀態碼)服務器處理請求出錯 |
但是,僅有開頭還不足以說明具體的問題,所以每一種類別下還有具體的狀態碼和對應描述,因為狀態碼太多,所以下面僅僅展示常見的狀態碼:
狀態碼 | 類別 | 描述 | 示例場景 |
---|---|---|---|
100 | 信息響應 | 請求已接收,客戶端應繼續發送請求 | 客戶端詢問服務器是否支持某些功能 |
101 | 信息響應 | 切換協議 | 客戶端請求切換到WebSocket協議 |
200 | 成功響應 | 請求成功 | 頁面加載成功 |
201 | 成功響應 | 資源創建成功 | 創建新用戶或上傳文件 |
204 | 成功響應 | 請求成功但無內容返回 | 刪除操作后不返回任何內容 |
301 | 重定向 | 永久重定向 | 網站遷移至新域名 |
302 | 重定向 | 臨時重定向 | 用戶登錄后跳轉到主頁 |
304 | 重定向 | 資源未修改,使用緩存 | 瀏覽器緩存的資源未過期 |
400 | 客戶端錯誤 | 請求無效或無法被服務器理解 | 請求參數缺失或格式錯誤 |
401 | 客戶端錯誤 | 未授權訪問 | 用戶未提供身份驗證 |
403 | 客戶端錯誤 | 禁止訪問 | 用戶權限不足 |
404 | 客戶端錯誤 | 資源未找到 | 請求的頁面或API不存在 |
405 | 客戶端錯誤 | 方法不允許 | 使用了不支持的HTTP方法(如POST代替GET) |
429 | 客戶端錯誤 | 請求過多 | 超過API速率限制 |
500 | 服務器錯誤 | 內部服務器錯誤 | 服務器代碼邏輯出錯 |
502 | 服務器錯誤 | 錯誤網關 | 服務器作為網關時收到無效響應 |
503 | 服務器錯誤 | 服務不可用 | 服務器過載或維護中 |
504 | 服務器錯誤 | 網關超時 | 服務器作為網關時未能及時從上游獲取響應 |
其中,更為常見的就是200(OK)、404(Not Found)、403(Forbidden)、302(Redirect)和504(Bad Gateway)
本次優先考慮200(OK)和404(Not Found),對于重定向將在后面的章節介紹,此處不具體描述
處理響應行
首先是HTTP響應版本,這個字段可以設置為一個固定值,因為一般情況下只會在升級的時候更改,所以可以考慮使用一個字符串指定
接著是HTTP響應狀態碼和狀態碼描述,因為本次只考慮200和404,所以只有兩種情況:
- 用戶請求的資源存在
- 用戶請求的資源不存在
根據這兩種情況,需要考慮的問題就是如何判斷用戶請求的資源是否存在。前面提到,URI
就是資源路徑,也就是說,用戶想要拿到的資源就在URI
中。根據這一點得出「判斷用戶請求的資源是否存在」只需要「在當前服務器的資源目錄中找到對應的文件是否存在」即可。現在的問題就轉變為「如何判斷一個文件是否存在」,這里可以使用文件流讀取對應的文件,如果文件不存在就給用戶返回一個空字符串,否則就將讀取到的文件添加到結果字符串即可
根據上面的思路,首先就是要獲取到URI
,這一點其實在HttpRequest
中已經做到了,所以在HttpResponse
中需要獲取一下即可,這里可以考慮讓讀取內容的函數接收一個URI
字符串,所以獲取文件內容函數的基本邏輯如下:
// 獲取文件內容
std::string getFileContent(std::string& uri)
{// 當前uri中即為用戶需要的文件,使用文件流打開文件std::fstream f(uri);// 如果文件為空,直接返回空字符串if(!f.is_open())return std::string();// 否則就讀取文件內容std::string content;std::string line;while (std::getline(f, line))content += line;f.close();return content;
}
接著,在創建一個函數用于處理獲取URI
以及構建文件內容,前面提到可以使用HttpResponse
對象獲取到對應的URI
,所以當前函數需要接收一個HttpRequest
對象,并且在HttpRequest
類中需要提供獲取URI
的函數:
=== “獲取URI
函數”
// 獲取URIstd::string getReqUri(){return _req_uri;}
=== “處理URI
以及構建文件內容函數”
void buildHttpResponse(HttpRequest& req)
{// 獲取uristd::string req_uri = req.getReqUri();// 根據uri獲取文件內容std::string content = getFileContent(req_uri);
}
現在已經解決了文件問題,也就是說現在可以根據文件是否存在決定狀態碼的值和描述,根據前面的思路可以得出文件存在會返回空字符串,那么此時就說明狀態碼應該是404,否則就是200,所以這里就可以通過是否為空設置對應的狀態碼,而狀態碼描述可以通過狀態碼進行匹配,例如:
// 根據狀態碼得到狀態碼描述
std::string setStatusCodeDesc(int status_code)
{switch (status_code){case 200:return "OK";case 404:return "Not Found";default:break;}return std::string();
}
接下來繼續完善構建函數buildHttpResponse
,因為要設置狀態碼和狀態碼描述,所以需要兩個成員接收這兩個值,便于后面構建HTTP響應結構:
void buildHttpResponse(HttpRequest &req)
{// 獲取uristd::string req_uri = req.getReqUri();// 根據uri獲取文件內容std::string content = getFileContent(req_uri);if (content.empty()){// 如果為真,說明文件不存在// 設置狀態碼為404并設置狀態碼描述_status_code = 404;_status_code_desc = setStatusCodeDesc(_status_code);}else{// 文件存在_status_code = 200;_status_code_desc = setStatusCodeDesc(_status_code);}
}
處理完狀態碼和狀態碼描述之后,接下來就是將HTTP版本、狀態碼和狀態碼描述構建出一個HTTP響應行,首先修改原有的構造函數,刪除不需要的字段:
// 默認HTTP版本
const std::string default_http_ver = "HTTP/1.1";
class HttpResponse
{
public:HttpResponse(): _http_ver(default_http_ver){}private:std::string _http_ver; // HTTP版本// ...
};
接著,在buildHttpResponse
函數中添加構建請求行的邏輯:
void buildHttpResponse(HttpRequest &req)
{// ...// 構建請求行_resp_line = _http_ver + std::to_string(_status_code) + _status_code_desc;
}
處理響應行
根據前面的要求「允許外部添加響應報頭中的屬性」,需要提供一個哈希表存儲key
和value
,所以首先需要創建一個類成員,接著添加一個函數用于執行添加邏輯:
class HttpResponse
{
public:// ...void insertRespHead(std::string& key, std::string& value){_kv[key] = value;}// ...private:// ...std::unordered_map<std::string, std::string> _kv;// ...
};
處理響應體
根據客戶端請求的內容讀取服務端存在的對應文件并返回給客戶端,沒有時返回404頁面,所以只需要在buildHttpResponse
中根據是否有文件內容給定具體文件即可,對于存在對應的文件的,因為getFileContent
返回的就是讀取到的文件內容,所以直接將結果給響應頭即可,但是對于404頁面,并沒有讀取到一個實際的文件內容,這里可以考慮直接給getFileContent
寫入一個固定文件,即404文件,這樣該函數獲取到的文件內容就是404文件
有了基本思路,現在就是缺少這類文件。在添加文件之前,先仔細了解一下不帶參數的URI
,在介紹HTTP協議基本結構與基本實現HTTPServer中提到了/
開始不一定是系統根目錄,而是Web應用根目錄,這個Web應用根目錄實際上就是當前服務器程序所在目錄下的一個文件夾,這個文件夾下放著一些靜態資源,例如HTML、圖片、視頻、CSS、JavaScript等,所以客戶端想訪問資源本質就是讓服務器在這個文件夾中找到對應的文件并將其中的內容返回給客戶端
有了上面的概念,下面就是在當前服務器程序所在的目錄創建一個Web應用根目錄,基本目錄結構如下:
主程序目錄- Web應用根目錄- src- HTML文件- assets- stylesheets- CSS文件- JavaScripts- JavaScript文件- public- images- 圖片文件- videos- 視頻文件- HttpServer程序
上面的目錄只是一個參考目錄,并不是固定的,可以根據自己或者其他地方的規定進行調整,下面將以這個目錄結構為例演示,當前創建的目錄結構如下:
HttpServer- wwwroot- src- assets- js- style- public- images- videos- HttpServer_main
接著,為了演示出客戶端正常接收到服務器存在的文件以及404文件,需要在src
目錄下創建兩個HTML文件,此處不給出具體的HTML文件代碼
接著,修改buildHttpResponse
邏輯,確保可以讀取到文件并將文件內容存儲到響應體中:
// 404頁面固定路徑
std::string default_404_page = "wwwroot/src/404.html";void buildHttpResponse(HttpRequest &req)
{// 獲取uristd::string req_uri = req.getReqUri();// 根據uri獲取文件內容std::string content = getFileContent(req_uri);if (content.empty()){// 如果為真,說明文件不存在// 設置狀態碼為404并設置狀態碼描述_status_code = 404;_status_code_desc = setStatusCodeDesc(_status_code);// 讀取404頁面并設置響應體_resp_body = getFileContent(default_404_page);}else{// 文件存在_status_code = 200;_status_code_desc = setStatusCodeDesc(_status_code);// 設置響應體_resp_body = content;}// 構建請求行_resp_line = _http_ver + std::to_string(_status_code) + _status_code_desc;
}
修改HttpServer
類
因為HttpServer
需要調用sendData
函數,該函數需要傳入一個字符串作為發送數據,而HTTP響應中存在一個序列化函數,可以調用這個函數可以傳入一個字符串,并將序列化后的字符串存儲到參數的字符串中,這樣就可以實現發送。所以整體handleHttpRequest
邏輯修改如下:
void handleHttpRequest(SockAddrIn sock_addr_in, int ac_socketfd)
{// ...// 構建HTTP響應HttpResponse resq;resq.buildHttpResponse(req);// 序列化std::string out_str;resq.serialize(out_str);// 發送給客戶端bs->sendData(out_str, ac_socketfd);
}
測試
主函數與上一節一樣,測試結果如下:
從上面的測試結果可以發現,的確可以接收到數據,如果將地址欄的內容修改為localhost:8080/index.html
,結果如下:
這個測試結果并不是像預期的那樣顯示主頁的內容,而是顯示404頁面。那么明明存在index.html
文件,為什么會出現無法讀取到index.html
?這是因為在解析URI
時并沒有考慮到URI
起始的/
,實際上getFileContent
函數得到的uri
字符串內容是/index.html
,而已有的index.html
文件路徑為wwwroot/src/index.html
,所以還需要在已有的uri
上還需要加入默認的Web應用目錄wwwroot/src
,修改如下:
// Web應用路徑
std::string default_webapp_dir = "wwwroot/";// HTML文件路徑
std::string default_html_dir = "src";// 404頁面固定路徑
std::string default_404_page = default_webapp_dir + default_html_dir + "/404.html";void buildHttpResponse(HttpRequest &req)
{// 獲取uri// ...// 補充uristd::string real_uri = default_webapp_dir + default_html_dir + req_uri;// 根據uri獲取文件內容std::string content = getFileContent(real_uri);// ...
}
再次測試,結果如下:
優化
從上面的測試可以發現,如果想要訪問index.html
文件還需要手動加上/index.html
,但是訪問一個實際的網站時,盡管沒有攜帶/index.html
,依舊可以訪問到網站的index.html
文件,例如訪問百度的首頁:
默認訪問:
在網址后添加/index.html
:
因為直接輸入網址,瀏覽器請求的默認就是Web應用根目錄下的某一個文件,只是默認情況下隱藏了IP地址+端口號后的/
,實現這個效果的方式很簡單,只需要在getFileContent
函數的開始判斷是否是/
,如果是就直接返回index.html
的內容即可。思路的確如此,但是在上面先處理了傳遞給getFileContent
的參數為添加了wwwroot/src
的字符串,所以實際上如果只有一個/
,那么傳遞給getFileContent
函數的參數為wwwroot/src/
,所以修改如下:
// 獲取文件內容
std::string getFileContent(std::string &uri)
{// 默認訪問index.html文件if(uri == "wwwroot/src/")uri = "wwwroot/src/index.html";// ...
}
再次測試,觀察結果:
可以發現已經默認到了index.html
的內容
雖然上面的思路的確可以實現問題,但是如果默認以/
結尾,那么只要判斷最后字符串是否是/
即可,這樣不論前面的內容是什么,只要是以/
結尾,都可以訪問到主頁,所以還可以修改為:
// 獲取文件內容
std::string getFileContent(std::string &uri)
{// 默認訪問index.html文件if(uri.back() == '/')uri = "wwwroot/src/index.html";// ...
}
現在,不論前面是什么都可以訪問到index.html
,哪怕是不存在的目錄: