020-Linux-应用层自定义协议与序列化

应用层自定义协议与序列化

1. 应用层

程序员写的一个个解决的实际问题,满足我们日常需求的网络程序,都是在应用层完成的。

1.1 再谈协议

协议是一种"约定"。socket api 的接口,在读写数据时,都是按 "字符串" 的方式来发送接收的。

这些收到和发出的"字符串"分别代表什么意思,都需要提前约定好。

其实, 协议就是双方约定好的结构化的数据。

1.2 网络版计算器

例如:我们需要实现一个服务器版的计算器,我们需要客户端把要计算的两个数字和符号发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。

此时我们定义两个结构体:

  • request结构体代表客户端发往服务器的信息,其中包含两个数字和一个符号。
  • response结构体代表服务器返回给客户端的信息,其中包含一个结果和一个提示位信息。
cpp 复制代码
struct request
{
    int x;
    int y;
    char oper;
};

struct response
{
    int result;
    int code; // 0:success 1:div zero 2:非法操作
};

由于我们发消息和收消息得到的是一个字符串,所以发送之前,需要先把消息转换成字符串,收到消息之后,把字符串转化为结构体。这个过程我们称之为"序列化"和"反序列化"。

1.3 序列化和反序列化

下图以发消息为例:

对于上面的方式,我们只需要保证一端发送时构造的数据,在另一端能够正常的解析,就是可以的。对于这种约定我们称为应用层协议。

我们实现上面的内容就需要引入序列化和反序列化,这里我们不单独实现,我们可以使用现成的方案---jsoncpp库。

这里还会有一个问题,有可能一方写入两条数据,被一次性发送过去,或者由于一些原因,只发了一半过去,还有一半没发,因为对于TCP通信是面向字节流的,它不在乎你的信息是什么样的,只知道你发的是一个一个字符组成的字节流信息。所以除了定协议,还需要分割完整的报文,具体会在后面的代码中实现。

2. 重新理解read、write、recv、send和tcp为什么支持全双工

同样以下面的发消息的过程为例:

我们使用TCP时,在OS中会创建两段缓冲区,发送缓冲区与对方的接收缓冲区连接,接收缓冲区与对方的发送缓冲区连接。

在写数据时,实际上就是把数据拷贝到发送缓冲区中,然后后面怎么进行传输,上层就不用管了。

在读数据时,实际上就是把接收缓冲区的信息拷贝到自己的空间中,至于消息怎么传输来的,上层也不需要管。

而read、write、recv、send本质上就是拷贝函数,把数据拷贝到缓冲区,或是把缓冲区的数据拷贝到自己的空间。

而发送数据也就是把发送缓冲区的数据通过协议栈和网络拷贝到对方的接收缓冲区。

此时TCP的这两条路互不干扰,可以同时进行收发,所以它们是全双工的。

【总结】

  1. 在任何一台主机上,TCP连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工。
  2. 这就是为什么一个tcp sockfd读写都是它的原因。
  3. 实际数据什么时候发,发多少,出错了怎么办,由TCP控制,所以TCP叫做传输控制协议。
  4. 上面的过程实际上就是生产者消费者模型,一边放数据,一边拿数据。
  5. IO函数阻塞的原因本质生就是在维护同步关系。

3. Jsoncpp

Jsoncpp 是一个用于处理JSON数据的C++库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。

3.1 特性

  1. 简单易用:Jsoncpp提供了直观的API,使得处理JSON数据变得简单。

  2. 高性能:Jsoncpp的性能经过优化,能够高效地处理大量JSON数据。

  3. 全面支持:支持JSON标准中的所有数据类型,包括对象、数组、字符串、数字、 布尔值和null。

  4. 错误处理:在解析JSON数据时,Jsoncpp提供了详细的错误信息和位置,方便开发者调试。

当使用Jsoncpp 库进行 JSON 的序列化和反序列化时,确实存在不同的做法和工具类可供选择。

3.2 安装

shell 复制代码
Ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

【注意】使用g++进行编译时,需要加上-ljsoncpp选项,因为这个是第三方库。

3.3 序列化

