本文学习于:C语言技术网(www.freecplus.net),在 b 站学习于 C 语言技术网,并加以自己的一些理解和复现,如有侵权会删除。
接下来对网络编程继续深入学习。通过上篇文章学习,感觉对每个点都记录会很花费时间,但是不记录又对有些地方理解一知半解,综合考虑,先运行出来,对每行代码如何执行要明白,实现什么功能也要明白,freecplus 框架里面知识,后面再仔细学习。
使用多线程方式搭建网络服务框架,在实际应用中会广泛一些,但是难度也会高一些。下面开始进行学习,在这之前有一些前置知识,要对多线程网络通信等知识进行学习。其中服务端程序如下,
#include "_freecplus.h" void *pthmain(void * arg); // 线程主函数 vectorvpthid; // 存放线程 id 的容器 void mainexit(int sig); // 信号 2 和 15 的处理函数 void pthmainexit(void * arg); // 线程清理函数 CLogFile logfile; CTcpServer TcpServer; // 创建服务端对象 // 处理业务的主函数 bool _main(const char *strrecvbuffer, char *strsendbuffer); // 心跳报文 bool biz000(const char *strrecvbuffer, char *strsendbuffer); // 身份验证业务处理函数 bool biz001(const char *strrecvbuffer, char *strsendbuffer); // 查询余业务处理函数 bool biz002(const char *strrecvbuffer, char *strsendbuffer); int main(int argc, char *argv[]){ if(argc != 3){ printf("Using:./ExitAndFreeServer port logfilenExample:./ExitAndFreeServer 5005 /tmp/ExitAndFreeServer.lognn"); return -1; } // 关闭全部的信号,也把僵尸进程关闭 for(int ii = 0; ii <= 64; ii++) signal(ii, SIG_IGN); // 打开日志文件 if(logfile.Open(argv[2], "a+") == false){ printf("logfile.Open(%s) failed.n", argv[2]); return -1; } // 设置信号,在 shell 状态下可用 “kill + 进程号”正常终止些进程 Ctrl + c // 但请不要用 “kill -9 + 进程号” signal(SIGINT, mainexit); signal(SIGTERM, mainexit); // 初始化 TcpServer 的通信端口 if(TcpServer.InitServer(atoi(argv[1])) == false){ logfile.Write("TcpServer.InitServer(%s) failed. n", argv[1]); return -1; } while(true){ if(TcpServer.Accept() == false){ // 等待客户端连接 logfile.Write("TcpServer.Accept() failed. n"); continue; } // 以下是子进程,负责与客户端通信 logfile.Write("客户端(%s)已连接。 n", TcpServer.GetIP()); pthread_t pthid; if(pthread_create(& pthid, NULL, pthmain, (void *)(long)TcpServer.m_connfd) != 0){ logfile.Write("pthread_create failed. n"); return -1; } vpthid.push_back(pthid); // 把线程 id 保存到 vpthid 容器中 } return 0; } void *pthmain(void * arg){ pthread_cleanup_push(pthmainexit, arg); // 设置线程清理函数 pthread_detach(pthread_self()); // 分离线程 pthread_setcanceltype(PTHREAD_CANCEL_DISABLE, NULL); // 设置取消方式为立即取消 int socket = (int)(long)arg; // 客户端的 socket 连接 int ibuflen = 0; char strrecvbuffer[1024], strsendbuffer[1024]; // 存放数据的缓冲区 while(true){ memset(strrecvbuffer, 0, sizeof(strrecvbuffer)); memset(strsendbuffer, 0, sizeof(strsendbuffer)); // 接收客户端发过来的请求报文 if(TcpRead(socket, strrecvbuffer, &ibuflen, 50) == false) break; logfile.Write("接收:%s n", strrecvbuffer); // 处理业务的主函数 if(_main(strrecvbuffer, strsendbuffer) == false) break; logfile.Write("发送:%s n", strsendbuffer); if(TcpWrite(socket, strsendbuffer) == false) break; // 向客户端回应报文 } pthread_cleanup_pop(1); pthread_exit(0); } void pthmainexit(void * arg){ logfile.Write("pthmainexit begin.n"); // 关闭与客户端的 socket close((int)(long)arg); // 从 vpthid 中删除本线程的 id for(int ii = 0; ii < vpthid.size(); ii++){ if(vpthid[ii] == pthread_self()){ vpthid.erase(vpthid.begin() + ii); } } logfile.Write("pthmainexit end.n"); } // 信号 2 和 15 的处理函数 void mainexit(int sig){ logfile.Write("mainexit begin. n"); // 关闭监听的 socket TcpServer.CloseListen(); // 取消全部的线程 for(int ii = 0; ii < vpthid.size(); ii++){ logfile.Write("cancel %ldn", vpthid[ii]); pthread_cancel(vpthid[ii]); } logfile.Write("mainexit end.n"); exit(0); } 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("非法报文:%sn", 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, "wucz") == 0) && (strcmp(password, "p@ssw0rd") == 0) ) sprintf(strsendbuffer, " 0 成功。 "); else sprintf(strsendbuffer, "-1 用户名或密码不正确。 "); return true; } bool biz002(const char *strrecvbuffer, char *strsendbuffer){ char cardid[51]; memset(cardid, 0, sizeof(cardid)); GetXMLBuffer(strrecvbuffer, "cardid", cardid, 50); if(strcmp(cardid, "62620000000001") == 0) sprintf(strsendbuffer, "0 成功。 100.50 "); else sprintf(strsendbuffer, "-1 卡号不存在。 "); return true; } bool biz000(const char *strrecvbuffer, char *strsendbuffer){ sprintf(strsendbuffer, "0 成功。 "); return true; }
客户端程序不变化,然后运行结果如下,
可以看出实现功能满足和多进程实现的一样。里面线程的一些知识,在后面会继续学习,但是其功能能够大致看懂。接下来继续学习。
其中要注意的点有,使用多进程中和 socketfd 和多线程里面的 socketfd 是不一样的,因此用两种方式来进行通信。这些在 up 主前面视频都讲过。
在实际项目开发中,除了完成程序的功能,还需要测试性能。 在充分了解服务端的性能后,才能决定如何选择服务端的框架,还有网络带宽、硬件配置等。 服务端的性能指标是面试中必问的。如果不了解性能指标,面试官会认为你没有实际开发经验或对网络编程一知半解。主要性能指标如下:1. 服务端的并发能力;(可以同时响应多少业务) 2. 服务端的业务处理能力;(每段时间可以响应多少业务请求) 3. 客户端业务响应时效;(响应需要的时间) 4. 网络宽带。(和网络流量请求)
2.3 服务端并发性能测试服务端最大并发量,即可以接受客户端连接的最大数量。注意客户端业务请求不要太频繁。重视 CPU 和内存使用率的变化(磁盘 I/O,网络 I/O)。在性能测试时,最好是客户端用一台独立的虚拟机,服务端测试程序用另外一台独立的虚拟机,不然会导致测试不准确。为了学习过程,我就在同一台虚拟机上面跑了,以后在正式测试中再使用其他方法。这里需要注意几个语句,
// 查看进程数量 ps -ef |grep ExitAndFreeServer|wc // 运行 sh 脚本文件,和 touch 配合使用 touch test.sh // 加入运行脚本文件 sh test.h // 查看资源内存 free -m // 查看 cpu 资源 top
其中客户端程序改为如下,只有心跳程序开启,然后运行,
#include "_freecplus.h" CTcpClient TcpClient; // 创建服务端对象 bool biz000(); // 发送心跳报文 bool biz001(); // 身份验证 bool biz002(); // 余额查询 int main(int argc, char *argv[]){ if(argc != 3){ printf("Using:./client ip portn Example:./client 127.0.0.1 5005nn"); return -1; } if(TcpClient.ConnectToServer(argv[1], atoi(argv[2])) == false){ // 向服务端发起连接请求 printf("TcpClient.ConnectToServer("%s", %s) failed.n", argv[1], argv[2]); return -1; } for(int ii = 0; ii < 10; ii++){ if(biz000() == false) break; sleep(10); } // 程序直接退出,析构函数会释放资源 } bool biz001(){ // char strbuffer[1024]; // 存放数据的缓存区 memset(strbuffer,0,sizeof(strbuffer)); snprintf(strbuffer, 1000, "1 wucz p@ssw0rd "); printf("发送:%sn",strbuffer); if (TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer,0,sizeof(strbuffer)); if (TcpClient.Read(strbuffer, 20) == false) return false; // 接收服务端的回应报文 printf("接收:%sn",strbuffer); int iretcode = -1; GetXMLBuffer(strbuffer, "retcode", &iretcode); if(iretcode == 0){printf("身份验证成功。n"); return true;} printf("身份验证失败。n"); return false; } bool biz002(){ char strbuffer[1024]; // 存放数据的缓冲区 snprintf(strbuffer, 1000, "2 62620000000001 "); printf("发送:%sn", strbuffer); if(TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer, 0, sizeof(strbuffer)); if(TcpClient.Read(strbuffer, 20) == false) return false; // 接收服务端的回应报文 printf("接收:%sn", strbuffer); int iretcode = -1; GetXMLBuffer(strbuffer, "retcode", &iretcode); if(iretcode == 0) {printf("查询余额成功。n"); return true; } printf("查询余额成功。n"); return true; } bool biz000(){ // 发送心跳报文 char strbuffer[1024]; // 存放数据的缓存区 memset(strbuffer,0,sizeof(strbuffer)); snprintf(strbuffer, 1000, "0 "); // printf("发送:%sn",strbuffer); if (TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer,0,sizeof(strbuffer)); if (TcpClient.Read(strbuffer,20) == false) return false; // 接收服务端的回应报文 // printf("接收:%sn",strbuffer); return true; }
在这里建立了一个 sh 文件脚本,把客户端程序运行个数 * 4000 ,然后放在 test.sh 文件脚本里,其效果如下。一定要注意的一点,在测试中,最高一次性不要太多,一点一点的加,我为了出效果,直接运行那么多,
在同一台虚拟机运行,第一次运行 test.sh 文件时,虚拟机开始有点卡;后来第二次运行 test.sh 文件时,资源不够,然后运行卡壳,系统承载不了那么多进程,其运行结果如下,
服务端最大业务处理能力,即每秒可以处理的业务请求数量。注重客户端的数量不要太多。重视 CPU 和内存使用率的变化。
这里对客户端程序改为,
for(int ii = 0; ii < 2000; ii++){ if(biz000() == false) break; usleep(100000); }
把 sh 文件中客户端程序运行脚本改为,
运行结果如下,
可以看出 CPU 消耗和内存消耗,及其进程的情况。然后为了测试每秒接受信号情况,对接收进行抓包,使用 linux 语句为,
grep "2021-10-30 22:11:19 接收" /tmp/ExitAndFreeServer.log|wc
这是在运行过一段时间以后的了,有些客户端程序运行结束了,少了一些,所以运行结果如下,
这就是从日志里面抓取的数据接收情况,分析上面运行结果就能够知道,在当下 19 s 接收的情况,相当于 1s 有 60 条记录。可以看出,这样客户端的压力还不够,继续加压,把测试客户端程序改为,
// 用 killall client 杀死所有相关进程 for(int ii = 0; ii < 2000; ii++){ if(biz000() == false) break; usleep(10000); }
再运行 test.sh 脚本,在运行两次以后,观察到结果如下,
能够看到,这样测试时候,CPU 资源已经被占用的还剩 16% 多了,现在压力已经相当大了,也可以查看到相应的进程数和内存情况。在过一会后又恢复到了正常情况。
为了能够看出处理业务情况,查看服务端数据报文,然后看 1 s 中报文情况,使用语句
grep "2021-10-30 22:41:25 接收" /tmp/ExitAndFreeServer.log|wc grep "2021-10-30 22:41:21 接收" /tmp/ExitAndFreeServer.log|wc
使用语句查看服务端报文情况结果如下
可以看出,在 1s 中接收到 18107 报文,然后 CPU 还剩资源 10% 多,因此知道服务端压力差不多是 18107 。
用相同的方法来测试多进程/多线程。测试的方法是一样的,没有什么变化。需要关注内存,CPU 的情况,这里为了学习进度就不进行实践了。得出结论,多线程比多进程在 CPU 和内存消耗情况都是占有优势的。
2.6 测试客户端的响应时间 客户端业务的响应时间,即是发出业务请求与收到服务端回应的时间间隔,关系到用户的体验。测试环境:1. 业务的闲时时/忙时;2. 不同的网络环境(局域网、互联网、移动通信网络)。需要测试需要一个计时器,需要精确到 微秒。
这里测试每个业务运行时间,用到了计时器功能,freecplus 把计时器封装好了,然后直接使用就行。用了定时器,来查看每个业务运行的时间,其中客户端程序如下,
#include "_freecplus.h" CTcpClient TcpClient; // 创建服务端对象 bool biz000(); // 发送心跳报文 bool biz001(); // 身份验证 bool biz002(); // 余额查询 int main(int argc, char *argv[]){ if(argc != 3){ printf("Using:./client ip portn Example:./client 127.0.0.1 5005nn"); return -1; } CTimer Timer; if(TcpClient.ConnectToServer(argv[1], atoi(argv[2])) == false){ // 向服务端发起连接请求 printf("TcpClient.ConnectToServer("%s", %s) failed.n", argv[1], argv[2]); return -1; } printf("TcpClient.ConnectToServer() 耗时%lfn", Timer.Elapsed()); // 身份验证 biz001(); printf("biz001() 耗时%lfn", Timer.Elapsed()); biz002(); // 余额查询 printf("biz002() 耗时%lfn", Timer.Elapsed()); biz000(); // 余额查询 printf("biz000() 耗时%lfn", Timer.Elapsed()); // 程序直接退出,析构函数会释放资源 } bool biz001(){ // char strbuffer[1024]; // 存放数据的缓存区 memset(strbuffer, 0, sizeof(strbuffer)); snprintf(strbuffer, 1000, "1 wucz p@ssw0rd "); // printf("发送:%sn",strbuffer); if (TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer,0,sizeof(strbuffer)); if (TcpClient.Read(strbuffer, 20) == false) return false; // 接收服务端的回应报文 // printf("接收:%sn",strbuffer); int iretcode = -1; GetXMLBuffer(strbuffer, "retcode", &iretcode); if(iretcode == 0)return true; //{ printf("身份验证成功。n"); } // printf("身份验证失败。n"); return false; } bool biz002(){ char strbuffer[1024]; // 存放数据的缓冲区 snprintf(strbuffer, 1000, "2 62620000000001 "); // printf("发送:%sn", strbuffer); if(TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer, 0, sizeof(strbuffer)); if(TcpClient.Read(strbuffer, 20) == false) return false; // 接收服务端的回应报文 // printf("接收:%sn", strbuffer); int iretcode = -1; GetXMLBuffer(strbuffer, "retcode", &iretcode); if(iretcode == 0)return true; // {printf("查询余额成功。n"); return true; } // printf("查询余额成功。n"); return true; } bool biz000(){ // 发送心跳报文 char strbuffer[1024]; // 存放数据的缓存区 memset(strbuffer,0,sizeof(strbuffer)); snprintf(strbuffer, 1000, "0 "); // printf("发送:%sn",strbuffer); if (TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer,0,sizeof(strbuffer)); if (TcpClient.Read(strbuffer,20) == false) return false; // 接收服务端的回应报文 // printf("接收:%sn",strbuffer); return true; }
结果如下所示,
测试的目的是根据业务的 需求,判断出对网络带宽的要求。测试网络带宽,参考这篇文章:https://www.linuxprobe.com/speedtest-network-in-linux.html。测试网络带宽能承载的业务量,不同的业务对宽带的利用率不一样。要求测试环境的各环节不能存在性能的瓶颈,唯一瓶颈就是网络带宽。注意是只发送数据,不接受回应;上行和下行分开测试。 其中测试服务端程序为,
#include "_freecplus.h" CTcpServer TcpServer; // 创建服务端对象 // 程序退出时调用的函数 void FathEXIT(int sig); // 父进程退出函数 void ChldEXIT(int sig); // 子进程退出函数 int main(int argc, char *argv[]){ // 关闭全部的信号,也把僵尸进程关闭 for(int ii = 0; ii <= 64; ii++) signal(ii, SIG_IGN); // 设置信号,在 shell 状态下可用 “kill + 进程号”正常终止些进程 Ctrl + c // 但请不要用 “kill -9 + 进程号” signal(SIGINT, FathEXIT); signal(SIGTERM, FathEXIT); // 初始化 TcpServer 的通信端口 if(TcpServer.InitServer(5005) == false){ printf("TcpServer.InitServer(5005) failed. n"); FathEXIT(-1); } while(true){ if(TcpServer.Accept() == false){ // 等待客户端连接 printf("TcpServer.Accept() failed. n"); continue; } // 父进程返回到循环首部 if(fork() > 0){TcpServer.CloseClient(); continue; } // 子进程重新设置退出信号 signal(SIGINT, ChldEXIT); signal(SIGTERM, ChldEXIT); TcpServer.CloseListen(); // 以下是子进程,负责与客户端通信 printf("客户端(%s)已连接。 n", TcpServer.GetIP()); char strbuffer[1024]; // 存放数据的缓冲区 while(true){ memset(strbuffer, 0, sizeof(strbuffer)); // 接收客户端发过来的请求报文 if(TcpServer.Read(strbuffer, 50) == false) break; printf("接收:%s n", strbuffer); strcat(strbuffer, "ok"); // 在客户端的报文后加上“ok” printf("发送:%s n", strbuffer); if(TcpServer.Write(strbuffer) == false)break; // 向客户端回应报文 } printf("客户端已断开。 n"); // 程序直接退出,析构函数会释放资源 ChldEXIT(-1); // 通信完成后,子进程退出。 } } void FathEXIT(int sig){ // 父进程退出函数 if(sig > 0){ // 免除不再受到其他信号的打扰 signal(sig, SIG_IGN);signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN); printf("catching the signal(%d). n", sig); } kill(0, 15); // 通知其它的子进程退出。 printf("父进程退出。 n"); // 编写善后代码(释放资源、提交或回滚事务) TcpServer.CloseClient(); exit(0); } void ChldEXIT(int sig){ // 子进程退出函数 if(sig > 0){ signal(sig, SIG_IGN);signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN); } printf("子进程退出。 n"); // 编写善后代码(释放资源、提交或回滚事务) TcpServer.CloseClient(); exit(0); }
测试客户端程序为,
#include "_freecplus.h" CTcpClient TcpClient; // 创建服务端对象 bool biz000(); // 发送心跳报文 bool biz001(); // 身份验证 bool biz002(); // 余额查询 int main(int argc, char *argv[]){ if(argc != 3){ printf("Using:./client ip portn Example:./client 127.0.0.1 5005nn"); return -1; } CTimer Timer; if(TcpClient.ConnectToServer(argv[1], atoi(argv[2])) == false){ // 向服务端发起连接请求 printf("TcpClient.ConnectToServer("%s", %s) failed.n", argv[1], argv[2]); return -1; } printf("TcpClient.ConnectToServer() 耗时%lfn", Timer.Elapsed()); for(int ii = 0; ii < 10000; ii++){ biz001(); } // 身份验证 biz001(); printf("biz001() 耗时%lfn", Timer.Elapsed()); return 0; biz002(); // 余额查询 printf("biz002() 耗时%lfn", Timer.Elapsed()); biz000(); // 余额查询 printf("biz000() 耗时%lfn", Timer.Elapsed()); // 程序直接退出,析构函数会释放资源 } bool biz001(){ // char strbuffer[1024]; // 存放数据的缓存区 memset(strbuffer, 0, sizeof(strbuffer)); snprintf(strbuffer, 1000, "1 wucz p@ssw0rd "); // printf("发送:%sn",strbuffer); if (TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer,0,sizeof(strbuffer)); if (TcpClient.Read(strbuffer, 20) == false) return false; // 接收服务端的回应报文 // printf("接收:%sn",strbuffer); int iretcode = -1; GetXMLBuffer(strbuffer, "retcode", &iretcode); if(iretcode == 0)return true; //{ printf("身份验证成功。n"); } // printf("身份验证失败。n"); return false; } bool biz002(){ char strbuffer[1024]; // 存放数据的缓冲区 snprintf(strbuffer, 1000, "2 62620000000001 "); // printf("发送:%sn", strbuffer); if(TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer, 0, sizeof(strbuffer)); if(TcpClient.Read(strbuffer, 20) == false) return false; // 接收服务端的回应报文 // printf("接收:%sn", strbuffer); int iretcode = -1; GetXMLBuffer(strbuffer, "retcode", &iretcode); if(iretcode == 0)return true; // {printf("查询余额成功。n"); return true; } // printf("查询余额成功。n"); return true; } bool biz000(){ // 发送心跳报文 char strbuffer[1024]; // 存放数据的缓存区 memset(strbuffer,0,sizeof(strbuffer)); snprintf(strbuffer, 1000, "0 "); // printf("发送:%sn",strbuffer); if (TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 memset(strbuffer,0,sizeof(strbuffer)); if (TcpClient.Read(strbuffer,20) == false) return false; // 接收服务端的回应报文 // printf("接收:%sn",strbuffer); return true; }
运行结果如下,
可以看出在双向发送和接收数据时,发送 10000 个心跳业务双向时间情况。现在改为单向测试,客户端只发送不接收了;服务端只接收不发送了。
// 服务端 while(true){ memset(strbuffer, 0, sizeof(strbuffer)); // 接收客户端发过来的请求报文 if(TcpServer.Read(strbuffer, 50) == false) break; printf("接收:%s n", strbuffer); } // 客户端 bool biz001(){ // char strbuffer[1024]; // 存放数据的缓存区 memset(strbuffer, 0, sizeof(strbuffer)); snprintf(strbuffer, 1000, "1 wucz p@ssw0rd "); // printf("发送:%sn",strbuffer); if (TcpClient.Write(strbuffer) == false) return false; // 向服务端发送请求报文 return true; memset(strbuffer,0,sizeof(strbuffer)); if (TcpClient.Read(strbuffer, 20) == false) return false; // 接收服务端的回应报文 // printf("接收:%sn",strbuffer); int iretcode = -1; GetXMLBuffer(strbuffer, "retcode", &iretcode); if(iretcode == 0)return true; //{ printf("身份验证成功。n"); } // printf("身份验证失败。n"); return false; }
现在查看运行效果,
可以看出单向测试效果还是不错的,直接体现了 TCP 单向传输带宽业务情况。我感觉 UP 主要讲了网络测试的方法,还是比较直接但是不太深入,以后至少遇到时候知道怎么处理,学习还在比较表面,以后在项目中再继续理解。
最近马上就要中期答辩了,在 11 月 6 号,今天准备开始写中期答辩文档和答辩 PPT 了。最近工作先签了海康微视,做一个保底工作,后面继续学习去找一个更好的工作。或者考虑考试公务员或者事业单位,后面看情况吧,C++继续学习着,等 C++ 网络编程,数据库部分学习完,然后肝完项目,深入学习算法,理解这些知识时候再看行测和申论。继续加油。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)