【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)并将计算结果序列化返回。

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux