Desh

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模式事件处理器关注的是已经存入业务缓冲区的读完成事件,启动处理方法后控制权将由内核归还给事件分离器。

具体原理可以参考IO设计模式:Reactor和Proactor对比 - 大CC - 博客园 (cnblogs.com)以及  两种高效的服务器设计模型:Reactor和Proactor模型_Sunshine_top的博客-CSDN博客_reactor模型
本服务器支持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源:

 

二,模拟幂等接口
接口幂等性是为了防止 前端重复提交/接口连接超时/消息重复消费 等原因导致的资源冲突。

 

 详细原理可以参考: 阿里面试官:接口的幂等性怎么设计?_普通网友的博客-CSDN博客

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主从同步等手段实现负载均衡,来提升并发性能。

posted on 2022-04-21 00:58  Desh  阅读(2129)  评论(0编辑  收藏  举报

导航