【Linux网络编程】应用层自定义协议与序列化

首先我们要知道,在之前的Socket编程学习中,我们通过 socket API 实现了简单的字符串发送和接收,即EchoServer。但在实际的开发场景中,我们需要传输的是"结构化的数据"。

socket API 本质上是面向字节流的,它并不理解什么是"结构体"或"类" 。因此,我们需要在应用层解决如何把业务数据在"结构体"和"网络字节流"之间进行转换的问题,这就是自定义协议序列化的由来。

一、什么是"协议"?

所谓的"协议",本质上就是通信双方约定好的结构化数据

比如我们要实现一个网络计算器,客户端需要把 1+2 发给服务端。

  • 方案一 :直接发送字符串 "1+2"。这就需要约定好格式:数字间用运算符隔开,没有空格等 。
  • 方案二:定义一个结构体来表示交互信息 。

无论采用哪种方案,只要保证一端发送的数据,另一端能够按照约定的规则正确解析,这就是应用层协议

二、序列化与反序列化

为了在网络上传输结构化的数据,我们需要进行转化:

序列化 (Serialization):发送数据时,将内存中的结构体/对象按照既定规则转换成"字节流"或"字符串"的过程 。

反序列化 (Deserialization):接收数据时,将收到的"字节流"或"字符串"按照相同规则还原回结构体/对象的过程 。

序列化和反序列化的目的

  • 在网络传输时,序列化目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列。

  • 序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。

我们可以认为网络通信和业务处理处于不同的层级,在进行网络通信时底层看到的都是二进制序列的数据,而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换,则需要对数据进行对应的序列化或反序列化操作。

这个过程使得上层业务逻辑不需要关心底层的网络字节流细节,只需处理结构体即可 。

三、重新理解 TCP 通信与 IO 系统调用

在编写协议处理代码前,必须深入理解 read、write、recv、send 这些系统调用的本质。

本质是拷贝

当我们调用 write(sockfd, buffer, ...) 时,并不是直接把数据发到了网络上,而是将数据从用户层缓冲区 拷贝到了内核层的 TCP 发送缓冲区

同理,read 也是从内核的接收缓冲区拷贝数据到用户层 。

全双工的物理基础

TCP 支持全双工通信(同时收发),是因为在操作系统内核中,一个 TCP 连接既有发送缓冲区 ,又有接收缓冲区

TCP 协议(传输控制协议)负责决定什么时候发数据、发多少、出错重传等细节 。

四、解决"粘包"问题:定制协议报文

TCP 是面向字节流的,它没有"报文"的概念。如果不做处理,接收端可能会一次读到半个请求,或者一次读到两个半请求(即粘包问题)。

我们需要在应用层明确报文的边界。常见的自定义协议格式如下:

协议头 + 有效载荷

有效载荷长度 + \r\n (分隔符) + 有效载荷内容 + \r\ n

例如,要发送 JSON 字符串 {"x":1, "y":2},封装后的报文可能是:

复制代码
16\r\n{"x":1, "y":2}\r\n

编码与解码实现

在代码实现中,我们需要处理字节流缓冲区:

Encode (打包) : 计算消息长度,拼接字符串:len + "\r\n" + message + "\r\n"

Decode (解包): 这是一个循环处理的过程,因为缓冲区里可能包含多条消息或不完整的消息:

  • 查找第一个分隔符 \r\n 的位置,提取出长度 len
  • 计算一条完整报文需要的总长度 total = len.size() + message_len + 2 * Sep.size()
  • 判断缓冲区剩余数据是否足够 total。如果不够,说明报文不完整,返回等待新数据 。
  • 如果足够,根据长度截取出一个完整的 message,并从缓冲区中移除已处理的字节 。

五、使用 Jsoncpp 库

在实际开发中,我们很少手写二进制序列化,而是使用成熟的序列化方案,如 JSON

安装

bash 复制代码
sudo apt-get install libjsoncpp-dev  # Ubuntu
sudo yum install jsoncpp-devel       # CentOS

核心操作

Json::Value 这是最核心的类,它可以表示 JSON 中的对象、数组、字符串、数字等。用法类似于 std::map

使用示例:

cpp 复制代码
Json::Value root;
root["datax"] = 10;
root["oper"] = '+';  // 支持自动类型转换

