【ONE·C++ || 网络基础(二)】

总言

  主要内容:演示socke套接字编程(TCP模式),介绍序列化和反序列化,并进行演示(json版本达成协议编写、守护进程介绍)。

文章目录

  • 总言
  • 4、基于套接字的TCP网络程序
    • 4.0、log.hpp
    • 4.1、version1.0:echo服务器(单进程单线程模式)
      • 4.1.1、成员变量与构造、析构
      • 4.1.2、初始化服务器:InitServer()
        • 4.1.2.1、socket、bind
        • 4.1.2.2、listen
      • 4.1.3、启动服务器:Start()
        • 4.1.3.1、accept
      • 4.1.4、该部分整体框架:
        • 4.1.4.1、tcp_server.hpp
        • 4.1.4.2、tcp_server.cc
        • 4.1.4.3、telnet 指令
    • 4.2、version2.0 && version2.1 (多进程版)
      • 4.2.1、version2.0:采用信号捕捉达成非阻塞等待
        • 4.2.1.1、tcp_server.hpp
        • 4.2.1.2、tcp_client.cc:connect函数介绍
      • 4.2.2、version2.1:采用孤儿进程达成非阻塞等待
        • 4.2.2.1、tcp_server.hpp
    • 4.4、version3.0(多线程版)
      • 4.4.1、tcp_server.hpp
    • 4.5、version4.0(线程池版)
      • 4.5.1、tcp_server.hpp
    • 4.6、TCP协议通讯流程
  • 5、序列化和反序列化(应用层·一)
    • 5.1、基本情况介绍
    • 5.2、网络版本的计算器NetCal编写(version1.0:自定义版协议)
      • 5.2.4、Sock.hpp && TcpServer.hpp
        • 5.2.4.1、Sock.hpp
        • 5.2.4.2、TcpServer.hpp
      • 5.2.5、CalServer.cc服务端
      • 5.2.3、Protocol.hpp:定制的协议
      • 5.2.6、CalClient.cc客户端
    • 5.3、网络版本的计算器NetCal编写(version2.0:json版协议)
      • 5.3.1、守护进程
        • 5.3.1.1、问题引入
        • 5.3.1.2、如何做到:setsid、daemon
        • 5.3.1.3、Daemon.hpp
        • 5.3.1.4、log.hpp
      • 5.3.2、使用json完成序列化
        • 5.3.2.1、基本使用介绍
      • 5.3.3、改动NetCal
        • 5.3.3.1、主要部分
        • 5.3.3.2、整体Protocol.hpp

  
  
  

4、基于套接字的TCP网络程序

4.0、log.hpp

#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志级别
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};// 完整的日志功能,至少有 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)//const char *format, ... 可变参数
{
// #ifndef DEBUG_SHOW
//     if(level== DEBUG) return;
// #endif//标准部分:固定输出的内容char stdBuffer[1024]; time_t timestamp = time(nullptr);snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);//自定义部分:允许用户根据自己的需求设置char logBuffer[1024]; va_list args; //定义一个va_list对象va_start(args, format); vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args); //相当于 args == nullptrprintf("%s%s\n", stdBuffer, logBuffer);
}

  
  
  
  
  

4.1、version1.0:echo服务器(单进程单线程模式)

  先来完善tcp_server.hpp的整体逻辑:

4.1.1、成员变量与构造、析构

  有了UDP的基础,此处的框架搭建也相同。

class TcpServer
{
public:TcpServer(uint16_t port, const std::string& ip=""):_port(port),_ip(ip), _linstensock(-1){ }~TcpServer(){ }// 初始化服务器bool InitServer(){}// 启动服务器void Start(){}private:uint16_t _port;//端口号std::string _ip;//IPint _listensock;//套接字
};

  
  

4.1.2、初始化服务器:InitServer()

4.1.2.1、socket、bind

  说明:初始化服务器,这里用法和UDP类似,区别在于socket时,第二参数类型填入的是SOCK_STREAM
  
  socket

NAMEsocket - create an endpoint for communicationSYNOPSIS#include <sys/types.h>          /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);
       The  socket has the indicated type, which specifies the communication semantics.  Currently definedtypes are:SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.   An  out-of-band data transmission mechanism may be supported.SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

  
  bind:

NAMEbind - bind a name to a socketSYNOPSIS#include <sys/types.h>          /* See NOTES */#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

  
  
  相关演示如下:

        // 1、创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAMif (_listensock < 0){logMessage(ERROR, "socket, 创建套接字失败: %s-%d ", errno, strerror(errno));exit(2);}logMessage(DEBUG, "socket, 创建套接字成功,sock: %d", _listensock);// 2、绑定struct sockaddr_in localaddr;bzero(&localaddr, 0);localaddr.sin_family = AF_INET;localaddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换localaddr.sin_port = htons(_port);if (bind(_listensock, (struct sockaddr *)&localaddr, sizeof localaddr) < 0){logMessage(ERROR, "bind, 绑定失败,%d:%s", errno, strerror(errno));exit(3);}logMessage(DEBUG, "bind, 绑定成功. ");

  
  
  

4.1.2.2、listen

  1)、相关函数介绍
  说明: 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接。而作为服务器,为了确保客户端能够随时享有通讯需求,服务器需要保持在等待被连接的状态。

  listen:该函数可将套接字sockfd状态设置为监听状态,并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略,这里设置不会太大。

NAMElisten - listen for connections on a socketSYNOPSIS#include <sys/types.h>          /* See NOTES */#include <sys/socket.h>int listen(int sockfd, int backlog);DESCRIPTIONlisten() marks the socket referred to by sockfd as a passive socket, that is, as asocket that will be used to accept incoming connection requests using accept(2).The sockfd argument is  a  file  descriptor  that  refers  to  a  socket  of  typeSOCK_STREAM or SOCK_SEQPACKET.The backlog argument defines the maximum length to which the queue of pending con‐nections for sockfd may grow.  If a connection request arrives when the  queue  isfull,  the  client  may receive an error with an indication of ECONNREFUSED or, ifthe underlying protocol supports retransmission, the request  may  be  ignored  sothat a later reattempt at connection succeeds.

  相关参数:
  socket:即创建套接字时的返回值
  backlog:关于该参数在后续TCP协议时详细解释,这里我们只需要先使用即可。通常设置如下:

const static int gbacklog = 20; //不能太大、也不能太小

  返回值:

RETURN VALUEOn  success,  zero is returned.  On error, -1 is returned, and errno is set appropriately.

  
  
  2)、建立链接
  使用演示:这里我们对监听的结果做了一下判断。

        //3、监听if (listen(_listensock, gbacklog) < 0){logMessage(ERROR, "listen, 监听失败,%d:%s", errno, strerror(errno));exit(4);}logMessage(DEBUG, "linsten, 监听成功, 初始化套接字完成。");

  相关演示结果:

在这里插入图片描述

  
  
  
  
  
  

4.1.3、启动服务器:Start()

4.1.3.1、accept

  说明:三次握手完成后,服务器调用accept()接受连接,如果此时时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

NAMEaccept, accept4 - accept a connection on a socketSYNOPSIS#include <sys/types.h>          /* See NOTES */#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);#define _GNU_SOURCE             /* See feature_test_macros(7) */#include <sys/socket.h>int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);DESCRIPTIONThe  accept()  system  call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET).  It extracts thefirst connection request on the queue of pending connections for the listening socket, sockfd, creates a new connectedsocket,  and returns a new file descriptor referring to that socket.  The newly created socket is not in the listeningstate.  The original socket sockfd is unaffected by this call.

  
  
  演示如下:accept之后就是正常的通讯流程。

            // 1、获取连接// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);// 1.2、连接int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if(servicesock < 0){logMessage(ERROR,"accept, 获取链接失败, servicesock: %d ",servicesock);continue;//本次失败了,结束此次循环即可,可下一次重新获取连接}

  
  
  
  

4.1.4、该部分整体框架:

4.1.4.1、tcp_server.hpp
#ifndef _TCP_SERVER_HPP
#define _TCP_SERVER_HPP#include<iostream>
#include<string>
#include<cstring>
#include<memory>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include"log.hpp"#define SIZE 1024// 业务函数:服务器用于处理业务,可根据需求自定义
// 【version1: echo版服务器】:服务端打印从客户端读取到的数据,并将其原封不动返回给客户端
static void service(int servicesock, const std::string &clientip, const uint16_t &clientport)
{char server_buffer[SIZE];while (true){// a、读取客户端发来的数据ssize_t s = read(servicesock, server_buffer, sizeof(server_buffer) - 1);if (s > 0){server_buffer[s] = '\0'; //\0的ASCII码是0std::cout << clientip << ":" << clientport << "# " << server_buffer << std::endl;}else if (s == 0){logMessage(NORMAL, "read, %s:%d 退出。", clientip.c_str(), clientport);break;}else{logMessage(ERROR, "read, 读取失败, %d:%s", errno, strerror(errno));break;}// b、将读取到的结果返回write(servicesock, server_buffer, strlen(server_buffer));}
}class TcpServer
{
private:const static int gbacklog = 20;//listen中的参数设置public:TcpServer(uint16_t port, const std::string& ip=""):_port(port),_ip(ip),_listensock(-1){ }~TcpServer(){ }// 初始化服务器bool InitServer(){// 1、创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAMif (_listensock < 0){logMessage(ERROR, "socket, 创建套接字失败: %s-%d ", errno, strerror(errno));exit(2);}logMessage(DEBUG, "socket, 创建套接字成功,sock: %d", _listensock);// 2、绑定struct sockaddr_in localaddr;bzero(&localaddr, 0);localaddr.sin_family = AF_INET;localaddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换localaddr.sin_port = htons(_port);if (bind(_listensock, (struct sockaddr *)&localaddr, sizeof localaddr) < 0){logMessage(ERROR, "bind, 绑定失败,%d:%s", errno, strerror(errno));exit(3);}logMessage(DEBUG, "bind, 绑定成功. ");//3、监听if (listen(_listensock, gbacklog) < 0){logMessage(ERROR, "listen, 监听失败,%d:%s", errno, strerror(errno));exit(4);}logMessage(DEBUG, "linsten, 监听成功, 初始化套接字完成。");return true;}// 启动服务器void Start(){// 网络通讯角度:作为一款网络服务器,永远不退出的!// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!while (true){// 1、获取连接// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);// 1.2、连接int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if(servicesock < 0){logMessage(ERROR,"accept, 获取链接失败, servicesock: %d ",servicesock);continue;//本次失败了,结束此次循环即可,可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port = ntohs(clientaddr.sin_port);      // uint16_t htons(uint16_t hostshort);std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, "accept, 成功获取连接,  servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据(服务端的业务处理)// version1: echo版服务器service(servicesock, client_ip, client_port);// 2.3、通讯结束,关闭套接字。close(servicesock);}}private:uint16_t _port;//端口号std::string _ip;//IPint _listensock;//套接字
};#endif

  
  
  
  

4.1.4.2、tcp_server.cc
#include"tcp_server.hpp"//使用手册
void Usage(std:: string proc)
{std::cout << "\nUsage: "<< proc << " port\n"<< std::endl;
}//服务端启动: ./tcp_server port
int main(int argc, char*argv[])
{//1、检测命令行参数是否正确if(argc != 2){Usage(argv[0]);exit(1);}uint16_t port = atoi(argv[1]);//说明:命令行参数为字符串,port端口号需要整型//2、使用智能指针管理服务器std::unique_ptr<TcpServer> server(new TcpServer(port));//3、初始化和启动服务器server->InitServer();server->Start();return 0;
}

  
  
  
  

4.1.4.3、telnet 指令

  演示结果一:先测试看看我们的代码能否成功通过。

在这里插入图片描述
  
  
  演示结果二:接下来,验证以下这种单进程模式的缺陷,服务器始终只能为一个客服端提供服务,当有多个客服端同时连接时,后者处于阻塞状态。
  现象观察:
在这里插入图片描述
  原因解释:
在这里插入图片描述

  基于上述分析,我们提出了以下方案,用以解决服务端只一次能够处理一个客户端的问题。
  
  
  
  

4.2、version2.0 && version2.1 (多进程版)

  说明: 采用多进程的方式解决问题。让父进程接收连接,让子进程处理业务,其中,需要解决父进程等待子进程的问题。
  
  

4.2.1、version2.0:采用信号捕捉达成非阻塞等待

4.2.1.1、tcp_server.hpp

  说明:仍旧是演示echo版服务器(service函数不变),这里只是使用了多进程,用以让服务端能够同时服务多个客服端。
  改动的部分在start函数中。这里使用了信号捕捉的方式,让父进程达成非阻塞式等待子进程:signal(SIGCHLD, SIG_IGN); (这种处理在Linux下的含义有在 信号章节 讲述过)。

    // 启动服务器void Start(){signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态// 网络通讯角度:作为一款网络服务器,永远不退出的!// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!while (true){// 1、获取连接// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);// 1.2、连接int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (servicesock < 0){logMessage(ERROR, "accept, 获取链接失败, servicesock: %d ", servicesock);continue; // 本次失败了,结束此次循环即可,可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port = ntohs(clientaddr.sin_port);      // uint16_t htons(uint16_t hostshort);std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, "accept, 成功获取连接,  servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据(服务端的业务处理)// ——————————【version2:多进程版本】———————————//pid_t pd = fork();assert(pd >= 0);if (pd == 0) // 对子进程:处理业务{close(_listensock); // 子进程用不到监听套接字,可以关掉(子进程能够继承父进程打开的文件及其fd)service(servicesock, client_ip, client_port);exit(0); // 正常退出}// 对父进程:继续循环接收客户端的连接请求close(servicesock); // 父进程用不到accept提供的套接字,可以关掉(对子进程无影响/文件描述符是有限资源,有上限)// ————————————————————————————————————————————//}}

  
  
  使用talnet验证当前版本的tcp_server.hpp

在这里插入图片描述
  
  
  
  

4.2.1.2、tcp_client.cc:connect函数介绍

  1)、相关函数介绍
   connect:客户端不需要像服务端一样手动bind,同时也不需要accept接收连接,但客户端需要有链接别人的能力,可以通过此函数达成。

NAMEconnect - initiate a connection on a socketSYNOPSIS#include <sys/types.h>          /* See NOTES */#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);DESCRIPTIONThe  connect()  system  call connects the socket referred to by the file descriptor sockfd to the address specified by addr.The addrlen argument specifies the size of addr.  The format of the address in addr is determined by the  address  space  ofthe socket sockfd; see socket(2) for further details.If  the  socket  sockfd  is of type SOCK_DGRAM then addr is the address to which datagrams are sent by default, and the onlyaddress from which datagrams are received.  If the socket is of type SOCK_STREAM or SOCK_SEQPACKET, this  call  attempts  tomake a connection to the socket that is bound to the address specified by addr.Generally,  connection-based  protocol sockets may successfully connect() only once; connectionless protocol sockets may useconnect() multiple times to change their association.  Connectionless sockets may dissolve the association by connecting  toan address with the sa_family member of sockaddr set to AF_UNSPEC (supported on Linux since kernel 2.2).RETURN VALUEIf the connection or binding succeeds, zero is returned.  On error, -1 is returned, and errno is set appropriately.

  
  
  2)、代码演示
  相关代码:


#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>#define SIZE 1024// 使用手册
void Usage(std::string proc)
{std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl;
}// 启动方式:udp_client server_ip server_port
int main(int argc, char *argv[])
{// 1、获取命令行传入的端口号、IP:注意将其转换为对应的类型// a、检测命令行参数是否正确if (argc != 3){Usage(argv[0]);exit(1);}// b、获取服务端端口号、IPstd::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 2、创建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){std::cerr << "client: 创建套接字失败。 socket: " << sock << std::endl;exit(2);}// 3、客户端需要有连接服务端的能力(PS:客户端不需要bind,一般由OS自动分配端口号)// a、准备工作:struct sockaddr_in serveraddr;bzero(&serveraddr, 0);serveraddr.sin_family = AF_INET;serveraddr.sin_port = htons(server_port);//端口号:需要进行网络字节序的转换serveraddr.sin_addr.s_addr = inet_addr(server_ip.c_str());//IP:// b、建立连接if(connect(sock, (struct sockaddr*)&serveraddr, sizeof serveraddr) < 0){std::cerr << "client: connect建立连接失败。" << std::endl;exit(3);}std::cout << "client: connect建立连接成功。" << std::endl;// 4、链接成功,即可通讯while(true){//a、客户端向服务端发送数据std::string message;std::cout << "client-请输入# ";std::getline(std::cin,message);send(sock, message.c_str(), message.size(),0);//b、客户端接收服务端传回的数据char client_buff[SIZE];ssize_t s = recv(sock,client_buff,sizeof(client_buff)-1,0);if(s > 0)//成功接收到数据{client_buff[s] = 0;std::cout << "client-服务端响应:" << client_buff << std::endl;}}return 0;
}

  
  演示结果如下:
在这里插入图片描述
  
  
  
  
  

4.2.2、version2.1:采用孤儿进程达成非阻塞等待

