伺服器專案實戰與總結(五)

白雪兒發表於2022-03-08

伺服器專案實戰與總結(五)

阻塞和非阻塞、同步和非同步

 

 

 

 

 

 同步:是應用程式自己主動讀取的,是從核心中的TCP接收緩衝區的資料主動搬到使用者區,比如recv/read函式。

非同步:不是應用程式自己主動讀取的,應用程式只需要告訴作業系統通訊方式等,然後作業系統搬運資料到使用者區,並通知應用程式它把資料搬運好了。

 

Unix、Linux上的五種IO模型

 

 

 

a. 阻塞

呼叫者呼叫了某個函式,等待這個函式返回,期間什麼也不做,不停的去檢查這個函式有沒有返回,必須等這個函式返回才能進行下一步動作。

 

 

 

b.非阻塞(NIO)

非阻塞等待,每隔一段時間就去檢測IO事件是否就緒。沒有就緒就可以做其他事。非阻塞I/O執行系統呼叫總是立即返回,不管事件是否已經發生,若事件沒有發生,則返回-1,此時可以根據 errno 區分這兩種情況,對於accept,recv 和 send,事件未發生時,errno 通常被設定成 EAGAIN。

 

 

 

c.IO複用

非阻塞等待,每隔一段時間就去檢測IO事件是否就緒。沒有就緒就可以做其他事。非阻塞I/O執行系統呼叫總是立即返回,不管事件是否已經發生,若事件沒有發生,則返回-1,此時可以根據 errno 區分這兩種情況,對於accept,recv 和 send,事件未發生時,errno 通常被設定成 EAGAIN。

 

 

 d.訊號驅動

Linux 用套介面進行訊號驅動 IO,安裝一個訊號處理函式,程式繼續執行並不阻塞,當IO事件就緒,進
程收到SIGIO 訊號,然後處理 IO 事件。

核心在第一個階段是非同步,在第二個階段是同步;與非阻塞IO的區別在於它提供了訊息通知機制,不需
要使用者程式不斷的輪詢檢查,減少了系統API的呼叫次數,提高了效率。

 

 

 e.非同步

Linux中,可以呼叫 aio_read 函式告訴核心描述字緩衝區指標和緩衝區的大小、檔案偏移及通知的方
式,然後立即返回,當核心將資料拷貝到緩衝區後,再通知應用程式。

 

 

 

 

 

 

Web伺服器簡介及HTTP協議

一個 Web Server 就是一個伺服器軟體(程式),或者是執行這個伺服器軟體的硬體(計算機)。其主
要功能是通過 HTTP 協議與客戶端(通常是瀏覽器(Browser))進行通訊,來接收,儲存,處理來自
客戶端的 HTTP 請求,並對其請求做出 HTTP 響應,返回給客戶端其請求的內容(檔案、網頁等)或返
回一個 Error 資訊。

 

 

 

通常使用者使用 Web 瀏覽器與相應伺服器進行通訊。在瀏覽器中鍵入“域名”或“IP地址:埠號”,瀏覽器則
先將你的域名解析成相應的 IP 地址或者直接根據你的IP地址向對應的 Web 伺服器傳送一個 HTTP 請
求。這一過程首先要通過 TCP 協議的三次握手建立與目標 Web 伺服器的連線,然後 HTTP 協議生成針
對目標 Web 伺服器的 HTTP 請求報文,通過 TCP、IP 等協議傳送到目標 Web 伺服器上。

HTTP協議(應用層的協議)

 

 

 

 

 

 

 

 

 

https協議預設埠是443

HTTP 請求報文格式

 

 

 