序列化

Json::StyledWriter / toStyledString(): 生成带缩进、格式好看的字符串,适合调试 。

Json::FastWriter: 生成紧凑的字符串(去掉了空格换行),体积小,适合网络传输 。

使用示例:

cpp 复制代码
Json::FastWriter writer;
std::string s = writer.write(root); // 输出: {"datax":10,"oper":43}

反序列化

使用 Json::Reader 将字符串解析回 Json::Value 对象 。

使用示例:

cpp 复制代码
Json::Reader reader;
Json::Value root;
if (reader.parse(json_string, root)) {
    int x = root["datax"].asInt(); // 提取数据
    char op = root["oper"].asInt();
}

六、实战用例

结合上述讲的几点,我们来构建一个网络版本的计算器,加强我们的理解。

服务端代码

首先我们需要对服务器进行初始化:

  • 调用socket函数,创建套接字。

  • 调用bind函数,为服务端绑定一个端口号。

  • 调用listen函数,将套接字设置为监听状态。

初始化完服务器后就可以启动服务器了,服务器启动后要做的就是不断调用accept函数,从监听套接字当中获取新连接,每当获取到一个新连接后就创建一个新线程,让这个新线程为该客户端提供计算服务。

TcpServer.hpp:

cpp 复制代码
#pragma once

#include <signal.h>
#include <functional>
#include "InetAddr.hpp"
#include "Socket.hpp"

using callback_t = std::function<std::string(std::string &)>;

class Tcpserver
{
public:
    Tcpserver(uint16_t port, callback_t cb)
        : _port(port), _listensocket(std::make_unique<TcpSocket>()), _cb(cb)
    {
        _listensocket->BuildListenSocketMethod(_port);
    }

    void HandlerRequest(std::shared_ptr<Socket> sockfd, InetAddr addr)
    {
        std::string inbuffer;
        while (true)
        {
            ssize_t n = sockfd->Recv(&inbuffer, 100);
            if (n > 0)
            {
                LOG(LogLevel::INFO) << addr.ToString() << "# " << inbuffer;

                // 处理收到的数据
                std::string ret_str = _cb(inbuffer); // 检查 序列化 解包
                if (ret_str.empty())  // 空串返回
                    continue;

                sockfd->Send(ret_str);
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "client " << addr.ToString() << " quit, close sockfd: " << sockfd->GetSockFd();
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "read client " << addr.ToString() << " error, sockfd: " << sockfd->GetSockFd();
                break;
            }
        }
        sockfd->CloseSocket();
    }

    void Run()
    {
        signal(SIGCHLD, SIG_IGN);

        while (true)
        {
            // 方法 1
            InetAddr clientaddr;
            auto sockfd = _listensocket->Accept(&clientaddr);
            if (sockfd == nullptr)
            {
                // Accept 失败(如临时资源不足或信号打断),避免忙循环
                sleep(1);
                continue;
            }
            LOG(LogLevel::INFO) << "获取新链接成功, sockfd is : " << sockfd->GetSockFd() << " client addr: " << clientaddr.ToString();

            // 方法 2
            // std::string *peerip;
            // uint16_t *peerport;
            // _listensocket->AcceptConnection(peerip, peerport);
            if (fork() == 0)
            {
                // 子进程
                _listensocket->CloseSocket();
                HandlerRequest(sockfd, clientaddr);
                exit(0);
            }
            sockfd->CloseSocket();
        }
    }
    ~Tcpserver()
    {
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensocket;
    callback_t _cb;
};

上述的Socket.hpp,InetAddr.hpp是对套接字、IP、端口号的封装,具体封装如下:

Socket.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <memory>
#include "Logger.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"

static const int defaultsockfd = -1;
#define Convert(addrptr) ((struct sockaddr *)addrptr)
static const int gbacklog = 5;

// 封装⼀个基类,Socket接⼝类
// 设计模式:模版⽅法类
class Socket
{
public:
    virtual ~Socket() {}
    virtual void CreateSocketOrDie() = 0;
    virtual void BindSocketOrDie(uint16_t port) = 0;
    virtual void ListenSocketOrDie(int backlog) = 0;
    virtual std::unique_ptr<Socket> AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
    virtual std::shared_ptr<Socket> Accept(InetAddr *addr) = 0;
    virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;
    virtual int GetSockFd() = 0;
    virtual void SetSockFd(int sockfd) = 0;
    virtual void CloseSocket() = 0;
    virtual bool Recv(std::string *buffer, int size) = 0;
    virtual void Send(const std::string &send_str) = 0;
    // TODO
public:
    void BuildListenSocketMethod(uint16_t port, int backlog = gbacklog)
    {
        CreateSocketOrDie();
        BindSocketOrDie(port);
        ListenSocketOrDie(backlog);
    }
    bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
    {
        CreateSocketOrDie();
        return ConnectServer(serverip, serverport);
    }
    void BuildNormalSocketMethod(int sockfd)
    {
        SetSockFd(sockfd);
    }
};

class TcpSocket : public Socket
{
public:
    TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd)
    {
    }
    ~TcpSocket()
    {
    }

