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

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



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

以多路復用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后台服務連接池
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內;

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 }
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 }
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 };
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;
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連接資源與會話資源管理:
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"); } }
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]

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

定位問題是由於輸入日志關閉選項-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源:
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 }


重復點擊關注,出現重復操作提示:
壓力測試:
使用webbench,編譯生成可執行文件:
1 make webbench
壓測啟動選項:
1 ./webbench -c 10000 -t 5 http://101.132.243.104:9006/7
-c: 客戶端數量
-t: 持續時間
注意壓測期間需要關閉日志,否則會嚴重影響壓測准確度;
並發訪問期間的CPU開銷:
並發量統計(默認模式ET+Proactor):