4.2.2.1、tcp_server.hpp

  说明: 仍旧是演示echo版本服务器、多进程模式。只是这次不采用信用捕捉,而是使用孤儿进程的方式达成非阻塞等待。
  写法如下: 在子进程中再创建子进程,由于子进程关闭,子子进程会成为孤儿进程,被1号进程领养。孤儿进程退出的时候,由OS自动回收孤儿进程!

    // 启动服务器void Start(){// 网络通讯角度:作为一款网络服务器,永远不退出的!// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!while (true){// 1、获取连接// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);// 1.2、连接int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (servicesock < 0){logMessage(ERROR, "accept, 获取链接失败, servicesock: %d ", servicesock);continue; // 本次失败了,结束此次循环即可,可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port = ntohs(clientaddr.sin_port);      // uint16_t htons(uint16_t hostshort);std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, "accept, 成功获取连接,  servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据(服务端的业务处理)// ——————————【version2.1:多进程版本】———————————//pid_t pd = fork();assert(pd != -1);if( pd == 0)//子进程{close(_listensock);//子进程:关闭不必要的套接字if(fork() == 0)// 在子进程中再fork子进程,得到子子进程(孙子进程){service(servicesock, client_ip, client_port);exit(0);// 孙子进程,由于子进程关闭,其成为孤儿进程,OS领养, OS在孤儿进程退出的时候,由OS自动回收孤儿进程!}exit(0);//关闭子进程,会导致子子进程变成孤儿进程}//对父进程:waitpid(pd, nullptr, 0);close(servicesock);//父进程:关闭不必要的套接字// —————————————————————————————————————————————//}}

  这里也可以使用if(fork() > 0)来判断,写法无区别。

            //version2.1 -- 多进程版pid_t id = fork();if(id == 0){// 子进程close(_listensock);if(fork() > 0)//子进程本身exit(0); //子进程本身立即退出// 孙子进程称为孤儿进程,OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程!service(servicesock, client_ip, client_port);exit(0);}// 父进程waitpid(id, nullptr, 0); //不会阻塞!close(servicesock);

  
  
  演示结果如下:
在这里插入图片描述

  
  
  
  
  

4.4、version3.0(多线程版)

4.4.1、tcp_server.hpp

  1)、准备工作
  1、为了让新线程执行业务处理,需要设置回调函数。这里我们将其设置在了TcpServer类中,并将其置为静态成员函数。(注意,线程结束时要关闭文件描述符和释放空间)
  PS:此部分涉及的函数在多线程中有学习。

class TcpServer
{
private://【version3:多线程版本】: 为新线程提供的回调函数,设置为静态成员函数是因为非静态成员函数有默认参数this,不符合回调函数的格式要求static void* threadRoutine(void* args){线程分离:结束服务,若不捕捉(捕捉属于阻塞式的,我们要求服务端不能阻塞等待),//则需要对线程进行分离pthread_detach(pthread_self());以避免系统层面的内存泄漏pthread_detach(pthread_self());threadData* data = static_cast<threadData *>(args);//C++11中的类型转换service(data->sock, data->ip, data->port);// 线程结束时,需要关闭文件描述符、并释放new出来的空间close(data->sock);delete data;return nullptr;}//……
}

  
  2、void*args参数设置:使用类可提供更多选择性。

//【version3:多线程版本】:要在新线程中调用server,要将其需要函数参数设置进void*args中。这里使用类来完成。
struct threadData
{int sock;std::string ip;uint16_t port;
};

  
  
  TcpServer类中需要改动的仍旧是start()部分,代码如下:

    // 启动服务器void Start(){//signal(SIGCHLD, SIG_IGN); // 【version2.1:多进程版本】对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态// 网络通讯角度:作为一款网络服务器,永远不退出的!// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!while (true){// 1、获取连接// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);// 1.2、连接int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (servicesock < 0){logMessage(ERROR, "accept, 获取链接失败, servicesock: %d ", servicesock);continue; // 本次失败了,结束此次循环即可,可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port = ntohs(clientaddr.sin_port);      // uint16_t htons(uint16_t hostshort);std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, "accept, 成功获取连接,  servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据(服务端的业务处理)// ——————————【version3:多线程版本】———————————//// a、准备工作threadData* td = new threadData;//在堆上td->sock = servicesock;td->ip = client_ip;td->port = client_port;pthread_t pid;// b、创建线程pthread_create(&pid, nullptr, threadRoutine, (void*)td);//PS:注意这里不需要close(servicesock);因为主线程和新线程共享资源// —————————————————————————————————————————————//}
}

  
  演示结果:
在这里插入图片描述

  
  
  
  
  

4.5、version4.0(线程池版)

  这里我们借用了之前写过的线程池库,详细见:多线程章节。

4.5.1、tcp_server.hpp

  该线程库大体逻辑不变,只是需要改动任务对象,将其换成我们所需要的部分(即task.hpp中封装的_func实际传入的是回调函数,task.hpp中需要的成员变量是为了满足该_func函数。)

#pragma once#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"// typedef std::function<void (int , const std::string &, const uint16_t &)> func_t; //写法一
using func_t = std::function<void (int , const std::string &, const uint16_t &, const std::string &)>; //写法二class Task
{
public:Task(){}//无参构造Task(int sock, const std::string ip, uint16_t port, func_t func)//构造: _sock(sock), _ip(ip), _port(port), _func(func){}void operator ()(const std::string &name){_func(_sock, _ip, _port, name);}
public:int _sock;std::string _ip;uint16_t _port;func_t _func;
};

  
  需要改动部分:service业务处理函数,新增了参数,用以辅佐观察(实则该函数可根据需求调节内容)

// 【version4:echo版服务器】:和上面的相同,只是为了显示是哪个新线程执行的业务处理,新增了参数
static void service(int servicesock, const std::string &clientip, const uint16_t &clientport, const std::string &threadname)
{char server_buffer[SIZE];while (true){// a、读取客户端发来的数据ssize_t s = read(servicesock, server_buffer, sizeof(server_buffer) - 1);if (s > 0){server_buffer[s] = '\0'; //\0的ASCII码是0std::cout << threadname << "| " << clientip << ":" << clientport << "# " << server_buffer << std::endl;}else if (s == 0){logMessage(NORMAL, "read, %s:%d 退出。", clientip.c_str(), clientport);break;}else{logMessage(ERROR, "read, 读取失败, %d:%s", errno, strerror(errno));break;}// b、将读取到的结果返回write(servicesock, server_buffer, strlen(server_buffer));}close(servicesock);// c、结束,需要将文件描述符关闭
}

  
  新增了成员变量std::unique_ptr<ThreadPool<Task>> _pthreadpool; ,所以,构造函数中的初始化列表也需要改动。其它改动部分仍旧在Start()中。

class TcpServer
{
private:const static int gbacklog = 20; // listen中的参数设置public:TcpServer(uint16_t port, const std::string &ip = "") //在构造时,初始化其它变量时也要初始化线程库: _port(port), _ip(ip), _listensock(-1),_pthreadpool(ThreadPool<Task>::getThreadPool()){}~TcpServer(){close(_listensock);}// 初始化服务器bool InitServer(){// 1、创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAMif (_listensock < 0){logMessage(ERROR, "socket, 创建套接字失败: %s-%d ", errno, strerror(errno));exit(2);}logMessage(DEBUG, "socket, 创建套接字成功,sock: %d", _listensock);// 2、绑定struct sockaddr_in localaddr;bzero(&localaddr, 0);localaddr.sin_family = AF_INET;localaddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换localaddr.sin_port = htons(_port);if (bind(_listensock, (struct sockaddr *)&localaddr, sizeof localaddr) < 0){logMessage(ERROR, "bind, 绑定失败,%d:%s", errno, strerror(errno));exit(3);}logMessage(DEBUG, "bind, 绑定成功. ");// 3、监听if (listen(_listensock, gbacklog) < 0){logMessage(ERROR, "listen, 监听失败,%d:%s", errno, strerror(errno));exit(4);}logMessage(DEBUG, "linsten, 监听成功, 初始化套接字完成。");return true;}// 启动服务器void Start(){_pthreadpool->run();//【version4】:在启动服务器时,一并将线程池中的线程启动(创建)// 网络通讯角度:作为一款网络服务器,永远不退出的!// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!while (true){// 1、获取连接// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);// 1.2、连接int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (servicesock < 0){logMessage(ERROR, "accept, 获取链接失败, servicesock: %d ", servicesock);continue; // 本次失败了,结束此次循环即可,可下一次重新获取连接}// 2、开始进行通讯服务// 2.1、获取客户端端口号、IPuint16_t client_port = ntohs(clientaddr.sin_port);      // uint16_t htons(uint16_t hostshort);std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);logMessage(DEBUG, "accept, 成功获取连接,  servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);// 2.2、根据需求处理客户端数据(服务端的业务处理)// ——————————【version4:线程池版本】———————————//// a、主线程派发任务对象:将客服端当作一个任务对象,创建后放入线程池中,后续由线程池中的新线程来执行。Task t(servicesock, client_ip, client_port, service);_pthreadpool->pushTask(t);// ———————————————————————————————————————————//}}private:uint16_t _port;                                 // 端口号std::string _ip;                                // IPint _listensock;                                // 套接字std::unique_ptr<ThreadPool<Task>> _pthreadpool; // 【version4】:指向线程池的指针
};

  
  
  演示结果如下:
在这里插入图片描述
  
  
  
  
  
  
  
  
  

4.6、TCP协议通讯流程

  
  
  
  

5、序列化和反序列化(应用层·一)

5.1、基本情况介绍

  1)、应用层与应用层协议
  应用层说明: 我们之前写的一个个解决实际问题、满足日常需求的网络程序,都是在应用层进行的。在之前几个小节的socket套接字编,都是属于应用层的开发(我们只是使用了传输层包装出来的接口而已),且这些套接字只是演示了数据收发过程,并非实际涉及协议。
  
  应用层协议: 协议是一种“约定”。通常,只要保证一端发送时构造的数据, 在另一端能够正确的进行解析, 这种“约定”就是应用层协议。例如,socket api的接口在读写数据时, 就是按 “字符串” 的方式来发送、接收数据的。
  
  
  2)、序列化和反序列化
  基本介绍:数据类型可以是字节流数据,也可以是结构化数据。通常,前者应用于网络传输,后者应用于上层业务。

序列化:把对象转换为字节序列的过程,称为对象的序列化。
反序列化:把字节序列恢复为对象的过程,称为对象的反序列化。

在这里插入图片描述

  
  思考问题:为什么要做这种序列化和反序列化的处理?
  
  
  

5.2、网络版本的计算器NetCal编写(version1.0:自定义版协议)

  目的说明:
  ①从编码角度介绍什么是序列化、什么是反序列化。
  ②手动定制协议→成熟的协议使用:这里只是针对当前场景自定义协议,这种完全自己写的协议会极大概率存在各种问题缺陷,且若要对Cal中的协议做扩展,需要改动很大, 而使用别人提供的成熟方案,相对而言还是能简单很多。因此,此部分内容学习主要是为了理解协议定制的整体流程。
  
  

5.2.4、Sock.hpp && TcpServer.hpp

在这里插入图片描述
  

5.2.4.1、Sock.hpp

  说明:该文件只是对sock函数接口进行了一次封装,以便在客户端、服务端两个.cc文件中调用。
  
  相关代码如下:

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"class Sock
{
private:const static int gbacklog = 20; // listen中参数设置:详细将在后续介绍public:// 构造Sock(){}; // 无参构造// 析构~Sock(){};// 创建套接字:int socket(int domain, int type, int protocol);int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "socket:创建套接字失败。%d:%s", errno, strerror(errno));exit(2); // 退出}logMessage(NORMAL, "socket:创建套接字成功, listensock:%d", listensock);return listensock; // 将套接字返回给TcpServer中的成员函数_listensock}// 绑定:int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);void Bind(int sock, uint16_t port, const std::string& ip = "0.0.0.0"){// 准备工作:sockaddr结构体struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);                    // 对端口号:需要转换为网络字节序inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 对ip:点分十进制风格-->网络字节序+四字节// 绑定:if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind:绑定失败。%d:%s", errno, strerror(errno));exit(3); // 退出}logMessage(NORMAL, "bind: 绑定成功。");}// 监听:int listen(int sockfd, int backlog);void Listen(int sock){if (listen(sock, gbacklog) < 0){logMessage(FATAL, "listen:监听失败。%d:%s", errno, strerror(errno));exit(4); // 退出}logMessage(NORMAL, "listen:监听成功。");}// 获取连接:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);int Accept(int listensock, std::string *ip, uint16_t *port) // 后两个*为输出型参数:这里的作用是将accept接收到的ip、port返回给TcpServer。{// 准备工作:用于接收源IP、源端口号struct sockaddr_in src;memset(&src, 0, sizeof(src));src.sin_family = AF_INET;socklen_t len = sizeof(src);// 获取连接int servicesock = accept(listensock, (struct sockaddr *)&src, &len);if (servicesock < 0){logMessage(FATAL, "accept:接收失败。%d:%s", errno, strerror(errno));exit(5);}logMessage(NORMAL, "accept:接收成功。servicesock:%d", servicesock);if (ip)                            // 判空:获取源IP*ip = inet_ntoa(src.sin_addr); // 四字节+网络字节序--->主机字节序+点分十进制if (port)                          // 判空:获取源端口号*port = ntohs(src.sin_port);   // 网络字节序--->主机字节序return servicesock;}// int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);bool Connect(int sock, const uint16_t &port, const std::string &ip){// 准备工作struct sockaddr_in aim;bzero(&aim, sizeof(aim));aim.sin_family = AF_INET;aim.sin_port = htons(port);                  // 主机字节序--->网络字节序aim.sin_addr.s_addr = inet_addr(ip.c_str()); // 主机字节序+点分十进制风格--->网络字节序+四字节// 连接if (connect(sock, (struct sockaddr *)&aim, sizeof(aim)) < 0){logMessage(FATAL, "connect:连接失败。%d:%s", errno, strerror(errno));return false;}logMessage(NORMAL, "connect:连接成功。");return true;}
};

  
  
  
  

5.2.4.2、TcpServer.hpp

  说明: 这里我们使用的仍旧是TCP网络通信,相关内容的编写在之前章节演示过,只是在其基础上封装了接口。

  代码如下: 使用的是线程版本的服务端。
  1、这里新增了两个函数,BindService是用于绑定业务处理函数,可在CalServer.cc中根据需求实现,并在启动服务器前将其绑定。
  2、Excute是提供给新线程的,用于执行业务函数。服务端能够处理的业务不止一种,因此可以使用vector<func_t>存储。如需要还可以使用unordered_map<std::string, func_t>,为每个函数附带名称,形成键值对。

#pragma once#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>namespace ns_tcpserver // 命名空间:
{class TcpServer; // 声明struct ThreadData{// 构造ThreadData(int sock, TcpServer *server): sock_(sock), server_(server){  }// 析构~ThreadData(){  }int sock_;TcpServer *server_; // 为了直接传递this指针,方便回调函数调用整个类成员及函数};using func_t = std::function<void(int)>;// PS:这里使用的是多线程版本的服务端class TcpServer{private:static void *ThreadRoutine(void *args) // 类中成员函数:为了满足线程回调函数的参数需求,设置为静态成员函数{// 线程分离:为了服务端不阻塞式等待,需要对线程分离pthread_detach(pthread_self());// 解析args参数ThreadData *pdata = static_cast<ThreadData *>(args);// 调用任务处理函数pdata->server_->Excute(pdata->sock_);// 线程处理完任务后,需要关闭套接字,释放申请出来的空间logMessage(NORMAL,"新线程执行完毕,关闭套接字:%d",pdata->sock_);close(pdata->sock_);delete pdata;return nullptr;}public:// 构造:初始化服务器TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0"){// 创建套接字listensock_ = sock_.Socket();// 绑定套接字sock_.Bind(listensock_, port, ip);// 监听套接字sock_.Listen(listensock_);}// 析构~TcpServer(){if (listensock_ >= 0)close(listensock_); // 关闭套接字}// 绑定服务:提供给TcpServer.cc,将服务器的业务处理函数插入到vector数组中void BindService(func_t func){func_.push_back(func);}// 新线程执行业务处理函数void Excute(int sock){for(auto& f : func_){f(sock);}}// 启动服务器void Start(){// while死循环:服务器启动后,主线程一直在运行,不断接收客户端请求,并派发线程对其进行处理。while (true){// 连接uint16_t client_port;                                                  // 客服端端口号std::string client_ip;                                                 // 客户端IPint servicesock = sock_.Accept(listensock_, &client_ip, &client_port); // 输出型参数,调用accept函数获取客户端的端口号和IPif (servicesock == -1)                                                 // 连接失败,重新连接continue;logMessage(NORMAL, "连接成功, 可以开始业务处理。servicsock:%d", servicesock);// 派发新线程进行业务处理pthread_t tid;ThreadData *pdata = new ThreadData(servicesock, this);pthread_create(&tid, nullptr, ThreadRoutine, (void *)pdata);}}private:int listensock_;           // 套接字Sock sock_;                // 将套接字封装:用于调用相关socket函数std::vector<func_t> func_; // 服务端用于业务处理的函数:这里使用了vector将函数存储,表示服务器提供的处理业务可能有多种};
}

  
  
  
  
  
  
  

5.2.5、CalServer.cc服务端

在这里插入图片描述

  
  相关代码:

