好的,TinyWebServer我們講了八個模塊中的5個,還剩下數據庫mysql模塊,定時器timer模塊,日記log模塊。
(更新中~~~~~~)
mysql模塊
項目中有簡單的注冊和登錄功能,所以要使用到數據庫。那么mysql模塊就是數據庫相關的模塊,主要的其實就是數據庫連接池。
首先數據庫連接池是只有一個的,那么怎么保證從項目的每一個地方獲取都是這個唯一的一個數據庫連接池呢?欸,想到什么了?單例模式。在這里我們使用Cpp11下簡潔的靜態局部變量實現單例模式:
connection_pool* connection_pool::GetInstance() { static connection_pool connPool; return &connPool; }
原理是是C++11標准規定了當一個線程正在初始化一個變量的時候,其他線程必須得等到該初始化完成以后才能訪問它。
OK我們上面解決了池子的問題,接下來我們考慮到,WebServer要有一定的並發度,所以我們要有多個數據庫連接資源放在數據庫連接池,當任務線程需要數據庫連接的時候就向池子申請一個。好的,那么我們便有了一個問題:怎樣保證數據庫連接的線程安全?
我們先得有一個保存數據庫連接的數據結構list,當然我們得先保證池子(或者說list的安全)的線程安全,所以有一個池子的互斥鎖lock,然后我們保證list中連接資源的安全,所以再有一個信號量reserve,用於管理池子的空閑連接數。
有了這兩個池子接下來的就是比較常規的做法了:在獲取/歸還數據庫連接資源前先用互斥鎖對池子加鎖,然后用信號量保證list空閑連接資源數。

