@TOC
1.APP需求和架构:多进程
如下手机开发人员可以获得用户的手机现在采用的网络是wifi还是2G.....
获得这些数据后对我们服务端后台很重要,若采用wifi后台处理方式不一样。天气实况(温压湿...)就是从全国分钟数据里取的,最多存3天就可以满足手机app需求,图片用linux下工具imagemagick用system函数调用。最下面一层是iphone等是客户端
测试多任务下文件操作和测试多任务下socket操作:多进程fp指针变量会复制多份,往一个文件里写会出问题,比如多进程日志切换(
logfile.Open里第三个参数备份为true:关闭一文件再往新文件里写,其他进程在关闭的文件里写时会出问题
),数据库连接池conn底层也是socket![](https://img.haomeiwen.com/i16331821/d8b3bc295429fd3f.png)
如下一个进程关了,其他进程没有关,大小不够没有日志切换,继续写不出问题
make demo21,./demo21,如下没出问题
![](https://img.haomeiwen.com/i16331821/9868b19b1ef9181f.png)
如下log文件为空,多进程不能关闭后再打开,所以多进程logfile.Open里第三个参数备份应为false不能切换
![](https://img.haomeiwen.com/i16331821/b4345ae244c09c29.png)
如下做日志切换会丢掉很多东西
![](https://img.haomeiwen.com/i16331821/153b3ef99c78b15d.png)
![](https://img.haomeiwen.com/i16331821/0422d70ebc2bd5c5.png)
数据库操作也是写文件,事务约束等检查效率比文件系统慢
![](https://img.haomeiwen.com/i16331821/cf1b772a45e6ec1e.png)
![](https://img.haomeiwen.com/i16331821/80f8bea8ea064cd3.png)
如下demo21多进程
![](https://img.haomeiwen.com/i16331821/bfe7f5149fc906ad.png)
//demo22.cpp
#include "_public.h"
CLogFile logfile;
void *pth1_main(void *arg)
{
for (int jj=0;jj<200000;jj++)
{
logfile.Write("pid=%10d,jj=%10d\n",getpid(),jj);
}
pthread_exit(0);
}
int main(int argc,char *argv[])
{
logfile.Open("/tmp/demo22.log","w",true,true);
for (int ii=0;ii<50;ii++)
{
pthread_t pthid;
pthread_create(&pthid,NULL,pth1_main,NULL);
}
sleep(60); //50个线程写完运行完大概需1分钟,主进程sleep1分钟再退让线程都能写完
return 0;
}
如下未切换日志,50个线程写未出问题
![](https://img.haomeiwen.com/i16331821/952a5e6bb519be6b.png)
如下切换日志logfile.Open第三个参数为true时,如上demo22.log文件490M,_public.cpp中改为100M。logfile.Open第四个参数一般为false,root用户userdel robert删不了因为有程序运行,su -robert禁用crontab再exit测试,
多线程日志切换不受影响,多进程日志切换受影响
![](https://img.haomeiwen.com/i16331821/b0c2607f46096d4d.png)
![](https://img.haomeiwen.com/i16331821/1883cbb0121ae490.png)
测试多任务下socket操作:如下注释掉fork()变为单进程进程号都为15878,demo25服务端recv到的再加上resp返回去
// 客户端demo23.cpp
#include "_public.h"
CTcpClient TcpClient;
CLogFile logfile;
int main(int argc,char *argv[])
{
logfile.Open("/tmp/demo23.log","w",false);
if (TcpClient.ConnectToServer("127.0.0.1",5015)==false) { printf("conn failed.\n"); return -1; }
char strSendBuffer[301],strRecvBuffer[301];
fork(); //fork出一个进程+父进程=两个进程
for (int ii=0;ii<20;ii++) //发20个报文
{
memset(strSendBuffer,0,sizeof(strSendBuffer));
memset(strRecvBuffer,0,sizeof(strRecvBuffer));
sprintf(strSendBuffer,"this is %10d(%10d)",ii,getpid());
logfile.Write("send=%s=\n",strSendBuffer);
if (TcpClient.Write(strSendBuffer)==false) { printf("send failed.\n"); return -1; }
sleep(1);
if (TcpClient.Read(strRecvBuffer,80)==false) { printf("recv failed.\n"); return -1; }
logfile.Write("recv=%s,%10d=\n",strRecvBuffer,getpid());
}
return 0;
}
//demo25.cpp服务端
#include "_public.h"
CTcpServer TcpServer;
CLogFile logfile;
int main(int argc,char *argv[])
{
logfile.Open("/tmp/demo25.log","w",false);
if (TcpServer.InitServer(5015)==false) { printf("init failed.\n"); return -1; }
TcpServer.Accept();
char strSendBuffer[301],strRecvBuffer[301];
for (int ii=0;ii<10;ii++)
{
memset(strSendBuffer,0,sizeof(strSendBuffer));
memset(strRecvBuffer,0,sizeof(strRecvBuffer));
if (TcpServer.Read(strRecvBuffer,80)==false) { printf("recv failed.\n"); return -1; }
logfile.Write("recv=%s=\n",strRecvBuffer);
sprintf(strSendBuffer,"%s resp",strRecvBuffer);
logfile.Write("send=%s=\n",strSendBuffer);
if (TcpServer.Write(strSendBuffer)==false) { printf("send failed.\n"); return -1; }
}
return 0;
}
make ,./demo25,./demo23,vi /tmp/demo23.log正常显示,因为报文数据量不够,冲突没有体现出来,如果数据量够且收发速度快,则会冲突,如下在客户端中
![](https://img.haomeiwen.com/i16331821/8bc50878f53b88cf.png)
![](https://img.haomeiwen.com/i16331821/328d8863e49f9310.png)
两个客户端进程接收时冲突
![](https://img.haomeiwen.com/i16331821/7da318ee4169ed84.png)
如下在客户端中两个进程都可发送,接收只让一个进程干。服务端永远一个进程
![](https://img.haomeiwen.com/i16331821/92e2bb46f48d8127.png)
多进程socket连接不会用来共享,返回不知道给哪个客户端进程,多进程用信号灯加锁麻烦,一般用多线程并加锁。
数据库连接也是socket服务端,不可能让多个进程同时写如事务
。多个线程可共用
socket连接(协调单个发),但不能说多个线程共享
一个socket连接(一起发),一个socket连接同一时间只能有一个线程使用,用完后提交事务,连接才释放出来![](https://img.haomeiwen.com/i16331821/7350f01d1e751e9f.png)
如下开始APP服务端设计,客户端就是手机app软件。第一次客户端将手机编号传给服务端,服务端将站点信息传给客户端。客户端得到日出日落时间分180份,现在时间-日出时间大概知道在原点什么位置,都由客户端做,传图就是传文件
短连接
:客户端即用户点击按钮一次建立一次socket连接请求,处理完一个就断开
,响应慢:建立一次socket连接费时间,服务端fork一个进程也要时间,之后和数据库连接也要时间长连接
:客户端与服务端socket一直连接着进行数据通信,没有数据通信时用心跳(之前文件传输都用的是长连接)
,用户关了app,连接才断开。费服务端资源:长连接连上后,数据库连接和进程都已准备好,一直通信完才断开。但响应快,用户看到数据越快越好控制在1秒内。如下项目组织(shtqapp是一个独立的项目)如下是数据结构设计
![](https://img.haomeiwen.com/i16331821/cef6b4e12d7b9e7b.png)
如下不需
收集
用户密码,T_USERINFO表和T_OBTCODE表放入shtqapp数据库用户里,T_USERLOG表放入shappdata数据库用户里如下用户使用日志表
用户位置
经纬海拔高度![](https://img.haomeiwen.com/i16331821/0369def56ef96468.png)
如下preview的sql语句是建表
![](https://img.haomeiwen.com/i16331821/447b3f42e6b6f840.png)
以下是多进程,正式上线时用http80端口(用户在某个网络内部80端口肯定开放,不开放外网上不了,qq微信虽不是用http协议但也采用80端口)
// client.cpp,模拟tcp手机客户端,客户端用短链接还是长连接由客户端自己安排
#include "_freecplus.h"
CTcpClient TcpClient;
char strSendBuffer[301],strRecvBuffer[301];
bool biz10000(); // 心跳
bool biz10001(); // 新用户登录:只传个设备编号id,服务端把城市站点信息传给客户端,手机利用定位匹配
int main(int argc,char *argv[])
{
//if (TcpClient.ConnectToServer("127.0.0.1",5015)==false) { printf("conn failed.\n"); return -1; }
if (TcpClient.ConnectToServer("172.16.0.15",5015)==false) { printf("conn failed.\n"); return -1; }
//if (TcpClient.ConnectToServer("118.89.50.198",5015)==false) { printf("conn failed.\n"); return -1; }
if (biz10000()==false) return 0; // 心跳
CTimer Timer;
if (biz10001()==false) return 0; // 新用户登录
printf("biz10001=%lf\n",Timer.Elapsed());
sleep(1);
return 0;
}
bool biz10000()
{
memset(strSendBuffer,0,sizeof(strSendBuffer));
memset(strRecvBuffer,0,sizeof(strRecvBuffer));
strcpy(strSendBuffer,"<bizid>10000</bizid>");
//printf("send=%s=\n",strSendBuffer);
if (TcpClient.Write(strSendBuffer)==false) { printf("send failed.\n"); return false; }
if (TcpClient.Read(strRecvBuffer,20)==false) { printf("recv failed.\n"); return false; }
//printf("recv=%s=\n",strRecvBuffer);
return true;
}
bool biz10001()
{
memset(strSendBuffer,0,sizeof(strSendBuffer));
memset(strRecvBuffer,0,sizeof(strRecvBuffer));
// 如下请求报文
strcpy(strSendBuffer,"<bizid>10001</bizid><userid>52:54:00:83:0f:c1</userid><ttytype>1</ttytype><lat>20.234518</lat><lon>115.90832</lon><height>150.5</height>");
//printf("send=%s=\n",strSendBuffer);
if (TcpClient.Write(strSendBuffer)==false) { printf("send failed.\n"); return false; }
//如下用一个循环接收全部的站点信息
while (1)
{
memset(strRecvBuffer,0,sizeof(strRecvBuffer));
if (TcpClient.Read(strRecvBuffer,20)==false) { printf("recv failed.\n"); return false; }
// printf("recv=%s=\n",strRecvBuffer); //手机端没数据库,手机软件真正处理方法把数据保存到xml文件里
if (strcmp(strRecvBuffer,"ok")==0) break; //接收到ok的话表示数据处理完了
}
return true;
}
// 上海天气APP软件服务端主程序shtqappserver.cpp多进程。
#include "_public.h"
#include "_ooci.h"
// 业务请求
struct st_biz
{
int bizid; // 业务代码
char userid[51]; // 设备ID
int ttytype; // 用户的设备类型,0-未知;1-IOS;2-Andriod,2-鸿蒙。
int usertype; // 用户分类,0-未知;1-普通用户;2-气象志愿者;3-内部用户。
double lon;
double lat;
double height;
char obtid[11];
char xmlbuffer[1001];
} stbiz;
// 把xml解析到参数stbiz结构中
void xmltobiz(char *strxmlbuffer);
CTcpServer TcpServer;
CLogFile logfile;
connection conn;
//如上定义为全局变量共享但不会在main函数主进程里连数据库,在子进程里连。如果在主进程连,子进程用是不行的
//conn若定义为局部变量,ChldEXIT()中conn不会调用析构函数,全局会调用释放资源。
//CTcpServer析构函数会自动调用CloseClient()方法关闭socket连接
char strRecvBuffer[TCPBUFLEN+10]; // 接收报文的缓冲区
char strSendBuffer[TCPBUFLEN+10]; // 发送报文的缓冲区
// 程序退出时调用的函数
void FathEXIT(int sig);
void ChldEXIT(int sig);
// 业务处理进程主函数
void ChldMain();
// 心跳业务
bool biz10000();
// 新用户登录业务,用户基本信息表和用户使用日志都插入一条记录,select全国气象站点数据返回给客户端
bool biz10001();
// 获取天气实况
bool biz10002();
// 插入用户请求日志表
bool InsertUSERLOG();
int main(int argc,char *argv[])
{
if (argc != 3)
{
printf("\n");
printf("Using:/htidc/shtqapp/bin/shtqappserver logfilename port\n");
printf("Example:/htidc/shtqapp/bin/shtqappserver /log/shtqapp/shtqappserver.log 5015\n\n");
printf("本程序是上海天气APP软件的服务端。\n");
printf("logfilename 日志文件名。\n");
printf("port 用于传输文件的TCP端口。\n");
return -1;
}
// 关闭全部的信号和输入输出
// 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
// 但请不要用 "kill -9 +进程号" 强行终止
CloseIOAndSignal(); signal(SIGINT,FathEXIT); signal(SIGTERM,FathEXIT);
// 打开程序运行日志,这是一个多进程程序,日志不能自动切换
if (logfile.Open(argv[1],"a+",false) == false)
{
printf("logfile.Open(%s) failed.\n",argv[1]); return -1;
}
logfile.Write("shtqappserver started(%s).\n",argv[2]);
if (TcpServer.InitServer(atoi(argv[2])) == false)
{
logfile.Write("TcpServer.InitServer(%s) failed.\n",argv[2]); exit(-1);
}
while (true)
{
// 等待客户端的连接
if (TcpServer.Accept() == false)
{
logfile.Write("TcpServer.Accept() failed.\n"); continue;
}
// 新的客户端连接来fork一个进程
if (fork() > 0)
{
// 父进程关闭刚建立起来的sock连接,并回到Accept继续监听
TcpServer.CloseClient(); continue;
}
// 进入子进程的流程
signal(SIGINT,ChldEXIT); signal(SIGTERM,ChldEXIT);
// 子进程需要关掉监听的sock
TcpServer.CloseListen();
// 子进程业务处理进程主函数
ChldMain();
ChldEXIT(0);
}
return 0;
}
// 父进程退出时调用的函数
void FathEXIT(int sig)
{
if (sig > 0) //如果收到信号就屏蔽信号
{
signal(sig,SIG_IGN); signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
logfile.Write("catching the signal(%d).\n",sig);
}
kill(0,15); //通知全部子进程退出
logfile.Write("shtqappserver EXIT.\n");
exit(0);
}
// 子进程退出时调用的函数
void ChldEXIT(int sig)
{
if (sig > 0) //如果不屏蔽的话会收到两个退出信号
{
signal(sig,SIG_IGN); signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
}
exit(0);
}
// 业务处理进程主函数
void ChldMain()
{
while (true)
{
memset(strRecvBuffer,0,sizeof(strRecvBuffer));
memset(strSendBuffer,0,sizeof(strSendBuffer));
// 接收客户端的业务请求报文,如果返回false,认为是客户端退出或网络原因,直接return不写错误日志
if (TcpServer.Read(strRecvBuffer,50) == false)
{
// 比如用户手机app回到桌面,程序挂起不需要app体面退出,Read失败很多不用写日志
// logfile.Write("TcpServer.Read() failed.\n");
return;
}
logfile.Write("strRecvBuffer=%s\n",strRecvBuffer); // xxxxxx
// 把参数解析出来,客户端与服务端通信全用xml,每种业务请求弄一个业务编号
xmltobiz(strRecvBuffer);
if (stbiz.bizid==10000) // 心跳报文
{
if (biz10000()==true) continue;
else return;
}
CTimer Timer;
// 连接数据库,第三个参数为true自动提交
if (conn.connecttodb("shtqapp/pwdidc@snorcl11g_198","Simplified Chinese_China.ZHS16GBK",true)!=0)
{
logfile.Write("conn.connettodb() failed.\n"); return;
}
logfile.Write("conn=%lf\n",Timer.Elapsed());
// 新用户登录
if (stbiz.bizid==10001)
{
if (biz10001()==true) continue;
else return;
}
// 获取天气实况
if (stbiz.bizid==10002)
{
if (biz10002()==true) continue;
else return;
}
// 协议:你发什么给我,我发什么给你。报文格式不符合要求的话直接return
logfile.Write("非法报文%s\n",strRecvBuffer); return;
}
}
// 把xml解析到参数starg结构中,客户端全部请求放入starg数据结构里
void xmltobiz(char *strxmlbuffer)
{
memset(&stbiz,0,sizeof(struct st_biz));
// 客户端发给服务端全部报文都会有个业务代码
GetXMLBuffer(strxmlbuffer,"bizid",&stbiz.bizid);
// logfile.Write("bizid=%d\n",stbiz.bizid);
// 用户设备ID
GetXMLBuffer(strxmlbuffer,"userid",stbiz.userid,50);
// logfile.Write("userid=%s\n",stbiz.userid);
GetXMLBuffer(strxmlbuffer,"obtid",stbiz.obtid,10);
// logfile.Write("obtid=%s\n",stbiz.obtid);
GetXMLBuffer(strxmlbuffer,"lat",&stbiz.lat);
// logfile.Write("lat=%lf\n",stbiz.lat);
GetXMLBuffer(strxmlbuffer,"lon",&stbiz.lon);
// logfile.Write("lon=%lf\n",stbiz.lon);
GetXMLBuffer(strxmlbuffer,"height",&stbiz.height);
// logfile.Write("height=%lf\n",stbiz.height);
strncpy(stbiz.xmlbuffer,strxmlbuffer,1000);
return;
}
///////////////////////////////////////////////1.心跳业务
bool biz10000()
{
memset(strSendBuffer,0,sizeof(strSendBuffer));
strcpy(strSendBuffer,"ok");
if (TcpServer.Write(strSendBuffer) == false)
{
logfile.Write("biz10000 TcpServer.Write() failed.\n"); return false;
}
return true;
}
/////////////////////////////////////2.新用户登录
bool biz10001()
{
CTimer Timer;
///////////////////////////////////////////2.1 插入用户基本信息表T_USERINFO:设备ID和设备类型
sqlstatement stmt(&conn);
stmt.prepare("insert into T_USERINFO(userid,downtime,ttytype,keyid) values(:1,sysdate,:2,SEQ_USERINFO.nextval)");
stmt.bindin(1, stbiz.userid,50);
stmt.bindin(2,&stbiz.ttytype);
if (stmt.execute() != 0)
{
if (stmt.m_cda.rc!=1) //出现主键冲突不管(之前下载过,表里留下了数据,所以会出现主键冲突)
{
logfile.Write("insert T_USERINFO failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); return false;
}
}
logfile.Write("insert T_USERINFO =%lf\n",Timer.Elapsed());
///////////////////////////////////////////////////////2.2 插入用户请求日志表
if (InsertUSERLOG()==false) return false; // 是模块通用功能,所以写成函数
logfile.Write("insert T_USERLOG =%lf\n",Timer.Elapsed());
//////////////////////////////////////////////////2.3 拿出全国站点参数信息返回给客户端
char strobtid[6],strobtname[31],strlon[11],strlat[11];
stmt.prepare("select obtid,obtname,lon,lat from T_OBTCODE where rsts=1 and rownum<=30");
stmt.bindout(1,strobtid,5);
stmt.bindout(2,strobtname,30);
stmt.bindout(3,strlon,10);
stmt.bindout(4,strlat,10);
if (stmt.execute() != 0)
{
logfile.Write("select T_OBTCODE failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); return false;
}
while (true)
{
memset(strobtid,0,sizeof(strobtid));
memset(strobtname,0,sizeof(strobtname));
memset(strlon,0,sizeof(strlon));
memset(strlat,0,sizeof(strlat));
memset(strSendBuffer,0,sizeof(strSendBuffer));
if (stmt.next()!=0) break;
//获得一条记录后生成一个buffer写过去
sprintf(strSendBuffer,"<obtid>%s</obtid><obtname>%s</obtname><lon>%s</lon><lat>%s</lat><endl/>",strobtid,strobtname,strlon,strlat);
if (TcpServer.Write(strSendBuffer) == false)
{
logfile.Write("biz10001 TcpServer.Write() failed.\n"); return false;
}
}
logfile.Write("select =%lf\n",Timer.Elapsed());
// 直到全部记录被处理完,最后发送一个ok
strcpy(strSendBuffer,"ok");
if (TcpServer.Write(strSendBuffer) == false)
{
logfile.Write("biz10001 TcpServer.Write() failed.\n"); return false;
}
return true;
}
/////////////////////////////////3.插入用户请求日志表
bool InsertUSERLOG()
{
sqlstatement stmt(&conn);
stmt.prepare("insert into T_USERLOG(logid,userid,atime,bizid,obtid,lon,lat,height,xmlbuffer) values(SEQ_USERLOG.nextval,:1,sysdate,:2,:3,:4,:5,:6,:7)");
stmt.bindin(1, stbiz.userid,50);
stmt.bindin(2,&stbiz.bizid);
stmt.bindin(3, stbiz.obtid,10);
stmt.bindin(4,&stbiz.lon);
stmt.bindin(5,&stbiz.lat);
stmt.bindin(6,&stbiz.height);
stmt.bindin(7, stbiz.xmlbuffer,10000);
if (stmt.execute() != 0)
{
logfile.Write("insert T_USERLOG failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); return false;
}
return true;
}
////////////////////////4.获取天气实况
bool biz10002()
{
return true;
}
如下现在只有一个心跳业务,创建同义词好处当shtqapp用户里表满了迁移到shappdata用户表里,代码不用变
2.测试服务端的性能:CTimer
一分钟4万3次请求,则1秒700次请求(测试一次访问请求要多少时间,再算出1秒可有多少次访问请求)
如下在client.cpp中,记下这个业务从发送请求到接收完成要多长时间
![](https://img.haomeiwen.com/i16331821/4354cf7c9afad6c0.png)
如下在shtqappserver.cpp中
用外网地址测试,总的说客户端做一次业务交互(连数据库和收发报文)在1秒中内可以完成
到底采用长连接还是短链接?如果一秒700次请求,同时在线一分钟则700乘60约近4万次,长连接4万socket连上,4万进程服务器不可接受,所以采用短链接(长短连接由客户端控制:客户端发多个报文中间再发心跳是长连接,短连接发一次请求,连接断开一次)。
如下同时运行50个客户端,业务耗时1秒以上,1秒50个没问题
。实际业务系统服务器性能至少高于10倍且APP软件服务器可以几台,最简单方法:来一个请求,服务端生成一个进程连一次数据库,这能满足要求![](https://img.haomeiwen.com/i16331821/712f32853cdb63c1.png)
客户端连接越多,服务端连数据库耗时越长,26:00到26:02服务端总耗时2秒
![](https://img.haomeiwen.com/i16331821/ffa0aa5bdaf4803b.png)
3.高性能网络服务开发:多线程,连接池
旧框架中前面4个字节是ascii码即美码
![](https://img.haomeiwen.com/i16331821/060ff7aa55855334.png)
新框架中前面4字节是整数,写进去不是0010,是二进制的整数。读报文时也是先读一个整数出来再得到后面报文长度。所以不需要宏,报文长度也没限制,整数取多大就取多大
![](https://img.haomeiwen.com/i16331821/c758a5bec093100a.png)
![](https://img.haomeiwen.com/i16331821/e68b4fd5e95a2e5c.png)
![](https://img.haomeiwen.com/i16331821/0b23c4155669fe81.png)
![](https://img.haomeiwen.com/i16331821/910aa54f08cf7448.png)
//上海天气APP软件服务端主程序shtqappserver.cpp多线程方式
#include "_freecplus.h"
#include "_ooci.h"
// 业务请求
struct st_biz
{
int bizid; // 业务代码
char userid[51]; // 设备ID
int ttytype; // 用户的设备类型,0-未知;1-IOS;2-Andriod,2-鸿蒙。
int usertype; // 用户分类,0-未知;1-普通用户;2-气象志愿者;3-内部用户。
double lon;
double lat;
double height;
char obtid[11];
char xmlbuffer[1001];
};
// 把xml解析到参数stbiz结构中
void xmltobiz(char *strxmlbuffer,struct st_biz *stbiz);
CTcpServer TcpServer;
CLogFile logfile;
// 程序退出时调用的函数
void EXIT(int sig);
// 与客户端通信线程的主函数
void *pth_main(void *arg);
// 心跳业务
bool biz10000(int clientfd);
// 新用户登录业务
bool biz10001(struct st_biz *stbiz,connection *conn,int clientfd);
// 获取天气实况
bool biz10002(struct st_biz *stbiz,connection *conn,int clientfd);
// 插入用户请求日志表
bool InsertUSERLOG(struct st_biz *stbiz,connection *conn);
// 存放客户端已连接的socket的容器
vector<int> vclientfd;
void AddClient(int clientfd); // 把客户端新的socket加入vclientfd容器中
void RemoveClient(int clientfd); // 关闭客户端的socket并从vclientfd容器中删除,
// 关闭客户端的socket并从vclientfd容器中删除,
void RemoveClient(int clientfd);
int main(int argc,char *argv[])
{
if (argc != 3)
{
printf("\n");
printf("Using:/htidc/shtqapp1/bin/shtqappserver logfilename port\n");
printf("Example:/htidc/shtqapp1/bin/shtqappserver /log/shtqapp/shtqappserver.log 5015\n\n");
printf("本程序是上海天气APP软件的服务端。\n");
printf("logfilename 日志文件名。\n");
printf("port 用于传输文件的TCP端口。\n");
return -1;
}
// 关闭全部的信号和输入输出
// 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
// 但请不要用 "kill -9 +进程号" 强行终止
CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
// 打开程序运行日志,这是一个多进程程序,日志不能自动切换
if (logfile.Open(argv[1],"a+",false) == false)
{
printf("logfile.Open(%s) failed.\n",argv[1]); return -1;
}
logfile.Write("shtqappserver started(%s).\n",argv[2]);
if (TcpServer.InitServer(atoi(argv[2])) == false)
{
logfile.Write("TcpServer.InitServer(%s) failed.\n",argv[2]); exit(-1);
}
// 保存服务端的listenfd到vclientfd
AddClient(TcpServer.m_listenfd);
while (true)
{
// 等待客户端的连接
if (TcpServer.Accept() == false)
{
logfile.Write("TcpServer.Accept() failed.\n"); continue;
}
pthread_t pthid; // 创建一线程,与新连接上来的客户端通信
if (pthread_create(&pthid,NULL,pth_main,(void*)(long)TcpServer.m_connfd)!=0)
{
logfile.Write("创建线程失败,程序退出。n"); close(TcpServer.m_connfd); EXIT(-1);
}
logfile.Write("%s is connected.\n",TcpServer.GetIP());
// 保存每个客户端的socket到vclientfd
AddClient(TcpServer.m_connfd);
}
return 0;
}
// 退出时调用的函数
void EXIT(int sig)
{
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
if (sig>0) signal(sig,SIG_IGN);
logfile.Write("tcpfileserver1 exit,sig=%d...\n",sig);
// 关闭vclientfd容器中全部的socket
for (int ii=0;ii<vclientfd.size();ii++)
{
close(vclientfd[ii]);
}
exit(0);
}
// 与客户端通信线程的主函数
void *pth_main(void *arg)
{
int clientfd=(long) arg; // arg参数为新客户端的socket。
pthread_detach(pthread_self());
struct st_biz stbiz;
connection conn; //不能定义为全局变量
int ibuflen=0;
char strRecvBuffer[1024]; //接收报文的缓冲区,不能定义为全局变量
while (true)
{
memset(strRecvBuffer,0,sizeof(strRecvBuffer));
// 接收客户端的业务请求报文,如果返回false,认为是客户端退出或网络原因,不写错误日志
if (TcpRead(clientfd,strRecvBuffer,&ibuflen,50) == false)
{
// logfile.Write("TcpRead() failed.\n");
break;
}
logfile.Write("strRecvBuffer=%s\n",strRecvBuffer); // xxxxxx
// 把参数解析出来
xmltobiz(strRecvBuffer,&stbiz);
if (stbiz.bizid==10000) // 心跳报文
{
if (biz10000(clientfd)==true) continue;
else break;
}
CTimer Timer;
if (conn.m_state==0)
{
// 连接数据库
if (conn.connecttodb("shtqapp/pwdidc@snorcl11g_198","Simplified Chinese_China.ZHS16GBK",true)!=0)
{
logfile.Write("conn.connettodb() failed.\n"); break;
}
logfile.Write("conn=%lf\n",Timer.Elapsed());
}
// 新用户登录
if (stbiz.bizid==10001)
{
if (biz10001(&stbiz,&conn,clientfd)==true) continue;
else break;
}
// 获取天气实况
if (stbiz.bizid==10002)
{
if (biz10002(&stbiz,&conn,clientfd)==true) continue;
else break;
}
// 体力活
logfile.Write("非法报文%s\n",strRecvBuffer); break;
}
RemoveClient(clientfd);
pthread_exit(0);
}
// 把xml解析到参数starg结构中
void xmltobiz(char *strxmlbuffer,struct st_biz *stbiz)
{
memset(stbiz,0,sizeof(struct st_biz));
// 业务代码
GetXMLBuffer(strxmlbuffer,"bizid",&stbiz->bizid);
// logfile.Write("bizid=%d\n",stbiz->bizid);
// 用户设备ID
GetXMLBuffer(strxmlbuffer,"userid",stbiz->userid,50);
// logfile.Write("userid=%s\n",stbiz->userid);
GetXMLBuffer(strxmlbuffer,"obtid",stbiz->obtid,10);
// logfile.Write("obtid=%s\n",stbiz->obtid);
GetXMLBuffer(strxmlbuffer,"lat",&stbiz->lat);
// logfile.Write("lat=%lf\n",stbiz->lat);
GetXMLBuffer(strxmlbuffer,"lon",&stbiz->lon);
// logfile.Write("lon=%lf\n",stbiz->lon);
GetXMLBuffer(strxmlbuffer,"height",&stbiz->height);
// logfile.Write("height=%lf\n",stbiz->height);
strncpy(stbiz->xmlbuffer,strxmlbuffer,1000);
return;
}
////////////////////////////////////////////////////////////////1.心跳业务
bool biz10000(int clientfd)
{
char strSendBuffer[1024]; // 发送报文的缓冲区
memset(strSendBuffer,0,sizeof(strSendBuffer));
strcpy(strSendBuffer,"ok");
if (TcpWrite(clientfd,strSendBuffer) == false)
{
logfile.Write("biz10000 TcpWrite() failed.\n"); return false;
}
return true;
}
////////////////////////////////////////////////////////////////2.新用户登录
bool biz10001(struct st_biz *stbiz,connection *conn,int clientfd)
{
char strSendBuffer[1024]; // 发送报文的缓冲区
CTimer Timer;
// 插入用户基本信息表T_USERINFO
sqlstatement stmt(conn);
stmt.prepare("insert into T_USERINFO(userid,downtime,ttytype,keyid) values(:1,sysdate,:2,SEQ_USERINFO.nextval)");
stmt.bindin(1, stbiz->userid,50);
stmt.bindin(2,&stbiz->ttytype);
if (stmt.execute() != 0)
{
if (stmt.m_cda.rc!=1)
{
logfile.Write("insert T_USERINFO failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); return false;
}
}
logfile.Write("insert T_USERINFO =%lf\n",Timer.Elapsed());
// 插入用户请求日志表
if (InsertUSERLOG(stbiz,conn)==false) return false;
logfile.Write("insert T_USERLOG =%lf\n",Timer.Elapsed());
char strobtid[6],strobtname[31],strlon[11],strlat[11];
stmt.prepare("select obtid,obtname,lon,lat from T_OBTCODE where rsts=1 and rownum<=30");
stmt.bindout(1,strobtid,5);
stmt.bindout(2,strobtname,30);
stmt.bindout(3,strlon,10);
stmt.bindout(4,strlat,10);
if (stmt.execute() != 0)
{
logfile.Write("select T_OBTCODE failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); return false;
}
while (true)
{
memset(strobtid,0,sizeof(strobtid));
memset(strobtname,0,sizeof(strobtname));
memset(strlon,0,sizeof(strlon));
memset(strlat,0,sizeof(strlat));
memset(strSendBuffer,0,sizeof(strSendBuffer));
if (stmt.next()!=0) break;
sprintf(strSendBuffer,"<obtid>%s</obtid><obtname>%s</obtname><lon>%s</lon><lat>%s</lat><endl/>",strobtid,strobtname,strlon,strlat);
if (TcpWrite(clientfd,strSendBuffer) == false)
{
logfile.Write("biz10001 TcpWrite() failed.\n"); return false;
}
}
logfile.Write("select =%lf\n",Timer.Elapsed());
// 最后发送一个ok
strcpy(strSendBuffer,"ok");
if (TcpWrite(clientfd,strSendBuffer) == false)
{
logfile.Write("biz10001 TcpWrite() failed.\n"); return false;
}
return true;
}
////////////////////////////////////////////////////////////////3.插入用户请求日志表
bool InsertUSERLOG(struct st_biz *stbiz,connection *conn)
{
sqlstatement stmt(conn);
stmt.prepare("insert into T_USERLOG(logid,userid,atime,bizid,obtid,lon,lat,height,xmlbuffer) values(SEQ_USERLOG.nextval,:1,sysdate,:2,:3,:4,:5,:6,:7)");
stmt.bindin(1, stbiz->userid,50);
stmt.bindin(2,&stbiz->bizid);
stmt.bindin(3, stbiz->obtid,10);
stmt.bindin(4,&stbiz->lon);
stmt.bindin(5,&stbiz->lat);
stmt.bindin(6,&stbiz->height);
stmt.bindin(7, stbiz->xmlbuffer,10000);
if (stmt.execute() != 0)
{
logfile.Write("insert T_USERLOG failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); return false;
}
return true;
}
//////////////////////////////////////////////////////////////4.获取天气实况
bool biz10002(struct st_biz *stbiz,connection *conn,int clientfd)
{
return true;
}
///////////////////////////////////////////////5.把客户端新的socket加入vclientfd容器中
void AddClient(int clientfd)
{
vclientfd.push_back(clientfd);
}
///////////////////////////////////////6.关闭客户端的socket并从vclientfd容器中删除,
void RemoveClient(int clientfd)
{
for (int ii=0;ii<vclientfd.size();ii++)
{
if (vclientfd[ii]==clientfd)
{ close(clientfd); vclientfd.erase(vclientfd.begin()+ii); return; }
}
}
![](https://img.haomeiwen.com/i16331821/0592cc792e6200c5.png)
多线程每启一个线程都要连数据库不划算,所以采用连接池。程序启动时初始化用一数组创建10个数据库连接connection,每次想要使用数据库连接就会从10个已创建好的connection看看哪个没锁就拿1个过来用。
数据库连接池的设计可用一个参数去控制连接池的总大小,比如这连接池里有10个connection连接就需要10把锁
。在sqlstatement每次想使用数据库连接时就会从10个已创建好的connection看看哪个没锁就拿1个过来用
//上海天气APP软件服务端主程序。shtqappserver1.cpp多线程方式,采用连接池。
#include "_freecplus.h"
#include "_ooci.h"
#define MAXCONNS 10 // 数据库连接池的大小。
pthread_mutex_t mutex[MAXCONNS]; // 锁数组。
connection conns[MAXCONNS]; // 数据库连接数组。
bool initconns(); // 初始数据库连接池。
connection *getconns(); // 从连接池中获取一个数据库连接。
bool freeconns(connection *in_conn); // 释放数据库连接。
// 业务请求
struct st_biz
{
int bizid; // 业务代码
char userid[51]; // 设备ID
int ttytype; // 用户的设备类型,0-未知;1-IOS;2-Andriod,2-鸿蒙。
int usertype; // 用户分类,0-未知;1-普通用户;2-气象志愿者;3-内部用户。
double lon;
double lat;
double height;
char obtid[11];
char xmlbuffer[1001];
};
// 把xml解析到参数stbiz结构中
void xmltobiz(char *strxmlbuffer,struct st_biz *stbiz);
CTcpServer TcpServer;
CLogFile logfile;
// 程序退出时调用的函数
void EXIT(int sig);
// 与客户端通信线程的主函数
void *pth_main(void *arg);
// 心跳业务
bool biz10000(int clientfd);
// 新用户登录业务
bool biz10001(struct st_biz *stbiz,int clientfd);
// 获取天气实况
bool biz10002(struct st_biz *stbiz,int clientfd);
// 插入用户请求日志表
bool InsertUSERLOG(struct st_biz *stbiz,connection *conn);
// 存放客户端已连接的socket的容器
vector<int> vclientfd;
void AddClient(int clientfd); // 把客户端新的socket加入vclientfd容器中
void RemoveClient(int clientfd); // 关闭客户端的socket并从vclientfd容器中删除,
// 关闭客户端的socket并从vclientfd容器中删除,
void RemoveClient(int clientfd);
int main(int argc,char *argv[])
{
if (argc != 3)
{
printf("\n");
printf("Using:/htidc/shtqapp1/bin/shtqappserver1 logfilename port\n");
printf("Example:/htidc/shtqapp1/bin/shtqappserver1 /log/shtqapp/shtqappserver1.log 5015\n\n");
printf("本程序是上海天气APP软件的服务端。\n");
printf("logfilename 日志文件名。\n");
printf("port 用于传输文件的TCP端口。\n");
return -1;
}
// 关闭全部的信号和输入输出
// 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
// 但请不要用 "kill -9 +进程号" 强行终止
CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
// 打开程序运行日志,这是一个多进程程序,日志不能自动切换
if (logfile.Open(argv[1],"a+",false) == false)
{
printf("logfile.Open(%s) failed.\n",argv[1]); return -1;
}
logfile.Write("shtqappserver started(%s).\n",argv[2]);
if (TcpServer.InitServer(atoi(argv[2])) == false)
{
logfile.Write("TcpServer.InitServer(%s) failed.\n",argv[2]); EXIT(-1);
}
// 保存服务端的listenfd到vclientfd
AddClient(TcpServer.m_listenfd);
if (initconns()==false) // 初始化数据库连接池。
{
logfile.Write("initconns() failed.\n"); EXIT(-1);
}
while (true)
{
// 等待客户端的连接
if (TcpServer.Accept() == false)
{
logfile.Write("TcpServer.Accept() failed.\n"); continue;
}
pthread_t pthid; // 创建一线程,与新连接上来的客户端通信
if (pthread_create(&pthid,NULL,pth_main,(void*)(long)TcpServer.m_connfd)!=0)
{
logfile.Write("创建线程失败,程序退出。n"); close(TcpServer.m_connfd); EXIT(-1);
}
logfile.Write("%s is connected.\n",TcpServer.GetIP());
// 保存每个客户端的socket到vclientfd
AddClient(TcpServer.m_connfd);
}
return 0;
}
// 退出时调用的函数
void EXIT(int sig)
{
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
if (sig>0) signal(sig,SIG_IGN);
logfile.Write("tcpfileserver1 exit,sig=%d...\n",sig);
// 关闭vclientfd容器中全部的socket
for (int ii=0;ii<vclientfd.size();ii++)
{
close(vclientfd[ii]);
}
for (int ii=0;ii<MAXCONNS;ii++)
{
logfile.Write("disconnect and pthread_mutex_destroy.\n");
conns[ii].disconnect();
pthread_mutex_destroy(&mutex[ii]);
}
exit(0);
}
// 与客户端通信线程的主函数
void *pth_main(void *arg)
{
int clientfd=(long) arg; // arg参数为新客户端的socket。
pthread_detach(pthread_self());
struct st_biz stbiz;
int ibuflen=0;
char strRecvBuffer[1024]; // 接收报文的缓冲区
while (true)
{
memset(strRecvBuffer,0,sizeof(strRecvBuffer));
// 接收客户端的业务请求报文,如果返回false,认为是客户端退出或网络原因,不写错误日志
if (TcpRead(clientfd,strRecvBuffer,&ibuflen,50) == false)
{
// logfile.Write("TcpRead() failed.\n");
break;
}
logfile.Write("strRecvBuffer=%s\n",strRecvBuffer); // xxxxxx
// 把参数解析出来
xmltobiz(strRecvBuffer,&stbiz);
if (stbiz.bizid==10000) // 心跳报文
{
if (biz10000(clientfd)==true) continue;
else break;
}
// 新用户登录
if (stbiz.bizid==10001)
{
if (biz10001(&stbiz,clientfd)==true) continue;
else break;
}
// 获取天气实况
if (stbiz.bizid==10002)
{
if (biz10002(&stbiz,clientfd)==true) continue;
else break;
}
// 体力活
logfile.Write("非法报文%s\n",strRecvBuffer); break;
}
RemoveClient(clientfd);
pthread_exit(0);
}
// 把xml解析到参数starg结构中
void xmltobiz(char *strxmlbuffer,struct st_biz *stbiz)
{
memset(stbiz,0,sizeof(struct st_biz));
// 业务代码
GetXMLBuffer(strxmlbuffer,"bizid",&stbiz->bizid);
// logfile.Write("bizid=%d\n",stbiz->bizid);
// 用户设备ID
GetXMLBuffer(strxmlbuffer,"userid",stbiz->userid,50);
// logfile.Write("userid=%s\n",stbiz->userid);
GetXMLBuffer(strxmlbuffer,"obtid",stbiz->obtid,10);
// logfile.Write("obtid=%s\n",stbiz->obtid);
GetXMLBuffer(strxmlbuffer,"lat",&stbiz->lat);
// logfile.Write("lat=%lf\n",stbiz->lat);
GetXMLBuffer(strxmlbuffer,"lon",&stbiz->lon);
// logfile.Write("lon=%lf\n",stbiz->lon);
GetXMLBuffer(strxmlbuffer,"height",&stbiz->height);
// logfile.Write("height=%lf\n",stbiz->height);
strncpy(stbiz->xmlbuffer,strxmlbuffer,1000);
return;
}
/////////////////////////////////////////////////////////////1.心跳业务
bool biz10000(int clientfd)
{
char strSendBuffer[1024]; // 发送报文的缓冲区
memset(strSendBuffer,0,sizeof(strSendBuffer));
strcpy(strSendBuffer,"ok");
if (TcpWrite(clientfd,strSendBuffer) == false)
{
logfile.Write("biz10000 TcpWrite() failed.\n"); return false;
}
return true;
}
////////////////////////////////////////////////////////2.新用户登录
bool biz10001(struct st_biz *stbiz,int clientfd)
{
CTimer Timer;
char strSendBuffer[1024]; // 发送报文的缓冲区
connection *conn=getconns(); // 获取一个数据库连接。
// 插入用户基本信息表T_USERINFO
sqlstatement stmt(conn);
stmt.prepare("insert into T_USERINFO(userid,downtime,ttytype,keyid) values(:1,sysdate,:2,SEQ_USERINFO.nextval)");
stmt.bindin(1, stbiz->userid,50);
stmt.bindin(2,&stbiz->ttytype);
if (stmt.execute() != 0)
{
if (stmt.m_cda.rc!=1)
{
logfile.Write("insert T_USERINFO failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); freeconns(conn); return false;
}
}
logfile.Write("insert T_USERINFO =%lf\n",Timer.Elapsed());
// 插入用户请求日志表
if (InsertUSERLOG(stbiz,conn)==false) { freeconns(conn); return false; }
logfile.Write("insert T_USERLOG =%lf\n",Timer.Elapsed());
char strobtid[6],strobtname[31],strlon[11],strlat[11];
stmt.prepare("select obtid,obtname,lon,lat from T_OBTCODE where rsts=1 and rownum<=30");
stmt.bindout(1,strobtid,5);
stmt.bindout(2,strobtname,30);
stmt.bindout(3,strlon,10);
stmt.bindout(4,strlat,10);
if (stmt.execute() != 0)
{
logfile.Write("select T_OBTCODE failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); freeconns(conn); return false;
}
while (true)
{
memset(strobtid,0,sizeof(strobtid));
memset(strobtname,0,sizeof(strobtname));
memset(strlon,0,sizeof(strlon));
memset(strlat,0,sizeof(strlat));
memset(strSendBuffer,0,sizeof(strSendBuffer));
if (stmt.next()!=0) break;
sprintf(strSendBuffer,"<obtid>%s</obtid><obtname>%s</obtname><lon>%s</lon><lat>%s</lat><endl/>",strobtid,strobtname,strlon,strlat);
if (TcpWrite(clientfd,strSendBuffer) == false)
{
logfile.Write("biz10001 TcpWrite() failed.\n"); freeconns(conn); return false;
}
}
logfile.Write("select =%lf\n",Timer.Elapsed());
// 最后发送一个ok
strcpy(strSendBuffer,"ok");
if (TcpWrite(clientfd,strSendBuffer) == false)
{
logfile.Write("biz10001 TcpWrite() failed.\n"); freeconns(conn); return false;
}
freeconns(conn);
return true;
}
///////////////////////////////////////////////////3.插入用户请求日志表
bool InsertUSERLOG(struct st_biz *stbiz,connection *conn)
{
sqlstatement stmt(conn);
stmt.prepare("insert into T_USERLOG(logid,userid,atime,bizid,obtid,lon,lat,height,xmlbuffer) values(SEQ_USERLOG.nextval,:1,sysdate,:2,:3,:4,:5,:6,:7)");
stmt.bindin(1, stbiz->userid,50);
stmt.bindin(2,&stbiz->bizid);
stmt.bindin(3, stbiz->obtid,10);
stmt.bindin(4,&stbiz->lon);
stmt.bindin(5,&stbiz->lat);
stmt.bindin(6,&stbiz->height);
stmt.bindin(7, stbiz->xmlbuffer,10000);
if (stmt.execute() != 0)
{
logfile.Write("insert T_USERLOG failed.\n%s\n%s\n",stmt.m_cda.message,stmt.m_sql); return false;
}
return true;
}
//////////////////////////////////////////////////4.获取天气实况
bool biz10002(struct st_biz *stbiz,int clientfd)
{
return true;
}
////////////////////////////////////////5.把客户端新的socket加入vclientfd容器中
void AddClient(int clientfd)
{
vclientfd.push_back(clientfd);
}
//////////////////////////////////6.关闭客户端的socket并从vclientfd容器中删除,
void RemoveClient(int clientfd)
{
for (int ii=0;ii<vclientfd.size();ii++)
{
if (vclientfd[ii]==clientfd)
{ close(clientfd); vclientfd.erase(vclientfd.begin()+ii); return; }
}
}
//////////////////////////////////7.初始数据库连接池:连接好数据库,初始化锁
bool initconns()
{
for (int ii=0;ii<MAXCONNS;ii++)
{
logfile.Write("%d,connecttodb and pthread_mutex_init.\n",ii);
// 连接数据库
if (conns[ii].connecttodb("shtqapp/pwdidc@snorcl11g_198","Simplified Chinese_China.ZHS16GBK",true)!=0)
{
logfile.Write("conns[%d].connettodb() failed.\n",ii); return false;
}
pthread_mutex_init(&mutex[ii],0); // 创建锁
}
return true;
}
///////////////////////////////8.获得连接池
connection *getconns()
{
// for (int jj=0;jj<1000;jj++)
while (true)
{
for (int ii=0;ii<MAXCONNS;ii++)
{
if (pthread_mutex_trylock(&mutex[ii])==0)
{
// logfile.Write("jj=%d,ii=%d\n",jj,ii);
logfile.Write("get conns is %d.\n",ii);
return &conns[ii];
}
}
usleep(10000);
}
}
///////////////////////////////9.释放连接池
bool freeconns(connection *in_conn)
{
for (int ii=0;ii<MAXCONNS;ii++)
{
if (in_conn==&conns[ii])
{
logfile.Write("free conn %d.\n",ii);
pthread_mutex_unlock(&mutex[ii]); in_conn=0; return true;
}
}
return false; //理论上不会来到这里,除非连接池搞乱了
}
每次要用数据库就从连接池里拿一个,如下是错误的,函数里传入的参数应该用指针的指针
![](https://img.haomeiwen.com/i16331821/2825f8774eac2b05.png)
心跳报文业务不需要连接池
多次./client,如下查看释放的
如下测100个客户端,系统负载
![](https://img.haomeiwen.com/i16331821/70005b404bff9ee7.png)
sh client.sh(50个./client&),如下采用连接池1秒内,不采用连接池只多线程耗时1秒往上:客户端起50个进程,服务端起50个线程再连数据库,数据库也50个进程,资源消耗大
![](https://img.haomeiwen.com/i16331821/99f48f8db4149977.png)
如上和浏览器的web服务器一样后面有个数据库连接池,客户发起一次请求短链接,后台有个线程去响应,这个线程到数据库连接池里拿一个连接,Apacheweb服务器就采用这种机制,现在1核2G内存运行数据库还有服务程序,资源紧张。物理服务器一个不够用多个外面再用nginx,就像redis用nginx提供api
网友评论