【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   4

const 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 == nullptr

    printf("%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;//IP
    int _listensock;//套接字
};

  
  

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

4.1.2.1、socket、bind

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

NAME
       socket - create an endpoint for communication

SYNOPSIS
       #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 defined
       types 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:

NAME
       bind - bind a name to a socket

SYNOPSIS
       #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_STREAM
        if (_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个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略,这里设置不会太大。

NAME
       listen - listen for connections on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int listen(int sockfd, int backlog);

DESCRIPTION
       listen() marks the socket referred to by sockfd as a passive socket, that is, as a
       socket 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  type
       SOCK_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  is
       full,  the  client  may receive an error with an indication of ECONNREFUSED or, if
       the underlying protocol supports retransmission, the request  may  be  ignored  so
       that a later reattempt at connection succeeds.

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

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

  返回值:

RETURN VALUE
       On  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()接受连接,如果此时时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

NAME
       accept, accept4 - accept a connection on a socket

SYNOPSIS
       #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);

DESCRIPTION
       The  accept()  system  call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET).  It extracts the
       first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected
       socket,  and returns a new file descriptor referring to that socket.  The newly created socket is not in the listening
       state.  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码是0
            std::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_STREAM
        if (_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、获取客户端端口号、IP
            uint16_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;//IP
    int _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、获取客户端端口号、IP
            uint16_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接收连接,但客户端需要有链接别人的能力,可以通过此函数达成。

NAME
       connect - initiate a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

DESCRIPTION
       The  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  of
       the 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 only
       address from which datagrams are received.  If the socket is of type SOCK_STREAM or SOCK_SEQPACKET, this  call  attempts  to
       make 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 use
       connect() multiple times to change their association.  Connectionless sockets may dissolve the association by connecting  to
       an address with the sa_family member of sockaddr set to AF_UNSPEC (supported on Linux since kernel 2.2).

RETURN VALUE
       If 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、获取服务端端口号、IP
    std::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、获取客户端端口号、IP
            uint16_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、获取客户端端口号、IP
            uint16_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码是0
            std::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_STREAM
        if (_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、获取客户端端口号、IP
            uint16_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;                                // IP
    int _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;                                                 // 客户端IP
                int servicesock = sock_.Accept(listensock_, &client_ip, &client_port); // 输出型参数,调用accept函数获取客户端的端口号和IP
                if (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;
        else
            resp.result_ = req.x_ / req.y_;
        break;
    case '%':
        if (req.y_ == 0) // 模零错误,需要设置状态码
            resp.code_ = 2;
        else
            resp.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;
#else
			std::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;
            else
                op_ = str[left_oper + SPACE_LINE]; // 提取运算符时也要注意跳过分隔符[空格]
            return true;
            //----------------------------------
#else
			std::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;
#else
			std::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;
            //----------------------------------
#else
			std::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;        // 文件尾字符\0
            out_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\n
        std::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;
        }
        else
            return ""; // 说明本次读写缓冲区中报文不完整,需要继续读写。
    }

    // 返回一个带有长度信息的完整报头:实际正文部分为 x_[空格]op_[空格]y_,需要为其添加长度信息,变为length\r\nx_[空格]op_[空格]y_\r\n
    // 例如:123 * 456 --->9\r\n123 * 456\r\n
    std::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; // 退出循环,关闭sockfd
                break;
            }
            // 对响应解析:是否获取到完整报文
            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

NAME
       setsid - creates a session and sets the process group ID

SYNOPSIS
       #include <unistd.h>

       pid_t setsid(void);

DESCRIPTION
       setsid()  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  the
       calling process are set to the PID of the calling process.  The calling process will be the only process in this new process group and in
       this new session.

RETURN VALUE
       On success, the (new) session ID of the calling process is returned.  On error, (pid_t) -1 is returned, and errno is set to indicate  the
       error.

ERRORS
       EPERM  The  process  group  ID  of any process equals the PID of the calling process.  Thus, in particular, setsid() fails if the calling
              process is already a process group leader.

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

NAME
       daemon - run in the background

SYNOPSIS
       #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)