#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
#include "Daemon.hpp"
#include <memory>
#include <signal.h>static void Usage(const std::string &process)
{std::cout << "\nUsage: " << process << " port\n"<< std::endl;
}ns_protocol::Response calculatorHelper(const ns_protocol::Request &req)
{// 根据op选项进行计算ns_protocol::Response resp(0, 0);switch (req.op_){case '+':resp.result_ = req.x_ + req.y_;break;case '-':resp.result_ = req.x_ - req.y_;break;case '*':resp.result_ = req.x_ * req.y_;break;case '/':if (req.y_ == 0) // 除零错误,需要设置状态码resp.code_ = 1;elseresp.result_ = req.x_ / req.y_;break;case '%':if (req.y_ == 0) // 模零错误,需要设置状态码resp.code_ = 2;elseresp.result_ = req.x_ % req.y_;break;default: // 输入错误,需要设置状态码resp.code_ = -1;break;}return resp; // 返回结果(响应:结构体对象)
}void calculator(int sock) // 服务端用于提供业务的函数
{std::string buffer;//临时缓冲区:用于存储recv读取上来的请求while (true){// Recv读取请求(客户端经过网络传输发来的数据,属于字节流数据,即客户端在发送前会做序列化处理)bool ret = ns_protocol::Recv(sock, buffer);if(!ret) //读取失败:直接结束break;// 解析协议,判断读取到的是否是完整的报文。std::string package = ns_protocol::Decode(buffer);if(package.empty())//报文不完整continue;//继续重新读取,直到读取到完整的报文logMessage(NORMAL,"本次请求: %s",package.c_str());// 当读取到完整的报文后,将请求反序列化(字节流→结构化,服务端要进行上层业务处理,使用的是结构化的数据,所以要进行反序列化)ns_protocol::Request req;req.Deserialized(package);// 进行业务处理,获取结构化存储的结果(结构化数据)ns_protocol::Response resp = calculatorHelper(req);// 将结果序列化(结构化→字节流,服务端要将处理后的结果通过网络传输返回给客户端,需要进行序列化处理)std::string result_string = resp.Serialize();// 对序列化后的结果添加长度信息,形成完整的报文result_string = ns_protocol::Encode(result_string);logMessage(NORMAL,"本次响应: %s",result_string.c_str());// 返回客户端响应ns_protocol::Send(sock, result_string);}
}// 启动:./CalServer port
int main(int argc, char *argv[])
{// 检查命令行参数,获取命令行端口号if (argc != 2){Usage(argv[0]);exit(1);}//signal(SIGPIPE,SIG_IGN);//此句在自定义daemon中写过//让进程守护进程化MyDaemon();// 创建服务器std::unique_ptr<ns_tcpserver::TcpServer> server(new ns_tcpserver::TcpServer(atoi(argv[1])));// 绑定业务处理函数:这里是网络计算器server->BindService(calculator);// 启动服务器server->Start();return 0;
}

  
  
  
  
  
  

5.2.3、Protocol.hpp:定制的协议

在这里插入图片描述

  以下为协议定制时的相关测试:

    //测试:用于测试序列化、反序列化·自定义写法是否正确//  1.1、测试序列化ns_protocol::Request req(1234, 5678, '+');std::string str = req.Serialize();std::cout << "Request-Serialize: " << str << std::endl;std::cout<< std::endl;// 1.2、测试反序列化ns_protocol::Request req2;req2.Deserialized(str);std::cout << "Request-Deserialize: " << std::endl;std::cout << "x: " << req2.x_ << "  y: " << req2.y_ << "  op: " << req2.op_ << std::endl;std::cout<< std::endl;// 2.1、测试序列化ns_protocol::Response resp(6012,0);std::string str2 = resp.Serialize();std::cout << "Response-Serialize: " << str2 << std::endl;std::cout<< std::endl;// 2.2、测试反序列化ns_protocol::Response resp2;resp2.Deserialized(str2);std::cout << "Response-Deserialize: " << std::endl;std::cout << "code: " << resp2.code_ << "  result: " << resp2.result_ << std::endl;std::cout<< std::endl;

  
  
  
  这里我们使用条件编译来完成,先使用自定义方案理解,之后会调整为成熟方案。如下:

#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>// 这里的协议是用于服务网络版计算器的
namespace ns_protocol
{// 控制条件编译:自定义方案 && 现成方案#define MYSELF 1// 处理分隔符:这里使用的是空格,定义成宏方便根据需求修改
#define SPACE " "
#define SPACE_LINE strlen(SPACE)
// 加入数据长度,并使用特殊字符(\r\n)区分各段
#define SEP "\r\n"
#define SEP_LINE strlen(SEP)/// 请求:结构体对象 ///class Request{public:// 构造Request(){};Request(int x, int y, char op): x_(x), y_(y), op_(op){}// 对请求进行序列化(结构化数据→字节流数据)std::string Serialize() // 将x_、y_、op_{
#ifdef MYSELF// version1: "x_[空格] op_[空格] y_"std::string str;str = std::to_string(x_); // 先将对应的运算数转换为字符类型:例如32-->"32"。这里注意与ASCII中值为32的字符区别str += SPACE;             // 中间以我们设置的间隔符分割(为了反序列化时能够提取每部分)str += op_;               // op_本身就是char类型str += SPACE;str += std::to_string(y_);return str;
#elsestd::cout << "TODO" << std::endl;
#endif}// 对请求进行反序列化(字节流数据→结构化数据)bool Deserialized(const std::string &str) // 获取x_、y_、op_{
#ifdef MYSELF//----------------------------------// version1: "x_[空格] op_[空格] y_" 根据分隔符提取有效数放入结构化对象中// 例如:"1234[空格]+[空格]5678"// a、找左运算数std::size_t left_oper = str.find(SPACE);if (left_oper == std::string::npos) // 没找到return false;// b、找右运算数std::size_t right_oper = str.rfind(SPACE);if (right_oper == std::string::npos) // 没找到return false;// c、提取运算数,赋值给结构化对象成员x_ = atoi((str.substr(0, left_oper)).c_str());            // string substr (size_t pos = 0, size_t len = npos) const;y_ = atoi((str.substr(right_oper + SPACE_LINE).c_str())); // 注意这里右运算符需要将[空格]跳过if (left_oper + SPACE_LINE > str.size())return false;elseop_ = str[left_oper + SPACE_LINE]; // 提取运算符时也要注意跳过分隔符[空格]return true;//----------------------------------
#elsestd::cout << "TODO" << std::endl;
#endif}public:int x_;   // 左运算数int y_;   // 右运算数char op_; // 运算符};/// 响应:结构体对象 ///class Response{public:// 构造函数Response(int result, int code): result_(result), code_(code){}Response() {}// 析构函数~Response() {}// 对响应序列化(结构化数据→字节流数据)std::string Serialize(){
#ifdef MYSELF// version1:"code_ [空格] result_"// 例如:"0[空格]6912"std::string str;str = std::to_string(code_);str += SPACE;str += std::to_string(result_);return str;
#elsestd::cout << "TODO" << std::endl;
#endif}// 对响应反序列化(字节流数据→结构化数据)bool Deserialized(const std::string &str){
#ifdef MYSELF//----------------------------------// version1:"code_ [空格] result_"// 例如:"0[空格]6912"// a、找分隔符std::size_t pos = str.find(SPACE);if (pos == std::string::npos) // 没找到return false;// b、获取状态码code_ = atoi((str.substr(0, pos)).c_str());// c、获取计算结果result_ = atoi((str.substr(pos + SPACE_LINE)).c_str());return true;//----------------------------------
#elsestd::cout << "TODO" << std::endl;
#endif}public:int result_; // 计算结果int code_;   // 状态码:用于判断结果是否正常};// 从网络中读取bool Recv(int sock, std::string &out_buffer){char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 阻塞式从网络中读取字节流数据if (s > 0)                                             // 读取成功{buffer[s] = 0;        // 文件尾字符\0out_buffer += buffer; // 使用+=,输出到out_buffer中,这样在多次读取的情况下能保证数据连续性}else if (s == 0){std::cout << "recv:quit." << std::endl;return false;}else{std::cout << "recv error." << std::endl;return false;}return true;}// 向网络中发送void Send(int sock, const std::string &str){ssize_t s = send(sock, str.c_str(), str.size(), 0);if (s < 0){std::cout << "send error" << std::endl;}}// length\r\nx_[空格]op_[空格]y_\r\n// 实际正文部分:x_[空格]op_[空格]y_,后续需要对正文部分序列化std::string Decode(std::string &buffer){// 找有没有\r\nstd::size_t pos = buffer.find(SEP);if (pos == std::string::npos) // 没找到,说明本次读取/接收到的报文不完整,需要继续读取/接收return "";// 执行到此,说明确实有\r\n,但不代表数据完整。此时需要提取length值,与实际正文做比较,判断是否读取到一个完整的报文int length_size = atoi(buffer.substr(0, pos).c_str()); // 获取长度信息int remain_size = buffer.size() - pos - 2 * SEP_LINE;  // 获取剩余长度if (remain_size >= length_size)                        // 说明此时缓冲区buffer中存在一个完整的报文,可以提取。{// 举例:【length\r\nXXXXXXXXX\r\nlength\r\nXXXXXX\r\n】// string& erase (size_t pos = 0, size_t len = npos);buffer.erase(0, pos + SEP_LINE);                 // 移除缓冲区中的length\r\n,即【XXXXXXXXX\r\nlength\r\nXXXXXX\r\n 】std::string str = buffer.substr(0, length_size); // 获取length长度的字串:即【XXXXXXXXX】buffer.erase(0, length_size + SEP_LINE);         // 移除缓冲区中正文及尾随的\r\n,即【length\r\nXXXXXX\r\n 】return str;}elsereturn ""; // 说明本次读写缓冲区中报文不完整,需要继续读写。}// 返回一个带有长度信息的完整报头:实际正文部分为 x_[空格]op_[空格]y_,需要为其添加长度信息,变为length\r\nx_[空格]op_[空格]y_\r\n// 例如:123 * 456 --->9\r\n123 * 456\r\nstd::string Encode(std::string &s){// 1、获取正文长度std::string package = std::to_string(s.size());// 2、加上SEP分隔符package += SEP;// 3、加上正文package += s;// 4、加上SEP分隔符package += SEP;return package;}}

  
  
  
  
  

5.2.6、CalClient.cc客户端

  
  相关代码如下:实际也可根据需求,将读取、发送分线程执行。

#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"
#include "Log.hpp"static void Usage(const std::string &process)
{std::cout << "\nUsage: " << process << " server_ip server_port\n"<< std::endl;
}//./CalClient server_ip server_port
int main(int argc, char *argv[])
{// 检查命令行参数,获取服务端端口号、IP地址if (argc != 3){Usage(argv[0]);exit(1);}std::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 创建套接字Sock sock;int sockfd = sock.Socket();// 连接服务端if (sock.Connect(sockfd, server_port, server_ip) < 0){std::cerr << "client:连接失败" << std::endl;exit(2);}// 与服务端通讯bool quit = false;std::string buffer;while (!quit){std::cout << std::endl;std::cout << "----------------------" << std::endl;// 创建一个请求ns_protocol::Request req;std::cout << "Please Enter # ";std::cin >> req.x_ >> req.op_ >> req.y_;logMessage(DEBUG, "请求结果为, x:%d, y:%d, op:%c", req.x_, req.y_, req.op_);// 将请求序列化std::string send_str = req.Serialize();logMessage(DEBUG, "序列化结果为,%s", send_str.c_str());// 添加长度信息send_str = ns_protocol::Encode(send_str);logMessage(DEBUG, "添加长度信息后, %s", send_str.c_str());// 发送给服务端ns_protocol::Send(sockfd, send_str);// 从服务端读取结果while (true){// 读取响应结果bool ret = ns_protocol::Recv(sockfd, buffer);if (!ret) // 读取失败{printf("DEBUG: 获取响应失败,退出。\n");quit = true; // 退出循环,关闭sockfdbreak;}// 对响应解析:是否获取到完整报文std::string package = ns_protocol::Decode(buffer);if (package.empty()) // 说明本次接收到的报文不完整,继续读取{logMessage(DEBUG, "报文不完整,继续读取。");continue;}logMessage(DEBUG, "读取到完整报文, %s\n", package.c_str());// 到此步骤,获取到了完整报文,可以反序列化ns_protocol::Response resp;resp.Deserialized(package);// 显示结果std::string err;switch (resp.code_){case 1:err = "除零错误";break;case 2:err = "模零错误";break;case -1:err = "非法操作";break;default:std::cout << "result: " << resp.result_ << std::endl;break;}if (!err.empty()) // 显示错误信息std::cout << "code: " << err << std::endl;break;}}close(sockfd);return 0;
}

  
  
  
  
  
  
  

5.3、网络版本的计算器NetCal编写(version2.0:json版协议)

5.3.1、守护进程

在这里插入图片描述

5.3.1.1、问题引入

  
  说明一:什么是前台进程?
  在 Linux 中,前台进程是指当前正在运行的进程,它与用户交互并占用终端。当用户在终端中输入命令时,该命令所启动的进程就是前台进程。前台进程会占用终端,直到它执行完毕或者被中断(例如按下 Ctrl+C)。在前台进程运行期间,用户可以通过键盘输入命令或者发送信号来与进程交互。
  守护进程全部都是在前台运行的。 任何xshell登陆,只允许一个前台进程和多个后台进程。

在这里插入图片描述
  
  
  说明二:进程有自己的pid、 ppid、组ID

在这里插入图片描述

