热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

用一个示例讲解我如何一步步实现高并发服务(基于C++)

去年做了一个远程升级的服务。客户端连接此服务可以下载更新程序。简单点说就是个TCPsever。基于C。运行环境是centOS6.5。刚开始客户端数量少而且访问不频繁࿰

去年做了一个远程升级的服务。客户端连接此服务可以下载更新程序。简单点说就是个TCP sever。基于C++。

运行环境是centOS 6.5

刚开始客户端数量少而且访问不频繁,所以没太关注并发的问题。当时用工具测试大概只能支持的几十 TPS的并发访问,再加大并发就会有数据串包的情况出现了。下面是我的优化过程。

下面给出的代码都不是完整的项目源码,我只是截取了关键部分用于说明主题

我选择的测试工具是一个tcp客户端工具,可以快捷的进行多客户端连接的测试。

这里写图片描述


线程安全的单例模式

版本1.0是通过多线程实现高并发的。我的工程里有两个类是单例模式,一个参数文件管理类,一个是日志管理类。一开始我没有考虑线程安全,于是第一步我先把这两个类改成线程安全测试看看效果。

增加线程安全前的代码片段(只给出参数文件管理类的实现)

//.h
class AppCof {
public:static AppCof* open_cof();private:AppCof();class CGarbo //它的唯一工作就是在析构函数中删除CSingleton的实例 { public: ~CGarbo() { if(AppCof::m_pInstance) delete AppCof::m_pInstance; } }; static CGarbo Garbo; //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数
...

//.cpp
AppCof* AppCof::open_cof(){if(m_pInstance == NULL){m_pInstance = new AppCof();}return m_pInstance;
}
...

增加线程安全后

//.h
class AppCof:boost::noncopyable
{
public:static AppCof* open_cof();
private:AppCof();static AppCof *m_pInstance;static void init();static pthread_once_t ponce_;...

void AppCof::init()
{m_pInstance = new AppCof();if(m_pInstance != NULL){m_pInstance->get_env();m_pInstance->read_cof();}
}AppCof* AppCof::open_cof(){pthread_once(&ponce_, &AppCof::init);return m_pInstance;
}...

这里有两个重点,一是pthread_once的用法,还有就是boost::noncopyable。

先说说前者,

int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));

本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。

boost::noncopyable这种用法其实从名字可以窥探一二,一个类继承自它就表示该类不能通过赋值,复制等手段创建新的对象了。


优化IO读写机制

这部分从select,epoll这些IO处理上下手。我用三个方案分别测试,效果还是比较明显。

版本1.0是这样的:

void run_srv(const char* i_port){for(;;){client = server.accept_client();std::thread t1(base_proc,client,i_port);usleep(200);t1.detach();}}void base_proc(Socket::TCP* i_client,const char* i_port){Socket::TCP* client = i_client;i_client = NULL;TmsProc* tmpc =new TmsProc(client);tmpc->run();delete tmpc;return ;
}