序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。Jsoncpp提供了多种方式进行序列化:

  1. 使用Json::Value的toStyledString方法:

    优点:将Json::Value对象直接转换为格式化的JSON字符串。

    cpp 复制代码
    #include <iostream>
    #include <string>
    #include <jsoncpp/json/json.h>
    
    int main()
    {
        Json::Value root;
        root["name"] = "joe";
        root["sex"] = "男";
        std::string s = root.toStyledString();
        std::cout << s << std::endl;
        return 0;
    } 
  2. 使用Json::StreamWriter方法:

    优点:提供了更多定制选项,如缩进、换行符等。

    cpp 复制代码
    #include <iostream>
    #include <string>
    #include <sstream>
    #include <memory>
    #include <jsoncpp/json/json.h>
    
    int main()
    {
        Json::Value root;
        root["name"] = "joe";
        root["sex"] = "男";
        Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂
        std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
        std::stringstream ss;
        writer->write(root, &ss);
        std::cout << ss.str() << std::endl;
    
        return 0;
    }
  3. 使用Json::FastWriter方法:

    优点:比StreamWriter更快,因为它不会添加额外的空格和换行符。

    cpp 复制代码
    #include <iostream>
    #include <string>
    #include <sstream>
    #include <memory>
    #include <jsoncpp/json/json.h>
    
    int main()
    {
        Json::Value root;
        root["name"] = "joe";
        root["sex"] = "男";
        Json::FastWriter writer;
        std::string s = writer.write(root);
        std::cout << s << std::endl;
        
        return 0;
    }

3.4 反序列化

反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp提供了以下方法进行反序列化:

  1. 使用Json::Reader:

    cpp 复制代码
    #include <iostream>
    #include <string>
    #include <jsoncpp/json/json.h>
    
    int main()
    {
        // JSON 字符串
        std::string json_string = "{\"name\":\"张三\",\"age\":30, \"city\":\"北京\"}";
    
        // 解析 JSON 字符串
        Json::Reader reader;
        Json::Value root;
    
        // 从字符串中读取 JSON 数据
        bool parsingSuccessful = reader.parse(json_string, root);
    
        if (!parsingSuccessful)
        {
            // 解析失败, 输出错误信息
            std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
            return 1;
        }
    
        // 访问 JSON 数据
        std::string name = root["name"].asString();
        int age = root["age"].asInt();
        std::string city = root["city"].asString();
    
        // 输出结果
        std::cout << "Name: " << name << std::endl;
        std::cout << "Age: " << age << std::endl;
        std::cout << "City: " << city << std::endl;
    
        return 0;
    }
  2. 使用 Json::CharReader 的派生类(不推荐了,上面的足够了):

    • 在某些情况下, 你可能需要更精细地控制解析过程, 可以直接使用Json::CharReader的派生类。
    • 但通常情况下, 使用Json::parseFromStream或Json::Reader的parse方法就足够了。

4. 网络版简易计算机代码实现

4.1 介绍

对于这个程序,我们分为服务端和客户端,大致实现思路如下:

  1. 客户端:
    1. 连接到服务端
    2. 构建请求
    3. 序列化请求
    4. 将请求发送给服务端
    5. 从服务端收到应答
    6. 将应答进行反序列化
    7. 得到结果
  2. 服务端:
    1. 连接到客户端
    2. 接收请求
    3. 反序列化请求
    4. 计算得到应答
    5. 序列化应答
    6. 将应答发送回客户端

对于服务端,我们希望根据OSI的7层模型中的上三层进行分层:

  • 会话层:管理应用程序间的通信会话,负责建立、维护和终止会话
  • 表示层:负责数据的格式化、加密和压缩,确保数据在不同系统间的交换是有效和安全的
  • 应用层:提供用户接口和应用程序间的通信服务

对于每一层负责的工作,进行解耦。

4.2 前情提要

依旧是之前常用的几个头文件,这里不做过多的解释,具体可以查看前面的文章。

4.2.1 InetAddr.hpp

封装ip+port。

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <arpa/inet.h>

class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        _ip = inet_ntoa(addr.sin_addr);
    }
public:
    InetAddr(){}
    
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        ToHost(addr);
    }

    std::string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    std::string AddrStr()
    {
        return _ip + ":" + std::to_string(_port);
    }

    ~InetAddr()
    {}