  问题说明:退出登录,不同的shell有不同处理。当我们启动服务端,其为前台进程进入同一个会话中,若关闭shell退出登录,该服务端进程可能会随着会话结束而被杀掉,那么此时用户端就无法访问到服务端了。即,不符合服务端一直运行的需求。
  
  为了解决这个问题,引入守护进程。自成一个会话的进程,即守护进程。
  
  
  
  

5.3.1.2、如何做到:setsid、daemon

  1)、setsid
  man 2 setsid

NAMEsetsid - creates a session and sets the process group IDSYNOPSIS#include <unistd.h>pid_t setsid(void);DESCRIPTIONsetsid()  creates  a new session if the calling process is not a process group leader.  The calling process is the leader of the new ses‐sion, the process group leader of the new process group, and has no controlling terminal.  The process group ID and  session  ID  of  thecalling process are set to the PID of the calling process.  The calling process will be the only process in this new process group and inthis new session.RETURN VALUEOn success, the (new) session ID of the calling process is returned.  On error, (pid_t) -1 is returned, and errno is set to indicate  theerror.ERRORSEPERM  The  process  group  ID  of any process equals the PID of the calling process.  Thus, in particular, setsid() fails if the callingprocess is already a process group leader.

  说明:setsid要成功被调用,必须保证当前进程不是进程组的组长。因此可以通过创建子进程的方式,保证当前进程不是进程组的组长。
  
  
  
  2)、daemon
  man daemon:系统中有相关函数,可以为我们做到让一个进程成为守护进程。

NAMEdaemon - run in the backgroundSYNOPSIS#include <unistd.h>int daemon(int nochdir, int noclose);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):daemon(): _BSD_SOURCE || (_XOPEN_SOURCE && _XOPEN_SOURCE < 500)DESCRIPTIONThe daemon() function is for programs wishing to detach themselves from the controlling terminal and run in the background as system dae‐mons.If nochdir is zero, daemon() changes the calling process's current working directory to the root directory ("/"); otherwise, the  currentworking directory is left unchanged.If noclose is zero, daemon() redirects standard input, standard output and standard error to /dev/null; otherwise, no changes are made tothese file descriptors.RETURN VALUE(This function forks, and if the fork(2) succeeds, the parent calls _exit(2), so that further errors are seen by  the  child  only.)   Onsuccess daemon() returns zero.  If an error occurs, daemon() returns -1 and sets errno to any of the errors specified for the fork(2) andsetsid(2).

  说明:虽然系统提供了相关函数,但一般项目里倾向于自定义编写相关代码。即在Linux中正确的写一个让进程守护进程化的代码。
  
  
  
  

5.3.1.3、Daemon.hpp

  1)、dev/null
  ls /dev/null:dev/null是一个特殊的设备文件,该文件接收到的任何数据都会被丢弃,也无法从该文件中读取到任何数据。因此,其被称为文件黑洞,也被成为位桶(bit bucket)。
在这里插入图片描述

  
  
  2)、相关编写

#pragma once#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void MyDaemon()
{// 1. 忽略信号,SIGPIPE,SIGCHLDsignal(SIGPIPE, SIG_IGN); // sighandler_t signal(int signum, sighandler_t handler);signal(SIGCHLD, SIG_IGN);// 2. 不要让自己成为组长if (fork() > 0)exit(0); // 将父进程退出,那么此时运行的就是子进程,其不会成为进程组的组长// 3. 调用setsid,该函数能够创建会话并设置进程组idsetsid(); // pid_t setsid(void);// 4. 标准输入,标准输出,标准错误的重定向int devnull_fd = open("/dev/null", O_RDONLY | O_WRONLY); // int open(const char *pathname, int flags);if(devnull_fd > 0)//文件打开成功{dup2(devnull_fd, 0);dup2(devnull_fd, 1);dup2(devnull_fd, 2);close(devnull_fd);}}

  注意这里dup2的使用:
在这里插入图片描述
  
  
  3)、结果演示

在这里插入图片描述
在这里插入图片描述

  
  
  

5.3.1.4、log.hpp

  说明:一旦服务端成为了守护进程,那么其相关的日志信息就不能直接向显示器打印,因此日志需要向文件中写入。我们可定期观察日志文件的内容,以此检查服务端运行情况。

#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./calculator.log"// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOWif(level== DEBUG) return;
#endif//日志的标准部分:日志等级、时间char stdBuffer[1024]; time_t timestamp = time(nullptr);snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);//日志的自定义部分:用户自定义输入的内容char logBuffer[1024]; va_list args;va_start(args, format);// vprintf(format, args);vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args); 将日志写到显示器上//printf("%s%s\n", stdBuffer, logBuffer);// 将日志写到文件中:FILE *fp = fopen(LOGFILE, "a");fprintf(fp, "%s%s\n", stdBuffer, logBuffer);fclose(fp);
}

  
  关于log:可根据自己的需求做调整,这里只是简单的使用演示。(实际我们对CalCilent.cc和CalServer.cc都使用了同一个log.hpp,为了方便日志观察可分别建立两个不同的log,这里只是简单举例log在这种场景编写下的作用)
在这里插入图片描述
  
  
  
  
  
  

5.3.2、使用json完成序列化

在这里插入图片描述
  

5.3.2.1、基本使用介绍

  1)、安装与编译
  JsonCpp是一款开源的C++库,专用于解析和生成JSON格式的数据。相关文档:JsonCpp。

  安装指令:sudo yum install jsoncpp-devel
