文章目錄
- 項目介紹
- 項目模塊和服務器主要設計模式
- 項目主要流程
- 前置知識
- 1.bind函數
- 2.定時器任務TimerTask和時間輪思想TimerWheel
- 3.正則表達式
- 4.通用型容器Any類
- 服務器設計模式
- 1)單Reactor單線程模式
- 2)單Reactor多線程模式
- 3)多Reactor多線程模式(本次項目主要借鑒的模式)
- 項目實現目標框架
- 一、Server服務器模塊
- 1)Buffer模塊
- 2)Socket模塊
- 知識點:開啟地址重用
- 3)Channel模塊
- 4)Poller模塊
- 5)EventLoop模塊
- 系統接口eventfd——線程間的事件通知
- 6)TimerTask和TimerWheel模塊
- 7) Connection模塊
- 8)Acceptor模塊
- 9)LoopThread模塊
- 線程間的同步和互斥知識點
- 10)LoopThreadPool模塊
- 二、HTTP協議模塊
- 設計思想
- 應用層協議:Http服務器設計
- 2)HttpRequest模塊
- 3)HttpResponse模塊
- 4)HttpContext對Http請求進行解析模塊
- HttpServer模塊:對前面幾個模塊的功能整合
- 遇到的問題
- 1.vector定義在_capacity之后,導致vector在使用時沒有開空間而直接使用[]訪問導致空間使用錯誤問題。
- 2.再調試Buffer類的接口函數時,預期結果是從i = 1打印到i = 300
- 3.
- 4.對Connection聯合調試時,未對_socket進行初始化,導致后續無法recv
- 5. 請求方法必須是Get/Head方法
- 項目涉及到的知識點
- md5sum + 文件名
- 上傳大文件
- 多態
- 給服務器上強度
- 1.長鏈接請求測試
- 2.超時鏈接釋放測試
- 3.數據中多條請求處理測試
- 4.通過Put方法上傳大文件測試
項目介紹
通過多Reactor多線程:多I/O多路復用+線程池(業務處理)的處理模式,搭建高并發服務器組件。
主Reactor線程監控監聽描述符,獲取新建連接,隨后分發給從屬Reactor進行通信事件監控,從Reactor監控各自描述符的讀寫事件,一旦事件觸發,則接收數據分發給Work線程池,Work線程池分配獨立的線程進行業務處理,處理完畢后,將響應交給子Reactor線程進行數據響應。
利用CPU多核資源達到并發業務處理的目的。
項目模塊和服務器主要設計模式
關于整個項目,分為兩大模塊
一、Server服務器模塊
二、應用層協議支持的的協議模塊。
項目主要流程
項?的主要流程:
- 在實例化TcpServer對象過程中,完成BaseLoop的設置,Acceptor對象的實例化,以及EventLoop線程池的實例化,以及std::shared_ptr的hash表的實例化。
- 為Acceptor對象設置回調函數:獲取到新連接后,為新連接構建Connection對象,設置
Connection的各項回調,并使?shared_ptr進?管理,并添加到hash表中進?管理,并為
Connection選擇?個EventLoop線程,為Connection添加?個定時銷毀任務,為Connection添加
事件監控 - 啟動BaseLoop。
- 通過Poller模塊對當前模塊管理內的所有描述符進?IO事件監控,有描述符事件就緒后,通過描述符對應的Channel進?事件處理。
- 所有就緒的描述符IO事件處理完畢后,對任務隊列中的所有操作順序進?執?。
- 由于epoll的事件監控,有可能會因為沒有事件到來?持續阻塞,導致任務隊列中的任務不能及時得到執?,因此創建了eventfd,添加到Poller的事件監控中,?于實現每次向任務隊列添加任務的時候,通過向eventfd寫?數據來喚醒epoll的阻塞。
- 實現向Channel提供可讀,可寫,錯誤等不同事件的IO事件回調函數,然后將Channel和對應的描述符添加到Poller事件監控中。
- 當描述符在Poller模塊中就緒了IO可讀事件,則調?描述符對應Channel中保存的讀事件處理函
數,進?數據讀取,將socket接收緩沖區全部讀取到Connection管理的??態接收緩沖區中。然
后調?由組件使?者傳?的新數據到來回調函數進?處理。 - 組件使?者進?數據的業務處理完畢后,通過Connection向使?者提供的數據發送接?,將數據
寫?Connection的發送緩沖區中。 - 啟動描述符在Poll模塊中的IO寫事件監控,就緒后,調?Channel中保存的寫事件處理函數,將發送緩沖區中的數據通過Socket進??向系統的實際數據發送。
前置知識
1.bind函數
bind函數一般用來綁定一些回調函數。
一般在各個模塊中,比如該項目下的Channel模塊,EventLoop模塊,Connection模塊等。
都需要監聽獲取到新鏈接,并對鏈接進行事件監控管理,一旦觸發新事件,就要對事件進行具體的處理,如數據接收發送解析處理等,業務處理等。
為了讓各個模塊更清晰明確地完成這些模塊的任務,就把具體的數據處理,事件處理的具體操作用具體的接口完成,在這些模塊中只需要綁定具體的接口即可完成數據處理。
這樣就降低了代碼直接的耦合,讓各個模塊直接的功能更加清晰。
bind函數使用案例
// function<返回值類型(參數一類型,參數二類型,...)> 函數名 = [選擇值傳遞,引用傳遞等](參數類型 形參名,參數類型 形參名, ...) -> 返回值類型{
// ...
// };
//參數1:綁定一個函數,參數2:傳遞該函數的參數,std::placeholders::_1 可以用來傳遞更多參數(但只能是int類型)
//bind (Fn&& fn, Arge& args,..) //使用案例
void print(const std::string& str, int num)
{std::cout << str << num << std::endl;
}int main()
{using Task = std::function<void()>; std::vector<Task> array;array.push_back(std::bind(print,"hello",10));array.push_back(std::bind(print,"hello world",10));array.push_back(std::bind(print,"nihao",10));array.push_back(std::bind(print,"bite",10));for(auto & func : array){func();}return 0;
}
2.定時器任務TimerTask和時間輪思想TimerWheel
在當前的并發服務器中,必須要重視一個問題:客戶端連接上服務器后,長時間不進行數據通信,但是也不關閉,就會造成資源空耗。
所以就需要設置定時任務,定時將超時的連接進行釋放。
下面是定時任務對象TimerTask類,用于封裝定時任務。
時間輪思想:
單獨創建一個定時器,并給該定時器設置一個超時時間,時間到了之后該定時器會去遍歷服務器的所有鏈接,檢測哪些連接是超時的,但是如果有上萬個鏈接,這樣無疑效率非常低。
所以就引進時間輪的算法來優化,提高效率。
定義?個數組,并且有?個指針,指向數組起始位置,這個指針每秒鐘向后?動?步,?到哪?,則代表哪?的任務該被執?了,那么如果我們想要定?個3s后的任務,則只需要將任務添加到tick+3位置,則每秒中??步,三秒鐘后tick?到對應位置,這時候執?對應位置的任務即可。
上述操作也有?些缺陷,?如我們如果要定義?個60s后的任務,則需要將數組的元素個數設置
為60才可以,如果設置??時后的定時任務,則需要定義3600個元素的數組。
因此,可以采?多層級的時間輪,有秒針輪,分針輪,時針輪, 60<time<3600則time/60就是分針輪
對應存儲的位置,當tick/3600等于對應位置的時候,將其位置的任務向分針,秒針輪進?移動。
復雜思考:
具體一個task任務放到定時器中的實例圖:
但是編寫時間輪模塊類還需要注意一個問題:
TimerTask定時任務對象和TimerWheel時間輪的聯合詳細解析圖:
3.正則表達式
使用示例
regex_match(src,matches,e);
src:源字符串
matches:容器
e:匹配規則
#include <iostream>
#include <string>
#include <regex>int main()
{std::string src = "/abcd/1234";//匹配規則std::regex e("/abcd/(\\d+)");//匹配以 /abcd/ 開始,后面跟的一個或多個數字字符的字符串,并在匹配過程中提取這個字符串// "\\"表示轉義字符,如果是 "\d+",這樣'\d'就是轉義字符了// d 表示后面匹配一個數字字符,d+匹配的是多個數字字符,直到匹配到第一個非數字字符為止std::smatch matches; //存放匹配成功的字符串的容器//第一個是src字符串本身,后面就是匹配成功的字符串。bool ret = std::regex_match(src,matches,e); if(ret == false)return -1;for(auto &s: matches){std::cout << s << std::endl;}return 0;
}
使用正則表達式對HTTP進行請求
- 1.提取匹配方法,GET|HEAD|PUSH|PUT|DELETE 等等
- 2.提取HTTP資源路徑。從請求方法之后的"空格"開始直到遇到"?"結束,這段字符串就是資源路徑。
- 3.提取查詢字符串。也就是提取"?“之后的字符串,直到空格截止(不要”?")
- 4.提取協議版本
int main()
{// HTTP請求行格式 "GET /bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1\r\n"std::string str = "GET /bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1";std::smatch matches; //存儲容器//請求方法的匹配: GET HEAD POST PUT DELETE ..std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) (\\HTTP/1\\.[01])"); // "."匹配任意非\r\n的單個字符,"*"表示能匹配前面的"."0次或多次//很明顯,這里就是匹配除了GET之后的所有字符// [^?] :匹配非"?"字符,*表示匹配0次或多次// \\?(.*) 匹配一個以"?"開始的字符(不要這個問號)并持續匹配直到遇到空格,注意有一個空格 // \\?(.*)與 (\\?.*)不同,第一個不要"?"了,第二個卻要"?"// (\\HTTP/1\\.[01]): 匹配HTTP/1. [01]:表示匹配0或者1 , "."表示匹配任意非\r\n的單個字符,但是這里我們想要的是真正的"."// 那就要使用 "\\."轉義成單個"."bool ret = std::regex_match(str,matches,e);if(ret == false)return -1;for(auto &s : matches)std::cout << s << std::endl;return 0;
}
4.通用型容器Any類
在Connection對鏈接進行管理時,不可避免地會涉及到獲取應用層協議信息,并進行解析和處理。d但是應用層協議有那么多種,我們不能只單獨實現針對一個協議的代碼,只能實現一個通用類型的容器來保存該這些應用層協議的請求和解析。
這個容器必須能保存各種不同類型結構的數據。
設計思想:
- 1.設計一個模板類,但是通過該類實例化對象的時候要傳遞模板參數,如:Any< int > a = 10…
- 我就是因為不知道數據類型菜設計的Any類,顯然這種方法行不通。
- 同樣設計一個類,只是這個類中再設計一個模板類,讓這個模板類存儲數據
class Any
{template<class T>class placeholder{private:T _val;};
};
但是這樣實例化對象的時候,同樣要傳遞模板參數。
所以,還應該設計一個父類holder,讓placeholder繼承該類,通過holder類的指針或者引用,即可訪問placeholder類的成員函數。即可實現Any類只存儲一個holder類的指針,就能訪問到存儲的數據。
Any類的設計思想:
具體實現:
class Any
{
private:class holder //父類,用來被繼承的{public:virtual ~holder() {}virtual const std::type_info& type() = 0 ; virtual holder* clone() = 0 ; };//子類才是用來保存數據的template<class T>class placeholder : public holder //子類繼承父類{public:placeholder(const T& val) : _val(val) {} //內置類型不做處理,自定義類型T會調用它的拷貝構造// 獲取子類對象的數據類型virtual const std::type_info& type() { return typeid(T); } // 針對當前子類對象自身,克隆一個新的子類對象virtual holder* clone() { return new placeholder(_val); }public:T _val;};holder* _content;public:Any() :_content(nullptr) {}template<class T>Any(const T& val) : _content(new placeholder<T>(val)) {} //根據數據類型構造一個Any對象Any(const Any& other) : _content(other._content == nullptr ? nullptr : other._content->clone()) {} //拷貝構造//這里的拷貝構造不能直接復制一個other地址,而是要復制other對象內的父類指針指向的子類對象(placeholder)的地址~Any() {delete _content;}Any& swap(Any& other) //加了const后,other的_content是const修飾的,而原本的_content是可修改的//這樣就會出現權限放大的情況{std::swap(_content, other._content); return *this; //返回對象本身是為了支持連續swap操作/賦值操作}template<class T> T* get() //返回子類對象保存的數據的指針{//我當前保存的數據類型與想要返回的數據類型不匹配assert(typeid(T) == _content->type()) ; //條件成立繼續,不成立報錯return &(((placeholder<T>*)_content)->_val);// _content是一個父類指針,先轉化成子類.}template<class T>Any& operator=(const T& val) //賦值運算符重載{Any(val).swap(*this); //為val構造一個臨時Any通用容器對象,然后與自身Any容器的_content指針交換//交換后不會再關心自身容器的_content了,因為交換之后,臨時對象出了作用域會調用析構函數進行銷毀//此時保留的就是val構造出來的_content;return *this; // 為了支持連續賦值}Any& operator=(const Any& other){Any(other).swap(*this);//調用拷貝構造為other對象構造一個新的臨時Any對象,用該對象與我自身的_content指針交換,交換后//原來_content指針保存的數據就跟隨臨時對象調用析構銷毀了//這樣既不影響到other,又能銷毀原指針指向的數據。return *this; //支持連續的賦值重載}
服務器設計模式
服務器使用Reactor的事件監聽和分發處理分離的設計模式。
簡單解釋就是:誰觸發了我的事件,我就處理誰。
下面有三種模式介紹,但本次項目最主要采用的是第三種模式:多Reactor多線程模式。
1)單Reactor單線程模式
優點:所有操作均在單一線程完成,實現較為簡單,沒有線程間競爭問題。
缺點:無法有效利用CPU多核資源,容易達到性能瓶頸。
2)單Reactor多線程模式
優點:利用了CPU多核的優勢,提高了性能,將業務處理和IO過程分離,降低了代碼的耦合度。
缺點:單Reactor線程包含了對客戶端進行監聽和響應,在高并發情況下,仍然是一個串行化過程,也就是在每一時刻都有多個客戶端發起請求,單Reactor線程需要處理完上一個客戶端的請求才能處理下個客戶端的請求,只有處理完請求時,才能處理下一波客戶端的新連接,容易達到性能瓶頸。
3)多Reactor多線程模式(本次項目主要借鑒的模式)
優點:充分利用CPU多核資源,主從Reactor各司其職。
缺點:執行流不是越多越好,如果執行流過多,反而會增加CPU對線程切換調度的成本。
項目實現目標框架
該項目要實現的是一個基于Reactor模式下的
one thread one loop式的高并發服務器。簡單理解就是一個Reactor從屬線程處理:
- 1.IO事件監控
- 2.IO事件處理
- 3.業務處理
也就是在多Reactor多線程的基礎上,刪掉專門進行業務處理的線程池,將IO事件的監聽,處理以及業務處理集為一體,由從屬Reactor線程一并處理。
下面來介紹Server服務器模塊的各個子功能模塊。
一、Server服務器模塊
該模塊就是對所有連接和線程進行管理。
具體分為:
- 1.監聽連接管理
- 2.通信連接管理
- 3.超時連接管理
1)Buffer模塊
下面是Buffer模塊的設計思路:
2)Socket模塊
Socket模塊是對socket套接字的封裝,該模塊實現了socket套接字的各項操作。
知識點:開啟地址重用
端口重用選項(SO_REUSEADDR)允許在同一地址和端口上綁定多個套接字,常用于多線程服務器和快速重啟服務器程序。
/* int setsockopt(int socket,int level,int opt_name,void* opt_val,socklen_t opt_len);
*/void ReuseAddress(){int val = 1;//SOL_SOCKET是對套接字操作級別int ret = setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR,(void*)&val,sizeof(val));val = 1;int ret1 = setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT,(void*)&val,sizeof(val));}
3)Channel模塊
Channel模塊相當于是為socket監控到的文件描述符而生的。
Channel模塊類設計思想
需要注意的是:Channel模塊并不是獨立的,由于要對文件描述符對應的連接進行事件監控,所以要調用Poller模塊的功能進行具體的事件監控。
4)Poller模塊
Poller是用來監聽管理Channel的,相當于所有一對一服務員的總經理。
監控IO事件,相當于把所有IO事件文件描述符都放到一個具體的容器中管理起來。
Poller模塊的底層數據結構就是使用紅黑樹將一個個要監控的時間添加進紅黑樹的節點中。
Poller模塊設計思路:
5)EventLoop模塊
系統接口eventfd——線程間的事件通知
總結:
向eventfd返回的文件描述符寫入數據時,會觸發事件通知,如果其他進程或線程正在等待(例如通過 poll()、select() 或 epoll() 監聽該文件描述符),它們會被喚醒。
讀取eventfd返回的文件符的數據時,會消費事件。
EventLoop模塊可以理解成上邊所說的Reactor模塊,它是對Poller模塊,TimerQueue模塊,
Socket模塊的?個整體封裝,進?所有描述符的事件監控。
同時,EventLoop模塊一定是一個EventLoop對應一個Connection連接。
Eventoop模塊為了保證線程安全,因此必須保證整個Connection連接的所有操作都要在同一線程下執行。
由于EventLoop模塊監控了所有事件,且添加了上面的TimerWheel時間輪,所以可以為每個鏈接都設置一個非活躍鏈接定時銷毀任務,只要鏈接在規定時間內沒有進行通信,就釋放該鏈接,避免鏈接空耗資源。
如何保證一個Connection連接的所有操作都放在同一個線程中執行?
將該鏈接的所有操作都放入到一個QueueTask任務隊列中,由EventLoop進行管理。
當事件處理完成后,再在EventLoop對應的線程中執行任務隊列的操作。
但是,因為EventLoop的所有事件監控都交給了Poller模塊中的epoll監控,如果沒有事件就緒,epoll就會阻塞住,則任務隊列中的任務就不能執行。
為了解決這個問題,eventfd的作用就出現了:當向任務隊列中添加任務時,就可以通過向eventfd中寫入數據來喚醒正在阻塞的epoll,從而讓任務池的操作能夠執行。
EventLoop具體操作流程:
- EventLoop的Poller模塊對當前管理的所有文件描述符進行事件監控,當有事件到來時,將事件分發給文件描述符對應的Channel進行事件處理。
- 當所有的就緒事件都處理完畢后,對任務隊列QueueTask中的所有任務進行執行。
- 由于epoll的事件監控,有可能因為沒有事件到來而阻塞,導致任務不能有效執行。所以創建了eventfd,添加到Poller的事件監控中,每次向任務隊列中添加任務時,通過向eventfd寫入數據,來喚醒阻塞的epoll。
EventLoop聯合其他模塊的思想流程圖:
6)TimerTask和TimerWheel模塊
7) Connection模塊
Connection模塊是對Buffer,Socket,Channel模塊進行整合并封裝,實現整體對套接字的管理。
Connection模塊為每一個服務器Accept到的新鏈接都設置一個Connection對象進行管理。
對鏈接的所有具體操作,都是通過這個模塊提供的功能完成。
在Connection中管理的幾個部分:
- 1.套接字管理
- 2.連接的事件管理
- 3.緩沖區管理,將Socket模塊接收到的數據拿過來放到我當下的緩沖區模塊中。
- 4.協議上下文的管理(Any類)(有可能請求還不完整,或者請求的正文還沒有發送完,則將數據保存到Any類中,等待下次請求發送完成后再取出來進行解析。
- 5.回調函數的管理,設置各種回調函數,然后交給Channel模塊具體執行。
Connection模塊設計思想:
為什么要將Conenction模塊交給shared_ptr進行管理呢?
因為一旦要對Connection鏈接進行操作,而Connection鏈接已經釋放,其他地方并不知道Connection釋放了,這就導致內存錯誤訪問。
使用shared_ptr對Connection進行操作的時候,保存了一份shared_ptr,因此就算其他地方進行了釋放操作,也只是對shared_ptr的計數器-1.并不會導致Connection釋放。
實現操作:讓Connection繼承std::enable_shared_from_this<Connection>
,這樣在對Connection操作時,就可以安全地傳遞一份shared_ptr<Connection>
。
Connection模塊聯合其他模塊的功能關系流程圖:
注意,Connection模塊也不是獨立的模塊,它不僅僅對上述模塊整合,由于該服務器是多Reactor多線程模式,有多線程就涉及到線程安全問題,所以有些操作必須只能在線程內執行,所以就關聯到EventLoop模塊。
8)Acceptor模塊
設計思想:
9)LoopThread模塊
線程間的同步和互斥知識點
線程的同步就是當多個線程獲取同一個共享資源時,需要進行排隊,不能每個線程都并發獲取同一個共享資源。所以線程的同步就是線程排隊獲取共享資源。
線程間的互斥,當我這個線程在申請共享資源時,你的線程就不能申請共享資源。
這樣實現了一個共享資源(臨界資源)只能同時有一個線程申請。這就是線程間的互斥。
可以看到,線程同步和互斥是同時存在的。
10)LoopThreadPool模塊
設計思想:
二、HTTP協議模塊
需要注意的是HTTP協議是?個運?在TCP協議之上的應?層協議,這?點本質上是告訴我們,HTTP服務器其實就是個TCP服務器,只不過在應?層基于HTTP協議格式進?數據的組織和解析來明確客?端的請求并完成業務處理。
因此實現HTTP服務器簡單理解,只需要以下?步即可
- 搭建?個TCP服務器,接收客戶端請求。
- 以HTTP協議格式進?解析請求數據,明確客戶端?的。
- 明確客戶端請求?的后提供對應服務。
- 將服務結果?HTTP協議格式進?組織,發送給客戶端
設計思想
應用層協議:Http服務器設計
2)HttpRequest模塊
3)HttpResponse模塊
4)HttpContext對Http請求進行解析模塊
這個模塊是?個HTTP請求接收的上下?模塊,主要是為了防?在?次接收的數據中,不是?個完整的HTTP請求,則解析過程并未完成,?法進?完整的請求處理,需要在下次接收到新數據后繼續根據上下?進?解析,最終得到?個HttpRequest請求信息對象,因此在請求數據的接收以及解析部分需要?個上下?來進?控制接收和處理節奏。
HttpServer模塊:對前面幾個模塊的功能整合
遇到的問題
1.vector定義在_capacity之后,導致vector在使用時沒有開空間而直接使用[]訪問導致空間使用錯誤問題。
2.再調試Buffer類的接口函數時,預期結果是從i = 1打印到i = 300
然而卻出現了斷層現象。
經過調試發現,
在獲取write前沿空間大小時,代碼寫錯了,應該用size的。
此時混淆了capacity()和size()函數的區別
以及忘記了reserve()和resize()函數的具體用法。
遂立即復習。
3.
4.對Connection聯合調試時,未對_socket進行初始化,導致后續無法recv
知識復習:在類的成員函數后面加上const,表示告訴編譯器我不會在函數內部對this指針進行修改。
5. 請求方法必須是Get/Head方法
在對http服務器進行測試時。
在判斷一個資源是否是靜態資源的時候,請求的方法應該必須是GET方法或者是HEAD方法。
所以在邏輯判斷時:
if(req._method != “GET” && req._method != “HEAD”)
如果請求方法不是GET方法且不是HEAD方法,則判斷錯誤,return false;
而我的代碼寫成了
if(req._method != “GET” || req._method != “HEAD”)
意味著就算該方法是GET方法,同樣滿足req._method != "HEAD"這個判斷條件,也會退出。
同時還學到了如何進行調試,先根據正常的HTTP訪問的流程,客戶端對服務器訪問發送數據時,服務器肯定會對到來的新鏈接設置回調處理,然后對HTTP請求進行解析,然后響應給客戶端。
根據這個流程看調用了哪些函數,分別進入了哪些函數,然后就打印一些調試信息一個個函數地查看是哪個函數的邏輯出現了問題!!
項目涉及到的知識點
md5sum + 文件名
對一個文件進行大量算法計算,最后得出一個標準字符串md5,如果兩個文件的md5值是一樣的,則說明這兩個文件的內容一模一樣,只要兩個文件內容有一點點點不一樣,則得出的md5值也會大有不同。
上傳大文件
dd if=/dev/zero of=./hello.txt bs=1G count=1
多態
通用類型容器Any類,保存的是holder類的指針(引用也可以),而holder類被placeholder類繼承,所以holder類的指針可以訪問到placeholder類的成員函數,而讓placeholder類通過實例化對象專門用來保存任意類型的數據。
給服務器上強度
1.長鏈接請求測試
2.超時鏈接釋放測試
3.數據中多條請求處理測試
4.通過Put方法上傳大文件測試
性能測試采?webbench進?服務器性能測試。Webbench是知名的?站壓?測試?具,webbench的
標準測試可以向我們展?服務器的兩項內容: 每秒鐘相應請求數 和 每秒鐘傳輸數據量 ,即QPS和吞
吐。