private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};
4.2.2 LockGuard.hpp

RAII式加锁。

cpp 复制代码
#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }

    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    pthread_mutex_t *_mutex;
};
4.2.3 Log.hpp

日志。

cpp 复制代码
#pragma once

#include <iostream>
#include <fstream>
#include <ctime>
#include <cstring>
#include <cstdarg>
#include <unistd.h>
#include <pthread.h>
#include "LockGuard.hpp"

namespace log_ns
{

    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    std::string GetTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                 curr_time->tm_year + 1900,
                 curr_time->tm_mon + 1,
                 curr_time->tm_mday,
                 curr_time->tm_hour,
                 curr_time->tm_min,
                 curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _pid;
        std::string _filename;
        int _line;
        std::string _time;
        std::string _message;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _type(SCREEN_TYPE), _logfile(logfile)
        {
        }

        void Enable(int type)
        {
            _type = type;
        }

        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                   lg._level.c_str(),
                   lg._pid,
                   lg._filename.c_str(),
                   lg._line,
                   lg._time.c_str(),
                   lg._message.c_str());
        }

        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;

            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                     lg._level.c_str(),
                     lg._pid,
                     lg._filename.c_str(),
                     lg._line,
                     lg._time.c_str(),
                     lg._message.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }

        void FlushLog(const logmessage &lg)
        {
            LockGuard LockGuard(&glock); // 加锁,为了防止多个线程同时打印日志造成输出信息错乱。
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }

        void logMessage(std::string filename, int line, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._pid = getpid();
            lg._filename = filename;
            lg._line = line;
            lg._time = GetTime();

            va_list ap;
            va_start(ap, format);
            char log_msg[1024];
            vsnprintf(log_msg, sizeof(log_msg), format, ap);
            va_end(ap);
            lg._message = log_msg;

            // print log
            FlushLog(lg);
        }

        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)

#define EnableFile()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)

#define LOG(Level, Format, ...)                                          \
    do                                                                   \
    {                                                                    \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)

}

4.3 Socket.hpp

将socket套接字进行封装,对套接字fd进行管理,如:向套接字写入、从套接字获取、构建Listen套接字等。

cpp 复制代码
#pragma once

#include <iostream>
#include <functional>
#include <memory>

#include <cstring>

#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>

#include "InetAddr.hpp"
#include "Log.hpp"

using namespace log_ns;

namespace socket_ns
{
    enum
    {
        SOCKET_ERROR = 1,
        BIND_ERROR,
        LISTEN_ERROR
    };

    const static int gblcklog = 8;

    class Socket;
    using SockSPtr = std::shared_ptr<Socket>;

    // 模板方法模式
    class Socket
    {
    public:
        virtual void CreateSocketOrDie() = 0;
        virtual void CreateBindOrDie(uint16_t port) = 0;
        virtual void CreateListenOrDie(int backlog = gblcklog) = 0;
        virtual SockSPtr Accepter(InetAddr* cliaddr) = 0;
        virtual bool Conntecor(const std::string& peerip, uint16_t peerport) = 0;
        virtual int Sockfd() = 0;
        virtual void Close() = 0;

        virtual ssize_t Recv(std::string* out) = 0;
        virtual int Send(std::string& in) = 0;
    public:
        void BuildListenSocket(uint16_t port)
        {
            CreateSocketOrDie();
            CreateBindOrDie(port);
            CreateListenOrDie();
        }

        bool BuildClientSocket(const std::string& peerip, uint16_t peerport)
        {
            CreateSocketOrDie();
            return Conntecor(peerip, peerport);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket()
        {}

        TcpSocket(int sockfd): _sockfd(sockfd)
        {}

        ~TcpSocket()
        {
            if (_sockfd > 0) ::close(_sockfd);
        }

        void CreateSocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(FATAL, "socket create error\n");
                exit(SOCKET_ERROR);
            }
            LOG(INFO, "socket create success, sockfd = %d\n", _sockfd);
        }

