C++高性能服務器TinyWebServer擴展開發-Redis數據庫連接池/模擬冪等接口


簡介:

本項目對TinyWebServer輕量級C++服務器項目(https://github.com/qinguoyi/TinyWebServer)進行了功能拓展,支持Redis后台服務與會話管理;並在此基礎上進行了新業務功能測試,包括設置超時Tokens與重復操作校驗機制,來模擬冪等接口。

項目代碼:DeshZhao/TinyWebServerRedis (github.com)

開發環境:

阿里雲ECS服務器:

實例:1核 2GB共享計算型 n4系列 III
I/O 優化實例:I/O 優化實例
系統盤:高效雲盤/dev/xvda40GB模塊屬性
帶寬:1Mbps按固定帶寬
CPU:1核
可用區:隨機分配
操作系統:CentOS 8.0 64位Linux64位
內存:2GB
 
點擊安全組->安全組規則:
查看阿里雲提供的服務器手動端口配置列表,增加端口信息:

 其中9006是默認的測試端口,6379是redis后台服務端口,還有workbench相關端口;

服務器框架:
包含 Epoll網絡端口監聽與超時事件定時器,任務線程池,Http主從狀態機,Redis數據庫連接池,同步/異步日志系統,Web前端,測試系統等:
事件處理IO設計模式:reactor/Proactor
核心差異:
1.讀寫分工:reactor的讀寫由事件處理器負責;Proactor模式讀寫由操作系統內核負責;
2.同步異步:reactor工作在同步模式,不間斷同步處理事件;Proactor理論上工作在異步模式(模擬異步),支持多任務並發執行;
多線程Reactor結構:
 
 
 

 

Proactor模式事件處理器關注的是已經存入業務緩沖區的讀完成事件,啟動處理方法后控制權將由內核歸還給事件分離器。

本服務器支持LT/ET兩種觸發方式,以及Reactor/Proactor兩種工作方式;默認啟動選項的IO方式是Proactor模式下ET方式來模擬異步Proactor模式,因為Proactor模式下的操作系統需要自帶異步API才可以實現異步的內核讀寫;該種組合是最高性能組合,底層實現是同步非阻塞的Epoll機制。Epoll通過同時動態維護一個雙向隊列(就緒隊列,存儲就緒態IO流)與紅黑樹(節點存儲socket對象句柄),來監控多個socket上的讀寫事件並根據數據處理狀態生成可讀,可寫fd來通知內核:

以多路復用IO為例,Epoll數據流包括:socket端口數據發生變化時,ep_poll_callback被調用將數據從TCP緩沖區拷貝到內核,並通知內核,使用epoll_wait將就緒的socket添加到就緒隊列中,同時拷貝就緒socket到用戶空間(events數組),期間可以設置主進程阻塞/非阻塞或等待;epoll_wait返回一個int,代表發生狀態變化的fd總數,主函數可以根據這些fd的類型和攜帶信息,通過epoll_ctl來維護紅黑樹--即內核事件注冊表,並接下來的循環中進一步處理該事件:

對於Epoll原理以及基礎操作,可以參考Epoll的本質(內部實現原理) - looyee - 博客園 (cnblogs.com)以及圖解 | epoll怎么實現的 - AlexCool-碼農的藝術 - 博客園 (cnblogs.com)等文章。

 

啟動框架:
 1     //命令行解析
 2     Config config;
 3     config.parse_arg(argc, argv);
 4 
 5     WebServer server;
 6 
 7     //初始化
 8     server.init(config.PORT, user, passwd, databasename, config.LOGWrite, 
 9                 config.OPT_LINGER, config.TRIGMode,  config.sql_num, config.redis_num, config.thread_num, 
10                 config.close_log, config.actor_model);
11     
12     //日志
13     server.log_write();
14 
15     //數據庫
16     server.sql_pool();
17     
18     server.redis_pool();
19 
20     //線程池
21     server.thread_pool();
22 
23     //觸發模式
24     server.trig_mode();
25 
26     //監聽
27     server.eventListen();
28 
29     //運行
30     server.eventLoop();

文件目錄詳見Github.

一,Redis后台服務連接池

准備工作: Redis安裝,后台服務啟動配置與登陸信息設置:
下載解壓redis6.0.5后進入文件conf/目錄,檢查是否存在redis.conf服務器配置文件;執行make:
1 make
2 make install

創建根目錄軟鏈接方便后台啟動與訪問redis:

1 cd /
2 ln -s redis-6.0.5 redis

前台啟動方式可以自行查找,配置后台啟動redis服務:

1 mkdir conf
2 mkdir data
3 cp redis.conf ./conf
4 cd conf
5 cat redis.conf | grep -v "#" | grep -v "^$" > redis-6379.conf
6 vim redis.conf

需要修改的關鍵字段包括:

1 daemonize yes //后台啟動
2 protected-mode no //設置為不受保護模式,遠程能夠連接;默認為yes
3 maxclients 0 //0表示無限制
4 requirepass password //password是redis登陸密碼

后台啟動操作:

1 redis-server conf/redis.conf

客戶端驗證:

1 redis-cli
2 127.0.0.1:6379>AUTH password
3 127.0.0.1:6379>INFO

編譯server可執行文件可能報錯,缺少libhiredis.so文件,要繼續增加軟鏈接:

 以及hiredis.c/hiredis.h相關頭文件復制到/usr/include內;

詳細Redis6.0以上版本安裝與啟動配置可以參考 CentOS8安裝redis6.2.4 - 知乎 (zhihu.com)
核心功能:
基礎組件:
                                                                                          Redis中間件
                                                                                       連接池與會話資源
                                                                                                     |
                                                                                                     |
socket->端口監聽+讀機制(非阻塞同步LT/ET) ->定時任務->任務隊列與線程池->http主從狀態機->寫機制(非阻塞同步LT/ET) ->socket
                                                                                        ---同步/異步日志---
 
線程池:
線程池是一種 靜態資源集合,本質上屬於服務器的硬件資源,在服務器啟動時就完成創建並初始化;處理客戶端請求時無需動態分配,處理結束之后無需釋放靜態資源,僅需歸還資源到池中;
主體包括任務隊列(生產者),工作線程(消費者)以及管理線程(管理者);核心代碼主要包含append(添加模板任務到任務隊列),worker/run(取出任務並調用模板方法執行)等成員函數;其中多線程對任務隊列的訪問屬於對共享資源的操作,需要競爭互斥鎖,來實現線程同步;
 1 template <typename T>
 2 threadpool<T>::threadpool( int actor_model, connection_pool *connPool, RedisConnectionPool *redisPool, int thread_number, int max_requests) : m_actor_model(actor_model),m_thread_number(thread_number), m_max_requests(max_requests), m_threads(NULL), m_connPool(connPool), m_redisPool(redisPool)
 3 {
 4     if (thread_number <= 0 || max_requests <= 0)
 5         throw std::exception();
 6     m_threads = new pthread_t[m_thread_number];  //消費者  7     if (!m_threads)
 8         throw std::exception();
 9     for (int i = 0; i < thread_number; ++i)
10     {
11         if (pthread_create(m_threads + i, NULL, worker, this) != 0)
12         {
13             delete[] m_threads;
14             throw std::exception();
15         }
16         if (pthread_detach(m_threads[i]))
17         {
18             delete[] m_threads;
19             throw std::exception();
20         }
21     }
22 }//線程池創建 23 template <typename T>
24 threadpool<T>::~threadpool()
25 {
26     delete[] m_threads;
27 }//線程池回收 28 template <typename T>
29 bool threadpool<T>::append(T *request, int state)
30 {
31     m_queuelocker.lock();
32     if (m_workqueue.size() >= m_max_requests)
33     {
34         m_queuelocker.unlock();
35         return false;
36     }
37     request->m_state = state;
38     m_workqueue.push_back(request); //生產者 39     m_queuelocker.unlock();
40     m_queuestat.post();
41     return true;
42 }
43 template <typename T>
44 bool threadpool<T>::append_p(T *request)
45 {
46     m_queuelocker.lock();
47     if (m_workqueue.size() >= m_max_requests)
48     {
49         m_queuelocker.unlock();
50         return false;
51     }
52     m_workqueue.push_back(request);
53     m_queuelocker.unlock();
54     m_queuestat.post();
55     return true;
56 }
57 template <typename T>
58 void *threadpool<T>::worker(void *arg)
59 {
60     threadpool *pool = (threadpool *)arg;
61     pool->run();
62     return pool;
63 }

對於pool對象指針的run方法,需要提供redis_num大於0的連接處理方法:

 1 template <typename T>
 2 void threadpool<T>::run()
 3 {
 4     while (true)
 5     {
 6         m_queuestat.wait();  //信號量等待被喚醒  7         m_queuelocker.lock();  //喚醒后加互斥鎖保護資源,線程同步  8         if (m_workqueue.empty())
 9         {
10             m_queuelocker.unlock();
11             continue;
12         }
13         T *request = m_workqueue.front();
14         m_workqueue.pop_front();
15         m_queuelocker.unlock();
16         if (!request)
17             continue;
18         if (1 == m_actor_model)
19         {
20             if (0 == request->m_state)
21             {
22                 if (request->read_once())
23                 {
24                     request->improv = 1;
25                     if(m_redisPool!=NULL && m_connPool==NULL)
26                     {
27                         RedisConnectionRAII rediscon(&request->redis, m_redisPool); 28                         request->localRedisConn = request->redis->m_pContext; 29                     }
30                     else
31                     {
32                         connectionRAII mysqlcon(&request->mysql, m_connPool);
33                     }
34                     request->process();
35                 }
36                 else
37                 {
38                     request->improv = 1;
39                     request->timer_flag = 1;
40                 }
41             }
42             else
43             {
44                 if (request->write())
45                 {
46                     request->improv = 1;
47                 }
48                 else
49                 {
50                     request->improv = 1;
51                     request->timer_flag = 1;
52                 }
53             }
54         }
55         else
56         {
57             if(m_redisPool!=NULL && m_connPool==NULL)
58             {
59                 RedisConnectionRAII rediscon(&request->redis, m_redisPool); 60                 request->localRedisConn = request->redis->m_pContext; 61             }
62             else
63             {
64                 connectionRAII mysqlcon(&request->mysql, m_connPool);
65             }
66             request->process();
67         }
68     }
69 }
信號量等待喚醒相較於條件變量等待喚醒具有一定優勢,可對比日志系統的阻塞隊列實現,以及了解虛假喚醒/喚醒丟失等陷阱;此外,localRedisConn作為http.h的新增redisContext結構體指針,防止指向連接池對象指針的m_redisPool離開作用域之后被自動刪除,將連接資源保存到了http本地,否則會導致后面所述的段錯誤;
1.Redis數據庫連接池
單例連接池對象:
 1 class RedisConnectionPool
 2 {
 3 public:
 4     CacheConn* GetRedisConnection();  //遍歷list<CacheConn*>
 5     void init(string url, string User, string PassWord, string DataBaseName, int Port, int MaxConn, int close_log); 
 6     
 7     static RedisConnectionPool *RedisPoolInstance();
 8     int GetFreeRedisConnection();
 9     bool RedisDisconnection(CacheConn* Conn);
10     void DestroyRedisPool();
11 
12 private:
13     RedisConnectionPool();
14     ~RedisConnectionPool();
15 
16     int m_MaxConn;  //最大連接數
17     int m_CurConn;  //當前已使用的連接數
18     int m_FreeConn; //當前空閑的連接數
19     locker lock;
20     list<CacheConn *> connList; //連接池
21     sem reserve;
22 
23 public:
24     friend class CacheConn;
25     string m_Url;             //主機地址
26     string m_Port;         //數據庫端口號
27     string m_User;         //登陸數據庫用戶名
28     string m_PassWord;     //登陸數據庫密碼
29     string m_DatabaseName; //使用數據庫名
30    // CacheConn* pm_rct;  //redis結構體
31     int m_close_log;    //日志開關
32 };
獲取單例對象:
1 RedisConnectionPool *RedisConnectionPool::RedisPoolInstance()
2 {
3     static RedisConnectionPool ConPool;
4     return &ConPool;
5 }
CacheConn是存儲redis會話信息的連接類,連接池內存儲的是CacheConn*對象指針:
 1 class CacheConn
 2 {
 3 public:
 4     int Init(string Url, int Port, int LogCtl, string r_PassWord);
 5 
 6     CacheConn();
 7     ~CacheConn();
 8 public:
 9     redisContext* m_pContext;
10     int m_close_log;
11 private:
12     int m_last_connect_time;
13     string R_password;
14 };
redisContext*是hiredis內定義的結構體指針,相當於Mysql連接的MYSQL結構體指針,這樣定義的目的是可以自由管理連接池內不同連接的會話信息:
 1 /* Context for a connection to Redis */
 2 typedef struct redisContext {
 3     int err; /* Error flags, 0 when there is no error */
 4     char errstr[128]; /* String representation of error when applicable */
 5     int fd;
 6     int flags;
 7     char *obuf; /* Write buffer */
 8     redisReader *reader; /* Protocol reader */
 9 
10     enum redisConnectionType connection_type;
11     struct timeval *timeout;
12 
13     struct {
14         char *host;
15         char *source_addr;
16         int port;
17     } tcp;
18 
19     struct {
20         char *path;
21     } unix_sock;
22 
23 } redisContext;
redis登陸認證與連接初始化,設置連接超時時間:
 1 int CacheConn::Init(string Url, int Port, int LogCtl, string r_PassWord)
 2 {
 3     //重連
 4     time_t cur_time = time(NULL);
 5     if(cur_time < m_last_connect_time +4){
 6         return 1;
 7     }
 8 
 9     m_last_connect_time = cur_time;
10 
11     struct timeval timeout
12     {
13         0,200000
14     };
15     
16     m_pContext = redisConnectWithTimeout(Url.c_str(), Port, timeout);
17     m_close_log = LogCtl;
18     R_password = r_PassWord;
19 
20     if(!m_pContext || m_pContext->err)
21     {
22         if(m_pContext)
23         {
24             redisFree(m_pContext);
25             m_pContext = NULL;
26         }
27         LOG_ERROR("redis connect failed");
28         return 1;
29     }
30     
31     redisReply* reply;
32     //登陸驗證:
33     if(R_password != "")
34     {
35         reply = (redisReply *)redisCommand(m_pContext, "AUTH %s", R_password.c_str());
36         if(!reply || reply->type == REDIS_REPLY_ERROR)
37         {
38             if(reply){
39                 freeReplyObject(reply);
40             }
41             return -1;
42         }
43         freeReplyObject(reply);
44     }
45 
46     reply = (redisReply *)redisCommand(m_pContext, "SELECT %d", 0);
47     if (reply && (reply->type == REDIS_REPLY_STATUS) && (strncmp(reply->str, "OK", 2) == 0))
48     {
49         freeReplyObject(reply);
50         return 0;
51     }
52     else
53     {
54         if (reply)
55             LOG_ERROR("select cache db failed:%s\n", reply->str);
56         return 2;
57     }
58 }

連接池中存放初始化會話連接:

 1 void RedisConnectionPool::init(string url, string User, string PassWord, string DBName, int Port, int MaxConn, int close_log)
 2 {
 3     m_Url = url;
 4     m_Port = Port;
 5     m_User = User;
 6     m_PassWord = PassWord;
 7     m_DatabaseName = DBName;
 8     m_close_log = close_log;
 9 
10     for (int i = 0; i < MaxConn; i++)
11     {
12         CacheConn *con = NULL;
13         con = new CacheConn;
14 
15         int r = con->Init(m_Url, Port, close_log, m_PassWord);
16         if( r != 0 || con == NULL)
17         {
18             if(r == 1)
19             {
20                 delete con;
21             }
22             LOG_ERROR("Redis Error");
23             exit(1);
24         }
25         LOG_INFO("redis con in pool init res: %lu", r);
26         connList.push_back(con);
27         ++m_FreeConn;
28     }
29 
30     reserve = sem(m_FreeConn);
31     m_MaxConn = m_FreeConn;
32 
33     LOG_ERROR("cache pool: %s, list size: %lu", m_DatabaseName.c_str(), connList.size());
34 }

redis會話類與RAII會話資源管理,將會話資源管理方法封裝在RedisConnectionRAII類中,RAII對象離開當前函數作用域時,自動析構釋放棧內存,調用方法歸還會話資源到連接池:

 1 RedisConnectionRAII::RedisConnectionRAII(CacheConn **Con, RedisConnectionPool *ConPool){
 2     *Con = ConPool->GetRedisConnection(); //二級指針操作會話對象,從池中獲取連接資源  3     
 4     conRAII = *Con;
 5     poolRAII = ConPool;
 6 }
 7 
 8 RedisConnectionRAII::~RedisConnectionRAII(){
 9     poolRAII->RedisDisconnection(conRAII); //歸還會話資源到連接池內
10 }

http主從狀態機:

有限狀態機,是一種抽象的理論模型,它能夠把有限個變量描述的狀態變化過程,以可構造可驗證的方式呈現出來。比如,封閉的有向圖。http從狀態機負責解析行,主狀態機根據從狀態機返回的HTTP code進行下一步處理,分別解析http請求的header,request_line以及context即從狀態機驅動主狀態機:

可以參考 Web服務器——HTTP狀態機解析_AlwaysSimple的博客-CSDN博客_http狀態機的解析;

增加了redis連接池之后需要修改向數據庫CRUD用戶信息的接口;具體包括,將用戶信息從redis的LIST users_list內同步到用戶空間,還有從http請求處理過程中的用戶信息更新與頁面跳轉邏輯,以及所有redis連接資源與會話資源管理:

initRedis_result用戶信息初始化函數:
 1 void http_conn::initRedis_result(RedisConnectionPool* ConnPool, int log_ctrl)
 2 {
 3     m_close_log = log_ctrl;
 4     redis = NULL;
 5     RedisConnectionRAII rediscon(&redis, ConnPool);
 6     if(redis == NULL)
 7     {
 8         LOG_ERROR("initRedis_result failed");
 9     }
10     localRedisConn = redis->m_pContext;
11     if(localRedisConn == NULL)
12     {
13         LOG_ERROR("local redis session lose");
14     }
15     else
16     {
17         LOG_ERROR("local redis session INFO: err:%lu,fd:%lu,flag:%lu", localRedisConn->err, localRedisConn->fd, localRedisConn->flags);
18 
19     }
20 
21     redisReply *ResLen = (redisReply*)redisCommand(redis->m_pContext, "LLEN users_list");
22     
23     for(int i=0;i<ResLen->integer;i++)
24     {
25         redisReply *Res = (redisReply*)redisCommand(redis->m_pContext, "LINDEX users_list %d", i+1);
26         vector<string>temp;
27         string x;
28         stringstream ss;
29         ss<<Res->str;
30         while(getline(ss,x,'+')) //每個LIST元素是用戶名+密碼的字符串 31         {
32             temp.push_back(x);
33         }
34         if(temp.size()>1)
35         {
36             users[temp[0]] = temp[1];
37         }
38         else
39         {
40             LOG_ERROR("user info in redis invalid");
41         }
42     }
43 }