DESCRIPTION
       The 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  current
       working 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 to
       these 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.)   On
       success daemon() returns zero.  If an error occurs, daemon() returns -1 and sets errno to any of the errors specified for the fork(2) and
       setsid(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,SIGCHLD
    signal(SIGPIPE, SIG_IGN); // sighandler_t signal(int signum, sighandler_t handler);
    signal(SIGCHLD, SIG_IGN);

    // 2. 不要让自己成为组长
    if (fork() > 0)
        exit(0); // 将父进程退出,那么此时运行的就是子进程,其不会成为进程组的组长

    // 3. 调用setsid,该函数能够创建会话并设置进程组id
    setsid(); // 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   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./calculator.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, 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_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;
            else
                op_ = 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;        // 文件尾字符\0
            out_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\n
        std::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;
        }
        else
            return ""; // 说明本次读写缓冲区中报文不完整,需要继续读写。
    }

    // 返回一个带有长度信息的完整报头:实际正文部分为 x_[空格]op_[空格]y_,需要为其添加长度信息,变为length\r\nx_[空格]op_[空格]y_\r\n
    // 例如:123 * 456 --->9\r\n123 * 456\r\n
    std::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;
    }

}

  
  
  
  
  
  
  
  
  
  

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++中将数据添加到文件的末尾

参考:https://blog.csdn.net/qq_23880193/article/details/44279283 C中文件的读取需要包含fstream文件&#xff0c;即&#xff1a;#include 文件的读取和写入是是通过流操作来的&#xff0c;这不像输入、输出流那样&#xff0c;库中已经定义了对象cin和cout 文件的读取需要声…

【 毕设项目源码推荐 javaweb 项目】 基于 springboot+vue 的图书个性化推荐系统的设计与实现(springboot003)

简介 :::warning 【 毕设项目源码推荐 javaweb 项目】 基于 springbootvue 的图书个性化推荐系统的设计与实现适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负…

Etcd 常用命令与备份恢复

1. etcd简介 官方网站&#xff1a;etcd.io 官方文档&#xff1a;etcd.io/docs/v3.5/op-guide/maintenance 官方硬件推荐&#xff1a;etcd.io/docs/v3.5/op-guide/hardware github地址&#xff1a;github.com/etcd-io/etcd etcd是CoreOS团队于2013年6月发起的开源项目&#xf…

算法--数据结构

这里写目录标题 本节内容链表与邻接表链表主要思想链表操作初始化在head结点后面插入普通插入删除操作 例子 双链表&#xff08;双向循环链表&#xff09;主要思想操作初始化双向插入删除第k个点 邻接表主要思想 栈和队列栈主要思想主要操作 队列主要思想操作 单调栈与单调队列…

Kafka中遇到的错误:

1、原因&#xff1a;kafka是一个去中心化结果的&#xff0c;所以在启动Kafka的时候&#xff0c;每一个节点上都需要启动。 启动的命令&#xff1a;kafka-server-start.sh -daemon /usr/local/soft/kafka_2.11-1.0.0/config/server.properties

【凡人修仙传】预计开播倒计时,线下举办超前观影活动,隆重期待

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析国漫资讯。 深度爆料凡人最新资讯&#xff0c;《凡人修仙传》这部备受期待的动漫作品&#xff0c;终于在新年之际宣布了定档日期。据悉&#xff0c;该动漫将于11月25日&#xff0c;也就是周六上午11点&#xff0c;与广大…

【C++干货铺】STL简述 | string类的使用指南

个人主页点击直达&#xff1a;小白不是程序媛 C系列专栏&#xff1a;C干货铺 代码仓库&#xff1a;Gitee 目录 什么是STL STL的版本 STL的六大组件 STL的缺陷 string类 C语言中的字符串 标准库中的string类 string类常用的接口使用指南 string类中常见的构造 strin…

全国5米高程DEM数据及衍生的坡度数据

坡度是地表单元陡缓的程度&#xff0c;通常把坡面的垂直高度和水平距离的比值称为坡度。坡度的表示方法有百分比法、度数法、密位法和分数法四种&#xff0c;其中以百分比法和度数法较为常用。 坡度是地表单元陡缓的程度&#xff0c;通常把坡面的垂直高度和水平距离的比值称为坡…

Jmeter 性能压测 —— 混合场景

性能测试&#xff0c;单场景的目的一般是为了发现缺陷、发现瓶颈。 完成所有单个重点场景的性能测试之后&#xff0c;还需要做一个混合场景的性能测试-评估系统整体性能。 1、场景设计 使用Jmeter 做混合场景设计 在一个测试计划&#xff0c;将每个重点测试场景各创建为一个…

【PC电脑windows环境下-[jetson-orin-NX]Linux环境下-下载工具esptool工具使用-相关细节-简单样例-实际操作】

【PC电脑windows环境下-[jetson-orin-NX]Linux环境下-下载工具esptool工具使用-相关细节-简单样例-实际操作】 1、概述2、实验环境3、 物品说明4-2、自我总结5、本次实验说明1、准备样例2、设置芯片3、编译4、下载5、验证 &#xff08;1&#xff09;windows环境下进行烧写1、下…

服务器的操作系统,你选择哪些?

OpenCloudOS CentOS CentOS Stream Ubuntu Debian Windows Server

两个int(32位)整数m和n的二进制表达中,求有多少个位(bit)不同?

我先来讲一讲这道题的大概意思&#xff1a;假设我们输入的是 0 和 1: 0的32位二进制中为&#xff1a;000……0(32位) 1的32位二进制中为&#xff1a;000……1(32位) 那么就有一位不同。 方法一&#xff1a; 既然是二进制中&#xff0c;那么就是满二进一&#xff0c;就像…

图解三傻排序 选择排序、冒泡排序、插入排序

&#xff08;1&#xff09;选择排序 // 交换 void swap(int arr[], int i, int j) {int tmp arr[i];arr[i] arr[j];arr[j] tmp; }// 选择排序 void selectionSort(int arr[],int len) {if (len < 2) return;for (int minIndex, i 0; i < len - 1; i) {minIndex i;f…

chrome driver下载、selenium安装及报错解决

目录 一、Chrome驱动下载 1.查看Chrome版本 2.下载驱动 3.驱动的路径 无法运行驱动 二、selenium的安装与使用 1.安装selenium 2.使用selenium 参考 一、Chrome驱动下载 1.查看Chrome版本 打开Chrome浏览器&#xff0c;点击右上角的三个点&#xff0c;再点击设置。 …

模态对话框和非模态对话框

创建到堆区这样非模态对话框就不会一闪而过 .exec使程序进入阻塞状态 ()[]{}lambda表达式 55号属性可以在对话框关闭的时候将堆区的内存释放掉从而防止内存泄露

Redis系列-Redis数据类型【3】

目录 Redis系列-Redis数据类型【3】字符串类型&#xff08;String&#xff09;SDS (simple dynamic string) 哈希类型&#xff08;Hash&#xff09;列表类型&#xff08;List&#xff09;集合类型&#xff08;Set&#xff09;有序集合类型&#xff08;ZSet&#xff09;字符串类…

安装pr提示VCRUNTIME140.dll丢失的修复方法,3个有效的方法

在学习和工作中&#xff0c;我们经常需要使用到PR和PS。然而&#xff0c;在安装这些软件时&#xff0c;有时会遇到一些错误提示&#xff0c;其中之一就是“VCRUNTIME140.dll丢失”&#xff0c;无法运行启动软件程序。那么&#xff0c;如何解决VCRUNTIME140.dll丢失的问题呢&…

累计概率分布、概率分布函数(概率质量函数、概率密度函数)、度量空间、负采样(Negative Sampling)

这里写自定义目录标题 机器学习的基础知识累计概率分布概率分布函数度量空间负采样&#xff08;Negative Sampling&#xff09;基于分布的负采样&#xff08;Distribution-based Negative Sampling&#xff09;&#xff1a;基于近邻的负采样&#xff08;Neighbor-based Negativ…

Rasa NLU中的组件

Rasa NLU部分主要是解决NER&#xff08;序列建模&#xff09;和意图识别&#xff08;分类建模&#xff09;这2个任务。Rasa NLP是一个基于DAG的通用框架&#xff0c;图中的顶点即组件。组件特征包括有顺序关系、可相互替换、可互斥和可同时使用。有向无环图&#xff08;DAG&…

一体化HIS医疗信息管理系统源码:云HIS、云电子病历、云LIS

基于云计算技术的B/S架构的HIS系统&#xff0c;为医疗机构提供标准化的、信息化的、可共享的医疗信息管理系统&#xff0c;实现医患事务管理和临床诊疗管理等标准医疗管理信息系统的功能。系统利用云计算平台的技术优势&#xff0c;建立统一的云HIS、云病历、云LIS&#xff0c;…