        void CreateBindOrDie(uint16_t port) override
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));

            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;

            if (::bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0)
            {
                LOG(FATAL, "bind error\n");
                exit(BIND_ERROR);
            }
            LOG(INFO, "bind success\n");
        }

        void CreateListenOrDie(int backlog) override
        {
            if (::listen(_sockfd, backlog) < 0)
            {
                LOG(FATAL, "listen error\n");
                exit(LISTEN_ERROR);
            }
            LOG(INFO, "listen success\n");
        }

        SockSPtr Accepter(InetAddr* cliaddr) override
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_sockfd, (struct sockaddr*)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                return nullptr;
            }
            *cliaddr = InetAddr(client);
            LOG(INFO, "accept success, get a new link, client: %s, sockfd: %d\n", cliaddr->AddrStr().c_str(), sockfd);

            return std::make_shared<TcpSocket>(sockfd);
        }

        bool Conntecor(const std::string& peerip, uint16_t peerport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(peerport);
            ::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);

            int n = ::connect(_sockfd, (struct sockaddr*)&server, sizeof(server));
            if (n < 0)
            {
                LOG(FATAL, "connect error\n");
                return false;
            }
            LOG(INFO, "connect success\n");
            return true;
        }

        int Sockfd()
        {
            return _sockfd;
        }

        void Close()
        {
            if (_sockfd > 0)
            {
                ::close(_sockfd);
                _sockfd = -1;
            }
        }

        ssize_t Recv(std::string* out) override
        {
            char inbuffer[4096];
            ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
            if (n > 0)
            {
                inbuffer[n] = '\0'; 
                *out += inbuffer;
            }
            return n;
        }

        virtual int Send(std::string& in) override
        {
            return ::send(_sockfd, in.c_str(), in.size(), 0);
        }

    private:
        int _sockfd;
    };
}

4.4 Protocal.hpp

封装了我们在传输过程中自定义的协议,对传入的内容进行序列化或反序列化,并完成报头的增删。

cpp 复制代码
#pragma once

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

static const std::string sep = "\r\n";

// 设计协议的报头和报文的完整格式
// "len"\r\n"{json}"\r\n --- 完整的报文
// len:有效载荷长度
// 第一个\r\n:区分len和json串
// 第二个\r\n:暂时没有别的作用,方便打印和debug

// 添加报头
std::string Encode(const std::string& jsonstr)
{
    int len = jsonstr.size();
    std::string lenstr = std::to_string(len);

    return lenstr + sep + jsonstr + sep;
}

// 获取json串
std::string Decode(std::string& packagestream)
{
    auto pos = packagestream.find(sep);
    // 连长度部分都可能不完整,直接不处理
    if (pos == std::string::npos) return "";
    // len部分肯定是完整的
    std::string lenstr = packagestream.substr(0, pos);
    int len = std::stoi(lenstr);

    // 计算出报文的长度
    int total = lenstr.size() + len + 2 * sep.size();

    // packagestream小于报文长度,报文不完整,不进行处理
    if (packagestream.size() < total) return "";
    // 至少包含一条完整的报文
    std::string jsonstr = packagestream.substr(pos + sep.size(), len);
    packagestream.erase(0, total);
    return jsonstr;
}

struct Request
{
    Request(){}

    ~Request(){}

    bool Serialize(std::string *out)
    {
        // 1.自己写
        // "x oper y"

        // 2.使用现成的库:xml、json(jsoncpp)、protobuf,这里使用json
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        *out = writer.write(root);

        return true;
    }

    bool Deserialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ret = reader.parse(in, root);

        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();

        return true;
    }

    void SetValue(int x, int y, char oper)
    {
        _x = x;
        _y = y;
        _oper = oper;
    }

    // x oper y
    int _x;
    int _y;
    char _oper; // + - * / %
};

struct Response
{
    Response(): _result(0), _code(0), _desc("success"){}

    ~Response(){}

    bool Serialize(std::string *out)
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;
        root["desc"] = _desc;

        Json::FastWriter writer;
        *out = writer.write(root);

        return true;
    }

    bool Deserialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ret = reader.parse(in, root);

        _result = root["result"].asInt();
        _code = root["code"].asInt();
        _desc = root["desc"].asString();

        return true;
    }

    void PrintResult()
    {
        std::cout << "result: " << _result << ", code: " << _code << ", desc: " << _desc << std::endl;
    }

    int _result;
    int _code; // 0:success 1:div zero 2:非法操作
    std::string _desc;
};

