学习C++项目—— 搭建多进程网络服务框架,增加业务和日志,心跳机制

2023-10-31

学习计算机网络编程

一、思路和学习方法

  本文学习于:C语言技术网(www.freecplus.net),在 b 站学习于 C 语言技术网,并加以自己的一些理解和复现,如有侵权会删除。
  接下来对网络编程继续深入学习。

二、网络编程继续深入

2.1 搭建多进程网络服务框架

  前置学习需要理解 linux 网络编程基础,linux 多线程,多进程的相关知识和使用。然后搭建一个多进程网络服务的框架。建立一个多进程的 socket 服务端,然后多个客户端都能和服务端进行通信。涉及到的是信号,多进程的相关知识。其实现原理是,服务端当接受到一个服务端的连接时,建立一个进程,对服务端进行服务,那么这个服务端就可以响应多个客户端
  这里程序的框架主要是在之前的封装以后的服务端和客户端 CTcpServer.cpp 和 CTcpClient.cpp 中改写,注意在服务端加入这两句,就能够对多个客户端进行响应。CTcpServer.cpp 代码如下,

/*
 * 程序名:book248.cpp,此程序用于演示用C++的方法封装socket服务端
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
class CTcpServer
{
public:
  int m_listenfd;   // 服务端用于监听的socket
  int m_clientfd;   // 客户端连上来的socket
 
  CTcpServer();
 
  bool InitServer(int port);  // 初始化服务端
 
  bool Accept();  // 等待客户端的连接
 
  // 向对端发送报文
  int  Send(const void *buf,const int buflen);
  // 接收对端的报文
  int  Recv(void *buf,const int buflen);
 
 ~CTcpServer();
};
 
int main()
{
  signal(SIGCHLD, SIG_IGN); // 屏蔽子进程退出的信号,避免产生僵尸进程
  
  CTcpServer TcpServer;
 
  if (TcpServer.InitServer(5005)==false)
  { printf("TcpServer.InitServer(5051) failed,exit...\n"); return -1; }
 
  while(true){
  if (TcpServer.Accept() == false) { printf("TcpServer.Accept() failed,exit...\n"); return -1; }
 
  if(fork() > 0){close(TcpServer.m_clientfd); continue;} // 父进程返回到循环首部
 
  close(TcpServer.m_listenfd);
  
  // 以下是子进程,负责与客户端通信
  printf("客户端已连接。\n");
 
  char strbuffer[1024];
 
  while (1)
  {
    memset(strbuffer,0,sizeof(strbuffer));
    if (TcpServer.Recv(strbuffer,sizeof(strbuffer))<=0) break;
    printf("接收:%s\n",strbuffer);
 
    strcpy(strbuffer,"ok");
    if (TcpServer.Send(strbuffer,strlen(strbuffer))<=0) break;
    printf("发送:%s\n",strbuffer);
  }
 
  printf("客户端已断开连接。\n");

  exit(0); // 子进程结束退出
  }
}
 
CTcpServer::CTcpServer()
{
  // 构造函数初始化socket
  m_listenfd=m_clientfd=0;
}
 
CTcpServer::~CTcpServer()
{
  if (m_listenfd!=0) close(m_listenfd);  // 析构函数关闭socket
  if (m_clientfd!=0) close(m_clientfd);  // 析构函数关闭socket
}
 
// 初始化服务端的socket,port为通信端口
bool CTcpServer::InitServer(int port)
{
  m_listenfd = socket(AF_INET,SOCK_STREAM,0);  // 创建服务端的socket
 
  // 把服务端用于通信的地址和端口绑定到socket上
  struct sockaddr_in servaddr;    // 服务端地址信息的数据结构
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;  // 协议族,在socket编程中只能是AF_INET
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 本主机的任意ip地址
  servaddr.sin_port = htons(port);  // 绑定通信端口
  if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
  { close(m_listenfd); m_listenfd=0; return false; }
 
  // 把socket设置为监听模式
  if (listen(m_listenfd,5) != 0 ) { close(m_listenfd); m_listenfd=0; return false; }
 
  return true;
}
 
bool CTcpServer::Accept()
{
  if ( (m_clientfd=accept(m_listenfd,0,0)) <= 0) return false;
 
  return true;
}
 
int CTcpServer::Send(const void *buf,const int buflen)
{
  return send(m_clientfd,buf,buflen,0);
}
 
int CTcpServer::Recv(void *buf,const int buflen)
{
  return recv(m_clientfd,buf,buflen,0);
}

  需要注意的是添加的 while(true) 和 if(fork() < 0) continue; 还有 exit(0) 函数,exit() 在 #include <stdlib.h> 头文件中如果没有上述语句,在客户端和服务端对接一次后,就结束了,其结果如下
在这里插入图片描述
  加上述语句,服务端用多进程方式对服务端进行服务,结果如下
在这里插入图片描述
  这样就完成了最最基础的框架。但是还需要改进。程序中加如以下程序signal(SIGCHLD, SIG_IGN); ,注意加入头文件 #include <signal.h> 是屏蔽子进程退出的信号,避免产生僵尸进程,其中,可以用

ps -ef | grep CTcpServer

查看僵尸进程,其中为 [CTcpServer] <defunct> 为僵尸进程。
僵尸进程就是子进程运行完,父进程没有回收资源,就叫僵尸进程,如果父进程结束,那子进程
也自然结束。

  还要关掉多余的 socket ,对于父进程来说,不用和客户端连接,要关掉 m_connfd 的 socket ;对于子进程来说,不用监听 socket ,要关掉 m_listened 的 socket ,因此加入程序,

if(fork() > 0){close(TcpServer.m_clientfd); continue;} // 父进程返回到循环首部

close(TcpServer.m_listenfd);
2.2 多进程服务程序的退出和资源释放

  freecplus 框架具体使用,参考 up 主博客来学习,网址如下:
http://www.freecplus.net/9ebb8276265b404488a3415be224ed85.html

  用 Ctrl+C 关闭进程,是不合适,因此采用其他方法来完成。这里为了方便使用,把作者写的 freecplus 框架放入程序中,里面封装了通信的一般方法,直接调用就行,对于 TCP 通信方面可以查看前面的 TCP 封装类,和那里的是一样的。还有如何使用 freecplus 框架,可以看作者的网站,写的很清楚,我就不赘叙了。使用多进程服务程序的退出和资源释放,其中程序如下。
  下面是使用 freecplus 框架写的关于多进程服务的退出和资源释放,是服务端程序 ExitAndFreeServer.cpp,

/*
 * 程序功能:服务端程序,多进程程序的退出和资源释放
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#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, 10) == 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);
}

  下面是使用 freecplus 框架写的关于多进程客户端程序,程序文件名是 freecplusClient.cpp ,其中程序如下,

/*
 * 程序名:book247.cpp,此程序用于演示用C++的方法封装socket客户端
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include "_freecplus.h"

CTcpClient TcpClient;

int main(int argc, char *argv[]){
  // 向服务器发起连接请求
  if (TcpClient.ConnectToServer("127.0.0.1",5005) == false)
  { printf("TcpClient.ConnectToServer(\"127.0.0.1\",5005) failed,exit...\n"); return -1; }

  // fork(); // 子进程
  char strbuffer[1024];
 
  for (int ii=0;ii<5;ii++)
  {
    memset(strbuffer,0,sizeof(strbuffer));
    snprintf(strbuffer,50,"%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);
  }
}

  其中 makefile 文件如下

all:ExitAndFreeServer freecplusClient

ExitAndFreeServer:ExitAndFreeServer.cpp
	g++ -g -o ExitAndFreeServer ExitAndFreeServer.cpp _freecplus.cpp

freecplusClient:freecplusClient.cpp
	g++ -g -o freecplusClient freecplusClient.cpp _freecplus.cpp

  记住,使用查看进程的 linux 指令为,

ps -ef | grep ExitAndFree(文件名)

  然后让他们进行通信,两个客户端与服务端通信,然后观察运行效果,
在这里插入图片描述
  当使用 ps 查看进程号,然后用 kill 杀死进程时候,服务端程序显示如下,
在这里插入图片描述
  当使用 ctrl + c 结束进程时,显示如下,
在这里插入图片描述
  可以看出,是成功完成上述要求功能的。

2.3 多进程服务程序的日志

  日志模块对于一个程序而言是必不可少的,对于服务端程序更重要。因为几乎所有服务端程序都是作为守护进程而运行的,依靠日志记录关键信息就很重要了,当然日志的作用远不止记录信息,它还可以用于恢复。本文中,依旧使用作者的 freecplus 开发框架,注意 CLogFile logfile 已经封装在 _freecplus.h 里面,具体的可以查看源代码,其中服务端程序为,

/*
 * 程序功能:
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include "_freecplus.h"

CLogFile logfile;
CTcpServer TcpServer;  // 创建服务端对象

// 程序退出时调用的函数
void FathEXIT(int sig);  // 父进程退出函数
void ChldEXIT(int sig);  // 子进程退出函数

int main(int argc, char *argv[]){
    if(argc != 3){
      printf("Using:./ExitAndFreeServer port logfile\nExample:./ExitAndFreeServer 5005 /tmp/ExitAndFreeServer.log\n\n"); 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, FathEXIT); signal(SIGTERM, FathEXIT);

     // 初始化 TcpServer 的通信端口
    if(TcpServer.InitServer(5005) == false){
	 logfile.Write("TcpServer.InitServer(5005) failed. \n"); FathEXIT(-1);
     }

    while(true){
       if(TcpServer.Accept() == false){  // 等待客户端连接
	     logfile.Write("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, 10) == 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);
       logfile.Write("catching the signal(%d). \n", sig);
     }

    kill(0, 15);  // 通知其它的子进程退出。
    logfile.Write("父进程退出。 \n");

    // 编写善后代码(释放资源、提交或回滚事务)
   TcpServer.CloseClient();

   exit(0);
}

void ChldEXIT(int sig){ // 子进程退出函数
    if(sig > 0){
       signal(sig, SIG_IGN);signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN);
     }

    logfile.Write("子进程退出。 \n");

    // 编写善后代码(释放资源、提交或回滚事务)
   TcpServer.CloseClient();

   exit(0);
}

  注意,跟踪日志的 linux 语句为,

tail -f /tmp/ExitAndFreeServer.log

  运行结果如下,
在这里插入图片描述

2.4 增加业务逻辑

  增加业务逻辑能够给客户端身份验证。业务报文的格式自定义,客户端与服务端协商好就可以,使用 xml ,增加可读性、可扩展性、容错性。举个例子,

客户端请求报文示例:
<bizcode>1</bizcode><username>wucz</username><password>p@ssw0rd</password>
服务端回应报文示例:
<retcode>0</retcode><message>成功。</message>
<retcode>-1</retcode><message>用户名或密码不正确。</message>

  下面例子要建立一个服务端设置密码,客户端要输入密码的方式,用XML来实现。其中客户端程序如下,

/*
 * 程序功能:
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include "_freecplus.h"

CLogFile logfile;
CTcpServer TcpServer;  // 创建服务端对象

// 程序退出时调用的函数
void FathEXIT(int sig);  // 父进程退出函数
void ChldEXIT(int sig);  // 子进程退出函数

//  处理业务的主函数
bool _main(const char *strrecvbuffer, char *strsendbuffer);
//  身份验证业务处理函数
bool biz001(const char *strrecvbuffer, char *strsendbuffer);

int main(int argc, char *argv[]){
    if(argc != 3){
      printf("Using:./ExitAndFreeServer port logfile\nExample:./ExitAndFreeServer 5005 /tmp/ExitAndFreeServer.log\n"); return -1;
     }

     // 关闭全部的信号,也把僵尸进程关闭
    for(int ii = 0; ii < 100; 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, FathEXIT); signal(SIGTERM, FathEXIT);

     // 初始化 TcpServer 的通信端口
    if(TcpServer.InitServer(5005) == false){
	 logfile.Write("TcpServer.InitServer(5005) failed. \n"); FathEXIT(-1);
     }

    while(true){
       if(TcpServer.Accept() == false){  // 等待客户端连接
	   logfile.Write("TcpServer.Accept() failed. \n"); continue;
         }
	 // 父进程返回到循环首部
	 if(fork() > 0){TcpServer.CloseClient(); continue; }

	 // 子进程重新设置退出信号
       signal(SIGINT, ChldEXIT); signal(SIGTERM, ChldEXIT);

       TcpServer.CloseListen();
     
         // 以下是子进程,负责与客户端通信
       logfile.Write("客户端(%s)已连接。 \n", TcpServer.GetIP());

       char strrecvbuffer[1024], strsendbuffer[1024];  //  存放数据的缓冲区

       while(true){
         memset(strrecvbuffer, 0, sizeof(strrecvbuffer));
         memset(strsendbuffer, 0, sizeof(strsendbuffer));

            // 接收客户端发过来的请求报文
         if(TcpServer.Read(strrecvbuffer, 80) == 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; // 向客户端回应报文
         }
       logfile.Write("客户端已断开。 \n");  // 程序直接退出,析构函数会释放资源
       ChldEXIT(-1); // 通信完成后,子进程退出。
     }
}

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("父进程退出。 \n");

    // 编写善后代码(释放资源、提交或回滚事务)
   TcpServer.CloseClient();

   exit(0);
}

void ChldEXIT(int sig){ // 子进程退出函数
    if(sig > 0){
       signal(sig, SIG_IGN);signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN);
     }

    logfile.Write("子进程退出。 \n");

    // 编写善后代码(释放资源、提交或回滚事务)
   TcpServer.CloseClient();

   exit(0);
}

bool _main(const char * strrecvbuffer, char * strsendbuffer){ // 处理业务的主函数
   int ibizcode = -1;

   GetXMLBuffer(strrecvbuffer, "bizcode", &ibizcode);

   switch(ibizcode){
     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, "wucz") == 0) && (strcmp(password, "p@ssw0rd") == 0) )
     sprintf(strsendbuffer, "<retcode>0</retcode><message>成功。</message>");
   else
     sprintf(strsendbuffer, "<retcode>-1</retcode><message>用户名或密码不正确。</message>");

   return true;
}

  服务端运行如下,

/*
 * 程序功能:
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include "_freecplus.h"

CTcpClient TcpClient;  // 创建服务端对象

bool biz001();  // 身份验证

int main(int argc, char *argv[]){
  if(TcpClient.ConnectToServer("127.0.0.1", 5005) == false){ // 向服务端发起连接请求
     printf("TcpClient.ConnectToServer(\"127.0.0.1\", 5005) failed.\n"); return -1;
   }

   // 身份验证
  if(biz001() == false){
     printf("biz001() failed.\n"); return -1; 
  }
/*
  sleep(10); 
  biz002();  // 余额查询

  sleep(5);
  biz002();  // 余额查询

  for(int ii = 0; ii < 3; ii++){
     if(biz000() == false) break;
     sleep(31);
   }*/
  //  程序直接退出,析构函数会释放资源
}