例如,HTTP請求頭部原始資訊:

 1 GET /topics/391887078 HTTP/2
 2 Host: bbs.csdn.net
 3 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0
 4 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
 5 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
 6 Accept-Encoding: gzip, deflate, br
 7 Referer: https://www.baidu.com/link?url=NZx_amw_JwKNfYbX5eBYtLsusVpiKpup9kp5fn7G5m5y6T0Je5Xdw8j4Vw2erCor&wd=&eqid=b590649600029c9000000005621f30a1
 8 Connection: keep-alive
 9 Cookie: uuid_tt_dd=10_37405684820-1640156095208-848782; log_Id_pv=43; Hm_lvt_6bcd52f51e9b3dce32bec4a3997715ac=1646229342,1646381047,1646724951,1646724980; Hm_up_6bcd52f51e9b3dce32bec4a3997715ac=%7B%22islogin%22%3A%7B%22value%22%3A%221%22%2C%22scope%22%3A1%7D%2C%22isonline%22%3A%7B%22value%22%3A%221%22%2C%22scope%22%3A1%7D%2C%22isvip%22%3A%7B%22value%22%3A%220%22%2C%22scope%22%3A1%7D%2C%22uid_%22%3A%7B%22value%22%3A%22qq_59374912%22%2C%22scope%22%3A1%7D%7D; Hm_ct_6bcd52f51e9b3dce32bec4a3997715ac=6525*1*10_37405684820-1640156095208-848782!5744*1*qq_59374912; log_Id_view=422; log_Id_click=15; __gads=ID=6a47c56ca9b87d48-22fa344982cf0021:T=1640156105:RT=1640156105:S=ALNI_MbXxcGg5d8Q8Gk0uY7yiJkJlILCjw; ssxmod_itna=eqfhGIxRhGkDCzDXbRx0InqDq7I5GQ=G3qW=beDl=CYxA5D8D6DQeGTbu5Cb3=HYjEh3h1jADnKFkBA+uRdkFt7f3pkmDb4GLDmKDySj10FDx1q0rD74irDDxD3ExWKDwDlKDgDQKZyExDaDGck302kDimHSr5sDiH5ot0DXxG1DQ5DsrGIklKD06OvkDDd33opuY4DUDRDj94LeCrqqDi3fmKzBx0OD09sDme7l2fCyDGa4vA0I1LF5CpdsxKDmh5DyhgkyeBka2SKDjukavrgDGHKbmcbOE0xYAm41Gb5cLnq=GXP6VVKDDW5B4VkD4D==; ssxmod_itna2=eqfhGIxRhGkDCzDXbRx0InqDq7I5GQ=G3qW=D6h92D0H9K03G1=2je6qN27u5SSnTK=12m=ArLqZS1gbd42Dn+0m6ja6vbiW64pgnUHXi1jbzYatAMyAyC+=I6Llmzal2s38=15aI0kxX+TjxAfj1SqmqZT6/b+393Qb5y4RRlu3plT3d2DR03BbVBrQySWbzD=aVAMxYPSn4uCGqXbKVUaoO6ZbsVefrvbhdOWbmG+efATEqRltpY0WSWtLeHL2u8ELm=RX7iBMkMGkf+/6EbWOuK4PDKkwFD7=DekqxD==; UserName=qq_59374912; UserInfo=5407bd51ea8f4231b7daaea5cb1b87d9; UserToken=5407bd51ea8f4231b7daaea5cb1b87d9; UserNick=%E5%AE%9E%E5%B9%B2375; AU=1B5; UN=qq_59374912; BT=1641803856440; p_uid=U000000; dc_sid=186a79b8d2fbe0fa93b76b7d5315051b; c_pref=https%3A//www.baidu.com/link; c_ref=https%3A//www.baidu.com/link; c_first_ref=www.baidu.com; c_first_page=https%3A//bbs.csdn.net/topics/391887078; c_segment=14; Hm_lpvt_6bcd52f51e9b3dce32bec4a3997715ac=1646724980; c_dl_prid=1646211467077_827614; c_dl_rid=1646211629296_539584; c_dl_fref=https://www.baidu.com/link; c_dl_fpage=/download/zsf250/10880412; c_dl_um=distribute.pc_aggpage_search_result.none-task-download-2%7Eaggregatepage%7Efirst_rank_ecpm_v1%7Erank_v31_ecpm-1-10880412.pc_agg_new_rank; csrfToken=2vdK7sXuLQapQ9JA68jKqDfd; dc_session_id=10_1646548189085.802620; dc_tos=r8f14j; c_page_id=default; FCNEC=[["AKsRol-GBWj_ZDmUuf7GGbQB1YuX6J-HDa1B1I-ytK-zIMCkE1mtQgjQ28bO9HJNF87kKMUQCuzsKtiOAGWX4dqza2ErJx3HxQCdz_-8DFKoP7wHFBPoWlCTmOqHJtkOJhbH_0_VJHXhmL8MAFwf4QlHl3y7Sf1WNg=="],null,[]]