Redis的LIST數據結構是五種基礎數據結構之一,是一種外部編碼方式,可以實現消息隊列;相關指令可見 Redis 列表(List) | 菜鳥教程 (runoob.com)

我們增加了入參log_ctrl是為了在全局變量m_close_log未被賦值之前,保證該函數作用域內所有的LOG宏能夠定位到m_close_log,讓日志可以正常關閉;

do_request報文處理函數:

http_conn::HTTP_CODE http_conn::do_request()
{
    if(localRedisConn == NULL)
    {
        LOG_ERROR("local redis session lose");
    }
    else
    {
        LOG_ERROR("local redis session INFO: err:%lu,fd:%lu,flag:%lu", localRedisConn->err, localRedisConn->fd, localRedisConn->flags);
    }
    strcpy(m_real_file, doc_root);
    int len = strlen(doc_root);
    printf("m_url:%s\n", m_url);
    const char *p = strrchr(m_url, '/');
...
//提取用戶名和密碼
//LIST新用戶注冊
if (*(p + 1) == '3')
        {
            if (users.find(name) == users.end())
            {
                m_lock.lock();
                string set_name = name;
                string set_pass = password;
                string insert2list = (set_name+'+'+set_pass);
                redisReply *res = (redisReply*)redisCommand(localRedisConn, "LPUSH users_list %s", insert2list.c_str());
                users.insert(pair<string, string>(set_name, set_pass));
                m_lock.unlock();

                if (1 <= res->integer)
                {
                    LOG_INFO("set a new user");
                    strcpy(m_url, "/log.html");
                }
                else
                {
                    LOG_ERROR("set a new user fail");
                    strcpy(m_url, "/registerError.html");
                }
            }
            else
                strcpy(m_url, "/registerError.html");
        }
}

 

