Linux與深入HTTP序列化和反序列化

深入HTTP序列化和反序列化

本篇介紹

在上一節已經完成了客戶端和服務端基本的HTTP通信,但是前面的傳遞并沒有完全體現出HTTP的序列化和反序列化,為了更好得理解其工作流程,在本節會以更加具體的方式分析到HTTP序列化和反序列化

本節會在介紹HTTP協議基本結構與基本實現HTTPServer的基礎之上繼續完善HTTP服務器,所以需要有對應的知識作為鋪墊才可以開始本節

基本實現思路

本次實現的HttpServer類主要完成接收客戶端發送的HTTP請求,也就是說,服務器需要根據客戶端的HTTP請求回復一個HTTP響應,所以必須要有的方法就是處理請求方法,但是前面已經提到過,HTTP的請求屬于結構化的數據,并且這個數據在傳遞給服務器時就已經做了序列化,服務器需要處理的就是將結構化的數據進行反序列化;同樣,服務器處理完畢后還需要發給客戶端,所以此處就需要服務器對處理的結果填充到HTTP響應結構對象中再返回給客戶端,此處就需要進行序列化

基于上面的原因,與前面序列化和反序列化與網絡計算器一樣,需要實現一個協議,包含HttpRequestHttpResponse類,用于處理序列化和反序列化

本次為了更好得理解序列化和反序列化,以HTTP請求為例,首先以請求行、請求報頭和請求體三個整體做序列化和反序列化,接著再深入請求行、請求報頭和請求體中的字段

根據這個兩個階段,需要實現的目標如下:

  1. 第一階段:打印出反序列化和序列化的結果
  2. 第二階段:向客戶端返回具體的靜態HTML文件

第一階段

創建HttpRequest

根據前面的基本思路,實現HttpRequest類就需要實現對應的反序列化。因為HTTP請求中帶有三種數據:

  1. 請求行
  2. 請求報頭
  3. 請求體

所以需要定義三個成員分別存放從請求獲取到的內容,所以基本結構如下:

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響應也有對應的三種數據:

  1. 請求行
  2. 請求報頭
  3. 請求體

所以需要定義三個成員分別存放從請求獲取到的內容,所以基本結構如下:

// 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;
}

接著修改HttpServerhandleHttpRequest函數:

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

為了存儲對應的keyvalue,可以考慮使用一個哈希表。這里,因為處理每一個每一條報頭數據不需要經過_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;
}

接下來修改deserializegetReqHeadFromTotal的邏輯:

=== “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響應來說,應該處理下面幾點:

  1. 允許根據請求內容是否合法自動構建響應狀態碼,并且根據狀態碼自動生成狀態碼描述
  2. 允許外部添加響應報頭中的屬性
  3. 根據客戶端請求的內容讀取服務端存在的對應文件并返回給客戶端,沒有時返回404頁面
HTTP狀態碼

根據上面的思路,首先需要處理的就是狀態碼,在介紹HTTP協議基本結構與基本實現HTTPServer一節提到,HTTP協議規定任何客戶端的請求都必須得到響應,而區分響應情況就是通過狀態碼

在HTTP中,狀態碼有以下幾種:

類別原因短語
1XXInformational(信息性狀態碼)接收的請求正在處理
2XXSuccess(成功狀態碼)請求正常處理完畢
3XXRedirection(重定向狀態碼)需要進行附加操作以完成請求
4XXClient Error(客戶端錯誤狀態碼)服務器無法處理請求
5XXServer 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,所以只有兩種情況:

  1. 用戶請求的資源存在
  2. 用戶請求的資源不存在

根據這兩種情況,需要考慮的問題就是如何判斷用戶請求的資源是否存在。前面提到,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;
}
處理響應行

根據前面的要求「允許外部添加響應報頭中的屬性」,需要提供一個哈希表存儲keyvalue,所以首先需要創建一個類成員,接著添加一個函數用于執行添加邏輯:

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,哪怕是不存在的目錄:

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/72387.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/72387.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/72387.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于Python+SQLite實現(Web)驗室設備管理系統

實驗室設備管理系統 應用背景 為方便實驗室進行設備管理&#xff0c;某大學擬開發實驗室設備管理系統 來管理所有實驗室里的各種設備。系統可實現管理員登錄&#xff0c;查看現有的所有設備&#xff0c; 增加設備等功能。 開發環境 Mac OSPyCharm IDEPython3Flask&#xff…