// 工厂模式
class Factory
{
public:
    static std::shared_ptr<Request> BuildRequestDefault()
    {
        return std::make_shared<Request>();
    }

    static std::shared_ptr<Response> BuildResponseDefault()
    {
        return std::make_shared<Response>();
    }
};

4.5 NetCal.hpp

计算器逻辑,只负责将拿到的数据进行计算然后返回到下层处理。

cpp 复制代码
#pragma once

#include <memory>

#include "Protocol.hpp"

class NetCal
{
public:
    NetCal(){}
    ~NetCal(){}

    std::shared_ptr<Response> Caluator(std::shared_ptr<Request> req)
    {
        auto resp = Factory::BuildResponseDefault();
        switch(req->_oper)
        {
        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;
                resp->_desc = "div zero";
            }
            else
            {
                resp->_result = req->_x / req->_y;
            }
            break;
        case '%':
            if (req->_y == 0)
            {
                resp->_code = 2;
                resp->_desc = "mod zero";
            }
            else
            {
                resp->_result = req->_x % req->_y;
            }
            break;
        default:
            resp->_code = 3;
            resp->_desc = "illegal operation";
            break;
        }

        return resp;
    }
private:
    
};

4.6 Service.hpp

服务端的IO逻辑,从获取到返回应答。

cpp 复制代码
#pragma once

#include <iostream>
#include <functional>

#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"

using namespace socket_ns;
using namespace log_ns;

using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;

class IOService
{
public:
    IOService(process_t process): _process(process)
    {}

    ~IOService()
    {}

    void IOExecute(SockSPtr sock, InetAddr& addr)
    {
        std::string packagestreamqueue;
        while(true)
        {
            // 1.读取
            ssize_t n = sock->Recv(&packagestreamqueue);
            if (n <= 0)
            {
                LOG(INFO, "client %s quit or read error\n", addr.AddrStr().c_str());
                break;
            }
            std::cout << "-------------------------------------------------------" << std::endl;
            std::cout << "packagestreamqueue: " << std::endl << packagestreamqueue << std::endl;

            // 2.报文解析
            // 有可能读取到的不是一个完整的报文
            std::string package = Decode(packagestreamqueue);
            if (package.empty()) continue;
            // 保证package一定是一个完整的json串
            auto req = Factory::BuildRequestDefault();
            std::cout << "package: " << std::endl << package << std::endl;

            // 3.反序列化
            req->Deserialize(package);

            // 4.业务处理
            auto resp = _process(req); // 通过请求,得到应答

            // 5.序列化应答
            std::string respjson;
            resp->Serialize(&respjson);
            std::cout << "respjson: " << std::endl << respjson << std::endl;

            // 6.添加报头
            respjson = Encode(respjson);
            std::cout << "respjson add header done: " << std::endl << respjson << std::endl;

            // 7.发送回去
            sock->Send(respjson);
        }
    }
private:
    process_t _process;
};

4.7 TcpServer.hpp

负责建立和断开连接,建立连接后创建子线程,其他的工作交给子线程完成。

cpp 复制代码
#pragma once

#include <functional>
#include "Socket.hpp"

using namespace socket_ns;

using service_io_t = std::function<void(SockSPtr, InetAddr&)>;

static const int gport = 8888;

class TcpServer
{
public:
    struct ThreadData
    {
        ThreadData(SockSPtr sockfd, TcpServer* self, const InetAddr& addr): _sockfd(sockfd), _self(self), _addr(addr)
        {}
        
        SockSPtr _sockfd;
        TcpServer* _self;
        InetAddr _addr;
    };

    TcpServer(service_io_t service, uint16_t port = gport): _port(port), _listensock(std::make_shared<TcpSocket>()), _isrunning(false), _service(service)
    {
        _listensock->BuildListenSocket(_port);
    }
    
    ~TcpServer(){}

    static void* Execute(void* args)
    {
        pthread_detach(pthread_self());// 线程分离,无需被join
        ThreadData* td = static_cast<ThreadData*>(args);
        td->_self->_service(td->_sockfd, td->_addr);
        td->_sockfd->Close();
        delete td;
        return nullptr;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 1.获取新连接
            InetAddr client;
            SockSPtr newsock = _listensock->Accepter(&client);
            if (newsock == nullptr) continue;
            LOG(INFO, "accept success, get a new link, client: %s, sockfd: %d\n", client.AddrStr().c_str(), newsock->Sockfd());

            // 2.进行通信
            pthread_t tid;
            ThreadData* td = new ThreadData(newsock, this, client);
            pthread_create(&tid, nullptr, Execute, td);
        }
        _isrunning = false;
    }

private:
    uint16_t _port;
    SockSPtr _listensock;
    bool _isrunning;
    service_io_t _service;
};

4.8 ServerMain.cc

服务端程序入口,将执行的逻辑串联起来。

cpp 复制代码
#include "TcpServer.hpp"
#include "Service.hpp"
#include "NetCal.hpp"

// ./server 8888
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    NetCal cal;
    IOService service(std::bind(&NetCal::Caluator, &cal, std::placeholders::_1));
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&IOService::IOExecute, &service, std::placeholders::_1, std::placeholders::_2), port);
    tsvr->Start();

    return 0;
}

4.9 ClientMain.cc

客户端逻辑较为简单,只需要建立连接后将构建的请求发送给服务端,然后将服务端的应答进行处理即可,这里不做过多的封装。

cpp 复制代码
#include <iostream>
#include <ctime>
#include <unistd.h>
#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"

using namespace socket_ns;
using namespace log_ns;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    SockSPtr sock = std::make_shared<TcpSocket>();
    if (!sock->BuildClientSocket(serverip, serverport))    {
        LOG(FATAL, "connect error\n");
        exit(1);
    }

    srand(time(nullptr) ^ getpid());
    const std::string opers = "+-*/%&^|";

    while (true)
    {
        // 1.构建数据
        int x = rand() % 10;
        int y = rand() % 10;
        char oper = opers[rand() % opers.size()];

        // 2.构建请求
        auto req = Factory::BuildRequestDefault();
        req->SetValue(x, y, oper);

        // 3.序列化
        std::string reqstr;
        req->Serialize(&reqstr);

        // 4.添加报头
        reqstr = Encode(reqstr);

        // 5.发送数据
        std::cout << "---------------------------------------" << std::endl;
        std::cout << "requset string: " << std::endl << reqstr << std::endl;
        sock->Send(reqstr);

        std::string packagestreamqueue;
        while (true)
        {
            // 6.读取应答
            ssize_t n = sock->Recv(&packagestreamqueue);
            if (n <= 0)
            {
                LOG(INFO, "server quit or read error\n");
                break;
            }

            // 7.报文解析
            std::string package = Decode(packagestreamqueue);
            if (package.empty()) continue;
            std::cout << "package: " << std::endl << package << std::endl;

            // 8.反序列化
            auto resp = Factory::BuildResponseDefault();
            resp->Deserialize(package);

            // 9.打印结果
            resp->PrintResult();

            break;
        }

        sleep(1);
    }

    return 0;
}

4.10 运行效果

相关推荐
草莓熊Lotso42 分钟前
Vibe Coding 时代:LangChain 与 LangGraph 全链路解析
linux·运维·服务器·数据库·人工智能·mysql·langchain
网安情报局6 小时前
除了 CDN,DDoS 攻击还有哪些更有效的防护方式?
网络
Promise微笑6 小时前
2026年国产替代油介损测试仪:油介损全场景解决方案与技术演进
大数据·网络·人工智能
蜡台7 小时前
Python包管理工具pip完全指南-----2
linux·windows·python
Ujimatsu7 小时前
虚拟机安装Debian 13.x及其常用软件(2026.4)
linux·运维·ubuntu
千百元7 小时前
zookeeper启不来了
linux·zookeeper·debian
AnalogElectronic9 小时前
linux 测试网络和端口是否连通的命令详解
linux·网络·php
Edward1111111110 小时前
4月28日防火墙问题
linux·运维·服务器
Rust研习社10 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
灰子学技术10 小时前
Envoy HTTP 流量层面的 Metric 指标分析
网络·网络协议·http