    void CreateSocketOrDie() override
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);

        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create tcp socket error";
            exit(SOCK_CREATE_ERROR);
        }
        LOG(LogLevel::INFO) << "create tcp socket success";
    }
    void BindSocketOrDie(uint16_t port) override
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(port);

        // InetAddr lc(port);
        // int n = bind(_sockfd, lc.Addr(), lc.Length());

        int n = bind(_sockfd, Convert(&local), sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socker error";
            exit(SOCK_BIND_ERROR);
        }
        LOG(LogLevel::INFO) << "bind socker success";
    }
    void ListenSocketOrDie(int backlog) override
    {
        int n = listen(_sockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCK_LISTEN_ERROR);
        }
        LOG(LogLevel::INFO) << "listen socket success";
    }

    std::shared_ptr<Socket> Accept(InetAddr *addr)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newsockfd = accept(_sockfd, Convert(&peer), &len); 
        if (newsockfd < 0)
        {
            LOG(LogLevel::WARNING) << "accept client error"  << strerror(errno);
            return nullptr;
        }
        addr->Init(peer);
        std::shared_ptr<Socket> s = std::make_shared<TcpSocket>(newsockfd);

        return s;
    }

    std::unique_ptr<Socket> AcceptConnection(std::string *peerip, uint16_t *peerport) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newsockfd = accept(_sockfd, Convert(&peer), &len);
        if (newsockfd < 0)
            return nullptr;
        *peerport = ntohs(peer.sin_port);
        // *peerip = inet_ntoa(peer.sin_addr);
        char buffer[64];
        inet_ntop(AF_INET, &(peer.sin_addr.s_addr), buffer, sizeof(buffer));
        *peerip = buffer;
        std::unique_ptr<Socket> s = std::make_unique<TcpSocket>(newsockfd);
        return s;
    }
    bool ConnectServer(std::string &serverip, uint16_t serverport) override
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        // server.sin_addr.s_addr = inet_addr(serverip.c_str());
        inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr.s_addr));
        server.sin_port = htons(serverport);
        int n = connect(_sockfd, Convert(&server), sizeof(server));
        if (n == 0)
            return true;
        else
            return false;
    }
    int GetSockFd() override
    {
        return _sockfd;
    }
    void SetSockFd(int sockfd) override
    {
        _sockfd = sockfd;
    }
    void CloseSocket() override
    {
        if (_sockfd > defaultsockfd)
            close(_sockfd);
    }
    //  读 序列化及反序列
    bool Recv(std::string *buffer, int size) override
    {
        char inbuffer[size];
        ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);
        if (n > 0)
        {
            inbuffer[n] = 0;
            *buffer += inbuffer; // 故意拼接的  增加
            return true;
        }
        else if (n == 0)
            return false;
        else
            return false;
    }
    void Send(const std::string &send_str) override
    {
        send(_sockfd, send_str.c_str(), send_str.size(), 0);
    }

private:
    int _sockfd;
};

InetAddr.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <functional>
#include <string>
#include "Logger.hpp"