深拷貝and淺拷貝!

一、什么是拷貝&#xff1f;什么是深拷貝和淺拷貝&#xff1f; &#xff08;1&#xff09;拷貝&#xff1a;拷貝就是為了復用原對象的部分or全部數據&#xff0c;在原對象的基礎上通過復制的方式創建一個新的對象。 拷貝對象可以分為三種類型&#xff1a;直接賦值、淺拷貝和深拷…

高頻面試題(含筆試高頻算法整理)基本總結回顧43

干貨分享&#xff0c;感謝您的閱讀&#xff01; &#xff08;暫存篇---后續會刪除&#xff0c;完整版和持續更新見高頻面試題基本總結回顧&#xff08;含筆試高頻算法整理&#xff09;&#xff09; 備注&#xff1a;引用請標注出處&#xff0c;同時存在的問題請在相關博客留言…

《靈珠覺醒:從零到算法金仙的C++修煉》卷三·天劫試煉(34)混元金斗裝萬物 - 0-1背包問題(二維DP)

《靈珠覺醒:從零到算法金仙的C++修煉》卷三天劫試煉(34)混元金斗裝萬物 - 0-1背包問題(二維DP) 哪吒在數據修仙界中繼續他的修煉之旅。這一次,他來到了一片神秘的混元谷,谷中有一座巨大的混元金斗,斗身閃爍著神秘的光芒。谷口有一塊巨大的石碑,上面刻著一行文字:“欲…

網絡爬蟲【簡介】

我叫補三補四&#xff0c;很高興見到大家&#xff0c;歡迎一起學習交流和進步 今天來講一講視圖 一、網絡爬蟲的定義 網絡爬蟲&#xff08;Web Crawler&#xff09;&#xff0c;又稱為網絡蜘蛛、網絡機器人等&#xff0c;是一種按照一定規則自動抓取互聯網信息的程序或腳本。它…

?AI時代到來,對電商來說是效率躍升,還是溫水煮青蛙

?凌晨三點的義烏商貿城&#xff0c;95后創業者小王&#xff0c;靜靜地盯著屏幕上的AI工具&#xff0c;竟露出了笑容。這個月他的跨境玩具店銷量提升了不少&#xff0c;從之前的狀態翻了3倍&#xff1b;而且團隊人數有所變化&#xff0c;從5人縮減到了2人&#xff08;其中包括他…

PDF文件密碼保護破解:安全解密的步驟與技巧

PDF文件加密后&#xff0c;需要特定的密碼才能訪問內容。以下是一些常見的方法來解密PDF文件&#xff1a; 方法一&#xff1a;使用Adobe Acrobat 如果你有Adobe Acrobat Pro&#xff0c;可以使用它來解密PDF文件。 打開Adobe Acrobat Pro&#xff1a; 啟動Adobe Acrobat Pro…

qt 自帶虛擬鍵盤的編譯使用記錄

一、windows 下編譯 使用vs 命令窗口&#xff0c;分別執行&#xff1a; qmake CONFIG"lang-en_GB lang-zh_CN" nmake nmake install 如果事先沒有 指定需要使用的輸入法語言就進行過編譯&#xff0c;則需要先 執行 nmake distclean 清理后執行 qmake 才能生效。 …

Java開發之數據庫應用:記一次醫療系統數據庫遷移引發的異常:從MySQL到PostgreSQL的“dual“表陷阱與突圍之路

記一次醫療系統數據庫遷移引發的異常&#xff1a;從MySQL到PostgreSQL的"dual"表陷阱與突圍之路 一、驚魂時刻&#xff1a;數據庫切換引發的系統雪崩 某醫療影像系統在進行國產化改造過程中&#xff0c;將原MySQL數據庫遷移至PostgreSQL。遷移完成后&#xff0c;系…

C++刷題(二):棧 + 隊列

&#x1f4dd;前言說明&#xff1a; 本專欄主要記錄本人的基礎算法學習以及刷題記錄&#xff0c;使用語言為C。 每道題我會給出LeetCode上的題號&#xff08;如果有題號&#xff09;&#xff0c;題目&#xff0c;以及最后通過的代碼。沒有題號的題目大多來自牛客網。對于題目的…