1 #include<stdio.h> 2 #include<mysql/mysql.h> 3 #include<string.h> 4 #include<list> 5 #include<pthread.h> 6 #include<iostream> 7 8 #include"sql_connection_pool.h" 9 10 using namespace std; 11 12 //構造函數 13 connection_pool::connection_pool() { 14 this->CurConn = 0; 15 this->FreeConn = 0; 16 this->MaxConn = 0; 17 } 18 19 //單例模式,靜態 20 connection_pool* connection_pool::GetInstance() { 21 static connection_pool connPool; 22 return &connPool; 23 } 24 25 //真正的初始化函數 26 void connection_pool::init(string url, string User, string PassWord, string DBName, int Port, unsigned int MaxConn) { 27 this->url = url; 28 this->Port = Port; 29 this->User = User; 30 this->PassWord = PassWord; 31 this->DatabaseName = DBName; 32 33 //先互斥鎖鎖住池子,創造MaxConn數據庫鏈接 34 lock.lock(); 35 for (int i = 0; i < MaxConn; i++) { 36 //新創建一個連接資源 37 MYSQL* con = NULL; 38 con = mysql_init(con); 39 40 if (con == NULL) { 41 cout << "mysqlinit Error:" << mysql_error(con)<<endl; 42 exit(1); 43 } 44 45 con = mysql_real_connect(con, url.c_str(), User.c_str(), PassWord.c_str(), DBName.c_str(), Port, NULL, 0); 46 if (con == NULL) { 47 cout << "mysql connect Error:" << mysql_error(con) << endl; 48 exit(1); 49 } 50 51 //把這個資源放入鏈表 52 connList.push_back(con); 53 ++FreeConn; 54 } 55 56 //初始化信號量和池子數量 57 reserve = sem(FreeConn); 58 this->MaxConn = FreeConn; 59 60 lock.unlock(); 61 } 62 63 //請求獲取一個連接資源 64 MYSQL* connection_pool::GetConnection() { 65 MYSQL* con = NULL; 66 67 if (connList.size() == 0) return NULL; 68 69 //請求一個資源,互斥鎖/信號量 准備 70 reserve.wait(); 71 lock.lock(); 72 73 con = connList.front(); //從鏈表頭取得一個資源 74 connList.pop_front(); 75 76 --FreeConn; 77 ++CurConn; 78 79 lock.unlock(); 80 return con; 81 } 82 83 //獲取空閑連接數 84 int connection_pool::GetFreeConn() { 85 return this->FreeConn; 86 } 87 88 //釋放當前連接資源con 89 bool connection_pool::ReleaseConnection(MYSQL* con) { 90 if (con == NULL) return false; 91 lock.lock(); 92 93 connList.push_back(con); 94 ++FreeConn; 95 --CurConn; 96 97 reserve.post(); 98 lock.unlock(); 99 100 return true; 101 } 102 103 //析構函數 104 connection_pool::~connection_pool() { 105 DestroyPool(); 106 } 107 108 //銷毀整個數據庫連接池 109 void connection_pool::DestroyPool() { 110 lock.lock(); 111 if (connList.size() > 0) { 112 list<MYSQL*>::iterator it; 113 for (it = connList.begin(); it != connList.end(); it++) { 114 MYSQL* con = *it; 115 mysql_close(con); //獲得每一個連接資源close掉 116 } 117 CurConn = 0; 118 FreeConn = 0; 119 connList.clear(); 120 } 121 lock.unlock(); 122 } 123 124 125 //連接池RAII 126 connectionRAII::connectionRAII(MYSQL** SQL, connection_pool* connPool) { 127 *SQL = connPool->GetConnection(); 128 conRAII = *SQL; 129 poolRAII = connPool; 130 } 131 connectionRAII::~connectionRAII() { 132 poolRAII->ReleaseConnection(conRAII); 133 }
timer模塊
定時器模塊的功能是定時檢查長時間無反應的連接,如果有服務器這邊就主動斷開連接。那么我們怎樣做到這一點呢?博主個人感覺有兩個關鍵問題:
① 定時器事件應該以一種怎樣的方式去觸發
這個問題其實很有意思,通常我們以前學習到處理信號的方式是把信號發生之后的要處理的邏輯全部放在信號的回調函數中。在這時候我們也許忽略了一個事實:在Linux環境下當我們回調一個信號的回調函數時候這段時間系統會忽略至少這個同樣的信號(這是當然的不然就有可能死循環等出錯),那么我們為了不讓這些被忽略的信號被忽略太久,我們得想盡辦法盡量縮短這個回調函數的執行時間。那么怎樣才能做到這樣呢?
一個理所當然的思路是:把回調函數的邏輯搬到主函數執行。那么怎樣做到這一點:統一事件源。原理很簡單,這時我們的信號回調函數不要處理邏輯,而是在回調函數中通過管道給主函數發送信息,那么當主函數監聽到讀時間並且判斷到是從管道讀端來的,那就知道這個信號到了我主函數應該處理了。
② 定時器以及應該以怎么樣的數據結構來保存
在游雙的《高性能服務器編程》這本書里面寫到三種定時器的存儲結構:鏈表、時間輪、時間堆。這個TinyWebServer使用的是最好實現的鏈表定時器。
我們有一個定時器結點類util_timer,每個結點表示一個客戶連接,它保存了雙向鏈表的前后指針,客戶數據client_data和回調函數。如果我們判斷到這個結點長時間無反應,所以我們調用這個回調函數傳入client_data,然后回調函數就會把這個客戶斷開,並且做一些善后工作。
我們還有鏈表類sort_timer_lst,這個鏈表是一個時間遞增的結點鏈表,即從鏈表頭到尾這個客戶的最后一次反應時間是遞增的。這個鏈表類當然有插入和刪除結點函數。並且還有adjust_timer調整鏈表位置函數,作用是當一個客戶有了反應,那么我們需要更新他的最后一次反應時間,那么為了維護鏈表的遞增特性,我們需要這么一個調整位置的函數。此外,這個類還有一個檢查函數(定時清掃),作用是我們上文提到統一了事件源,把信號回調函數邏輯搬到主函數執行,所以這個定時清掃檢查邏輯就是在這個檢查函數。主函數判斷到信號來了,就執行這個函數進行檢查鏈表中長時間無反應的結點進行清掃。