在这里插入图片描述
  
  
  
  2)、jsoncpp中主要的类
  Json::Value:可以表示所有支持的类型,如:int , double ,string , object, array等.

  Json::Reader:将文件流或字符串创解析到Json::Value中,主要使用parse函数。Json::Reader的构造函数还允许用户使用特性Features来自定义Json的严格等级。

  Json::Writer:与Json ::Reader相反,将Json::Value转换成字符串流等,Writer类是一个纯虚类,并不能直接使用。在此我们使用 Json::Writer 的子类:Json::FastWriter(将数据写入一行,没有格式),Json::StyledWriter(按json格式化输出,易于阅读)。
  
  
  3)、相关使用演示
  验证代码:

#include <string>
#include <iostream>
#include <jsoncpp/json/json.h>int main()
{// 假设有三个变量:int a = 19;double b = 3.14;char c = '*';// 使用json存储:Json::Value root; // 定义一个万能对象:Json::Value,用来表示Json中的任何一种value抽象数据类型root["a"] = a;root["b"] = b;root["c"] = c;Json::Value sub;sub["s1"] = "hello";sub["s2"] = "json";root["sub"] = sub; // 对象中放入对象(套娃使用// 序列化:Json::StyledWriter writer1;std::string str1 = writer1.write(root);Json::FastWriter writer2;std::string str2 = writer2.write(root);// 两种方式的结果演示:前者会做一些字段分隔处理,方便查看。一般直接使用可用后者。std::cout << str1 << std::endl;printf("\n\n");std::cout << str2 << std::endl;// 反序列化:Json::Value buffer1;Json::Value buffer2;Json::Reader reader;reader.parse(str1, buffer1);reader.parse(str1, buffer2);std::cout << buffer1["a"].asInt() << std::endl;std::cout << buffer1["b"].asDouble() << std::endl;std::cout << buffer1["c"].asInt() << std::endl;return 0;
}

  结果演示:
在这里插入图片描述

  
  
  
  

5.3.3、改动NetCal

  基于上述,我们对之前的协议做出修改,使用json来完成序列化和反序列化。