class InetAddr
{
private:
    void Net2Host()
    {
        _port = ntohs(_addr.sin_port);
        // _ip = inet_ntoa(_addr.sin_addr);

        char ipbuffer[64]; // 不需要调用函数内部的区域,防止覆盖
        inet_ntop(AF_INET, &(_addr.sin_addr.s_addr), ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }
    void Host2Net()
    {
        bzero(&_addr, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        // _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
        inet_pton(AF_INET, _ip.c_str(), &(_addr.sin_addr.s_addr));
    }

public:
    InetAddr()
    {}
    InetAddr(const struct sockaddr_in &client) : _addr(client)
    {
        Net2Host();
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0") : _port(port), _ip(ip)
    {
        Host2Net();
    }

    void Init(const struct sockaddr_in &client)
    {
        _addr = client;
        Net2Host();
    }
    
    uint16_t Port()
    {
        return _port;
    }
    std::string Ip()
    {
        return _ip;
    }
    struct sockaddr *Addr()
    {
        return (sockaddr *)&_addr;
    }
    socklen_t Length()
    {
        socklen_t len = sizeof(_addr);
        return len;
    }

    std::string ToString()
    {
        return _ip + "-" + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return _ip == addr._ip && _port == addr._port;
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr; // 网络风格地址

    std::string _ip; // 主机风格地址
    uint16_t _port;
};

而 Logger.hpp 是一个封装好的日志类,用于输出日志,便于调试。

最后的服务端启动文件:

Main.cc:

cpp 复制代码
#include "Calculator.hpp"// 业务 // 应用层
#include "Parser.hpp"    // 报文解析,序列反序列化,打包解包 // 表示层
#include "TcpServer.hpp" // 网络通信开断连接 // 会话层
#include "Daemon.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage : " << proc << " serverport" << std::endl;
}

// ./tcp_client serverport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t serverport = std::stoi(argv[1]);

    Daemon();

    // EnableConsoleLogStrategy();
    EnableFileLogStrategy();

    // 1. 计算机对象
    std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();

    // 2. 协议解析模块
    std::unique_ptr<Parser> parser = std::make_unique<Parser>([&cal](Request &rq) -> Response
                                                              { return cal->Exec(rq); });

    // 3. 网络通信模块
    std::unique_ptr<Tcpserver> tcpsock = std::make_unique<Tcpserver>(serverport, [&parser](std::string &inbuffer) -> std::string
                                                                     { return parser->Parse(inbuffer); });
    tcpsock->Run();

    while (true)
    {
    }

    return 0;
}

Daemon.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);

    if (fork() > 0)
        exit(0);

    setsid();

    int fd = open("dev/null", O_RDWR);
    if (fd >= 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
    }
}

Calculator.hpp:

cpp 复制代码
#pragma once

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

class Calculator
{
public:
    Calculator()
    {
    }
    Response Exec(Request &rq)
    {
        Response rp;
        switch (rq.Oper())
        {
        case '+':
            rp.SetResult(rq.X() + rq.Y());
            break;
        case '-':
            rp.SetResult(rq.X() - rq.Y());
            break;
        case '*':
            rp.SetResult(rq.X() * rq.Y());
            break;
        case '/':
        {
            if (rq.Y() == 0)
            {
                rp.SetCode(1);  // 1 -> /
            }
            else
            {
                rp.SetResult(rq.X() / rq.Y());
            }
        }
        break;
        case '%':
        {
            if (rq.Y() == 0)
            {
                rp.SetCode(2);  // 2 -> %
            }
            else
            {
                rp.SetResult(rq.X() % rq.Y());
            }
        }
        break;
        default:
            rp.SetCode(3);  // false
            break;
        }
        return rp;
    }
    ~Calculator()
    {
    }
};

协议定制

要实现一个网络版的计算器,就必须保证通信双方能够遵守某种协议约定,因此我们需要设计一套简单的约定。数据可以分为请求数据和响应数据,因此我们分别需要对请求数据和响应数据进行约定。

采用C++当中的类来实现:

  • 请求类中需要包括两个操作数,以及对应需要进行的操作。
  • 响应类中需要包括一个计算结果,除此之外,响应类中还需要包括一个状态字段,表示本次计算的状态,因为客户端发来的计算请求可能是无意义的。

规定状态字段对应的含义:

  • 状态字段为0,表示计算成功。
  • 状态字段为1,表示出现除0错误。
  • 状态字段为2,表示出现模0错误。
  • 状态字段为3,表示非法计算。