10 Upgrade-Insecure-Requests: 1
11 Sec-Fetch-Dest: document
12 Sec-Fetch-Mode: navigate
13 Sec-Fetch-Site: cross-site
14 Cache-Control: max-age=0

 

HTTP響應報文格式

 

 

 例如:

 1 HTTP/2 200 OK
 2 server: openresty
 3 date: Tue, 08 Mar 2022 07:43:54 GMT
 4 content-type: text/html; charset=utf-8
 5 vary: Accept-Encoding
 6 x-response-time: 118
 7 x-xss-protection: 1; mode=block
 8 x-content-type-options: nosniff
 9 x-download-options: noopen
10 x-readtime: 118
11 strict-transport-security: max-age=31536000
12 content-encoding: gzip
13 X-Firefox-Spdy: h2

 

HTTP請求方法

HTTP/1.1 協議中共定義了八種方法(也叫“動作”)來以不同方式操作指定的資源:

1. GET:向指定的資源發出“顯示”請求。使用 GET 方法應該只用在讀取資料,而不應當被用於產生“副
作用”的操作中,例如在 Web Application 中。其中一個原因是 GET 可能會被網路蜘蛛等隨意訪
問。
2. HEAD:與 GET 方法一樣,都是向伺服器發出指定資源的請求。只不過伺服器將不傳回資源的本文
部分。它的好處在於,使用這個方法可以在不必傳輸全部內容的情況下,就可以獲取其中“關於該
資源的資訊”(元資訊或稱後設資料)。
3. POST:向指定資源提交資料,請求伺服器進行處理(例如提交表單或者上傳檔案)。資料被包含
在請求本文中。這個請求可能會建立新的資源或修改現有資源,或二者皆有。
4. PUT:向指定資源位置上傳其最新內容。
5. DELETE:請求伺服器刪除 Request-URI 所標識的資源。
6. TRACE:回顯伺服器收到的請求,主要用於測試或診斷。
7. OPTIONS:這個方法可使伺服器傳回該資源所支援的所有 HTTP 請求方法。用'*'來代替資源名稱,
向 Web 伺服器傳送 OPTIONS 請求,可以測試伺服器功能是否正常運作。
8. CONNECT:HTTP/1.1 協議中預留給能夠將連線改為管道方式的代理伺服器。通常用於SSL加密服
務器的連結(經由非加密的 HTTP 代理伺服器)。

 

補充,面試考:get和post請求的區別

GET比POST更不安全,因為引數直接暴露在URL上,所以不能用來傳遞敏感資訊。

 

GET和POST還有一個重大區別,簡單的說:

GET產生一個TCP資料包;POST產生兩個TCP資料包。

長的說:

對於GET方式的請求,瀏覽器會把http header和data一併傳送出去,伺服器響應200(返回資料);

而對於POST,瀏覽器先傳送header,伺服器響應100 continue,瀏覽器再傳送data,伺服器響應200 ok(返回資料)。

也就是說,GET只需要汽車跑一趟就把貨送到了,而POST得跑兩趟,第一趟,先去和伺服器打個招呼“嗨,我等下要送一批貨來,你們開啟門迎接我”,然後再回頭把貨送過去。

因為POST需要兩步,時間上消耗的要多一點,看起來GET比POST更有效。因此Yahoo團隊有推薦用GET替換POST來優化網站效能。但這是一個坑!跳入需謹慎。為什麼?

1. GET與POST都有自己的語義,不能隨便混用。

2. 據研究,在網路環境好的情況下,發一次包的時間和發兩次包的時間差別基本可以無視。而在網路環境差的情況下,兩次包的TCP在驗證資料包完整性上,有非常大的優點。

3. 並不是所有瀏覽器都會在POST中傳送兩次包,Firefox就只傳送一次。

 

HTTP狀態碼

 

 

 

伺服器程式設計基本框架和兩種高效的事件處理模式

伺服器程式設計基本框架

雖然伺服器程式種類繁多,但其基本框架都一樣,不同之處在於邏輯處理。

 

 

 

 

  兩種高效的事件處理模式

