【Online Calculator】


If we need to implement a server version of the adder. We need the client to send the two addends to be calculated, and then the server will perform the calculation, and finally return the result to the client

Agreed scheme 1:
1. The client sends a string in the form of "1+1";
2. There are two operands in this string, both of which are integers;
3. There will be a character between the two numbers for operation operator, the operator can only be +;
4. There is no space between the number and the operator;

Protocol 2:
1. Define a structure to represent the information we need to interact with;
2. When sending data, convert this structure into a string according to a rule, and convert the string back into a structure according to the same rule when receiving data 3.
This process is called "serialization" and "deserialization


Convert structured data into strings, also called byte streams, and then push them to the server for reading

insert image description here

No matter we adopt scheme 1, scheme 2, or other schemes, as long as we ensure that the data constructed at one end can be correctly parsed at the other end, it is ok. This kind of agreement is the application layer protocol

  • Server initialization work

insert image description here

  • start server
    insert image description here

TcpServer.hpp

The ThreadRoutine function is a thread function used to process client requests in a multi-threaded server program.
TcpServer function: a constructor that creates a TCP server object and specifies the port and IP address to be monitored.
BindService(func_t func) function: Add a client request processing function to the TcpServer object, save it in the func_ container, so as to execute the processing function corresponding to the client request later.
Excute(int sock): Execute the processing function corresponding to the client request. This function receives a socket descriptor as a parameter, traverses each processing function in the func_ container, and passes the socket descriptor to these processing functions in turn for processing.
Start function: start an infinite loop, and constantly monitor the client's connection request. Then create a new thread to handle each client request, and support adding multiple functions to handle client requests.

#pragma once

#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>

namespace ns_tcpserver
{
    
    
    using func_t = std::function<void(int)>;

    class TcpServer;

    class ThreadData
    {
    
    
    public:
        ThreadData(int sock, TcpServer *server):sock_(sock), server_(server)
        {
    
    }
        ~ThreadData() {
    
    }
    public:
        int sock_;
        TcpServer *server_;
    };

    class TcpServer
    {
    
    
    private:
        static void *ThreadRoutine(void *args)
        {
    
    
            pthread_detach(pthread_self());
            ThreadData *td = static_cast<ThreadData *>(args);
            td->server_->Excute(td->sock_);
            close(td->sock_);
            // delete td;
            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_);
        }
        void BindService(func_t func) 
        {
    
     
            func_.push_back(func);
        }
        void Excute(int sock)
        {
    
    
            for(auto &f : func_)
            {
    
    
                f(sock);
            }
        }
        void Start()
        {
    
    
            for (;;)
            {
    
    
                std::string clientip;
                uint16_t clientport;
                int sock = sock_.Accept(listensock_, &clientip, &clientport);
                if (sock == -1)
                    continue;
                logMessage(NORMAL, "create new link success, sock: %d", sock);
                pthread_t tid;
                ThreadData *td = new ThreadData(sock, this);
                pthread_create(&tid, nullptr, ThreadRoutine, td);
            }
        }
        ~TcpServer()
        {
    
    
            if (listensock_ >= 0)
                close(listensock_);
        }

    private:
        int listensock_;
        Sock sock_;
        std::vector<func_t> func_;
        // std::unordered_map<std::string, func_t> func_;
    };
}

Protocol.hpp

  • A simple protocol is implemented for transporting computation requests and responses. It contains two classes Request and Response, which are used to represent requests and responses respectively.
  • In the Request class, there is a member function Serialize(), which is used to serialize the request object into a string. In this function, the Json library is used to convert the request object into a string in Json format.
  • There is also a member function Deserialized() for deserializing a string into a request object. Similarly, the Json library is used to convert the string in Json format into a request object
  • In the Response class, there are also two member functions, Serialize() and Deserialized(), which are used to serialize and deserialize the response object into a string. Here, the response object is also converted into a string in Json format.
  • In addition, there are some auxiliary functions, which are used to convert the string into a message in the specified format, and parse the received message into a complete request or response. These functions include the Decode() and Encode() functions, which are used to convert a string into a message in a specified format, and the Recv() and Send() functions, which are used to receive and send a message.
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <jsoncpp/json/json.h>

namespace ns_protocol
{
    
    
// #define MYSELF 0

#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof!

    class Request
    {
    
    
    public:
        // 1. 自主实现 "length\r\nx_ op_ y_\r\n"
        // 2. 使用现成的方案
        std::string Serialize()
        {
    
    
#ifdef MYSELF
            std::string str;
            str = std::to_string(x_);
            str += SPACE;
            str += op_; // TODO
            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
        }
        // "x_ op_ y_"
        // "1234 + 5678"
        bool Deserialized(const std::string &str)
        {
    
    
#ifdef MYSELF
            std::size_t left = str.find(SPACE);
            if (left == std::string::npos)
                return false;
            std::size_t right = str.rfind(SPACE);
            if (right == std::string::npos)
                return false;
            x_ = atoi(str.substr(0, left).c_str());
            y_ = atoi(str.substr(right + SPACE_LEN).c_str());
            if (left + SPACE_LEN > str.size())
                return false;
            else
                op_ = str[left + SPACE_LEN];
            return true;
#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(str, root);
            x_ = root["x"].asInt();
            y_ = root["y"].asInt();
            op_ = root["op"].asInt();
            return true;
#endif
        }

    public:
        Request()
        {
    
    
        }
        Request(int x, int y, char op) : x_(x), y_(y), op_(op)
        {
    
    
        }
        ~Request() {
    
    }

    public:
        // 约定
        // x_ op y_ ? y_ op x_?
        int x_;   // 是什么?
        int y_;   // 是什么?
        char op_; // '+' '-' '*' '/' '%'
    };

    class Response
    {
    
    
    public:
        // "code_ result_"
        std::string Serialize()
        {
    
    
#ifdef MYSELF
            std::string s;
            s = std::to_string(code_);
            s += SPACE;
            s += std::to_string(result_);

            return s;
#else
            Json::Value root;
            root["code"] = code_;
            root["result"] = result_;
            root["xx"] = x_;
            root["yy"] = y_;
            root["zz"] = op_;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        // "111 100"
        bool Deserialized(const std::string &s)
        {
    
    
#ifdef MYSELF
            std::size_t pos = s.find(SPACE);
            if (pos == std::string::npos)
                return false;
            code_ = atoi(s.substr(0, pos).c_str());
            result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
            return true;
#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(s, root);
            code_ = root["code"].asInt();
            result_ = root["result"].asInt();
            x_ =  root["xx"].asInt();
            y_ =  root["yy"].asInt();
            op_ =  root["zz"].asInt();
            return true;
#endif
        }

    public:
        Response()
        {
    
    
        }
        Response(int result, int code, int x, int y, char op) 
        : result_(result), code_(code), x_(x), y_(y), op_(op)
        {
    
    
        }
        ~Response() {
    
    }

    public:
        // 约定!
        // result_? code_? code_ 0? 1?2?3?
        int result_; // 计算结果
        int code_;   // 计算结果的状态码

        int x_;
        int y_;
        char op_;
    };

    // 临时方案
    // 调整方案2: 我们期望,你必须给我返回一个完整的报文
    bool Recv(int sock, std::string *out)
    {
    
    
        // UDP是面向数据报:
        // TCP 面向字节流的:
        // recv : 你怎么保证,你读到的inbuffer,是一个完整完善的请求呢?不能保证
        // "1234 + 5678" : 1234 +
        // "1234 + 5678" : 1234 + 5678 123+99
        // "1234 "
        // 必须是:"1234 + 5678"
        // 单纯的recv是无法解决这个问题的,需要对协议进一步定制!
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0); // 9\r\n123+789\r\n
        if (s > 0)
        {
    
    
            buffer[s] = 0;
            *out += buffer;
        }
        else if (s == 0)
        {
    
    
            // std::cout << "client quit" << std::endl;
            return false;
        }
        else
        {
    
    
            // std::cout << "recv error" << std::endl;
            return false;
        }
        return true;
    }

    void Send(int sock, const std::string str)
    {
    
    
        // std::cout << "sent in" << std::endl;
        int n = send(sock, str.c_str(), str.size(), 0);
        if (n < 0)
            std::cout << "send error" << std::endl;
    }
    // "length\r\nx_ op_ y_\r\n..." // 10\r\nabc
    // "x_ op_ y_\r\n length\r\nXXX\r\n"
    std::string Decode(std::string &buffer)
    {
    
    
        std::size_t pos = buffer.find(SEP);
        if(pos == std::string::npos) return "";
        int size = atoi(buffer.substr(0, pos).c_str());
        int surplus = buffer.size() - pos - 2*SEP_LEN;
        if(surplus >= size)
        {
    
    
            //至少具有一个合法完整的报文, 可以动手提取了
            buffer.erase(0, pos+SEP_LEN);
            std::string s = buffer.substr(0, size);
            buffer.erase(0, size + SEP_LEN);
            return s;
        }
        else
        {
    
    
            return "";
        }
    }
    // "XXXXXX"
    // "123\r\nXXXXXX\r\n"
    std::string Encode(std::string &s)
    {
    
    
        std::string new_package = std::to_string(s.size());
        new_package += SEP;
        new_package += s;
        new_package += SEP;
        return new_package;
    }

}

CalServer.cc server side

  • This code implements a simple calculation server. In the main function, first judge whether the number of input parameters is correct, if not, output instructions for use and exit the program.
  • Next, a TcpServer object is created and bound to a function called calculator. This function is used to process the received calculation request and return the calculation result. The Start() function of the TcpServer object is used to start the server.
  • In the calculator function, first read the request sent by the client, and perform protocol analysis to ensure that a complete message is obtained. Then, deserialize the message into a request object and perform business logic processing. Finally, serialize the calculation result and add length information to form a complete message and send it to the client.
  • There are also some auxiliary functions in the code, for example, the Usage() function is used to output usage instructions, the calculatorHelper() function is used for auxiliary calculations, and the Decode() and Encode() functions are used to convert strings into messages in the specified format.
  • In addition, the MyDaemon() function is also called in the main function to convert the program into a daemon process to prevent the program from being accidentally terminated when it is running in the background.
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Daemon.hpp"
#include <memory>
#include <signal.h>

using namespace ns_tcpserver;
using namespace ns_protocol;

static void Usage(const std::string &process)
{
    
    
    std::cout << "\nUsage: " << process << " port\n"
              << std::endl;
}

static Response calculatorHelper(const Request &req)
{
    
    
    Response resp(0, 0, req.x_, req.y_, req.op_);
    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 (0 == req.y_)
            resp.code_ = 1;
        else
            resp.result_ = req.x_ / req.y_;
        break;
    case '%':
        if (0 == req.y_)
            resp.code_ = 2;
        else
            resp.result_ = req.x_ % req.y_;
        break;
    default:
        resp.code_ = 3;
        break;
    }
    return resp;
}

void calculator(int sock)
{
    
    
    std::string inbuffer;
    while (true)
    {
    
    
        // 1. 读取成功
        bool res = Recv(sock, &inbuffer); // 在这里我们读到了一个请求?
        if (!res)
            break;
        // std::cout << "begin: inbuffer: " << inbuffer << std::endl;
        // 2. 协议解析,保证得到一个完整的报文
        std::string package = Decode(inbuffer);
        if (package.empty())
            continue;
        // std::cout << "end: inbuffer: " << inbuffer << std::endl;
        // std::cout << "packge: " << package << std::endl;
        logMessage(NORMAL, "%s", package.c_str());
        // 3. 保证该报文是一个完整的报文
        Request req;
        // 4. 反序列化,字节流 -> 结构化
        req.Deserialized(package); // 反序列化
        // 5. 业务逻辑
        Response resp = calculatorHelper(req);
        // 6. 序列化
        std::string respString = resp.Serialize(); // 对计算结果进行序列化
        // 7. 添加长度信息,形成一个完整的报文
        // "length\r\ncode result\r\n"
        // std::cout << "respString: " << respString << std::endl;
        respString = Encode(respString);
        // std::cout << "encode: respString: " << respString << std::endl;
        // 8. send这里我们暂时先这样写,多路转接的时候,我们再来谈发送的问题
        Send(sock, respString);
    }
}

// void handler(int signo)
// {
    
    
//     std::cout << "get a signo: " << signo << std::endl;
//     exit(0);
// }

// ./CalServer port
int main(int argc, char *argv[])
{
    
    
    if (argc != 2)
    {
    
    
        Usage(argv[0]);
        exit(1);
    }
    // 一般经验:server在编写的时候,要有较为严谨性的判断逻辑
    // 一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法写入的问题!
    // signal(SIGPIPE, SIG_IGN);
    MyDaemon();
    std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
    server->BindService(calculator);
    server->Start();
    // Request req(123, 456, '+');
    // std::string s = req.Serialize();
    // std::cout << s << std::endl;

    // Request temp;
    // temp.Deserialized(s);
    // std::cout << temp.x_ << std::endl;
    // std::cout << temp.op_ << std::endl;
    // std::cout << temp.y_ << std::endl;
    return 0;
}

CalClient.cc client

This code is a client program written in C++, which mainly realizes the communication with the server and implements a simple calculator function. Specifically, the main functions of the code are as follows:

  • Obtain the IP address and port number of the server through command line parameters; Create a socket and connect to the server through the Connect function;
  • Read the calculation requirements input by the user in a loop, serialize them and add the length header and send them to the server;
  • Receive the data returned by the server, judge whether the calculation is successful after decoding, and output the calculation result if successful, otherwise output an error message; close the socket and exit the program.

Among them, the custom Sock class and Protocol namespace are used in the code, and you can see that there are corresponding definitions in other code files.

#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"

using namespace ns_protocol;

static void Usage(const std::string &process)
{
    
    
    std::cout << "\nUsage: " << process << " serverIp serverPort\n"
              << std::endl;
}

// ./client server_ip server_port
int main(int argc, char *argv[])
{
    
    
    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_ip, server_port))
    {
    
    
        std::cerr << "Connect error" << std::endl;
        exit(2);
    }
    bool quit = false;
    std::string buffer;
    while (!quit)
    {
    
    
        // 1. 获取需求
        Request req;
        std::cout << "Please Enter # ";
        std::cin >> req.x_ >> req.op_ >> req.y_;

        // 2. 序列化
        std::string s = req.Serialize();
        // std::string temp = s;
        // 3. 添加长度报头
        s = Encode(s);
        // 4. 发送给服务端
        Send(sockfd, s);

        // 5. 正常读取
        while (true)
        {
    
    
            bool res = Recv(sockfd, &buffer);
            if (!res)
            {
    
    
                quit = true;
                break;
            }
            std::string package = Decode(buffer);
            if (package.empty())
                continue;
            Response resp;
            resp.Deserialized(package);
            std::string err;
            switch (resp.code_)
            {
    
    
            case 1:
                err = "除0错误";
                break;
            case 2:
                err = "模0错误";
                break;
            case 3:
                err = "非法操作";
                break;
            default:
                std::cout << resp.x_ << resp.op_ << resp.y_ << " = " << resp.result_ << " [success]" << std::endl;
                break;
            }
            if(!err.empty()) std::cerr << err << std::endl;
            // sleep(1);
            break;
        }
    }
    close(sockfd);
    return 0;
}

Sock.hpp Some classes related to network programming

This code is a class related to network programming written in C++, named Sock. Its main function is to encapsulate some common network programming functions, such as creating sockets, binding ports, listening for connection requests, accepting client connections, connecting to servers, etc. Specifically, the main functions of the code are as follows:

  • Create a socket and implement the Socket function;
  • Bind the port and implement the Bind function;
  • Listen for connection requests and implement the Listen function;
  • Accept client connections, implement
  • Accept function;
  • Connect to the server and implement the Connect function.

Among them, some constants and private variables are defined in the code, for example, the gbacklog constant indicates the length of the listening queue, and there is only one private variable in the Sokc class, namely the gbacklog
constant. In addition, the custom logMessage function is also used in the code, and you can see that there are corresponding definitions in other code files.

 #pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"

class Sock
{
    
    
private:
    const static int gbacklog = 20;

public:
    Sock() {
    
    }
    int Socket()
    {
    
    
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
    
    
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", listensock);
        return listensock;
    }
    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
    
    
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
    
    
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
    }
    void Listen(int sock)
    {
    
    
        if (listen(sock, gbacklog) < 0)
        {
    
    
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }
    // 一般经验
    // const std::string &: 输入型参数
    // std::string *: 输出型参数
    // std::string &: 输入输出型参数
    int Accept(int listensock, std::string *ip, uint16_t *port)
    {
    
    
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
        if (servicesock < 0)
        {
    
    
            logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            return -1;
        }
        if(port) *port = ntohs(src.sin_port);
        if(ip) *ip = inet_ntoa(src.sin_addr);
        return servicesock;
    }
    bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
    
    
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }
    ~Sock() {
    
    }
};

Daemon.hpp converts the current program into a daemon process

Daemon process: It is a process of its own, and it will not be affected when you log out and log in
insert image description here
insert image description here

This code is a C++ function named MyDaemon. Its main function is to convert the current program into a daemon process, so that the program runs in the background and is out of the control of the terminal. Specifically, the main functions of the code are as follows:

  • Ignore SIGPIPE and SIGCHLD signals to avoid exceptions in network programming;
  • Create a child process, let the parent process exit, and avoid becoming the group leader process;
  • Call the setsid function to create a new session and make the current process the new session leader and process leader;
  • Redirects stdin, stdout, and stderr to /dev/null, preventing the daemon from printing output to the terminal.

In short, the main function of this code is to convert the current program into a daemon process, so that the program runs in the background and is out of the control of the terminal.

#pragma once

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void MyDaemon()
{
    
    
    // 1. 忽略信号,SIGPIPE,SIGCHLD
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);

    // 2. 不要让自己成为组长
    if (fork() > 0)
        exit(0);
    // 3. 调用setsid
    setsid();
    // 4. 标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息
    int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
    if(devnull > 0)
    {
    
    
        dup2(0, devnull);
        dup2(1, devnull);
        dup2(2, devnull);
        close(devnull);
    }
}

Guess you like

Origin blog.csdn.net/weixin_47952981/article/details/130024702