只有当响应结构体当中的状态字段为0时,计算结果才是有意义的,否则计算结果无意义。

Protocol.hpp:

cpp 复制代码
#pragma once

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

class Request
{
public:
    Request()
    {
    }

    // 序列 反序列化对象
    bool Serializ(std::string *out)
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::StyledWriter writer;
        *out = writer.write(root);
        if (out->empty())
            return false;
        return true;
    }

    bool Deserialize(std::string &in)
    {
        Json::Reader reader;
        Json::Value droot;
        bool ret = reader.parse(in, droot);
        if (!ret)
            return false;
        _x = droot["x"].asInt();
        _y = droot["y"].asInt();
        _oper = droot["oper"].asInt();
        return true;
    }
    int X()
    {
        return _x;
    }

    int Y()
    {
        return _y;
    }

    char Oper()
    {
        return _oper;
    }
    ~Request()
    {
    }

public:
    //  约定
    int _x;
    int _y;
    char _oper;
};

class Response
{
public:
    Response() : _result(0), _code(0)
    {
    }
    bool Serializ(std::string *out)
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;
        Json::StyledWriter writer;
        *out = writer.write(root);
        if (out->empty())
            return false;
        return true;
    }

    bool Deserialize(std::string &in)
    {
        Json::Reader reader;
        Json::Value droot;
        bool ret = reader.parse(in, droot);
        if (!ret)
            return false;
        _result = droot["result"].asInt();
        _code = droot["code"].asInt();
        return true;
    }

    void SetResult(int r)
    {
        _result = r;
    }
    void SetCode(int c)
    {
        _code = c;
    }
    void Print()
    {
        std::cout << _result << "[" << _code << "]" << std::endl;
    }
    ~Response()
    {
    }

private:
    int _result;
    int _code;
};

bool DigitSafeCheck(const std::string &str)
{
    for (int i = 0; i < str.size(); i++)
    {
        if (!(str[i] > '0' && str[i] <= '9'))
        {
            return false;
        }
    }
    return true;
}

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

class Protocol
{
public:
    static std::string Package(const std::string &jsonstr)
    {
        // jsonstd -> len\r\njsonstr\r\n
        if (jsonstr.empty())
        {
            return std::string();
        }
        std::string jsonlen = std::to_string(jsonstr.size());
        return jsonlen + sep + jsonstr + sep;
    }

    static int UnPackage(std::string &origin_str, std::string *package)  // 输入输出
    {
        if (package == nullptr)
            return -2;
        auto pos = origin_str.find(sep);
        if (pos == std::string::npos)
        {
            return 0;
        }
        std::string len_str = origin_str.substr(0, pos);
        if (!DigitSafeCheck(len_str))
        {
            return -1;
        }
        int digit_len = std::stoi(len_str);

        int target_len = len_str.size() + digit_len + 2 * sep.size();
        if (origin_str.size() < target_len)
        {
            return 0;
        }

        *package = origin_str.substr(pos + sep.size(), digit_len);
        origin_str.erase(0, target_len);  //  移除

        return package->size();
    }
};

在上述源码中,不仅含有请求类、响应类,也有对应的协议类,用于解包封包。

为了更好的解耦,我们将报文解析的过程单独封装成一个Parser类。

Parser.hpp:

cpp 复制代码
#pragma once

#include "Protocol.hpp"
#include "Calculator.hpp"
#include "Parser.hpp"
#include "Logger.hpp"
#include <iostream>
#include <string>
#include <functional>

using handler_t = std::function<Response(Request &)>;
//  只负责报文解析
class Parser
{
public:
    Parser(handler_t handler) : _handler(handler)
    {
    }
    std::string Parse(std::string &inbuffer)
    {
        std::string package_ptr;
        while (true) // 循环处理多个请求
        {
            // 1. 解包
            std::string jsonstr;
            int n = Protocol::UnPackage(inbuffer, &jsonstr);
            if (n == 0)
            {
                // return std::string();
                break;
            }
            else if (n < 0)
            {
                exit(1);
            }
            // 解包成功
            LOG(LogLevel::DEBUG) << jsonstr;
            // 2. 反序列化
            Request rq;
            rq.Deserialize(jsonstr);

            // 3. 业务处理
            Response rp = _handler(rq);

            // 4. 序列化
            std::string send_str;
            rp.Serializ(&send_str);

            // 5. 打包
            package_ptr += Protocol::Package(send_str);
        }
        // 6. 返回
        return package_ptr;
    }
    ~Parser()
    {
    }

private:
    handler_t _handler;
};

