一、相關知識
1、有限狀態機:
有限狀態機(Finite State Machine, FSM)是一種用于描述對象在其生命周期內可能經歷的不同狀態及其狀態轉換規則的模型。它廣泛應用于游戲開發、網絡協議、詞法解析、UI邏輯控制等領域。以下是C++中有限狀態機的簡介:
有限狀態機的核心概念
- 狀態(State)
對象可能處于的某種特定情況(如游戲中的角色“空閑”、“移動”、“攻擊”)。 - 事件(Event)
觸發狀態轉換的外部或內部條件(如用戶輸入“跳躍”、定時器超時)。 - 轉換(Transition)
定義在特定事件發生時,從當前狀態轉移到目標狀態的規則(如“空閑 → 移動”)。 - 動作(Action)
狀態轉換時執行的操作(如播放動畫、發送網絡包)。
2、http報文:
HTTP報文分為請求報文(瀏覽器端向服務器發送)和響應報文(服務器處理后返回給瀏覽器端)兩種,每種報文必須按照特有格式生成,才能被瀏覽器端識別。
(1)請求報文:由請求行、請求頭部、空行、請求數據四部分組成
- 請求行:用來說明請求類型(方法)、要訪問的資源以及使用的http的版本
- 請求頭部:用來說明服務器要使用的附加信息,由“名/值”對組成,每對一行,中間用冒號隔開
- 空行:請求頭后面的空行是必須的,即使第四行請求數據為空行,第三行也必須是空行
- 請求數據:也叫主體,可以添加任意類型的數據
以下是通過抓包得到的http請求報文:
GET http://jsuacm.cn/ HTTP/1.1 //Get為請求方法,URL為請求資源,1.1為http版本
Host: jsuacm.cn
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3877.400 QQBrowser/10.8.4506.400
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9//”請求數據”(GET方式的請求一般不包含)