1 #ifndef LST_TIMER 2 #define LST_TIMER 3 4 #include<time.h> 5 #include<netinet/in.h> 6 7 class util_timer; 8 struct client_data 9 { 10 sockaddr_in address; 11 int sockfd; 12 util_timer* timer; //客戶對應的定時器,和25行相互 13 }; 14 15 //鏈表結點,包含事件和客戶數據 16 class util_timer 17 { 18 public: 19 util_timer() : prev(NULL),next(NULL) {} 20 21 public: 22 time_t expire; //記錄時間 23 //!!!定時器的執行函數,到時間就調用這個 24 void (*cb_func)(client_data*); 25 client_data* user_data; //客戶數據,和12行相互 26 util_timer* prev; //雙向鏈表 27 util_timer* next; //雙向鏈表 28 }; 29 30 31 //鏈表,按事件升序排序 32 class sort_timer_lst 33 { 34 public: 35 //鏈表的構造與析構函數 36 sort_timer_lst() : head(NULL), tail(NULL) {}; 37 ~sort_timer_lst() { 38 util_timer* tmp = head; 39 while (tmp) { 40 head = tmp->next; 41 delete tmp; 42 tmp = head; 43 } 44 } 45 //插入結點 46 void add_timer(util_timer* timer) { 47 if (!timer) return; 48 if (!head) { 49 head = tail = timer; 50 return; 51 } 52 //如果新的定時器超時時間小於當前頭部結點 53 //直接將當前定時器結點作為頭部結點 54 if (timer->expire < head->expire) { 55 timer->next = head; 56 head->prev = timer; 57 head = timer; 58 return; 59 } 60 61 //至少不是插入到頭,調用函數繼續插入 62 add_timer(timer, head); 63 } 64 //調整定時器,任務發生變化時,調整定時器在鏈表中的位置 65 void adjust_timer(util_timer* timer) { 66 if (!timer) return; 67 util_timer* tmp = timer->next; 68 69 //因為只會增加,所以如果在最后肯定無需調整 70 if (!tmp || (timer->expire < tmp->expire)) return; 71 72 //分兩種情況:頭/非頭。思路都是先刪除,再調用插入函數重新插入 73 if (timer == head) { 74 head = head->next; 75 head->prev = NULL; 76 timer->next = NULL; 77 add_timer(timer, head); 78 } 79 else { 80 timer->prev->next = timer->next; 81 timer->next->prev = timer->prev; 82 add_timer(timer, timer->next); 83 } 84 } 85 //刪除定時器 86 void del_timer(util_timer* timer) { 87 if (!timer) return; 88 //即整個鏈表就剩下一個結點,直接刪除 89 if (timer == head && timer == tail) { 90 delete timer; 91 head = NULL; 92 tail = NULL; 93 return; 94 } 95 //被刪除的定時器為頭結點 96 if (timer == head) { 97 head = head->next; 98 head->prev = NULL; 99 delete timer; 100 return; 101 } 102 //被刪除的是尾結點 103 if (timer == tail) { 104 tail = tail->prev; 105 tail->next = NULL; 106 delete timer; 107 return; 108 } 109 //不是頭尾,普通刪除 110 timer->prev->next = timer->next; 111 timer->next->prev = timer->prev; 112 delete timer; 113 return; 114 } 115 //定時任務處理函數 116 void tick() { 117 if (!head) return; 118 time_t cur = time(NULL); //獲取當前時間 119 util_timer* tmp = head; 120 while (tmp) 121 { 122 if (cur < tmp->expire) break; //就到這里了,后面的執行時間都還沒到 123 tmp->cb_func(tmp->user_data); //滿足超市條件,調用cb_func刪除連接 124 //執行完之后,刪除鏈表頭並移動頭 125 head = tmp->next; 126 if (head) 127 head->prev = NULL; 128 delete tmp; 129 tmp = head; 130 } 131 } 132 133 private: 134 //把timer插入到鏈表中,經過上面的檢測到這里至少不是插入到頭 135 void add_timer(util_timer* timer, util_timer* lst_head) { 136 util_timer* prev = lst_head; 137 util_timer* tmp = prev->next; 138 //遍歷當前結點之后的鏈表,按照超時時間找到目標定時器對應的位置,常規雙向鏈表插入操作 139 while (tmp) 140 { 141 //插入到prev后,tmp之前 142 if (timer->expire < tmp->expire) { 143 prev->next = timer; 144 timer->next = tmp; 145 tmp->prev = timer; 146 timer->prev = prev; 147 break; 148 } 149 prev = tmp; 150 tmp = tmp->next; 151 } 152 //上面沒有插入成功,證明要插入到最后面 153 if (!tmp) { 154 prev->next = timer; 155 timer->prev = prev; 156 timer->next = NULL; 157 tail = timer; 158 } 159 } 160 161 private: 162 util_timer* head; 163 util_timer* tail; 164 }; 165 166 167 #endif
log模塊
log是日志模塊,一個合格的服務器當然少不了日志來記錄錯誤異常等等信息。我們想設計一個日志模塊,他能順利寫日志但是又不要占用主線程時間去寫,所以我們設計異步寫日志的模塊。
怎么是異步寫日志呢?我們考慮設計一個日志隊列,這個隊列主要是用一個循環數組模擬隊列來存儲日志,這里要注意這個隊列只是存儲我們真正的目的是要寫到文件里,所以只是存儲並未達到目的。但是考慮到文件IO操作是比較慢的,所以我們采用的異步IO就是先寫到內存里,然后日志線程自己有空的時候寫到文件里。
所以這一模塊的關鍵就是日志隊列和寫日志的線程。
先來思考日志隊列,他的需求就是時不時會有一段日志塞到這個隊列中,又時不時會有這其中的一段日志被取出來,那么當然是隊列不滿才能往里塞,隊列不空才能有東西取出來。稍加思考這是什么?欸,就是經典的生產者消費者模型。所以也就沒什么好說的了,常規處理:要一個互斥鎖和信號量,操作前都加鎖就行。