bool biz001(){  
  char strbuffer[1024]; // 存放数据的缓存区

  memset(strbuffer, 0, sizeof(strbuffer));
  snprintf(strbuffer, 1000, "<bizcode>1</bizcode><username>wucz</username><password>p@ssw0rd</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;
}

  运行结果如下,可以看到已经建立好了连接,
在这里插入图片描述
  运行结果如下,可以看到已经建立好了连接。那样,框架就已经搭建好,然后在里面写业务就可以了,在 _main 中来实现,之用增加希望的业务就可以。

2.5 TCP 短连接和长连接

  client 与 server 建立连接进行通信,通信完成后释放连接,建立连接时需要 3 次握手,释放需要 4 次挥手,连接的建立和释放都需要时间, server 还有创建新进程或线程的开销。
  短连接:client/server 间只需要进行一次或连续多次通信,通信完成后马上断开了。管理起来比较简单,不需要额外的控制手段。
  长连接:client/server 间需要多次通信,通信的频率和次数不确定,所以 client 和 server 需要保持这个连接,常用的方法就是采用心跳机制。根据不同的应用场景采取不同的策略,选择合适的方法。

2.6 长连接的心跳机制

  采用 client 与 server 采用长连接,在连接空闲时,client 没若干秒向 server 发送一个心跳报文,server 也回复一个心跳保温,确认连接继续生效中。
  如果 server 在约定的时间内没有收到 client 的任何报文,则认为客户端已掉线,就主动断开连接,释放资源。心跳报文在 60 s 之内,不要超过 120 s;
  客户端程序如下,

/*
 * 程序功能:
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include "_freecplus.h"

CLogFile logfile;
CTcpServer TcpServer;  // 创建服务端对象

// 程序退出时调用的函数
void FathEXIT(int sig);  // 父进程退出函数
void ChldEXIT(int sig);  // 子进程退出函数

//  处理业务的主函数
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 logfile\nExample:./ExitAndFreeServer 5005 /tmp/ExitAndFreeServer.log\n\n"); 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, FathEXIT); signal(SIGTERM, FathEXIT);

     // 初始化 TcpServer 的通信端口
    if(TcpServer.InitServer(5005) == false){
	 logfile.Write("TcpServer.InitServer(5005) failed. \n"); FathEXIT(-1);
     }

    while(true){
       if(TcpServer.Accept() == false){  // 等待客户端连接
	     logfile.Write("TcpServer.Accept() failed. \n"); continue;
         }
	 // 父进程返回到循环首部
	 if(fork() > 0){TcpServer.CloseClient(); continue; }

	 // 子进程重新设置退出信号
       signal(SIGINT, ChldEXIT); signal(SIGTERM, ChldEXIT);

       TcpServer.CloseListen();
     
         // 以下是子进程,负责与客户端通信
       logfile.Write("客户端(%s)已连接。 \n", TcpServer.GetIP());

       char strrecvbuffer[1024], strsendbuffer[1024];  //  存放数据的缓冲区

       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; // 向客户端回应报文
         }
       logfile.Write("客户端已断开。 \n");  // 程序直接退出,析构函数会释放资源
       ChldEXIT(-1); // 通信完成后,子进程退出。
     }
}

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("父进程退出。 \n");

    // 编写善后代码(释放资源、提交或回滚事务)
   TcpServer.CloseClient();

   exit(0);
}

void ChldEXIT(int sig){ // 子进程退出函数
    if(sig > 0){
       signal(sig, SIG_IGN);signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN);
     }

    logfile.Write("子进程退出。 \n");

    // 编写善后代码(释放资源、提交或回滚事务)
   TcpServer.CloseClient();

   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("非法报文:%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, "wucz") == 0) && (strcmp(password, "p@ssw0rd") == 0) )
     sprintf(strsendbuffer, "<retcode>0</retcode><message>成功。</message>");
   else
     sprintf(strsendbuffer, "<retcode>-1</retcode><message>用户名或密码不正确。</message>");

   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, "<retcode>0</retcode><message>成功。</message><ye>100.50</ye>");
   else
     sprintf(strsendbuffer, "<retcode>-1</retcode><message>卡号不存在。</message>");

   return true;
}

bool biz000(const char *strrecvbuffer, char *strsendbuffer){
   sprintf(strsendbuffer, "<retcode>0</retcode><message>成功。</message>");

   return true;
}

  客户端程序如下,

/*
 * 程序功能:
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include "_freecplus.h"

CTcpClient TcpClient;  // 创建服务端对象

bool biz000();  // 发送心跳报文
bool biz001();  // 身份验证
bool biz002();  // 余额查询

int main(int argc, char *argv[]){
  if(argc != 3){
     printf("Using:./client ip port\n Example:./client 127.0.0.1 5005\n\n"); return -1;
   }

  if(TcpClient.ConnectToServer(argv[1], atoi(argv[2])) == false){ //  向服务端发起连接请求
     printf("TcpClient.ConnectToServer(\"%s\", %s) failed.\n", argv[1], argv[2]); return -1;
   }

   // 身份验证
  if(biz001() == false){
     printf("biz001() failed.\n"); return -1; 
  }
  sleep(10); 
  biz002();  // 余额查询

  sleep(5);
  biz002();  // 余额查询

  for(int ii = 0; ii < 3; ii++){
     if(biz000() == false) break;
     sleep(31);
   }
  //  程序直接退出,析构函数会释放资源
}

bool biz001(){  // 
  char strbuffer[1024]; // 存放数据的缓存区

  memset(strbuffer,0,sizeof(strbuffer));
  snprintf(strbuffer, 1000, "<bizcode>1</bizcode><username>wucz</username><password>p@ssw0rd</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;
}


bool biz002(){
  char strbuffer[1024];  // 存放数据的缓冲区
  snprintf(strbuffer, 1000, "<bizcode>2</bizcode><cardid>62620000000001</cardid>");
  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 true;
}

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;
}

  运行结果如下,
在这里插入图片描述
  通过每隔一段时间同客户端和服务端进行一次通信,这样检测是否还在连接。在运行完成之后,客户端断开了连接。

三、总结

  最近事情比较多,真的比较忙,不过也还在认真的学习网络编程基本知识,现在感觉学习还是比较囫囵吞枣的,只是调试运行成功,然后理解每个语句功能,在 freecplus 框架里的内容还没有认真的理解,后面过程继续深入理解。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

学习C++项目—— 搭建多进程网络服务框架,增加业务和日志,心跳机制 的相关文章

  • c和java语言中的换行符

    现在行分隔符取决于系统 但在 C 程序中我使用 n 作为行分隔符 无论我在 Windows 还是 Linux 中运行它都可以正常工作 为什么 在java中 我们必须使用 n 因为它与系统相关 那么为什么我们在c中使用 n 作为新行 而不管我
  • VB.NET 相当于 C# 属性简写吗?

    是否有与 C 等效的 VB NET public string FirstName get set 我知道你能做到 Public Property name As String Get Return name ToString End Ge
  • 如何在 C# 中从 UNIX 纪元时间转换并考虑夏令时?

    我有一个从 unix 纪元时间转换为 NET DateTime 值的函数 public static DateTime FromUnixEpochTime double unixTime DateTime d new DateTime 19
  • 如何访问另一个窗体上的ListView控件

    当单击与 ListView 所在表单不同的表单中的按钮时 我试图填充 ListView 我在 Form1 中创建了一个方法以在 Form2 中使用 并将参数传递给 Form1 中的方法 然后填充 ListView 当我调试时 我得到了传递的
  • ASP.NET:获取自 1970 年 1 月 1 日以来的毫秒数

    我有一个 ASP NET VB NET 日期 我试图获取自 1970 年 1 月 1 日以来的毫秒数 我尝试在 MSDN 中寻找方法 但找不到任何东西 有谁知道如何做到这一点 从 NET 4 6 开始 该方法ToUnixTimeMillis
  • 关于在 Windows 上使用 WiFi Direct Api?

    我目前正在开发一个应用程序 我需要在其中创建链接 阅读 无线网络连接 在桌面应用程序 在 Windows 10 上 和平板电脑 Android 但无关紧要 之间 工作流程 按钮 gt 如果需要提升权限 gt 创建类似托管网络的 WiFi 网
  • 将 Excel 导入到 Datagridview

    我使用此代码打开 Excel 文件并将其保存在 DataGridView 中 string name Items string constr Provider Microsoft Jet OLEDB 4 0 Data Source Dial
  • Rx 中是否有与 Task.ContinueWith 运算符等效的操作?

    Rx 中是否有与 Task ContinueWith 运算符等效的操作 我正在将 Rx 与 Silverlight 一起使用 我正在使用 FromAsyncPattern 方法进行两个 Web 服务调用 并且我想这样做同步地 var o1
  • 在一个字节中存储 4 个不同的值

    我有一个任务要做 但我不知道从哪里开始 我不期待也绝对不想要代码中的答案 我想要一些关于该怎么做的指导 因为我感到有点失落 将变量打包和解包到一个字节中 您需要在一个字节中存储 4 个不同的值 这些值为 NAME RANGE BITS en
  • 如何将整数转换为 void 指针?

    在 C 中使用线程时 我面临警告 警告 从不同大小的整数转换为指针 代码如下 include
  • 批量更新 SQL Server C#

    我有一个 270k 行的数据库 带有主键mid和一个名为value 我有一个包含中值和值的文本文件 现在我想更新表格 以便将每个值分配给正确的中间值 我当前的方法是从 C 读取文本文件 并为我读取的每一行更新表中的一行 必须有更快的方法来做
  • Visual Studio 中的测试单独成功,但一组失败

    当我在 Visual Studio 中单独运行测试时 它们都顺利通过 然而 当我同时运行所有这些时 有些通过 有些失败 我尝试在每个测试方法之间暂停 1 秒 但没有成功 有任何想法吗 在此先感谢您的帮助 你们可能有一些共享数据 检查正在使用
  • 如何将自定义 JSON 文件添加到 IConfiguration 中?

    我正在使用 asp net Autofac 我正在尝试加载自定义 JSON 配置文件 并基于该文件创建 实例化 IConfiguration 实例 或者至少将我的文件包含到默认情况下构建的 IConfiguration asp net 中
  • 使用 Moq 使用内部构造函数模拟类型

    我正在尝试模拟 Microsoft Sync Framework 中的一个类 它只有一个内部构造函数 当我尝试以下操作时 var fullEnumerationContextMock new Mock
  • 将 log4net 与 Autofac 结合使用

    我正在尝试将 log4net 与 Autofac 一起使用 我粘贴了这段代码http autofac readthedocs org en latest examples log4net html http autofac readthed
  • std::async 与重载函数

    可能的重复 std bind 重载解析 https stackoverflow com questions 4159487 stdbind overload resolution 考虑以下 C 示例 class A public int f
  • 如何在 C# 中调整图像大小同时保持高质量?

    我从这里找到了一篇关于图像处理的文章 http www switchonthecode com tutorials csharp tutorial image editing saving cropping and resizing htt
  • 使用 GROUP 和 SUM 的 LINQ 查询

    请帮助我了解如何使用带有 GROUP 和 SUM 的 LINQ 进行查询 Query the database IEnumerable
  • 检查Windows控制台中是否按下了键[重复]

    这个问题在这里已经有答案了 可能的重复 C 控制台键盘事件 https stackoverflow com questions 2067893 c console keyboard events 我希望 Windows 控制台程序在按下某个
  • 在客户端系统中安装后桌面应用程序无法打开

    我目前正在使用 Visual Studio 2017 和 4 6 1 net 框架 我为桌面应用程序创建了安装文件 安装程序在我的系统中完美安装并运行 问题是安装程序在其他计算机上成功安装 但应用程序无法打开 edit 在客户端系统中下载了

随机推荐

  • C语言,一维数组实验五

    A C语言实验 最值 描述 有一个长度为n的整数序列 其中最大值和最小值不会出现在序列的第一和最后一个位置 请写一个程序 把序列中的最小值与第一个数交换 最大值与最后一个数交换 输出转换好的序列 输入 输入包括两行 第一行为正整数n 1 n
  • 7-14 求整数段和 (15分)

    7 14 求整数段和 15分 给定两个整数A和B 输出从A到B的所有整数以及这些数的和 输入格式 输入在一行中给出2个整数A和B 其中 100 A B 100 其间以空格分隔 输出格式 首先顺序输出从A到B的所有整数 每5个数字占一行 每个
  • 【03】pytorch 自定义transform操作-踩坑记录

    1 椒盐噪声是什么 就是图片上出现的黑白点 类似于老式电视机出现雪花屏幕的感觉 transforms是pytorch定义的一个类 对图像进行各种变换 进行图像变换的目的是数据增强 使得模型的鲁棒性更加的强 尽管pytorch已经提供了很多的
  • 用numpy里边的random函数生成随机数据&&&同时进行画图练习

    import numpy as np import matplotlib pyplot as plt import sklearn from sklearn linear model import LinearRegression from
  • python 散点图 不同颜色_Python中的散点图和颜色映射

    这是一个例子 import numpy as np import matplotlib pyplot as plt x np random rand 100 y np random rand 100 t np arange 100 plt
  • Java泛型(泛型类、反射、类型通配符)-黑马视频笔记

    泛型 学习参考视频 B站黑马 本文结构 一 什么是泛型 二 泛型类 接口 三 泛型方法 四 类型通配符 五 类型擦除 六 泛型和数组 七 泛型和反射 一 什么是泛型 背景 JAVA推出泛型以前 程序员可以构建一个元素类型为Object的集合
  • vue开发项目(PC端和移动端共用一套代码)(一)

    编写两套代码 通过路由加载不同端的文件 1 创建vue项目 2 基本配置 2 1 html设置 创建两端的vue文件 在App vue中 添加 2 2 路由设置 在router文件夹下 创建m pc两个文件夹 路径如下 router m i
  • MOS管开关设计知识-(五种MOS管开关电路图方式)

    在使用MOS管设计开关电源或者马达驱动电路的时候 大部分人都会考虑MOS的导通电阻 最大电压等 最大电流等 也有很多人仅仅考虑这些因素 这样的电路也许是可以工作的 但并不是优秀的 作为正式的产品设计也是不允许的 下面是我对MOSFET及MO
  • 在Docker上部署FastApi(最新)

    目录 1 文件上传与新建目录 文件目录 2 修改requirements txt文件 3 修改Dockerfile txt文件 4 打包成镜像 5 运行启动 6 查看运行状态与日志 1 文件上传与新建目录 新建以下目录 其中 py文件是自己
  • 使用 github 的 Action 功能实现 Microsoft office E5 订阅自动续订

    在使用期限内 微软会根据 API 调用情况看账号是否是用于开发 如果符合的话 会在距离到期 30 天时自动续期 如果不符合就不给续订了 所以可以使用一些办法多多使用 这样就可以持续续订 可以使用 github 的 Action 实现 默认读
  • python数据可视化03

    一 正弦曲线与余弦曲线图 import numpy as np import matplotlib pyplot as plt plt rcParams font sans serif SimHei plt rcParams axes un
  • Mac和Linux中Apache RocketMQ的安装和使用(亲测有效,不服来战)

    一 项目需要用到Apache RocketMQ Apache RocketMQ is an open source distributed messaging and streaming data platform 这是阿里开源的一个消息中
  • 操作系统(王道)

    1 1 1 操作系统概念 裸机 硬件只听得懂二进制指令 gt 操作系统 属于软件 提供良好交互界面 gt 应用软件 gt 用户使用 操作系统是指控制和管理整个计算机系统的硬件和软件资源 并合理地组织和调度计算机工作和资源的分配 以提供给用户
  • mysql查看所有用户

    使用shell命令MySQL uroot p 输入密码后再select user host from mysql user 结果
  • vue2中使用axios http请求出现的问题解决

    使用axios处理post请求时 出现的问题解决 默认情况下 axios post url params then res gt res data 当url是远程接口链接时 会报404的错误 Uncaught in promise Erro
  • 【论文摘要】标签合并Bamboo: Building Mega-Scale Vision Dataset Continually with Human-Machine Synergy

    Bamboo Building Mega Scale Vision Dataset Continually with Human Machine Synergy 前言 论文链接 https arxiv org pdf 2203 07845
  • git上线流程

    1 本地分支上传远端 git push 2 切换到master主分支 git checkout master 3 拉取最新 git pull 4 新建分支 merge 日期 先切换 git checkout merge 20210528 切
  • 几种常见的排序算法、搜索与二分法查找

    目录 一 排序算法 1 1 冒泡排序 1 2 选择排序 1 3 插入排序 1 4 快速排序 1 5 常见排序算法效率比较 二 搜索 2 1 二分法查找 一 排序算法 1 1 冒泡排序 冒泡排序算法的运作原理 1 比较相邻的元素 如果第一个比
  • 交换机与路由器技术-35-端口多路复用PAT

    目录 一 端口多路复用 PAT 1 1 概述 1 2 端口映射 服务器映射 1 3 配置端口多路复用 1 3 1 方式一 使用单独的公网IP 第一步 定义内网和外网接口 第二步 定义内网地址范围 外网地址 1 使用ACL 允许某个范围的内网
  • 学习C++项目—— 搭建多进程网络服务框架,增加业务和日志,心跳机制

    学习计算机网络编程 一 思路和学习方法 本文学习于 C语言技术网 www freecplus net 在 b 站学习于 C 语言技术网 并加以自己的一些理解和复现 如有侵权会删除 接下来对网络编程继续深入学习 二 网络编程继续深入 2 1