void TmsProc::run(){Writelog::Trace(9,"业务处理开始");try{for(;;){tm.tv_sec &#61; 3;tm.tv_usec &#61; 0;FD_ZERO(&set);FD_SET(p_client->_socket_id,&set);int iret &#61; select(p_client->_socket_id&#43;1,&set,NULL,NULL,&tm);if(iret < 0){Writelog::Trace(2,"select出错:%s",strerror(errno));return;}if(iret &#61;&#61; 0){Writelog::Trace(2,"select超时");return;}Writelog::Trace(9,"监控到可以进行接收");//收取信息if(read_sock() &#61;&#61; false){Writelog::Trace(3,"检测到客户端套接字异常&#xff0c;准备断开连接");break;}...

很简单&#xff0c;主要流程都在run函数里。这个函数可以优化的地方有几处。比如两个if的判断可以改成if elseif的形式。因为两次if虽然是互斥的但是程序都会判断一次&#xff0c;效率比较低。

另外接收数据的条件可以用FD_ISSET判断是否有数据可读&#xff0c;如果有才真正接收&#xff0c;否则不处理。

所以第一种优化方案很快出炉


void TmsProc::run(){tm.tv_sec &#61; 60;tm.tv_usec &#61; 0;try{for(;;){FD_ZERO(&set);FD_SET(p_client->_socket_id,&set);int iret &#61; select(p_client->_socket_id&#43;1,&set,NULL,NULL,&tm);if(iret < 0){pLog_tmsProc->Trace(2,"select出错:%s",strerror(errno));return;}else if(iret &#61;&#61; 0){pLog_tmsProc->Trace(2,"select超时");return;}if(FD_ISSET(p_client->_socket_id,&set)){pLog_tmsProc->Trace(9,"监控到可以进行接收");if(read_sock() &#61;&#61; false){pLog_tmsProc->Trace(3,"检测到客户端套接字异常&#xff0c;准备断开连接");break;}...

注意到我把超时时间改成了60秒&#xff0c;

tm.tv_sec &#61; 60;

这是我在实际测试时发现&#xff0c;当并发量大时&#xff0c;程序在处理数量多的连接时&#xff0c;前面分配成功的线程会超时退出&#xff0c;看下日志就明白了:

14:39:40][140579076663072]:准备accept
[14:39:40][140579076663072]:接待并分配文件描述符[44],主服务描述符[3]
[14:39:40][140579076663072]:接到连接请求&#xff0c;准备启动线程TCP:0x1e89e90,IP:10.0.0.106,PORT:19803
[14:39:40][140579076663072]:启动服务线程于140577928623872
[14:39:40][140579076663072]:等待接收客户端连接
[14:39:40][140579076663072]:准备accept
[14:39:40][140579076663072]:接待并分配文件描述符[46],主服务描述符[3]
[14:39:40][140579076663072]:接到连接请求&#xff0c;准备启动线程TCP:0x1e8a170,IP:10.0.0.106,PORT:19804
[14:39:40][140577928623872]:接到来自TMS端口的请求
[14:39:40][140577928623872]:业务处理开始
[14:39:40][140577918134016]:接到来自TMS端口的请求
[14:39:40][140577918134016]:业务处理开始
[14:39:40][140579076663072]:启动服务线程于140577918134016
[14:39:40][140579076663072]:等待接收客户端连接
[14:39:40][140579076663072]:准备accept
[14:39:40][140579076663072]:接待并分配文件描述符[48],主服务描述符[3]
[14:39:40][140579076663072]:接到连接请求&#xff0c;准备启动线程TCP:0x1e8a450,IP:10.0.0.106,PORT:19805
[14:39:40][140577500821248]:接到来自TMS端口的请求
[14:39:40][140577500821248]:业务处理开始
[14:39:40][140579076663072]:启动服务线程于140577500821248
[14:39:40][140579076663072]:等待接收客户端连接
[14:39:40][140579076663072]:准备accept
[14:39:41][140579076663072]:接待并分配文件描述符[50],主服务描述符[3]

因为工具是模拟多个客户端同时发起请求&#xff0c;于是就有了上面这样的分配线程的过程&#xff0c;会持续的时间比较长(还要写日志)&#xff0c;也就是同时发生的连接数越多&#xff0c;超时时间就要设置越长。超时改成60秒后。经过工具实测&#xff0c;500连接/500毫秒(TPS已经达到1000)的处理都正常。

这里写图片描述

性能大大提高。

但是问题还是很明显&#xff0c;就是超时时间。随着连接数的增大&#xff0c;超时也要一直增大才能保证没有线程"掉队"&#xff0c;但是这个时间太大了会影响真正接收数据时的效率。

第二种优化方案思路来源于apache和nginx。

apache和nginx他俩的一个重要区别是前者基于多线程实现高并发&#xff0c;而后者基于多进程(fork)。

而众所周知&#xff0c;nginx很多场景的高并发是好于apache的。

所以我的第二种方案&#xff0c;基本思路是为每个连接fork一个单独的进程处理。独立进程有个最大的好处是**不需要加锁了(不解释)。**修改好的代码片段如下(我已经把所有带锁的地方都去掉了&#xff0c;这里不贴出来了)。


void run_srv(const char* i_port){for(;;){client &#61; server.accept_client();pWriteLogInstance->Trace(1,"接到连接请求&#xff0c;准备启动进程TCP:%p,IP:%s,PORT:%u",client,client->ip().c_str(),client->port());if(client->_socket_id < 0){if(errno &#61;&#61; EINTR || errno &#61;&#61; ECONNABORTED) continue; else { cout << "accept error" << endl; return; } }fpid &#61; fork();if(fpid < 0){pWriteLogInstance->Trace(9,"fork error");}else if(fpid > 0)//father{pWriteLogInstance->Trace(1,"father process start");client->close();}else //child{server.close();pWriteLogInstance->Trace(1,"child process start");TmsProc* tmpc &#61;new TmsProc(client);tmpc->run();delete tmpc;if(client !&#61; NULL){client->close();delete client;client &#61; NULL;}exit(-6);}usleep(10);

void TmsProc::run(){while(1){//收取信息if(read_sock() &#61;&#61; false){pLog_tmsProc->Trace(3,"检测到客户端套接字异常&#xff0c;准备断开连接");send_info.is_bad_qry &#61; true;break;}if(stc_tms.un_parse_size &#61;&#61; 0){pLog_tmsProc->Trace(3,"没有接受到有效数据&#xff0c;客户端关闭了");break;}....

测试结果来看效果也是不错的&#xff0c;而且相比较前一种方案&#xff0c;没有了超时时间的问题。

第三种方案&#xff0c;我考虑试试 IO 处理中的王者&#xff0c;epoll


epoll是Linux内核为处理大批量文件描述符而作了改进的poll&#xff0c;是Linux下多路复用IO接口select/poll的增强版本&#xff0c;它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候&#xff0c;它无须遍历整个被侦听的描述符集&#xff0c;只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。


为了简单起见&#xff0c;我这里只是用了单线程的epoll&#xff0c;用循环来轮询客户端的socket id来处理多个客户端连接的情况。单线程的epoll号称也能处理1万以上的并发量&#xff0c;我要测试下是不是有这边牛X。

epoll方案的代码如下:

void TmsProc::run(){struct epoll_event event; // 告诉内核要监听什么事件 struct epoll_event wait_event[OPEN_MAX]; //内核监听完的结果 Socket::TCP server;pLog_tmsProc->Trace(9,"准备监听端口:%d",this->port);server.listen_on_port(this->port,OPEN_MAX);Socket::TCP* client;//4.epoll相应参数准备 int fd[OPEN_MAX&#43;1]; int i &#61; 0, maxi &#61; 0; int number &#61; 0;memset(fd,-1, sizeof(fd)); fd[0] &#61; server._socket_id;pLog_tmsProc->Trace(9,"epoll 开始准备");int epfd &#61; epoll_create(OPEN_MAX&#43;1); if( -1 &#61;&#61; epfd ){ pLog_tmsProc->Trace(9,"epoll create error");return; } event.data.fd &#61; server._socket_id; //监听套接字 event.events &#61; EPOLLIN; // 表示对应的文件描述符可以读//5.事件注册函数&#xff0c;将监听套接字描述符 sockfd 加入监听事件 int ret &#61; epoll_ctl(epfd, EPOLL_CTL_ADD, server._socket_id, &event); if(-1 &#61;&#61; ret){ pLog_tmsProc->Trace(9,"epoll_ctl error"); return; } pLog_tmsProc->Trace(9,"业务处理开始");while(1){// 监视并等待多个文件&#xff08;标准输入&#xff0c;udp套接字&#xff09;描述符的属性变化&#xff08;是否可读&#xff09; // 没有属性变化&#xff0c;这个函数会阻塞&#xff0c;直到有变化才往下执行&#xff0c;这里没有设置超时 pLog_tmsProc->Trace(9,"epoll 开始监听");number &#61; epoll_wait(epfd, wait_event, OPEN_MAX, -1); for(int i &#61; 0; i < number; i&#43;&#43;){if( (wait_event[i].events & EPOLLERR) || ( wait_event[i].events & EPOLLHUP ) || !(wait_event[i].events & EPOLLIN) ){pLog_tmsProc->Trace(9,"epoll error");close(wait_event[i].data.fd);continue;}else if(server._socket_id &#61;&#61; wait_event[i].data.fd ) { while(1){client &#61; server.accept_client();if(client->_socket_id &#61;&#61; -1){if( errno &#61;&#61; EAGAIN || errno &#61;&#61; EWOULDBLOCK ){break;}else{pLog_tmsProc->Trace(9,"accept error");break;}}Socket::TCP::make_socket_non_blocking(client->_socket_id);event.data.fd &#61; client->_socket_id; //监听套接字 event.events &#61; EPOLLIN | EPOLLERR | EPOLLHUP | EPOLLET; // 表示对应的文件描述符可以读 //6.1.3.事件注册函数&#xff0c;将监听套接字描述符 connfd 加入监听事件 pLog_tmsProc->Trace(9,"为客户端注册epoll监听"); ret &#61; epoll_ctl(epfd, EPOLL_CTL_ADD, client->_socket_id, &event);if(ret < 0){ pLog_tmsProc->Trace(9,"epoll_ctl error"); } event.data.fd &#61; client->_socket_id;}}else{//收取信息if(read_sock(wait_event[i].data.fd) &#61;&#61; false){pLog_tmsProc->Trace(3,"检测到客户端套接字异常&#xff0c;准备断开连接");close(wait_event[i].data.fd); }...

只能说epoll确实比较给力&#xff0c;我这只是个单线程的服务&#xff0c;用工具测试上述代码&#xff0c;500个并发也是妥妥的。

这里写图片描述


推荐阅读
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • 本文详细介绍了MysqlDump和mysqldump进行全库备份的相关知识,包括备份命令的使用方法、my.cnf配置文件的设置、binlog日志的位置指定、增量恢复的方式以及适用于innodb引擎和myisam引擎的备份方法。对于需要进行数据库备份的用户来说,本文提供了一些有价值的参考内容。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 本文讨论了在数据库打开和关闭状态下,重新命名或移动数据文件和日志文件的情况。针对性能和维护原因,需要将数据库文件移动到不同的磁盘上或重新分配到新的磁盘上的情况,以及在操作系统级别移动或重命名数据文件但未在数据库层进行重命名导致报错的情况。通过三个方面进行讨论。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • mysql-cluster集群sql节点高可用keepalived的故障处理过程
    本文描述了mysql-cluster集群sql节点高可用keepalived的故障处理过程,包括故障发生时间、故障描述、故障分析等内容。根据keepalived的日志分析,发现bogus VRRP packet received on eth0 !!!等错误信息,进而导致vip地址失效,使得mysql-cluster的api无法访问。针对这个问题,本文提供了相应的解决方案。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
author-avatar
吴小熙1108
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有