精通游戲測試筆記(持續更新)

第一章、游戲測試的兩條規則 不要恐慌 不要將這次發布當作最后一次發布 不要相信任何人 把每次發布當作最后一次發布 第二章&#xff1a;成為一名游戲測試工程師

Windows功能之FTP服務器搭建

一、創作背景 之前有用linux系統搭建過ftp服務器&#xff0c;最近想著用windows系統也順便搭建一個&#xff0c;看網上有第三方服務軟件一鍵部署&#xff0c;記得windows可以不借助第三方軟件就可以搭建&#xff0c;就想順便操作試試&#xff0c;結果老是連接不上&#xff0c;費…

星型組網模塊的兩種交互方式優缺點解析

星型組網模塊簡介 星型組網模塊工作在433MHz頻段&#xff1b;星型組網模塊集主機&#xff08;協調器&#xff09;、終端為一體&#xff0c;星型組網模塊具有長距離、高速率兩種傳輸模式&#xff0c;一個主機&#xff08;協調器&#xff09;支持多達200個節點與其通訊&#xff0…

二分+前綴和——森林的最大美麗值

森林的最大美麗值(二分差分數組) 題目分析 求最小值的最大值&#xff0c;聯想到二分。 第一階段二段性分析 對于所有樹的高度都可以大于等于mid&#xff0c;那么我們可以確定高度小于mid的值一定也可以&#xff0c;但是此時我需要找的是最大的高度&#xff0c;那么mid一定比…

Pytorch實現之最小二乘梯度歸一化設計

簡介 簡介:LSGAN提出了一種利用最小二乘法來計算兩個數據分布之間的距離,該論文在此基礎上采用梯度歸一化來進一步穩定訓練。 論文題目:LSN-GAN: A Novel Least Square Gradient Normalization for Generative Adversarial Networks(LSN-GAN:一種新的生成對抗網絡的最小…

JavaScript基礎-全局作用域

在JavaScript編程中&#xff0c;理解變量的作用域是編寫高效、可維護代碼的關鍵之一。全局作用域是指變量在整個程序范圍內都可訪問的狀態&#xff0c;這意味著它們可以在任何函數或代碼塊中被讀取和修改。然而&#xff0c;過度使用全局變量也可能導致一些問題&#xff0c;如命…

【2025.3.13】記一次雙系統筆記本加裝固態硬盤記錄 linux擴容 linux更換/home和/opt所在硬盤 windows無法調整亮度

文章目錄 &#x1f315;事情經過&#x1f315;更換/home和/opt的掛載硬盤&#x1f319;目的&#x1f319;初始化1t固態硬盤&#x1f319;打開Linux查看硬盤信息&#x1f319;給新1t固態硬盤分區&#x1f319;格式化分區&#x1f319;把新1t固態硬盤先掛載到/mnt/ssd_1t 用于后續…

山東省新一代信息技術創新應用大賽-計算機網絡管理賽項(樣題)

目錄 競賽試題 網絡拓撲 配置需求 虛擬局域網 IPv4地址部署 OSPF及路由部署 配置合適的靜態路由組網 MSTP及VRRP鏈路聚合部署 IPSEC部署 路由選路部署 設備與網絡管理部署 1.R1 2.R2 3.S1 4.S2 5.S3 競賽試題 本競賽使用HCL(華三云實驗室)來進行網絡設備選擇…

【測試語言基礎篇】Python基礎之List列表

一、Python 列表(List) 序列是Python中最基本的數據結構。序列中的每個元素都分配一個數字 - 它的位置&#xff0c;或索引&#xff0c;第一個索引是0&#xff0c;第二個索引是1&#xff0c;依此類推。 Python有6個序列的內置類型&#xff0c;但最常見的是列表和元組。序列都可…

大數據面試之路 (二) hive小文件合并優化方法

大量小文件容易在文件存儲端造成瓶頸&#xff0c;影響處理效率。對此&#xff0c;您可以通過合并Map和Reduce的結果文件來處理。 一、合并小文件的常見場景 寫入時產生小文件&#xff1a;Reduce任務過多或數據量過小&#xff0c;導致每個任務輸出一個小文件。 動態分區插入&…