常用的頭部信息匯總:
頭部名稱 | 類型 | 作用說明 | 示例值 |
---|---|---|---|
Host | 請求頭 | 指定請求的目標主機名和端口號(HTTP/1.1 必須包含) | Host: www.example.com |
User-Agent | 請求頭 | 標識客戶端(瀏覽器、操作系統等)信息 | Mozilla/5.0 (Windows NT 10.0)... |
Accept | 請求頭 | 指定客戶端可接受的響應內容類型(MIME 類型) | text/html,application/xhtml+xml |
Accept-Language | 請求頭 | 指定客戶端可接受的語言 | en-US,en;q=0.9,zh-CN;q=0.8 |
Accept-Encoding | 請求頭 | 指定客戶端可接受的編碼方式(如壓縮格式) | gzip, deflate |
Authorization | 請求頭 | 提供身份驗證憑證(如 Bearer Token、Basic Auth) | Bearer <token> |
Referer | 請求頭 | 標明當前請求的來源頁面 URL | https://www.google.com/ |
If-Match | 請求頭 | 條件請求頭,用于驗證資源 ETag 是否匹配 | "67ab43" |
If-None-Match | 請求頭 | 條件請求頭,驗證資源 ETag 是否不匹配(用于緩存更新) | "67ab43" |
If-Modified-Since | 請求頭 | 條件請求頭,驗證資源是否在指定時間后被修改 | Wed, 21 Oct 2023 07:28:00 GMT |
Cookie | 請求頭 | 客戶端隨請求發送的 Cookie 數據 | sessionid=abc123 |
GET/ POST的區別:
GET最常見的一種請求方式,當客戶端要從服務器中讀取文檔時,當點擊網頁上的鏈接或者通過在瀏覽器的地址欄輸入網址來瀏覽網頁的,使用的都是GET方式。GET方法要求服務器將URL定位的資源放在響應報文的數據部分,回送給客戶端。使用GET方法時,請求參數和對應的值附加在URL后面,利用一個問號(“?”)代表URL的結尾與請求參數的開始,傳遞參數長度受限制。
GET方式的請求一般不包含”請求數據”部分,請求數據以地址的形式表現在請求行。顯然,這種方式不適合傳送私密數據。另外,由于不同的瀏覽器對地址的字符限制也有所不同,一般最多只能識別1024個字符,所以如果需要傳送大量數據的時候,也不適合使用GET方式。
和get一樣很常見,對于上面提到的不適合使用GET方式的情況,可以考慮使用POST方式,因為使用POST方法可以允許客戶端給服務器提供信息較多。POST方法將請求參數封裝在HTTP請求數據中,以名稱/值的形式出現,可以傳輸大量數據,這樣POST方式對傳送的數據大小沒有限制,而且也不會顯示在URL中
(簡單來講,就是GET一般用于我們點擊網頁其他鏈接時使用,POST一般就是我們從網頁上下載資源的時候使用)
(2) 響應報文: 由狀態行+消息報頭+空行+響應正文四個部分組成
- 狀態行:由HTTP協議版本號,狀態碼,狀態消息 三部分組成;
- 消息報頭:用來說明客戶端要使用的一些附加信息;
- 空行
- 響應正文:主要就是服務端向客戶端發送的數據,比如一個頁面、照片、視頻等
以下是抓取的一段響應報文
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 20 Oct 2021 06:46:15 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 737265<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content=""><meta name="author" content=""><link rel="icon" href="../../favicon.ico"><title>吉首大學 </title>
HTTP狀態碼與請求方法
HTTP有5種類型的狀態碼,具體的:
-
1xx:指示信息—表示請求已接收,繼續處理。
-
2xx:成功—表示請求正常處理完畢。
200 OK:客戶端請求被正常處理。
206 Partial content:客戶端進行了范圍請求。 -
3xx:重定向—要完成請求必須進行更進一步的操作。
301 Moved Permanently:永久重定向,該資源已被永久移動到新位置,將來對該資源訪問都要使用本響應返回的若干個URI之一。
302 Found:臨時重定向,請求的資源臨時從不同的URI中獲得。 -
4xx:客戶端錯誤—請求有語法錯誤,服務器無法處理請求。
400 Bad Request:請求報文存在語法錯誤。
403 Forbidden:請求被服務器拒絕。
404 Not Found:請求不存在,服務器上找不到請求的資源。 -
5xx:服務器端錯誤—服務器處理請求出錯。
500 Internal Server Error:服務器在執行請求時出現錯誤。
執行邏輯:
- 首先主線程接收到一個客戶端發來的事件,如讀/寫等,并將該事件按照事件類型標識,并加入任務隊列
eventLoop()
......//處理客戶連接上接收到的數據else if (events[i].events & EPOLLIN){dealwithread(sockfd);}else if (events[i].events & EPOLLOUT){dealwithwrite(sockfd);......}
void WebServer::dealwithread(int sockfd)
{util_timer *timer = users_timer[sockfd].timer;//reactor(反應堆),就是IO多路復用,收到事件后,根據事件類型分配給某個線程if (1 == m_actormodel){if (timer){adjust_timer(timer);}//若監測到讀事件,將該事件放入請求隊列m_pool->append(users + sockfd, 0); //users是一個數組指針,sockfd是索引,因此這個表示的就是當前處理的客戶端的對象//stat:0表示read事件,1表示write事件while (true){if (1 == users[sockfd].improv){if (1 == users[sockfd].timer_flag){deal_timer(timer, sockfd);users[sockfd].timer_flag = 0;}users[sockfd].improv = 0;break;}}}
- 如果有事件添加進入任務隊列,則會通知線程池,有空閑線程則會取出任務來進行執行,執行線程執行函數run()(內核是run函數,但是run不是靜態函數,所以不能作為線程執行函數,而是在外層套了一個殼的worker()函數)
void threadpool<T>::run()
{while (true){m_queuestat.wait();//等待信號m_queuelocker.lock();if (m_workqueue.empty()){m_queuelocker.unlock();continue;}T *request = m_workqueue.front();m_workqueue.pop_front();m_queuelocker.unlock();if (!request)continue;if (1 == m_actor_model) //reactor{if (0 == request->m_state){if (request->read_once()){request->improv = 1;connectionRAII mysqlcon(&request->mysql, m_connPool);request->process();}else{request->improv = 1;request->timer_flag = 1;}}else{if (request->write()){request->improv = 1;}else{request->improv = 1;request->timer_flag = 1;}}}else{connectionRAII mysqlcon(&request->mysql, m_connPool);request->process();}}
}
- 然后根據事件的類型(request->state),來選擇執行相應的函數,這里我們以讀事件為例。首先,客戶端發來請求響應,服務端需要先將客戶端發來的請求響應的內容保存下來然后再進行解析,保存請求響應的函數即是read_once()函數
該函數的邏輯也比較簡單,主要就是將套接字發送的內容儲存到m_read_buf這個緩存區中
//循環讀取客戶數據,直到無數據可讀或對方關閉連接
//非阻塞ET工作模式下,需要一次性將數據讀完
bool http_conn::read_once()
{if (m_read_idx >= READ_BUFFER_SIZE){return false;}int bytes_read = 0;//LT讀取數據if (0 == m_TRIGMode){bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);m_read_idx += bytes_read;if (bytes_read <= 0){return false;}return true;}//ET讀數據else{while (true){bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);if (bytes_read == -1){if (errno == EAGAIN || errno == EWOULDBLOCK)break;return false;}else if (bytes_read == 0){return false;}m_read_idx += bytes_read;}return true;}
}
- 我們得到緩沖區中的請求響應后,就需要對其進行解析,解析函數為process()函數,這個函數會先使用process_read()函數對請求響應進行解析,然后使用process_write()輸出回應報文
void http_conn::process()
{HTTP_CODE read_ret = process_read(); // 請求報文處理,限定結果在枚舉范圍之內if (read_ret == NO_REQUEST) //如果請求不完整,需要繼續接收請求數據{modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);return;}bool write_ret = process_write(read_ret);//相應報文處理if (!write_ret){close_conn();}modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}
- process_read()函數是請求報文解析函數,是http的核心函數之一,我們可以結合下面的圖來看
/* 該函數為請求報文處理函數,通過while循環來實現對主從狀態機的封裝,其中主狀態機為process_read()函數,從狀態機為parse_line()函數*/
http_conn::HTTP_CODE http_conn::process_read()
{LINE_STATUS line_status = LINE_OK;HTTP_CODE ret = NO_REQUEST;char *text = 0;//m_checked_state 主狀態機狀態為CHECK_STATE_REQUESTLINE時,該條件涉及解析消息體//line_status 從狀態機狀態轉移為LINE_OK時,該條件涉及解析請求行while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK)){text = get_line(); //得到改行的具體內容m_start_line = m_checked_idx;LOG_INFO("%s", text);switch (m_check_state){case CHECK_STATE_REQUESTLINE: //請求行,初始化的時候定義了{ret = parse_request_line(text);if (ret == BAD_REQUEST)return BAD_REQUEST;break;}case CHECK_STATE_HEADER: //頭信息{ret = parse_headers(text);if (ret == BAD_REQUEST)return BAD_REQUEST;else if (ret == GET_REQUEST){return do_request();}break;}case CHECK_STATE_CONTENT: //消息體{ret = parse_content(text);if (ret == GET_REQUEST)return do_request();line_status = LINE_OPEN;break;}default:return INTERNAL_ERROR;}}return NO_REQUEST;
}
6. 其中,主狀態機則為process_read()函數,從狀態機為parse_line()函數,parse_line主要作用就是從讀緩沖區中讀取一行內容,并將每一行結尾的\r\n改為\0\0。
//從狀態機,用于分析出一行內容
//返回值為行的讀取狀態,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line()
{char temp;for (; m_checked_idx < m_read_idx; ++m_checked_idx){temp = m_read_buf[m_checked_idx];if (temp == '\r'){if ((m_checked_idx + 1) == m_read_idx)return LINE_OPEN;else if (m_read_buf[m_checked_idx + 1] == '\n'){m_read_buf[m_checked_idx++] = '\0';m_read_buf[m_checked_idx++] = '\0';return LINE_OK;}return LINE_BAD;}else if (temp == '\n'){if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r'){m_read_buf[m_checked_idx - 1] = '\0';m_read_buf[m_checked_idx++] = '\0';return LINE_OK;}return LINE_BAD;}}return LINE_OPEN;
}
- 如果返回LINE_OK,表示改行已經讀完,并且該行的地址m_check_idx也已經更新,接著通過get_line()這個函數就返回改行具體的內容了。
然后進入switch循環,首先my_check_state的初始狀態為CHECK_STATE_REQUESTLINE,即解析請求行,然后執行parse_request_line函數來解析,函數如下:
主要作用就是解析出m_method、m_url這兩個屬性,然后將my_check_state的狀態修改為CHECK_STATE_HEADER。
//解析http請求行,獲得請求方法,目標url及http版本號
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{m_url = strpbrk(text, " \t");//用于查找第一個出現指定字符串的位置,如果找到,則返回指向該字符的指針,否則返回NULLif (!m_url){return BAD_REQUEST;}*m_url++ = '\0';char *method = text;if (strcasecmp(method, "GET") == 0)m_method = GET;else if (strcasecmp(method, "POST") == 0){m_method = POST;cgi = 1; //是否啟用POST}elsereturn BAD_REQUEST;m_url += strspn(m_url, " \t"); //從m_url開始,跳過空白字符,返回第一個非空白字符的指針,該作用是確保指針指向有效的字符m_version = strpbrk(m_url, " \t");if (!m_version)return BAD_REQUEST;*m_version++ = '\0';m_version += strspn(m_version, " \t");if (strcasecmp(m_version, "HTTP/1.1") != 0) //檢查版本是否為1.1return BAD_REQUEST;if (strncasecmp(m_url, "http://", 7) == 0) //如果前綴包括http://,則去掉{m_url += 7;m_url = strchr(m_url, '/');}if (strncasecmp(m_url, "https://", 8) == 0) //同上{m_url += 8;m_url = strchr(m_url, '/');}if (!m_url || m_url[0] != '/')return BAD_REQUEST;//當url為/時,顯示判斷界面if (strlen(m_url) == 1)strcat(m_url, "judge.html");m_check_state = CHECK_STATE_HEADER;return NO_REQUEST;
}
- 接下來是解析頭部信息HEADER,函數如下:
其邏輯為如果檢測到該行頭部為“Connection:”、“keep-alive”等,則賦予相應的屬性值,但是m_check_state不變;
如果檢測到“Content-length”,這代表接下來是客戶端發送過來的內容了,這樣的話就轉變m_check_state的屬性為CHECK_STATE_CONTENT,代表下一次循環就要解析內容了
//解析http請求的一個頭部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{if (text[0] == '\0'){if (m_content_length != 0){m_check_state = CHECK_STATE_CONTENT;return NO_REQUEST;}return GET_REQUEST;}else if (strncasecmp(text, "Connection:", 11) == 0){text += 11;text += strspn(text, " \t");if (strcasecmp(text, "keep-alive") == 0){m_linger = true;}}else if (strncasecmp(text, "Content-length:", 15) == 0){text += 15;text += strspn(text, " \t");m_content_length = atol(text);}else if (strncasecmp(text, "Host:", 5) == 0){text += 5;text += strspn(text, " \t");m_host = text;}else{LOG_INFO("oop!unknow header: %s", text);}return NO_REQUEST;
}
- 以下是解析內容體的函數,因為在該項目中,客戶端主要傳輸的對象很簡單,就只有輸入的用戶名和密碼。
如果,你希望在這個項目的基礎上繼續改進,一個主要的改進方向就是這個,你可以上傳文件、圖片等等。
那么既然客戶端有上次內容,那么服務器肯定需要對內容做一個回應或者處理,那么就引出了接下來的do_request() 函數
//判斷http請求是否被完整讀入
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{if (m_read_idx >= (m_content_length + m_checked_idx)){text[m_content_length] = '\0';//POST請求中最后為輸入的用戶名和密碼m_string = text;return GET_REQUEST;}return NO_REQUEST;
}
-
do_request() 函數因為比較長,所以在這里就不直接粘貼出來了,大家對照著源碼來學習吧。
-
有請求報文就會有響應報文,響應報文主要根據請求報文返回的狀態來定義,在process()函數中,響應報文函數為process_write()函數
該函數的作用主要返回幾種狀態碼,如404、200等,這些狀態碼的含義之前也已經敘述了,大家可以翻到上面去看。
bool http_conn::process_write(HTTP_CODE ret)
{switch (ret){case INTERNAL_ERROR:{add_status_line(500, error_500_title);add_headers(strlen(error_500_form));if (!add_content(error_500_form))return false;break;}case BAD_REQUEST:{add_status_line(404, error_404_title);add_headers(strlen(error_404_form));if (!add_content(error_404_form))return false;break;}case FORBIDDEN_REQUEST:{add_status_line(403, error_403_title);add_headers(strlen(error_403_form));if (!add_content(error_403_form))return false;break;}case FILE_REQUEST:{add_status_line(200, ok_200_title);if (m_file_stat.st_size != 0){add_headers(m_file_stat.st_size);m_iv[0].iov_base = m_write_buf;m_iv[0].iov_len = m_write_idx;m_iv[1].iov_base = m_file_address;m_iv[1].iov_len = m_file_stat.st_size;m_iv_count = 2;bytes_to_send = m_write_idx + m_file_stat.st_size;return true;}else{const char *ok_string = "<html><body></body></html>";add_headers(strlen(ok_string));if (!add_content(ok_string))return false;}}default:return false;}m_iv[0].iov_base = m_write_buf;m_iv[0].iov_len = m_write_idx;m_iv_count = 1;bytes_to_send = m_write_idx;return true;
}
這些基本上就是http的整個運行的邏輯框架了,大家有什么不懂的,評論區見~