編譯與運行:
config.cpp內增加了參數redis_num的默認初值,以及編譯選項-r配置:
 1 Config::Config(){
 2          redis_num = 8;
 3 }
 4 void Config::parse_arg(int argc, char*argv[]){
 5     int opt;
 6     const char *str = "p:l:m:o:s:r:t:c:a:";
 7     while ((opt = getopt(argc, argv, str)) != -1)
 8     {
 9         switch (opt)
10         {
11         case 'r':
12         {
13             redis_num = atoi(optarg);
14             break;
15         }
16      }
17 }

相應的,makefile內也需要增加hiredis依賴:

 1 CXX ?= g++
 2 
 3 DEBUG ?= 1
 4 ifeq ($(DEBUG), 1)
 5     CXXFLAGS += -g
 6 else
 7     CXXFLAGS += -O2
 8 
 9 endif
10 
11 server: main.cpp  ./timer/lst_timer.cpp ./http/http_conn.cpp ./log/log.cpp ./CGImysql/sql_connection_pool.cpp ./CGIRedis/redis_connection_pool.cpp webserver.cpp config.cpp
12     $(CXX) -o server  $^ $(CXXFLAGS) -lpthread -lmysqlclient -lhiredis 13 
14 clean:
15     rm  -r server

注意,調試過程中遇到段錯誤需要復現並使用gdb定位的時候,推薦將代碼優化等級降低為O0;

編譯,啟動選項:

1 sh ./build.sh
2 ./server [-p port] [-l LOGWrite] [-m TRIGMode] [-o OPT_LINGER] [-s sql_num] [-r redis_num] [-t thread_num] [-c close_log] [-a actor_model]

 

故障排除:
1.連接池對象生命周期:

 RAII機制將會話資源歸還連接池的動作封裝在了RedisConnectionRAII的析構函數中,相當於模擬了一類棧資源;因此,保存redis連接對象CacheConn的指針或引用到http本地無法確保在RedisConnectionRAII對象rediscon離開threadpool<http_conn>::run()的作用域之后,還可以被正常訪問;因此需要使用結構體指針將連接資源m_pContext提前保存,否則會報告hiredis錯誤;

 

2.日志開關bug(全局宏覆蓋范圍,進程啟動順序影響):

 定位問題是由於輸入日志關閉選項-c 1后,initRedis_result執行順序,比http連接請求的初始化順序靠前:

http.h的本地變量m_close_log沒有被賦值導致全局宏失效:

1 #define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}

解決方法是在http.cpp中增加全局變量,並增加initRedis_result入參;

 ps:

如果gdb提示沒有棧信息,嘗試增加debuginfo源:

 

二,模擬冪等接口
接口冪等性是為了防止 前端重復提交/接口連接超時/消息重復消費 等原因導致的資源沖突。
Tokens設置原理:
需要在冪等操作的上游操作進行告知服務器,生成具備時效性的tokens返回給客戶端,並同時存入redis;在tokens有效的時段內進行的首次成功請求被認定為有效請求,該次請求將觸發刪除tokens,因此超時時段內的其他同類請求均被視為重復請求;超出該時段的同類請求又將被分配新的定時token。
 
 我們將初始化用戶信息作為上游操作,將關注等操作作為下游冪等操作;需要對http.cpp內的initRedis_result方法進行上游操作拓展:
 1  //全局Tokens存入Redis,並設置過期時間
 2     time_t cur = time(NULL);
 3     string str_time_cur=to_string(cur);
 4     redisReply *ExistToken = (redisReply*)redisCommand(redis->m_pContext, "EXISTS Token_pictrue");
 5     redisReply *DelTokensRes = NULL;
 6     if(0 == ExistToken->integer)
 7     {
 8         m_Token_picture="xxxpicture"+str_time_cur;
 9         redisReply *SetToken_picture = (redisReply*)redisCommand(redis->m_pContext, "SET Token_pictrue %s", m_Token_picture.c_str());
10         if(SetToken_picture->str == "OK")
11         {
12             redisCommand(redis->m_pContext, "EXPIRE Token_pictrue 60");
13         }
14     }
15     else
16     {
17         DelTokensRes = (redisReply*)redisCommand(redis->m_pContext, "DEL Token_pictrue");
18         if(1 == DelTokensRes->integer)
19         {
20             LOG_ERROR("Token_pictrue clear success");
21         }
22         else
23         {
24             LOG_ERROR("Token_pictrue clear fail");
25         }
26     }
27     ExistToken = (redisReply*)redisCommand(redis->m_pContext, "EXISTS Token_video");
28     if(0 == ExistToken->integer)
29     {
30         m_Token_video="xxxvideo"+str_time_cur;
31         redisReply *SetToken_video = (redisReply*)redisCommand(redis->m_pContext, "SET Token_video %s", m_Token_video.c_str());
32         if(SetToken_video->str == "OK")
33         {
34             redisCommand(redis->m_pContext, "EXPIRE Token_video 60");
35         }
36     }
37     else
38     {
39         DelTokensRes = (redisReply*)redisCommand(redis->m_pContext, "DEL Token_video");
40         if(1 == DelTokensRes->integer)
41         {
42             LOG_ERROR("Token_video clear success");
43         }
44         else
45         {
46             LOG_ERROR("Token_video clear fail");
47         }
48     }
49     ExistToken = (redisReply*)redisCommand(redis->m_pContext, "EXISTS Token_fans");
50     if(0 == ExistToken->integer)
51     {
52         m_Token_fans="xxxfans"+str_time_cur;
53         redisReply *SetToken_fans = (redisReply*)redisCommand(redis->m_pContext, "SET Token_fans %s", m_Token_video.c_str());
54         if(SetToken_fans->str == "OK")
55         {
56             redisCommand(redis->m_pContext, "EXPIRE Token_fans 60");
57         }
58     }
59     else
60     {
61         DelTokensRes = (redisReply*)redisCommand(redis->m_pContext, "DEL Token_video");
62         if(1 == DelTokensRes->integer)
63         {
64             LOG_ERROR("Token_fans clear success");
65         }else
66         {
67             LOG_ERROR("Token_fans clear fail");
68         }
69     }

並對處理http請求報文處理的下游操作方法do_request方法進行拓展:

 1     redisReply *CheckTokens = NULL;
 2     redisReply *Reset_Token = NULL;
 3     if (*(p + 1) == '0')
 4     {
 5         char *m_url_real = (char *)malloc(sizeof(char) * 200);
 6         strcpy(m_url_real, "/register.html");
 7         strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
 8 
 9         free(m_url_real);
10     }
11     else if (*(p + 1) == '1')
12     {
13         char *m_url_real = (char *)malloc(sizeof(char) * 200);
14         strcpy(m_url_real, "/log.html");
15         strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
16 
17         free(m_url_real);
18     }
19     else if (*(p + 1) == '5')
20     {
21         CheckTokens = (redisReply*)redisCommand(redis->m_pContext, "EXISTS Token_pictrue");
22         if(1 == CheckTokens->integer)
23         {
24             char *m_url_real = (char *)malloc(sizeof(char) * 200);
25             strcpy(m_url_real, "/picture.html");
26             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
27 
28             free(m_url_real);
29             Reset_Token = (redisReply *)redisCommand(redis->m_pContext, "DEL Token_pictrue");//冪等操作,刪除
30             if(1 == Reset_Token->integer)
31             {
32                 LOG_INFO("Token_pictrue is moved");
33             }
34         }
35         else
36         {
37             char *m_url_real = (char *)malloc(sizeof(char) * 200);
38             strcpy(m_url_real, "/repeated.html");
39             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
40 
41             free(m_url_real);
42             LOG_ERROR("Http request about picture is expired");
43         }
44     }
45     else if (*(p + 1) == '6')
46     {
47         CheckTokens = (redisReply*)redisCommand(redis->m_pContext, "EXISTS Token_video");
48         if(1 == CheckTokens->integer)
49         {
50             char *m_url_real = (char *)malloc(sizeof(char) * 200);
51             strcpy(m_url_real, "/video.html");
52             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
53 
54             free(m_url_real);
55             Reset_Token = (redisReply *)redisCommand(redis->m_pContext, "DEL Token_video");//冪等操作,刪除
56             if(1 == Reset_Token->integer)
57             {
58                 LOG_INFO("Token_video is moved");
59             }
60         }
61         else
62         {
63             char *m_url_real = (char *)malloc(sizeof(char) * 200);
64             strcpy(m_url_real, "/repeated.html");
65             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
66 
67             free(m_url_real);
68             LOG_ERROR("Http request about video is expired");
69         }
70     }
71     else if (*(p + 1) == '7')
72     {
73         CheckTokens = (redisReply*)redisCommand(redis->m_pContext, "EXISTS Token_fans");
74         if(1 == CheckTokens->integer)
75         {
76             char *m_url_real = (char *)malloc(sizeof(char) * 200);
77             strcpy(m_url_real, "/fans.html");
78             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
79 
80             free(m_url_real);
81 
82             Reset_Token = (redisReply *)redisCommand(redis->m_pContext, "DEL Token_fans");//冪等操作,刪除
83             if(1 == Reset_Token->integer)
84             {
85                 LOG_INFO("Token_fans is moved");
86             }
87         }
88         else
89         {
90             char *m_url_real = (char *)malloc(sizeof(char) * 200);
91             strcpy(m_url_real, "/repeated.html");
92             strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
93 
94             free(m_url_real);
95             LOG_ERROR("Http request about fans is expired"); //過期請求提示與重復操作頁面跳轉 96         }
97     }

此外,增加了解析request_line中攜帶tokens的具體方法,但需要對前端ajax頁面進行修改,並增加無法解析到特定tokens的http狀態碼NO_TOKENS,目前暫時沒有實現;request_line解析方法拓展部分:

 1 string Token_req = text;
 2     m_Token_picture="";
 3     m_Token_video="";
 4     m_Token_fans="";
 5     string::size_type idx0=Token_req.find("m_Token_picture");
 6     if(idx0 == string::npos)
 7     {
 8         //return NO_TOKENS;//updating http_code
 9     }
10     else
11     {
12         string::size_type idx1=Token_req.find("m_Token_video");
13         m_Token_picture=Token_req.substr(idx0,idx1-idx0);
14         string::size_type idx2=Token_req.find("m_Token_fans");
15         m_Token_video=Token_req.substr(idx1,idx2-idx1);
16         m_Token_fans=Token_req.substr(idx2,Token_req.size()-1);
17     }

以及構造tokens響應方法:

 1 bool http_conn::add_response(const char *format, ...)
 2 {
 3     if (m_write_idx >= WRITE_BUFFER_SIZE)
 4         return false;
 5     va_list arg_list;
 6     va_start(arg_list, format);
 7     int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
 8     if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))
 9     {
10         va_end(arg_list);
11         return false;
12     }
13     m_write_idx += len;
14     va_end(arg_list);
15 
16     LOG_INFO("request:%s", m_write_buf);
17 
18     return true;
19 }
20 bool http_conn::add_Tokens(const char *token)
21 {
22     return add_response("%s", token);
23 }
查看日志,redis連接池初始化與會話資源自動管理過程:

 

功能測試:
注冊/登陸/后台數據查看

 重復點擊關注,出現重復操作提示:

壓力測試:

使用webbench,編譯生成可執行文件:

1 make webbench

壓測啟動選項:

1 ./webbench -c 10000 -t 5 http://101.132.243.104:9006/7

-c: 客戶端數量

-t: 持續時間

注意壓測期間需要關閉日志,否則會嚴重影響壓測准確度;

並發訪問期間的CPU開銷:

 並發量統計(默認模式ET+Proactor):

 

 

 

總結與拓展
壓測QPS較低,並發性能下降,通過降低並發性能來增加會話連接資源的靈活度。主要原因是由於redis數據庫連接池的會話對象的構造與析構開銷導致,以及初始化連接池等靜態資源的多次傳遞;后續考慮通過Redis主從同步等手段實現負載均衡,來提升並發性能。


免責聲明!

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



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