TinyWebServer:一個Linux下C++輕量級Web服務器(中)


好的,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 }
mysql模塊

 

 

 

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
timer模塊

 

 

 

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

社長的系列講解博文:http://mp.weixin.qq.com/mp/homepage?__biz=MzAxNzU2MzcwMw==&hid=6&sn=353ef6eadc7a8daf9c82d005c15adcd2&scene=18#wechat_redirect

單例模:https://light-city.club/sc/design_pattern/singleton/singleton/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM