基于阻塞隊(duì)列生產(chǎn)者消費(fèi)者模型線程池的多線程Web服務(wù)器
代碼地址:WebServer_GitHub_Addr
README
摘要
本實(shí)驗(yàn)通過(guò)C++語(yǔ)言,實(shí)現(xiàn)了一個(gè)基于阻塞隊(duì)列線程池的多線程Web服務(wù)器。該服務(wù)器支持通過(guò)http協(xié)議發(fā)送報(bào)文,跨主機(jī)抓取服務(wù)器上特定資源。與此同時(shí),該Web服務(wù)器后臺(tái)通過(guò)C++語(yǔ)言,通過(guò)原生系統(tǒng)線程調(diào)用
pthread.h
,實(shí)現(xiàn)了一個(gè)基于阻塞隊(duì)列的線程池,該線程池支持手動(dòng)設(shè)置線程池中線程數(shù)量。與此同時(shí),該線程池通過(guò)維護(hù)任務(wù)隊(duì)列,每次Web服務(wù)器獲取請(qǐng)求時(shí),后臺(tái)服務(wù)器就會(huì)將特定請(qǐng)求對(duì)應(yīng)的特定任務(wù)加載到線程池中,等待線程池中線程的調(diào)用。由于在本項(xiàng)目中,每次的遠(yuǎn)端抓取都是短鏈接,因此在理論上,該Web服務(wù)器可以接收無(wú)數(shù)個(gè)請(qǐng)求。按照實(shí)驗(yàn)要求,本W(wǎng)eb服務(wù)器可以接受并解析 HTTP 請(qǐng)求,然后從服務(wù)器的文件系統(tǒng)中讀取被 HTTP 請(qǐng)求的文件,并根據(jù)該文件是否存在而向客戶端發(fā)送正確的響應(yīng)消息。
在服務(wù)端終端中,服務(wù)器能夠按照不同等級(jí)打印日志信息,并且在收到請(qǐng)求時(shí)打印報(bào)文信息。
執(zhí)行效果
定義代碼下./wwwroot
目錄為服務(wù)器資源的根目錄,定義./wwwroot/index.html
為服務(wù)器主頁(yè)。
- 當(dāng)客戶端請(qǐng)求資源存在時(shí),服務(wù)器將返回對(duì)應(yīng)資源。
- 當(dāng)客戶端請(qǐng)求路徑是
/
根目錄時(shí),服務(wù)器默認(rèn)返回主頁(yè)。 - 當(dāng)客戶端申請(qǐng)路徑不存在時(shí),服務(wù)端返回
./wwwroot/error/404.html
作為返回資源。
服務(wù)器開機(jī)
瀏覽器遠(yuǎn)端連接
服務(wù)器是騰訊云的遠(yuǎn)端服務(wù)器,瀏覽器是本地瀏覽器。這是一個(gè)跨局域網(wǎng)的跨主機(jī)測(cè)試。
客戶端申請(qǐng)不存在的資源
客戶端發(fā)送請(qǐng)求時(shí),后臺(tái)服務(wù)器所獲取到的報(bào)文
多個(gè)客戶端發(fā)起請(qǐng)求
該服務(wù)器理論上支持無(wú)數(shù)個(gè)短鏈接進(jìn)行請(qǐng)求
環(huán)境準(zhǔn)備
系統(tǒng): Linux version 3.10.0-1160.11.1.el7.x86_64
編譯器版本: gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
資源文件/源文件準(zhǔn)備目錄結(jié)構(gòu)
-
MyThreadPool
目錄維護(hù)手寫的線程池源代碼 -
./wwwroot/
目錄維護(hù)服務(wù)器資源,其中./wwwroot/
是客戶端訪問(wèn)的根目錄 - 其他文件均為HttpServer源代碼
RUN
服務(wù)默認(rèn)端口號(hào):8080
tips: 如果運(yùn)行過(guò)程中二進(jìn)制程序HttpServer沒(méi)有可執(zhí)行權(quán)限 chmod 755 HttpServer
生成可執(zhí)行:make clean;make
啟動(dòng)服務(wù)器:./HttpServer 8080
在瀏覽器中輸入:ip:port
即可訪問(wèn)服務(wù)器主頁(yè)
或輸入:ip:port/index.html
也可以訪問(wèn)服務(wù)器主頁(yè)
輸入不存在的路徑: 43:xxx:xxx:xxx:8080/a.txt
代碼詳解
Web服務(wù)器實(shí)現(xiàn)架構(gòu)
第一層封裝:對(duì)套接字的基本操作進(jìn)行封裝 Sock.hpp
/* 具體接口實(shí)現(xiàn)可以見源代碼 */
/* Sock.hpp */
class Sock
{
private:
const static int gbacklog = 20;
public:
Sock() {}
~Sock() {}
/* 以下接口內(nèi)部調(diào)用的都是套接字的底層調(diào)用 */
int Socket(); // 創(chuàng)建獲取套接字 -- 返回值: 監(jiān)聽套接字
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"); // 綁定
void Listen(int sock); // 設(shè)置監(jiān)聽狀態(tài)
int Accept(int listensock, std::string *ip, uint16_t *port); // 接收連接 -- 返回值: 服務(wù)套接字
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port);
};
第二層封裝:對(duì)Http服務(wù)器的封裝 HttpServer.hpp
- 通過(guò)維護(hù)
Task
類,可以把線程所需要執(zhí)行的動(dòng)作進(jìn)行封裝。通過(guò)Task
類的operator()()
重載,線程可以直接執(zhí)行內(nèi)部綁定好的方法。 - 通過(guò)維護(hù)
HttpServer
類,當(dāng)初始化并執(zhí)行HttpServer::Start()
后,啟動(dòng)線程池,Accept成功后,把相對(duì)應(yīng)的任務(wù)加載到線程池中,等待線程池中線程的調(diào)用。
#ifndef __Yufc_HttpServer
#define __Yufc_HttpServer
#include "Sock.hpp"
#include <functional>
#include "MyThreadPool/threadPool.hpp"
using func_t = std::function<void(int)>;
/* 任務(wù)類 */
struct Task
{
public:
func_t func__;
int sock__;
public:
Task() {}
Task(func_t func, int sock) : func__(func), sock__(sock) {}
void operator()()
{
func__(sock__);
}
};
/* http 服務(wù)器類 */
class HttpServer
{
private:
int __listen_sock;
uint16_t __port;
Sock __sock;
func_t __func;
yufc_thread_pool::ThreadPool<Task> *__thread_pool = yufc_thread_pool::ThreadPool<Task>::getThreadPool();
public:
HttpServer(const uint16_t &port, func_t func) : __port(port), __func(func);
~HttpServer();
void Start();
};
#endif
Http服務(wù)器的啟動(dòng) HttpServer.cc
執(zhí)行HttpServer.cc
后,main()
創(chuàng)建一個(gè)服務(wù)器對(duì)象指針,并調(diào)用其中的Start()
啟動(dòng)服務(wù)器。
void HandlerHttpRequest(int sockfd);
函數(shù)維護(hù)每個(gè)線程所要執(zhí)行的任務(wù),其中包括以下內(nèi)容。
- 讀取Http請(qǐng)求
- 解析Http報(bào)文
- 創(chuàng)建一個(gè)http響應(yīng)
- 發(fā)送響應(yīng)至客戶端
/* 一般http都要有自己的web根目錄 */
#define ROOT "./wwwroot"
/* 如果客戶端只請(qǐng)求了一個(gè)/ ,一般返回默認(rèn)首頁(yè) */
#define HOME_PAGE "index.html"
void HandlerHttpRequest(int sockfd); // 具體實(shí)現(xiàn)可見源代碼
int main(int argc, char **argv)
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
httpserver->Start();
return 0;
}
ulity.hpp
提供Util
類,里面提供static void cutString(const std::string &s, const std::string &sep, std::vector<std::string> *out)
接口。在解析http報(bào)文時(shí),可以把傳入的字符串s
按照間隔為sep
的方式進(jìn)行切割,并把結(jié)果放到out
里面。
Usage.hpp
提供Usage
函數(shù),作為HttpServer的使用手冊(cè)。
Log.hpp
提供void logMessage(int level, const char *format, ...)
函數(shù),負(fù)責(zé)打印服務(wù)器日志信息。
日志等級(jí)有:
DEBUG 調(diào)試
NORMAL 正常運(yùn)行
WARNING 警告 – 進(jìn)程繼續(xù)運(yùn)行
ERROR 非致命錯(cuò)誤 – 進(jìn)程繼續(xù)運(yùn)行
FATAL 致命錯(cuò)誤 – 進(jìn)程終止
線程池實(shí)現(xiàn)架構(gòu)
thread.hpp
- 對(duì)原生線程進(jìn)行了簡(jiǎn)單封裝
typedef void*(*func_t_)(void*); // 函數(shù)指針
class ThreadData
{
public:
void* __args;
std::string __name;
};
class Thread
{
private:
std::string __name; // 線程名字
pthread_t __tid; // 線程tid
func_t_ __func; // 線程要調(diào)用的函數(shù)
ThreadData __tdata; // 線程數(shù)據(jù)
public:
Thread(int num, func_t_ callback, void* args);
// num-自定義的線程編號(hào) callback-線程要執(zhí)行的任務(wù) args-callback的參數(shù)
~Thread();
void start();
void join();
std::string name(); // 返回線程名字
};
lockGuard.hpp
- 用RAII的鎖的封裝風(fēng)格對(duì)互斥鎖進(jìn)行封裝
//封裝一個(gè)鎖
class Mutex
{
private:
pthread_mutex_t *__pmtx;
public:
Mutex(pthread_mutex_t *mtx)
:__pmtx(mtx){}
~Mutex()
{}
public:
void lock() // 加鎖
{
pthread_mutex_lock(__pmtx);
}
void unlock() // 解鎖
{
pthread_mutex_unlock(__pmtx);
}
};
class lockGuard
{
public:
lockGuard(pthread_mutex_t *mtx)
:__mtx(mtx)
{
__mtx.lock();
}
~lockGuard()
{
__mtx.unlock();
}
private:
Mutex __mtx;
};
threadPool.hpp
線程池整體架構(gòu)
#define _DEBUG_MODE_ false
const int g_thread_num = 3; // 默認(rèn)3個(gè)線程
/* 具體實(shí)現(xiàn)可見源代碼 */
namespace yufc_thread_pool
{
template <class T>
class ThreadPool
{
private:
std::vector<Thread *> __threads; // 線程們
int __num; // 線程數(shù)量
std::queue<T> __task_queue; // 任務(wù)隊(duì)列
pthread_mutex_t __lock; // 互斥鎖
pthread_cond_t __cond; // 條件變量
static ThreadPool<T> *thread_ptr; // 單例模式
static pthread_mutex_t __mutexForPool; // 保護(hù)getThreadPool對(duì)互斥鎖
private:
//構(gòu)造放成私有的 -- 讓線程池成為單例模式
ThreadPool(int thread_num = g_thread_num);
ThreadPool(const ThreadPool<T>& other) = delete;
const ThreadPool<T>& operator=(const ThreadPool<T>& other) = delete;
public:
~ThreadPool();
public:
static ThreadPool<T>* getThreadPool(int num = g_thread_num); // 懶漢模式--獲取線程池
void run(); // 啟動(dòng)線程池
void pushTask(const T &task); // 向線程池添加任務(wù)
static void *routine(void *args); // 線程要做的事
public:
// 需要一批,外部成員訪問(wèn)內(nèi)部屬性的接口提供給static的routine,不然routine里面沒(méi)法加鎖
// 下面這些接口,都是沒(méi)有加鎖的,因?yàn)槲覀冋J(rèn)為,這些函數(shù)被調(diào)用的時(shí)候,都是在安全的上下文中被調(diào)用的
// 因?yàn)檫@些函數(shù)調(diào)用之前,已經(jīng)加鎖了,調(diào)用完,lockGuard自動(dòng)解鎖
pthread_mutex_t *getMutex();
void waitCond(); // 等待條件變量就緒
bool isEmpty(); // 判斷任務(wù)隊(duì)列是否為空
T getTask(); // 獲取一個(gè)任務(wù)
};
template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;
template <typename T>
pthread_mutex_t ThreadPool<T>::__mutexForPool = PTHREAD_MUTEX_INITIALIZER;
//static/全局可以這樣初始化,這把鎖是用來(lái)保護(hù)getThreadPool的
}
實(shí)現(xiàn)的一些具體細(xì)節(jié)
Http報(bào)文解析
因?yàn)槲覀兪盏降亩际莌ttp請(qǐng)求,因此,對(duì)http請(qǐng)求報(bào)文進(jìn)行解析,可以獲得客戶端信息。
以下是一段完整的http報(bào)文
GET /index.html HTTP/1.1
Host: 43.xxx.xxx.xxx:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
因?yàn)槲覀冃枰崛】蛻粜枰L問(wèn)的資源,因此我們需要提取客戶輸入的資源路徑。
http報(bào)文的第一行的第二個(gè)字段,則表示客戶端需要提取資源的路徑。
與此同時(shí),http報(bào)文每行的間隔是特殊字符串\r\n
。
因此在HttpServer.cc
中的HandlerHttpRequest
函數(shù)中,我們首先把http報(bào)文中的每一行分出來(lái),并把第一行的第二個(gè)字符串提取出來(lái),就能得到資源的路徑。
單例模式的線程池
在本次實(shí)驗(yàn)中,我們希望一個(gè)進(jìn)程只能產(chǎn)生一個(gè)線程池,因此我們把線程池設(shè)置成單例模式。這是一個(gè)懶漢方式的單例模式。
**懶漢:**第一次需要這個(gè)對(duì)象的時(shí)候構(gòu)建該對(duì)象。
**餓漢:**在main()
執(zhí)行之前構(gòu)建該對(duì)象。
-
把構(gòu)造函數(shù)私有化
-
通過(guò)
static ThreadPool<T>* getThreadPool(int num = g_thread_num)
來(lái)獲取線程池在這個(gè)接口中,如果執(zhí)行流是第一次執(zhí)行該函數(shù),則該函數(shù)會(huì)構(gòu)造一個(gè)線程池對(duì)象并返回它的指針。如果執(zhí)行流不是第一次執(zhí)行該函數(shù),則該函數(shù)則會(huì)返回
this
,即自己。
另外,為了保證線程池運(yùn)行時(shí)的線程安全,在線程池中多個(gè)操作中添加了互斥鎖對(duì)操作進(jìn)行保護(hù)。
實(shí)驗(yàn)結(jié)果分析和思考
改進(jìn)部分:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-466035.html
- 實(shí)現(xiàn)多執(zhí)行流,支持多個(gè)客戶端進(jìn)行連接。
- 封裝線程池,使得服務(wù)器可以并行地對(duì)http請(qǐng)求進(jìn)行響應(yīng)
不足部分:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-466035.html
- 給客戶端返回對(duì)報(bào)文是靜態(tài)網(wǎng)頁(yè),暫時(shí)沒(méi)有實(shí)現(xiàn)可以支持多個(gè)客戶端之間共同交互的功能
- 回應(yīng)報(bào)文比較粗糙,可以進(jìn)一步美化。
- 可以進(jìn)一步優(yōu)化服務(wù)器,把服務(wù)器進(jìn)程設(shè)置成守護(hù)進(jìn)程,讓它長(zhǎng)期執(zhí)行并提供服務(wù)。
- 沒(méi)有模擬丟包的情況
到了這里,關(guān)于Web服務(wù)器實(shí)現(xiàn)|基于阻塞隊(duì)列線程池的Http服務(wù)器|線程控制|Http協(xié)議的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!