1 #ifndef BLOCK_QUEUE_H 2 #define BLOCK_QUEUE_H 3 4 #include <iostream> 5 #include <stdlib.h> 6 #include <pthread.h> 7 #include <sys/time.h> 8 #include "../lock/locker.h" 9 using namespace std; 10 11 template <class T> 12 class block_queue 13 { 14 public: 15 block_queue(int max_size = 1000) 16 { 17 if (max_size <= 0) 18 { 19 exit(-1); 20 } 21 22 m_max_size = max_size; 23 m_array = new T[max_size]; 24 m_size = 0; 25 m_front = -1; 26 m_back = -1; 27 } 28 29 void clear() 30 { 31 m_mutex.lock(); 32 m_size = 0; 33 m_front = -1; 34 m_back = -1; 35 m_mutex.unlock(); 36 } 37 38 ~block_queue() 39 { 40 m_mutex.lock(); 41 if (m_array != NULL) 42 delete[] m_array; 43 44 m_mutex.unlock(); 45 } 46 //判斷隊列是否滿了 47 bool full() 48 { 49 m_mutex.lock(); 50 if (m_size >= m_max_size) 51 { 52 53 m_mutex.unlock(); 54 return true; 55 } 56 m_mutex.unlock(); 57 return false; 58 } 59 //判斷隊列是否為空 60 bool empty() 61 { 62 m_mutex.lock(); 63 if (0 == m_size) 64 { 65 m_mutex.unlock(); 66 return true; 67 } 68 m_mutex.unlock(); 69 return false; 70 } 71 //返回隊首元素 72 bool front(T& value) 73 { 74 m_mutex.lock(); 75 if (0 == m_size) 76 { 77 m_mutex.unlock(); 78 return false; 79 } 80 value = m_array[m_front]; 81 m_mutex.unlock(); 82 return true; 83 } 84 //返回隊尾元素 85 bool back(T& value) 86 { 87 m_mutex.lock(); 88 if (0 == m_size) 89 { 90 m_mutex.unlock(); 91 return false; 92 } 93 value = m_array[m_back]; 94 m_mutex.unlock(); 95 return true; 96 } 97 98 int size() 99 { 100 int tmp = 0; 101 102 m_mutex.lock(); 103 tmp = m_size; 104 105 m_mutex.unlock(); 106 return tmp; 107 } 108 109 int max_size() 110 { 111 int tmp = 0; 112 113 m_mutex.lock(); 114 tmp = m_max_size; 115 116 m_mutex.unlock(); 117 return tmp; 118 } 119 //往隊列添加元素,需要將所有使用隊列的線程先喚醒 120 //當有元素push進隊列,相當於生產者生產了一個元素 121 //若當前沒有線程等待條件變量,則喚醒無意義 122 bool push(const T& item) 123 { 124 125 m_mutex.lock(); 126 if (m_size >= m_max_size) 127 { 128 129 m_cond.broadcast(); 130 m_mutex.unlock(); 131 return false; 132 } 133 134 m_back = (m_back + 1) % m_max_size; 135 m_array[m_back] = item; 136 137 m_size++; 138 139 m_cond.broadcast(); 140 m_mutex.unlock(); 141 return true; 142 } 143 //pop時,如果當前隊列沒有元素,將會等待條件變量 144 bool pop(T& item) 145 { 146 147 m_mutex.lock(); 148 while (m_size <= 0) 149 { 150 151 if (!m_cond.wait(m_mutex.get())) 152 { 153 m_mutex.unlock(); 154 return false; 155 } 156 } 157 158 m_front = (m_front + 1) % m_max_size; 159 item = m_array[m_front]; 160 m_size--; 161 m_mutex.unlock(); 162 return true; 163 } 164 165 //增加了超時處理 166 bool pop(T& item, int ms_timeout) 167 { 168 struct timespec t = { 0, 0 }; 169 struct timeval now = { 0, 0 }; 170 gettimeofday(&now, NULL); 171 m_mutex.lock(); 172 if (m_size <= 0) 173 { 174 t.tv_sec = now.tv_sec + ms_timeout / 1000; 175 t.tv_nsec = (ms_timeout % 1000) * 1000; 176 if (!m_cond.timewait(m_mutex.get(), t)) 177 { 178 m_mutex.unlock(); 179 return false; 180 } 181 } 182 183 if (m_size <= 0) 184 { 185 m_mutex.unlock(); 186 return false; 187 } 188 189 m_front = (m_front + 1) % m_max_size; 190 item = m_array[m_front]; 191 m_size--; 192 m_mutex.unlock(); 193 return true; 194 } 195 196 private: 197 locker m_mutex; 198 cond m_cond; 199 200 T* m_array; 201 int m_size; 202 int m_max_size; 203 int m_front; 204 int m_back; 205 }; 206 207 #endif
那么剩下的就是寫日志線程,這一部分也比較簡單就是新建一個線程,這個線程不斷while當日志隊列有日志就從里面取出來寫到文件去,這個過程記得加鎖就行。