5.3.3.1、主要部分

  对请求:使用json版本的序列化和反序列化如下:

    class Request{public:// 构造Request(){};Request(int x, int y, char op): x_(x), y_(y), op_(op){}// 对请求进行序列化(结构化数据→字节流数据)std::string Serialize() // {// 序列化Json::Value root; // 定义一个万能对象,将需要的键值对存入root["x"] = x_;root["y"] = y_;root["op"] = op_;Json::FastWriter writer; // 进行序列化并将结果返回return writer.write(root);}// 对请求进行反序列化(字节流数据→结构化数据)bool Deserialized(const std::string &str) {// 反序列化Json::Value root;Json::Reader reader;reader.parse(str, root); // 对str进行反序列化,将结果取出x_ = root["x"].asInt();y_ = root["y"].asInt();op_ = root["op"].asInt();return true;}public:int x_;   // 左运算数int y_;   // 右运算数char op_; // 运算符};

  
  
  对响应:使用json版的序列化和反序列化如下。

    class Response{public:// 构造函数Response(int result, int code): result_(result), code_(code){}Response() {}// 析构函数~Response() {}// 对响应序列化(结构化数据→字节流数据)std::string Serialize(){// 序列化Json::Value root;root["code"] = code_;root["result"] = result_;Json::FastWriter writer;return writer.write(root); // 将序列化后的结果返回}// 对响应反序列化(字节流数据→结构化数据)bool Deserialized(const std::string &str){// 反序列化Json::Value root;Json::Reader reader;reader.parse(str, root);code_ = root["code"].asInt();result_ = root["result"].asInt();return true;}public:int result_; // 计算结果int code_;   // 状态码:用于判断结果是否正常};

  
  
  演示结果如下:
在这里插入图片描述

  
  
  

5.3.3.2、整体Protocol.hpp

  以下是完整的代码实现:方便观察总览。

#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>// 这里的协议是用于服务网络版计算器的
namespace ns_protocol
{// t控制条件编译:自定义方案 && 现成方案
// #define MYSELF 1// 处理分隔符:这里使用的是空格,定义成宏方便根据需求修改
#define SPACE " "
#define SPACE_LINE strlen(SPACE)
// 加入数据长度,并使用特殊字符(\r\n)区分各段
#define SEP "\r\n"
#define SEP_LINE strlen(SEP)/// 请求:结构体对象 ///class Request{public:// 构造Request(){};Request(int x, int y, char op): x_(x), y_(y), op_(op){}// 对请求进行序列化(结构化数据→字节流数据)std::string Serialize() // 将x_、y_、op_{
#ifdef MYSELF// version1: "x_[空格] op_[空格] y_"std::string str;str = std::to_string(x_); // 先将对应的运算数转换为字符类型:例如32-->"32"。这里注意与ASCII中值为32的字符区别str += SPACE;             // 中间以我们设置的间隔符分割(为了反序列化时能够提取每部分)str += op_;               // op_本身就是char类型str += SPACE;str += std::to_string(y_);return str;
#else// 序列化Json::Value root; // 定义一个万能对象,将需要的键值对存入root["x"] = x_;root["y"] = y_;root["op"] = op_;Json::FastWriter writer; // 进行序列化并将结果返回return writer.write(root);
#endif}// 对请求进行反序列化(字节流数据→结构化数据)bool Deserialized(const std::string &str) // 获取x_、y_、op_{
#ifdef MYSELF//----------------------------------// version1: "x_[空格] op_[空格] y_" 根据分隔符提取有效数放入结构化对象中// 例如:"1234[空格]+[空格]5678"// a、找左运算数std::size_t left_oper = str.find(SPACE);if (left_oper == std::string::npos) // 没找到return false;// b、找右运算数std::size_t right_oper = str.rfind(SPACE);if (right_oper == std::string::npos) // 没找到return false;// c、提取运算数,赋值给结构化对象成员x_ = atoi((str.substr(0, left_oper)).c_str());            // string substr (size_t pos = 0, size_t len = npos) const;y_ = atoi((str.substr(right_oper + SPACE_LINE).c_str())); // 注意这里右运算符需要将[空格]跳过if (left_oper + SPACE_LINE > str.size())return false;elseop_ = str[left_oper + SPACE_LINE]; // 提取运算符时也要注意跳过分隔符[空格]return true;//----------------------------------
#else// 反序列化Json::Value root;Json::Reader reader;reader.parse(str, root); // 对str进行反序列化,将结果取出x_ = root["x"].asInt();y_ = root["y"].asInt();op_ = root["op"].asInt();return true;
#endif}public:int x_;   // 左运算数int y_;   // 右运算数char op_; // 运算符};/// 响应:结构体对象 ///class Response{public:// 构造函数Response(int result, int code): result_(result), code_(code){}Response() {}// 析构函数~Response() {}// 对响应序列化(结构化数据→字节流数据)std::string Serialize(){
#ifdef MYSELF// version1:"code_ [空格] result_"// 例如:"0[空格]6912"std::string str;str = std::to_string(code_);str += SPACE;str += std::to_string(result_);return str;
#else// 序列化Json::Value root;root["code"] = code_;root["result"] = result_;Json::FastWriter writer;return writer.write(root); // 将序列化后的结果返回
#endif}// 对响应反序列化(字节流数据→结构化数据)bool Deserialized(const std::string &str){
#ifdef MYSELF//----------------------------------// version1:"code_ [空格] result_"// 例如:"0[空格]6912"// a、找分隔符std::size_t pos = str.find(SPACE);if (pos == std::string::npos) // 没找到return false;// b、获取状态码code_ = atoi((str.substr(0, pos)).c_str());// c、获取计算结果result_ = atoi((str.substr(pos + SPACE_LINE)).c_str());return true;//----------------------------------
#else// 反序列化Json::Value root;Json::Reader reader;reader.parse(str, root);code_ = root["code"].asInt();result_ = root["result"].asInt();return true;
#endif}public:int result_; // 计算结果int code_;   // 状态码:用于判断结果是否正常};// 从网络中读取bool Recv(int sock, std::string &out_buffer){char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 阻塞式从网络中读取字节流数据if (s > 0)                                             // 读取成功{buffer[s] = 0;        // 文件尾字符\0out_buffer += buffer; // 使用+=,输出到out_buffer中,这样在多次读取的情况下能保证数据连续性}else if (s == 0){std::cout << "recv:quit." << std::endl;return false;}else{std::cout << "recv error." << std::endl;return false;}return true;}// 向网络中发送void Send(int sock, const std::string &str){ssize_t s = send(sock, str.c_str(), str.size(), 0);if (s < 0){std::cout << "send error" << std::endl;}}// length\r\nx_[空格]op_[空格]y_\r\n// 实际正文部分:x_[空格]op_[空格]y_,后续需要对正文部分序列化std::string Decode(std::string &buffer){// 找有没有\r\nstd::size_t pos = buffer.find(SEP);if (pos == std::string::npos) // 没找到,说明本次读取/接收到的报文不完整,需要继续读取/接收return "";// 执行到此,说明确实有\r\n,但不代表数据完整。此时需要提取length值,与实际正文做比较,判断是否读取到一个完整的报文int length_size = atoi(buffer.substr(0, pos).c_str()); // 获取长度信息int remain_size = buffer.size() - pos - 2 * SEP_LINE;  // 获取剩余长度if (remain_size >= length_size)                        // 说明此时缓冲区buffer中存在一个完整的报文,可以提取。{// 举例:【length\r\nXXXXXXXXX\r\nlength\r\nXXXXXX\r\n】// string& erase (size_t pos = 0, size_t len = npos);buffer.erase(0, pos + SEP_LINE);                 // 移除缓冲区中的length\r\n,即【XXXXXXXXX\r\nlength\r\nXXXXXX\r\n 】std::string str = buffer.substr(0, length_size); // 获取length长度的字串:即【XXXXXXXXX】buffer.erase(0, length_size + SEP_LINE);         // 移除缓冲区中正文及尾随的\r\n,即【length\r\nXXXXXX\r\n 】return str;}elsereturn ""; // 说明本次读写缓冲区中报文不完整,需要继续读写。}// 返回一个带有长度信息的完整报头:实际正文部分为 x_[空格]op_[空格]y_,需要为其添加长度信息,变为length\r\nx_[空格]op_[空格]y_\r\n// 例如:123 * 456 --->9\r\n123 * 456\r\nstd::string Encode(std::string &s){// 1、获取正文长度std::string package = std::to_string(s.size());// 2、加上SEP分隔符package += SEP;// 3、加上正文package += s;// 4、加上SEP分隔符package += SEP;return package;}}

  
  
  
  
  
  
  
  
  
  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/137209.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

竞赛选题 深度学习疲劳驾驶检测 opencv python

文章目录 0 前言1 课题背景2 实现目标3 当前市面上疲劳驾驶检测的方法4 相关数据集5 基于头部姿态的驾驶疲劳检测5.1 如何确定疲劳状态5.2 算法步骤5.3 打瞌睡判断 6 基于CNN与SVM的疲劳检测方法6.1 网络结构6.2 疲劳图像分类训练6.3 训练结果 7 最后 0 前言 &#x1f525; 优…

NSS [鹏城杯 2022]压缩包

NSS [鹏城杯 2022]压缩包 考点&#xff1a;条件竞争/逻辑漏洞&#xff08;解压失败不删除已经解压文件&#xff09; 参考&#xff1a;回忆phpcms头像上传漏洞以及后续影响 | 离别歌 (leavesongs.com) 源码有点小多 <?php highlight_file(__FILE__);function removedir($…

不想努力了,有没有不用努力就能考上硕士的方法

今年&#xff0c;硕士研究生考试报考人数再次刷新了纪录&#xff0c;高达474万人次。 这些年考研一直在扩招&#xff0c;但是录取率却越来越低&#xff0c;内卷血腥程度可想而知&#xff01; 2020年研究生报考人数341万&#xff0c;录取人数99.05万&#xff0c;录取率29.05%。…

物联网水表有什么弊端吗?

物联网水表作为新一代智能水表&#xff0c;虽然在很大程度上提高了水资源的管理效率&#xff0c;但也存在一定的弊端。在这篇文章中&#xff0c;我们将详细讨论物联网水表的弊端&#xff0c;以帮助大家更全面地了解这一技术。 一、安全隐患 1.数据泄露&#xff1a;物联网水表通…

Zabbix深入解析与实战

1.Zabbix 1.1.监控概述 监控是指对行为、活动或其他变动中信息的一种持续性关注&#xff0c;通常是为了对人达成影响、管理、指导或保护的目的 监控 监视主机架构状态控制&#xff0c;事后追责目标&#xff1a;早发现早处理(故障、性能、架构) 网站扩容(用数据说话) 为什么要…

基于SSM的社区生鲜电商平台

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

Google Analytics优缺点分析

Google Analytics是一款广泛使用的网站分析工具&#xff0c;它能够帮助网站和应用程序的拥有者收集和分析数据&#xff0c;并提供有关用户行为、流量来源和转化率等方面的详细见解。本文小编将讲讲关于Google Analytics的优缺点分析。 一、Google Analytics优点 1、免费使用&a…

万宾科技内涝积水监测仪使用效果一览

当一个城市突降暴雨&#xff0c;对城市管理部门来讲首当其中的是防止积水成患。随着城市人口快速增长&#xff0c;基础设施建设也日益受到更多的关注&#xff0c;城市内涝问题频繁增加&#xff0c;会给城市带来严重的经济损失和人员的安全问题。城市生命线工程建设过程中&#…

线程池创建、执行、销毁的原理解析

目录 线程池的执行原理线程执行参考&#xff1a; 线程池的执行原理 假设最大核心数是2&#xff0c;非核心线程数为1&#xff0c;队列长度是3 来第一个任务的时候&#xff0c;没有工作线程在工作&#xff0c;需要创建一个 来第二个任务的时候&#xff0c;发现当前核心线程数…

数字滤波器分析---零极点分析

数字滤波器分析---零极点分析 zplane 函数绘制线性系统的极点和零点。 例如&#xff0c;在 -1/2 处为零且在 0.9e−j2π0.3 和 0.9ej2π0.3 处有一对复极点的简单滤波器为 zer -0.5; pol 0.9*exp(j*2*pi*[-0.3 0.3]); 要查看该滤波器的零极点图&#xff0c;您可以使用 z…

Simulia仿真之CST的使用指南(1) | 百世慧®

前言 CST是Computer Simulation Technology的缩写,是全球范围内众多工程师都喜欢使用的高端电磁仿真分析软件。 本文又名《智慧的CST生活》这里一共列出了120个章节&#xff0c;其中包括CST使用过程中的有用小技巧&#xff0c;还有给用户提供技术支持的过程中常见的问题。本专…

金蝶云星空表单插件实现父窗体打开子窗体,并携带参数到子窗体

文章目录 金蝶云星空表单插件实现父窗体打开子窗体&#xff0c;并携带参数到子窗体父窗体打开子窗体准备设置携带参数打开子窗体子窗体接收参数 金蝶云星空表单插件实现父窗体打开子窗体&#xff0c;并携带参数到子窗体 父窗体打开子窗体准备 BillShowParameter OtherInAdd n…

TDD、BDD、ATDD以及SBE的概念和区别

在软件开发或是软件测试中会遇到以下这些词&#xff1a;TDD 、BDD 、ATDD以及SBE&#xff0c;这些词代表什么意思呢&#xff1f; 它们之间有什么关系吗&#xff1f; TDD 、BDD 、ATDD以及SBE的基本概念 TDD&#xff1a;&#xff08;Test Driven Development&#xff09;是一种…

Git 入门使用

一、Git 入门 1.1 Git简介 Git是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或小或大的项目。Git是由Linus Torvalds为了帮助管理Linux内核开发而开发的一个开放源码的版本控制软件。 Git是目前世界上最先进的分布式版本控制系统&#xff0c;没有之一&a…

行情分析——加密货币市场大盘走势(11.10)

大饼今日继续上涨&#xff0c;正如预期&#xff0c;跌不下来&#xff0c;思路就是逢低做多。现在已经上涨到36500附近&#xff0c;目前从MACD日线来看&#xff0c;后续还要继续上涨&#xff0c;当然稳健的可以不做。昨日的策略已经达到止盈&#xff0c;也是顺利的落袋为安啦。一…

黑洞路由的几种应用场景

第一种在内网中产生环路&#xff1a; 这种核心交换机上肯定写一条默认路由 0.0.0.0 0 10.0.0.1 出口路由要写一条192.168.0.0 16 10.0.0.2 如果出口路由访问一条不存在的内网网段&#xff0c;又或者访问的那台终端停机了&#xff0c;那就会产生三层环路&#xff0c;数据包在…

Sentinel网关限流

背景 在微服务架构下&#xff0c;每个服务的性能都不同&#xff0c;为避免出现流量洪峰将服务冲垮&#xff0c;需要依赖限流工具来保护服务的稳定性。sentinel是阿里提供的限流工具&#xff0c;社区活跃&#xff0c;功能也很全面&#xff0c;包含实时监控、流控、熔断等功能。…

实现智慧工地的高效建筑管理,数据分析起着关键作用!

智慧工地是利用物联网、云计算、大数据等技术&#xff0c;实现对建筑工地实时监测、管理和控制的一种新型建筑管理方式。 智慧工地架构&#xff1a; 1、终端层&#xff1a;充分利用物联网技术、移动应用、智能硬件设备提高现场管控能力。通过RFID、传感器、摄像头、手机等终端…

很多个pdf怎么合并在一起?

很多个pdf怎么合并在一起&#xff1f;作为一个办公室的伙伴&#xff0c;对于PDF格式肯定不会陌生。它强大的功能为我们的工作提供了许多便利。由于PDF文件格式的稳定性和安全性较高&#xff0c;我们通常在工作或学习中使用它来传输文件&#xff0c;很多人都喜欢将办公文件都做成…

NVM安装node后提示没有对应npm包(即:无法将“npm”项识别为 cmdlet、函数、脚本文件)

背景 windows11 node版本降低到v12.22.12后&#xff0c;执行&#xff1a;nvm -v npm -v npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果 包括路径&#xff0c;请确保路径正确&#xff0c;然后再试一次。 所在位置 …