注意: 协议定制好后必须要被客户端和服务端同时看到,这样它们才能遵守这个约定,那么客户端和服务端都应该包含这个头文件。

客户端代码

客户端首先也需要进行初始化,调用socket函数,创建套接字。

客户端初始化完毕后需要调用connect函数连接服务端,当连接服务端成功后,客户端就可以向服务端发起计算请求了。

用户输入两个数和一个操作符构建一个计算请求,然后将该请求发送给服务端。

当服务端处理完该计算请求后,会对客户端进行响应,因此客户端发送完请求后还需要读取服务端发来的响应数据。

客户端在向服务端发送或接收数据时,可以使用write或read函数进行发送或接收,也可以使用send或recv函数对应进行发送或接收。

send函数

函数原型:

cpp 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要发送的数据。
  • len:需要发送数据的字节个数。
  • flags:发送的方式,一般设置为0,表示阻塞式发送。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

recv函数

函数原型:

cpp 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • len:数据的个数,表示从该文件描述符中读取数据的字节数。
  • flags:读取的方式,一般设置为0,表示阻塞式读取。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。

Client.cc:

cpp 复制代码
#include <iostream>
#include "Socket.hpp"
#include "Protocol.hpp"
#include "Parser.hpp"

void Usage(std::string proc)
{
    std::cerr << "Usage : " << proc << " serverip serverport" << std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    std::unique_ptr<Socket> client = std::make_unique<TcpSocket>();
    if (client->BuildConnectSocketMethod(serverip, serverport))
    {
        std::string inbuffer;
        while (true)
        {
            // 1. 构建请求
            Request rq;
            std::cout << "Please Enter X: ";
            std::cin >> rq._x;
            std::cout << "Please Enter Y: ";
            std::cin >> rq._y;
            std::cout << "Please Enter Oper: ";
            std::cin >> rq._oper;

            // 2. 序列化
            std::string jsonstr;
            rq.Serializ(&jsonstr);

            // 3. 打包
            std::string sendstr = Protocol::Package(jsonstr);

            // 4. 发送
            client->Send(sendstr);

            // 5. 接收
            client->Recv(&inbuffer, 100);
            std::string package;
            int n = Protocol::UnPackage(inbuffer, &package);  
            if (n > 0)
            {
                Response rp;
                bool r = rp.Deserialize(package);

                if (r)
                {
                    rp.Print();
                }
            }
        }
    }
    return 0;
}

上述 demo 就是一个轻量级 TCP 服务示例,使用自定义包协议传输 JSON 请求。父进程监听端口并对每个连接 fork 子进程处理:子进程循环读取、解帧、调用业务(Calculator)并将计算结果序列化返回。

相关推荐
JoannaJuanCV3 小时前
自动驾驶—CARLA仿真(24)sensor_synchronization demo
网络·人工智能·自动驾驶·carla
..过云雨3 小时前
15-2.【Linux系统编程】进程信号 - 信号保存(信号处理流程的三种状态:未决、阻塞、递达,信号保存由未决表完成、sigset_t信号集类型及相关函数)
linux·c++·后端·信号处理
林疏safe3 小时前
常见网络安全产品以及中国网络安全行业全景分析最新学习。
网络
Kiri霧3 小时前
Go Defer语句详解
java·服务器·golang
坏一点3 小时前
Yocto项目构建(3)——构建和部署树莓派镜像
linux·驱动开发·嵌入式硬件
脑壳疼___3 小时前
手写海康OpenApi签名规范,实现手动调用api(sdk:artemis-http-client)
网络·网络协议·http
Ronin3053 小时前
【Linux网络】多路转接select
linux·网络·select·多路转接
咕噜签名-铁蛋3 小时前
阿里云飞天操作系统:云时代的技术基石与创新引擎
服务器
zhuzewennamoamtf3 小时前
Linux设备树理解和应用
linux·运维·服务器