伺服器程式通常需要處理三類事件:I/O 事件、訊號及定時事件。有兩種高效的事件處理模式:Reactor
和 Proactor,同步 I/O 模型通常用於實現 Reactor 模式,非同步 I/O 模型通常用於實現 Proactor 模式。

Reactor模式

要求主執行緒(I/O處理單元)只負責監聽檔案描述符上是否有事件發生,有的話就立即將該事件通知工作
執行緒(邏輯單元),將 socket 可讀可寫事件放入請求佇列,交給工作執行緒處理。除此之外,主執行緒不做
任何其他實質性的工作。讀寫資料,接受新的連線,以及處理客戶請求均在工作執行緒中完成。
使用同步 I/O(以 epoll_wait 為例)實現的 Reactor 模式的工作流程是:
1. 主執行緒往 epoll 核心事件表中註冊 socket 上的讀就緒事件。
2. 主執行緒呼叫 epoll_wait 等待 socket 上有資料可讀。
3. 當 socket 上有資料可讀時, epoll_wait 通知主執行緒。主執行緒則將 socket 可讀事件放入請求佇列。
4. 睡眠在請求佇列上的某個工作執行緒被喚醒,它從 socket 讀取資料,並處理客戶請求,然後往 epoll
核心事件表中註冊該 socket 上的寫就緒事件。
5. 當主執行緒呼叫 epoll_wait 等待 socket 可寫。
6. 當 socket 可寫時,epoll_wait 通知主執行緒。主執行緒將 socket 可寫事件放入請求佇列。
7. 睡眠在請求佇列上的某個工作執行緒被喚醒,它往 socket 上寫入伺服器處理客戶請求的結果。

 

Reactor 模式的工作流程:

 

 Proactor模式

Proactor 模式將所有 I/O 操作都交給主執行緒和核心來處理(進行讀、寫),工作執行緒僅僅負責業務邏
輯。使用非同步 I/O 模型(以 aio_read 和 aio_write 為例)實現的 Proactor 模式的工作流程是:
1. 主執行緒呼叫 aio_read 函式向核心註冊 socket 上的讀完成事件,並告訴核心使用者讀緩衝區的位置,
以及讀操作完成時如何通知應用程式(這裡以訊號為例)。
2. 主執行緒繼續處理其他邏輯。
3. 當 socket 上的資料被讀入使用者緩衝區後,核心將嚮應用程式傳送一個訊號,以通知應用程式資料
已經可用。
4. 應用程式預先定義好的訊號處理函式選擇一個工作執行緒來處理客戶請求。工作執行緒處理完客戶請求
後,呼叫 aio_write 函式向核心註冊 socket 上的寫完成事件,並告訴核心使用者寫緩衝區的位置,以
及寫操作完成時如何通知應用程式。
5. 主執行緒繼續處理其他邏輯。
6. 當使用者緩衝區的資料被寫入 socket 之後,核心將嚮應用程式傳送一個訊號,以通知應用程式資料
已經傳送完畢。
7. 應用程式預先定義好的訊號處理函式選擇一個工作執行緒來做善後處理,比如決定是否關閉 socket。

Proactor 模式的工作流程:

 

 模擬 Proactor 模式

使用同步 I/O 方式模擬出 Proactor 模式。原理是:主執行緒執行資料讀寫操作,讀寫完成之後,主執行緒向
工作執行緒通知這一”完成事件“。那麼從工作執行緒的角度來看,它們就直接獲得了資料讀寫的結果,接下
來要做的只是對讀寫的結果進行邏輯處理。
使用同步 I/O 模型(以 epoll_wait為例)模擬出的 Proactor 模式的工作流程如下:
1. 主執行緒往 epoll 核心事件表中註冊 socket 上的讀就緒事件。
2. 主執行緒呼叫 epoll_wait 等待 socket 上有資料可讀。
3. 當 socket 上有資料可讀時,epoll_wait 通知主執行緒。主執行緒從 socket 迴圈讀取資料,直到沒有更
多資料可讀,然後將讀取到的資料封裝成一個請求物件並插入請求佇列。
4. 睡眠在請求佇列上的某個工作執行緒被喚醒,它獲得請求物件並處理客戶請求,然後往 epoll 核心事
件表中註冊 socket 上的寫就緒事件。
5. 主執行緒呼叫 epoll_wait 等待 socket 可寫。
6. 當 socket 可寫時,epoll_wait 通知主執行緒。主執行緒往 socket 上寫入伺服器處理客戶請求的結果。

同步 I/O 模擬 Proactor 模式的工作流程:

 

 

執行緒同步機制類封裝及執行緒池實現

執行緒池

執行緒池是由伺服器預先建立的一組子執行緒,執行緒池中的執行緒數量應該和 CPU 數量差不多。執行緒池中的所
有子執行緒都執行著相同的程式碼。當有新的任務到來時,主執行緒將通過某種方式選擇執行緒池中的某一個子
執行緒來為之服務。相比與動態的建立子執行緒,選擇一個已經存在的子執行緒的代價顯然要小得多。至於主
執行緒選擇哪個子執行緒來為新任務服務,則有多種方式:
主執行緒使用某種演算法來主動選擇子執行緒。最簡單、最常用的演算法是隨機演算法和 Round Robin(輪流
選取)演算法,但更優秀、更智慧的演算法將使任務在各個工作執行緒中更均勻地分配,從而減輕伺服器
的整體壓力。
主執行緒和所有子執行緒通過一個共享的工作佇列來同步,子執行緒都睡眠在該工作佇列上。當有新的任
務到來時,主執行緒將任務新增到工作佇列中。這將喚醒正在等待任務的子執行緒,不過只有一個子線
程將獲得新任務的”接管權“,它可以從工作佇列中取出任務並執行之,而其他子執行緒將繼續睡眠在
工作佇列上。

 

執行緒池的一般模型為:

 

 

程式碼

locker.c

  1 #ifndef LOCKER_H
  2 #define LOCKER_H
  3 
  4 #include <exception>
  5 #include <pthread.h>
  6 #include <semaphore.h>
  7 
  8 // 執行緒同步機制封裝類
  9 
 10 // 互斥鎖類
 11 class locker {
 12 public:
 13     locker() {
 14         if(pthread_mutex_init(&m_mutex, NULL) != 0) {
 15             throw std::exception();
 16         }
 17     }
 18 
 19     ~locker() {
 20         pthread_mutex_destroy(&m_mutex);
 21     }
 22 
 23     bool lock() {
 24         return pthread_mutex_lock(&m_mutex) == 0;
 25     }
 26 
 27     bool unlock() {
 28         return pthread_mutex_unlock(&m_mutex) == 0;
 29     }
 30 
 31     pthread_mutex_t *get()
 32     {
 33         return &m_mutex;
 34     }
 35 
 36 private:
 37     pthread_mutex_t m_mutex;
 38 };
 39 
 40 
 41 // 條件變數類
 42 class cond {
 43 public:
 44     cond(){
 45         if (pthread_cond_init(&m_cond, NULL) != 0) {
 46             throw std::exception();
 47         }
 48     }
 49     ~cond() {
 50         pthread_cond_destroy(&m_cond);
 51     }
 52 
 53     bool wait(pthread_mutex_t *m_mutex) {
 54         int ret = 0;
 55         ret = pthread_cond_wait(&m_cond, m_mutex);
 56         return ret == 0;
 57     }
 58     bool timewait(pthread_mutex_t *m_mutex, struct timespec t) {
 59         int ret = 0;
 60         ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
 61         return ret == 0;
 62     }
 63     bool signal() {
 64         return pthread_cond_signal(&m_cond) == 0;
 65     }
 66     bool broadcast() {
 67         return pthread_cond_broadcast(&m_cond) == 0;
 68     }
 69 
 70 private:
 71     pthread_cond_t m_cond;
 72 };
 73 
 74 
 75 // 訊號量類
 76 class sem {
 77 public:
 78     sem() {
 79         if( sem_init( &m_sem, 0, 0 ) != 0 ) {
 80             throw std::exception();
 81         }
 82     }
 83     sem(int num) {
 84         if( sem_init( &m_sem, 0, num ) != 0 ) {
 85             throw std::exception();
 86         }
 87     }
 88     ~sem() {
 89         sem_destroy( &m_sem );
 90     }
 91     // 等待訊號量
 92     bool wait() {
 93         return sem_wait( &m_sem ) == 0;
 94     }
 95     // 增加訊號量
 96     bool post() {
 97         return sem_post( &m_sem ) == 0;
 98     }
 99 private:
100     sem_t m_sem;
101 };
102 
103 #endif

 

