C++高性能服务器TinyWebServer扩展开发-Redis数据库连接池/模拟幂等接口
简介:
本项目对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源:
详细原理可以参考: 阿里面试官:接口的幂等性怎么设计?_普通网友的博客-CSDN博客
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):