多进程通信
进程通信方式
- 管道:
- 消息队列
- 信号
- 共享内存
- 信号量
- 套接字socket
多进程通信的优化
- 将socket通信的客户端以及服务端进行封装,让代码更清晰高效
- 解决了僵尸进程的问题,防止子进程占用过多的内存资源
- 关闭多余的socket,防止系统打开的文件描述符过多:ulimit -a
- 程序退出资源的释放,利用exit(0)退出程序,调用析构函数,释放资源
- 多进程服务进程的运行日志,多进程日志文件不能切换。一般分开写日志
- gdb对多进程进行调试。
- 增加业务代码:实现登录用户与密码验证
服务端程序
#include "../_freecplus.h"
CTcpServer TcpServer; // 创建服务端对象。
CLogFile logfile;
// 程序退出时调用的函数
void FathEXIT(int sig);
void ChldEXIT(int sig);
// 父进程退出时调用的函数
void FathEXIT(int sig)
{
if(sig>0)
{
signal(sig,SIG_IGN);
signal(SIGINT,SIG_IGN);
signal(SIGTERM,SIG_IGN);
printf("catch the signal %d\n",sig);
}
kill(0,15);//父进程退出,子进程也全部退出
printf("父进程退出\n");
exit(0);
}
void ChldEXIT(int sig)
{
if(sig>0)
{
signal(sig,SIG_IGN);
signal(SIGINT,SIG_IGN);
signal(SIGTERM,SIG_IGN);
}
printf("子进程退出\n");
exit(0);
}
int main(int argc,char *argv[])
{
//关闭全部的信号
logfile.Open("/tmp/log/demo1.log","a+");
for(int i=0;i<100;i++)
{
signal(i,SIG_IGN);
}
//设置信号,在shell下可用kill+进程号正常终止进程
signal(SIGINT,FathEXIT);signal(SIGTERM,FathEXIT);
signal(SIGCHLD,SIG_IGN);//忽略子进程退出的信号,避免产生僵尸进程
if (TcpServer.InitServer(5005)==false) // 初始化TcpServer的通信端口。
{
// printf("TcpServer.InitServer(5005) failed.\n"); return -1;
logfile.Write("TcpServer.InitServer(5005) failed.\n");return -1;
}
while(true)
{
if (TcpServer.Accept()==false) // 等待客户端连接。
{
printf("TcpServer.Accept() failed.\n"); return -1;
}
//printf("客户端(%s)已连接。\n",TcpServer.GetIP());
logfile.Write("客户端(%s)已经连接。\n",TcpServer.GetIP());
if(fork()>0)
{
TcpServer.CloseClient();//父进程关闭多余的客户端连接上来的socket
continue;
}
TcpServer.CloseListen();//子进程关闭监听的socket
char strbuffer[1024]; // 存放数据的缓冲区。
signal(SIGINT,ChldEXIT);signal(SIGTERM,ChldEXIT);
while (true)
{
memset(strbuffer,0,sizeof(strbuffer));
if (TcpServer.Read(strbuffer,300)==false) break; // 接收客户端发过来的请求报文。
//printf("接收:%s\n",strbuffer);
logfile.Write("接收:%s\n",strbuffer);
strcat(strbuffer,"ok"); // 在客户端的报文后加上"ok"。
//printf("发送:%s\n",strbuffer);
logfile.Write("发送:%s\n",strbuffer);
if (TcpServer.Write(strbuffer)==false) break; // 向客户端回应报文。
}
//printf("客户端已断开。\n"); // 程序直接退出,析构函数会释放资源。
logfile.Write("客户端已经断开。\n");
exit(0);
}
}
客户端程序
#include "../_freecplus.h"
int main(int argc,char *argv[])
{
CTcpClient TcpClient; // 创建客户端的对象。
if (TcpClient.ConnectToServer("192.168.74.10",5005)==false) // 向服务端发起连接请求。
{
printf("TcpClient.ConnectToServer(\"172.16.0.15\",5858) failed.\n"); return -1;
}
char strbuffer[1024]; // 存放数据的缓冲区。
printf("pid=%d\n",getpid());
for (int ii=0;ii<20;ii++) // 利用循环,与服务端进行5次交互。
{
memset(strbuffer,0,sizeof(strbuffer));
snprintf(strbuffer,50,"pid=%d:这是第%d个超级女生,编号%03d。",getpid(),ii+1,ii+1);
printf("发送:%s\n",strbuffer);
if (TcpClient.Write(strbuffer)==false) break; // 向服务端发送请求报文。
memset(strbuffer,0,sizeof(strbuffer));
if (TcpClient.Read(strbuffer,20)==false) break; // 接收服务端的回应报文。
printf("接收:%s\n",strbuffer);
sleep(1);
}
// 程序直接退出,析构函数会释放资源。
}
运行窗口
- 服务端后台运行
- 客户端发起连接
- 查看服务端运行的进程,无僵尸进程的产生
- 查看服务端的打印日志,日志正常打印输出
- 杀死父进程
- 业务代码
服务端业务主体代码
while (true)
{
memset(strrecvbuffer,0,sizeof(strrecvbuffer));
memset(strsendbuffer,0,sizeof(strsendbuffer));
if (TcpServer.Read(strrecvbuffer,30)==false) break; // 接收客户端发过来的请求报文。
logfile.Write("接收:%s\n",strrecvbuffer);
// 处理业务的主函数。
if (_main(strrecvbuffer,strsendbuffer)==false) ChldEXIT(-1);
logfile.Write("发送:%s\n",strsendbuffer);
if (TcpServer.Write(strsendbuffer)==false) break; // 向客户端回应报文。
}
bool _main(const char *strrecvbuffer,char *strsendbuffer) // 处理业务的主函数。
{
int ibizcode=-1;
GetXMLBuffer(strrecvbuffer,"bizcode",&ibizcode);
switch (ibizcode)
{
case 0: // 心跳
biz000(strrecvbuffer,strsendbuffer); break;
case 1: // 身份验证。
biz001(strrecvbuffer,strsendbuffer); break;
case 2: // 查询余额。
biz002(strrecvbuffer,strsendbuffer); break;
default:
logfile.Write("非法报文:%s\n",strrecvbuffer); return false;
}
return true;
}
// 身份验证业务处理函数。
bool biz001(const char *strrecvbuffer,char *strsendbuffer)
{
char username[51],password[51];
memset(username,0,sizeof(username));
memset(password,0,sizeof(password));
GetXMLBuffer(strrecvbuffer,"username",username,50);
GetXMLBuffer(strrecvbuffer,"password",password,50);
if ( (strcmp(username,"wwc")==0) && (strcmp(password,"19960207")==0) )
sprintf(strsendbuffer,"<retcode>0</retcode><message>成功。</message>");
else
sprintf(strsendbuffer,"<retcode>-1</retcode><message>用户名或密码不正确。</message>");
return true;
}
客户端主体代码
while(1){
if(biz001()==false);
break;
}
// 身份验证。
bool biz001()
{
char strbuffer[1024]; // 存放数据的缓冲区。
memset(strbuffer,0,sizeof(strbuffer));
char name[10];
memset(name,0,sizeof(name));
char password[10];
memset(password,0,sizeof(password));
printf("请输入用户名:");
scanf("%s",name);
printf("请输入密码:");
scanf("%s",password);
snprintf(strbuffer,1000,"<bizcode>1</bizcode><username>%s</username><password>%s</password>",name,password);
printf("发送:%s\n",strbuffer);
if (TcpClient.Write(strbuffer)==false) return false; // 向服务端发送请求报文。
memset(strbuffer,0,sizeof(strbuffer));
if (TcpClient.Read(strbuffer,20)==false) return false; // 接收服务端的回应报文。
printf("接收:%s\n",strbuffer);
int iretcode=-1;
GetXMLBuffer(strbuffer,"retcode",&iretcode);
if (iretcode==0) { printf("身份验证成功。\n"); return true; }
printf("身份验证失败。\n");
return false;
}
多线程通信
多线程服务程序退出
思路
- 在主进程中设置程序退出的信号2 和15的信号处理函数
- 在信号处理函数中取消主进程的监听,设置全部的线程取消
- 在线程处理函数中首先注册线程清理函数,将线程设置为分离,将取消类型设置为(立即取消、延迟取消),并且最后执行线程清理函数(注册清理函数和执行函数成对出现)
- 编写清理函数,将线程连接的socket关闭
需要注意的点:收到2,15信号,执行信号函数取消同样会调用线程清理函数,我这里usleep(200),即可发现这个问题
服务端代码
#include "../_freecplus.h"
void* pthmain(void* arg);
void mainexit(int sig);
vector<pthread_t>vpthid;
CTcpServer TcpServer; // 创建服务端对象。
int main(int argc,char *argv[])
{
signal(2,mainexit);signal(5,mainexit);
if (TcpServer.InitServer(5005)==false) // 初始化TcpServer的通信端口。
{
printf("TcpServer.InitServer(5858) failed.\n"); return -1;
}
while(true)
{
if (TcpServer.Accept()==false) // 等待客户端连接。
{
printf("TcpServer.Accept() failed.\n"); return -1;
}
printf("客户端(%s)已连接。\n",TcpServer.GetIP());
pthread_t pthid1;
if(pthread_create(&pthid1,NULL,pthmain,(void*)(long)TcpServer.m_connfd)!=0)
{
printf("creat pthid1 fail\n");
return -1;
}
vpthid.push_back(pthid1);
}
return 0;
}
//2,15信号处理函数
void mainexit(int sig)
{
printf("mainexit begin.\n");
TcpServer.CloseListen();//关闭监听的socket
for(int i=0;i<vpthid.size();i++)
{
printf("cancel %ld\n",vpthid[i]);
pthread_cancel(vpthid[i]);//取消全部的线程
}
printf("mainexit end.\n");
usleep(20);
exit(0);
}
//设置线程清理函数
void pthmainexit(void* arg)
{
printf("线程清理函数开始。\n");
close((int)(long)arg);//关闭线程连接的socket
for(int i=0;i<vpthid.size();i++)
{
if(vpthid[i]==pthread_self())
{
vpthid.erase(vpthid.begin()+i);
}
}
printf("线程清理函数结束.\n");
}
void* pthmain(void* arg)
{
pthread_cleanup_push(pthmainexit,arg);//注册线程清理函数
pthread_detach(pthread_self());
pthread_setcanceltype(PTHREAD_CANCEL_DISABLE,NULL);
int sockfd=(int)(long)(arg);
while (true)
{
char strbuffer[1024]; // 存放数据的缓冲区。
int buflen=0;
memset(strbuffer,0,sizeof(strbuffer));
if (TcpRead(sockfd,strbuffer,&buflen,300)==false) break; // 接收客户端发过来的请求报文。
printf("接收:%s\n",strbuffer);
strcat(strbuffer,"ok"); // 在客户端的报文后加上"ok"。
printf("发送:%s\n",strbuffer);
if (TcpWrite(sockfd,strbuffer,buflen)==false) break; // 向客户端回应报文。
}
printf("客户端已断开。\n");
pthread_cleanup_pop(1);//执行线程清理函数
}
运行窗口
性能测试
服务端并发量测试
- 实验环境:两台独立的虚拟机,客户端给服务端只发送心跳报文,测试1000台客户端连接服务端后,服务端均为多进程,查看服务端内存和cpu的变化情况
实验前
cpu的运行情况:
内存的消耗情况:
实验后
服务端运行的进程数
cpu的运行情况:
内存的消耗情况:
服务端业务性能测试
- 实验环境:两台独立的虚拟机,客户端给服务端只发送心跳报文,测试100台客户端同时连接服务端后,服务端均为多进程,查看服务端cpu的变化情况,以及每秒接受的信息量。
== 客户端业务==
for (int ii=0;ii<2000;ii++)
{
if (biz000()==false) break;
usleep(10000);
}
bool biz000() // 发送心跳报文。
{
char strbuffer[1024]; // 存放数据的缓冲区。
memset(strbuffer,0,sizeof(strbuffer));
snprintf(strbuffer,1000,"<bizcode>0</bizcode>");
//printf("发送:%s\n",strbuffer);
if (TcpClient.Write(strbuffer)==false) return false; // 向服务端发送请求报文。
memset(strbuffer,0,sizeof(strbuffer));
if (TcpClient.Read(strbuffer,20)==false) return false; // 接收服务端的回应报文。
//printf("接收:%s\n",strbuffer);
return true;
}
服务端消耗内存
之前:
之后:
结论:消耗内存大概为16M
服务端cpu情况:
服务端每秒接收的数据量:
多进程/多线程服务端性能
多线程:
内存消耗对比:
之前:
之后:
cpu消耗情况:
多线程1s处理报文的数量:
结论:1.连接相同客户端,处理相同的业务,多线程消耗内存为7M,多进程为16M
2.多进程cpu剩余30%,多进程剩余20%
3.多进程和多线程1s处理的报文数基本相同,都为8000多。
客户端业务响应时间
环境:两台centos7局域网上客户端在不同阶段的测试响应时间
1.客户端
2.客户端给服务端发送心跳报文,此时服务端闲时,计算从发送到接受的耗时
3.客户端给服务端发送心跳报文,此时服务端忙时,计算从发送到接受的耗时