1 #include <string.h> 2 #include <time.h> 3 #include <sys/time.h> 4 #include <stdarg.h> 5 #include "log.h" 6 #include <pthread.h> 7 using namespace std; 8 9 Log::Log() 10 { 11 m_count = 0; 12 m_is_async = false; 13 } 14 15 Log::~Log() 16 { 17 if (m_fp != NULL) 18 { 19 fclose(m_fp); 20 } 21 } 22 //異步需要設置阻塞隊列的長度,同步不需要設置 23 bool Log::init(const char *file_name, int log_buf_size, int split_lines, int max_queue_size) 24 { 25 //如果設置了max_queue_size,則設置為異步 26 if (max_queue_size >= 1) 27 { 28 m_is_async = true; 29 m_log_queue = new block_queue<string>(max_queue_size); 30 pthread_t tid; 31 //flush_log_thread為回調函數,這里表示創建線程異步寫日志 32 pthread_create(&tid, NULL, flush_log_thread, NULL); 33 } 34 35 m_log_buf_size = log_buf_size; 36 m_buf = new char[m_log_buf_size]; 37 memset(m_buf, '\0', m_log_buf_size); 38 m_split_lines = split_lines; 39 40 time_t t = time(NULL); 41 struct tm *sys_tm = localtime(&t); 42 struct tm my_tm = *sys_tm; 43 44 45 const char *p = strrchr(file_name, '/'); 46 char log_full_name[256] = {0}; 47 48 if (p == NULL) 49 { 50 snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name); 51 } 52 else 53 { 54 strcpy(log_name, p + 1); 55 strncpy(dir_name, file_name, p - file_name + 1); 56 snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name); 57 } 58 59 m_today = my_tm.tm_mday; 60 61 m_fp = fopen(log_full_name, "a"); 62 if (m_fp == NULL) 63 { 64 return false; 65 } 66 67 return true; 68 } 69 70 void Log::write_log(int level, const char *format, ...) 71 { 72 struct timeval now = {0, 0}; 73 gettimeofday(&now, NULL); 74 time_t t = now.tv_sec; 75 struct tm *sys_tm = localtime(&t); 76 struct tm my_tm = *sys_tm; 77 char s[16] = {0}; 78 switch (level) 79 { 80 case 0: 81 strcpy(s, "[debug]:"); 82 break; 83 case 1: 84 strcpy(s, "[info]:"); 85 break; 86 case 2: 87 strcpy(s, "[warn]:"); 88 break; 89 case 3: 90 strcpy(s, "[erro]:"); 91 break; 92 default: 93 strcpy(s, "[info]:"); 94 break; 95 } 96 //寫入一個log,對m_count++, m_split_lines最大行數 97 m_mutex.lock(); 98 m_count++; 99 100 if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //everyday log 101 { 102 103 char new_log[256] = {0}; 104 fflush(m_fp); 105 fclose(m_fp); 106 char tail[16] = {0}; 107 108 snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday); 109 110 if (m_today != my_tm.tm_mday) 111 { 112 snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name); 113 m_today = my_tm.tm_mday; 114 m_count = 0; 115 } 116 else 117 { 118 snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines); 119 } 120 m_fp = fopen(new_log, "a"); 121 } 122 123 m_mutex.unlock(); 124 125 va_list valst; 126 va_start(valst, format); 127 128 string log_str; 129 m_mutex.lock(); 130 131 //寫入的具體時間內容格式 132 int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ", 133 my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, 134 my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s); 135 136 int m = vsnprintf(m_buf + n, m_log_buf_size - 1, format, valst); 137 m_buf[n + m] = '\n'; 138 m_buf[n + m + 1] = '\0'; 139 log_str = m_buf; 140 141 m_mutex.unlock(); 142 143 if (m_is_async && !m_log_queue->full()) 144 { 145 m_log_queue->push(log_str); 146 } 147 else 148 { 149 m_mutex.lock(); 150 fputs(log_str.c_str(), m_fp); 151 m_mutex.unlock(); 152 } 153 154 va_end(valst); 155 } 156 157 void Log::flush(void) 158 { 159 m_mutex.lock(); 160 //強制刷新寫入流緩沖區 161 fflush(m_fp); 162 m_mutex.unlock(); 163 }
日志模塊本身不難理解,其實難理解的是寫日志函數中的各種宏以及文件/字符串函數的靈活應用。
參考資料:
TinyWebServer項目地址:https://github.com/qinguoyi/TinyWebServer
單例模:https://light-city.club/sc/design_pattern/singleton/singleton/