threadpoll.c

  1 #ifndef THREADPOOL_H
  2 #define THREADPOOL_H
  3 
  4 #include <list>
  5 #include <cstdio>
  6 #include <exception>
  7 #include <pthread.h>
  8 #include "locker.h"
  9 
 10 // 執行緒池類,將它定義為模板類是為了程式碼複用,模板引數T是任務類
 11 template<typename T>
 12 class threadpool {
 13 public:
 14     /*thread_number是執行緒池中執行緒的數量,max_requests是請求佇列中最多允許的、等待處理的請求的數量*/
 15     threadpool(int thread_number = 8, int max_requests = 10000);
 16     ~threadpool();
 17     bool append(T* request);
 18 
 19 private:
 20     /*工作執行緒執行的函式,它不斷從工作佇列中取出任務並執行之*/
 21     static void* worker(void* arg);
 22     void run();
 23 
 24 private:
 25     // 執行緒的數量
 26     int m_thread_number;  
 27     
 28     // 描述執行緒池的陣列,大小為m_thread_number    
 29     pthread_t * m_threads;
 30 
 31     // 請求佇列中最多允許的、等待處理的請求的數量  
 32     int m_max_requests; 
 33     
 34     // 請求佇列
 35     std::list< T* > m_workqueue;  
 36 
 37     // 保護請求佇列的互斥鎖
 38     locker m_queuelocker;   
 39 
 40     // 是否有任務需要處理
 41     sem m_queuestat;
 42 
 43     // 是否結束執行緒          
 44     bool m_stop;                    
 45 };
 46 
 47 template< typename T >
 48 threadpool< T >::threadpool(int thread_number, int max_requests) : 
 49         m_thread_number(thread_number), m_max_requests(max_requests), 
 50         m_stop(false), m_threads(NULL) {
 51 
 52     if((thread_number <= 0) || (max_requests <= 0) ) {
 53         throw std::exception();
 54     }
 55 
 56     m_threads = new pthread_t[m_thread_number];
 57     if(!m_threads) {
 58         throw std::exception();
 59     }
 60 
 61     // 建立thread_number 個執行緒,並將他們設定為脫離執行緒。
 62     for ( int i = 0; i < thread_number; ++i ) {
 63         printf( "create the %dth thread\n", i);
 64         if(pthread_create(m_threads + i, NULL, worker, this ) != 0) {
 65             delete [] m_threads;
 66             throw std::exception();
 67         }
 68         
 69         if( pthread_detach( m_threads[i] ) ) {
 70             delete [] m_threads;
 71             throw std::exception();
 72         }
 73     }
 74 }
 75 
 76 template< typename T >
 77 threadpool< T >::~threadpool() {
 78     delete [] m_threads;
 79     m_stop = true;
 80 }
 81 
 82 template< typename T >
 83 bool threadpool< T >::append( T* request )
 84 {
 85     // 操作工作佇列時一定要加鎖,因為它被所有執行緒共享。
 86     m_queuelocker.lock();
 87     if ( m_workqueue.size() > m_max_requests ) {
 88         m_queuelocker.unlock();
 89         return false;
 90     }
 91     m_workqueue.push_back(request);
 92     m_queuelocker.unlock();
 93     m_queuestat.post();
 94     return true;
 95 }
 96 
 97 template< typename T >
 98 void* threadpool< T >::worker( void* arg )
 99 {
100     threadpool* pool = ( threadpool* )arg;
101     pool->run();
102     return pool;
103 }
104 
105 template< typename T >
106 void threadpool< T >::run() {
107 
108     while (!m_stop) {
109         m_queuestat.wait();
110         m_queuelocker.lock();
111         if ( m_workqueue.empty() ) {
112             m_queuelocker.unlock();
113             continue;
114         }
115         T* request = m_workqueue.front();
116         m_workqueue.pop_front();
117         m_queuelocker.unlock();
118         if ( !request ) {
119             continue;
120         }
121         request->process();
122     }
123 
124 }
125 
126 #endif

 

  專案整體流程程式碼實現

 

 

 

 

參考連結:https://www.cnblogs.com/logsharing/p/8448446.html

 

相關文章