基于Tcp协议的应用层协议定制

前言:本文默认读者已掌握 TCP 协议相关网络接口知识,将聚焦于应用层协议的设计与剖析,有关底层通信机制及业务逻辑部分仅作简要概述,不再展开详述。

目录

服务器

一、通信

二、协议

1.序列化与反序列化

[2. 封包与解包](#2. 封包与解包)

三、业务

客户端

四、源码


本文将基于TCP协议构建一个网络计算器服务。业务逻辑相对弱化一些,目的是完整演示服务端开发的核心流程。而把重点放在应用层协议的设计过程,包括请求/响应报文结构定义、数据传输机制等关键实现细节;同时讲解Socket通信层的工程化封装,通过抽象连接建立、数据收发、资源管理等基础操作,构建可扩展的网络通信模块。本文形成的协议设计方法论与组件封装方案,可直接复用于各类TCP服务端开发场景。

服务器

框架设计

TCP协议作为面向字节流的传输层协议,其不维护报文边界的特点可能导致粘包/半包问题。为此我们需要设计应用层协议,通过报文头部标识、数据完整性校验等机制确保可靠通信。若您对以上内容很懵这很正常,到下文解决自定义协议时会细讲。

首先我们梳理本文要完成的核心文件及功能,如下:

1.通信

  • TcpServer.hpp:服务器相关的类以及类方法的实现------主要完成通信功能。
  • TcpServer.cc:服务器主函数(main)的实现------对服务器接口的调用,即启动服务器。
  • TcpClient.cc:客户端主函数(main)的实现------启动客户端,并与服务器通信。

2.协议

  • Protocol.hpp:自定义协议,完成序列化、反序列化、封包,解包等。

3.业务

  • NetCal.hpp:网络计算器的实现。

一、通信

由于C++标准库未原生提供网络通信支持,而网络编程中连接管理、数据收发等底层操作虽遵循固定模式却存在大量重复劳动,因此我们将首先实现一个高内聚的Socket封装类。该模块通过抽象TCP通信的核心流程,统一处理连接建立维护、收发数据等基础功能,为后续业务开发提供稳定可靠的通信基础设施。

这里我们使用模板方法模式,即基类Socket大部分方法都是纯虚方法,让Tcp协议类和Udp类作为子类进行继承。

创建Socket.hpp文件,实现类的声明,如下:

cpp 复制代码
static const int gbacklog = 8;//允许8个客户端连接
namespace SocketMoudule
{
    class Socket
    {
    public:
        virtual void SocketOrDie() = 0; //打开网络文件
        virtual void BindOrDie(uint16_t) = 0; //绑定端口
        virtual void ListenOrDie(int) = 0; //监听
        virtual shared_ptr<Socket> Accept(InetAddr *) = 0; //接收请求
        virtual void Close() = 0;    //关闭网络文件
        virtual int Recv(string *) = 0;    //收数据
        virtual int Send(string &) = 0;    //发数据
        virtual void Connect(const std::string &, uint16_t) = 0; //与服务器建立连接

    public:
        void BuildTcpServerMoudule(uint16_t port, int backlog = gbacklog)
        {
            //初始化
            SocketOrDie();
            BindOrDie(port);
            ListenOrDie(backlog);
        }
        void BuildUdpServerMoudule(uint16_t port)
        {
            void SocketOrDie();
            void BindOrDie(port);
        }
    };
    //Tcp协议通信
    class TcpSocket : public Socket
    {
    public:
    private:
        int _socketfd;
    };
    //Udp协议通信
    class UdpSocket : public Socket
    {
    public:
    private:
    };
}

其中InetAddr类是对sockaddr_in等相关信息的封装,如下:

cpp 复制代码
class InetAddr
{
public:
    InetAddr() {}
    InetAddr(sockaddr_in &peer)
        : _addr(peer)
    {
        _port = ntohs(peer.sin_port);
        char buffer[32];
        inet_ntop(AF_INET, &peer.sin_addr, buffer, sizeof(peer));
        _ip = buffer;
    }
    InetAddr(uint16_t port)
        : _port(port), _ip(to_string(INADDR_ANY))
    {
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = INADDR_ANY;
    }
    InetAddr(uint16_t port, string ip)
        : _port(port), _ip(ip)
    {
        _addr.sin_family = AF_INET;
        //主机序->网络序
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        //_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }
    string tostring_port()
    {
        return to_string(_port);
    }
    string tostring_ip()
    {
        return _ip;
    }
    bool operator==(InetAddr addr)
    {
        return _port == addr._port && _ip == addr._ip;
    }
    sockaddr *getaddr()
    {
        return (sockaddr *)&_addr;
    }
    socklen_t getlen()
    {
        return sizeof(_addr);
    }
    string stringaddr()
    {
        return tostring_ip() + ":" + tostring_port() + " ";
    }

private:
    uint16_t _port;
    string _ip;
    sockaddr_in _addr;
};

为什么要用OrDie后来缀命名呢?

OrDie :源自C/C++开发中的assert_or_die()等函数命名范式,表示关键资源必须成功初始化,否则程序应立即终止 的严格错误处理策略。常见于Google等公司的内部代码库(如ParseOrDie()),形成了一种防御性编程的文化符号

表示:"宁可程序立刻崩溃暴露问题,也不允许在错误状态下苟延残喘"。


注:以上接口基本没人能一次性想出来,想出来了参数也不一定设对,而应该在开发过程中根据需求或发现问题,而进行添加或修改。

函数具体实现很简单,一一调用对应的网络接口即可,这里就不再讲解,文末会给出源码。

完成TcpServer.hpp文件

创建TcpServer类,它主要完成打开网络文件,端口绑定,监听,接收请求。这些功能我们都在Socket中封装了,我们实例化出TcpSocket对象,然后对它的接口就行调用即可。

所以TcpServer类成员,要包含两个成员变量。

  • unique_ptr<Socket> _listensockptr:指向一个TcpSocket对象。
  • 回调数据处理方法:这个成员到后文再设计。

在实际中会有很多客户端与服务器进行连接,通常需要并发的处理客户端需求,可以使用多进程、多线程、线程池、进程池等。这里我们就做简单一点,使用多进程完成并发功能。

而子进程默认情况下是需要父进程进行等待的,这就会造成主进程阻塞,无法接收其他客户的请求,和单执行流没区别了。基于这样的问题有两种解决方法:

  1. SIGCHIL信号(17号)的处理方法设为默认。
  2. 主进程创建子进程a后再创建孙子进程b,此时a退出,让b进程去完成任务,b的父亲是a,a退出b进程成为了孤儿进程,会交给系统管理。主进程就不用管了。

如果不理解第1点,可以看一下文章,然后锁定到特殊信号的SIGCHIL信号进行学习:

Linux信号的诞生与归宿:内核如何管理信号的生成、阻塞和递达?_内核是如何产生信号的-CSDN博客z

这里我们用方法2解决,如下:

cpp 复制代码
using namespace SocketMoudule;
class TcpServer
{
public:
    TcpServer(uint16_t port)
        : _listensockptr(make_unique<TcpSocket>())
    {
        //进行初始化,同时启动服务器
        _listensockptr->BuildTcpServerMoudule(_port);
        Start();
    }
    void Start()
    {
        while(true)
        {
            InetAddr addr;
            auto sock = _listensockptr->Accept(&addr);
            if(sock == nullptr) continue;
            LOG(Level::INFO)<<addr.stringaddr()<<"accept success...";
            pid_t pid = fork();
            if(pid < 0)
            {
                LOG(Level::FATAL)<<"fork fail";
                exit(FORK_ERRO);
            }
            if(pid == 0)
            {
                if(fork()>0)
                    exit(SUCCESS);
                //回调方法
                //......
                sock->Close();
                exit(SUCCESS);
            }
            else
            {
                sock->Close();
            }
        }
    }
private:
    unique_ptr<Socket> _listensockptr;
    //回调函数
};

说明:FORK_ERRO、SUCCESS是退出码,本质是枚举类型,在文件Common.hpp中定义,LOG是我写的一个打印日志的接口, 大家把它当作cout理解就行,当然需要日志源码的可以私信我。

二、协议

TCP协议作为面向字节流的传输层协议,可能导致粘包/半包问题。

**粘包:**发送方连续发送多个独立数据包,接收方可能一次性读取到合并数据。比如发送6和7,接收到67。

**半包:**发送方传输大尺寸数据包,接收方首次读取到部分数据。比如发送hello,接收到he和llo。

1.序列化与反序列化

粘包问题

对于粘包问题,可以在数据之间添加一些标识符来区分它们,比如": ",当接收到完整的数据包后根据这些标识符就可以区分出它们。这个操作就是序列化和反序列化

数据包以什么格式传递是通信双方(服务器和客户端)约定好的,即协议。序列化和反序列化就是协议的一部分。

注意:TCP协议和UDP协议是传输层 协议,这里解决粘包/半包问题的是应用层协议,不要混淆。

我们的业务网络计数器,需要客户端传入两个运算对象和一个运算符。而服务器给客户端返回的是一个运算结果,和一个错误码(用来标识运算结果是否有效,比如除0或其他非法操作需要标识错误)。即有两类数据

在文件Protocol中创建两个类,Request和Response分别对这两类数据进行序列化和反序列化,如下:

cpp 复制代码
class Request
{
public:
    Request() {}
    Request(int x, int y, char oper)
        : _x(x), _y(y), _oper(oper)
    {}
    //序列化
    string Serialize();
    //反序列化
    void DeSerialize(string &message);
private:
    int _x;
    int _y;
    char _oper;
};
class Response
{
public:
    Response() {}
    Response(int result, int code)
        : _result(result), _code(code)
    {}
    string Serialize();
    void DeSerialize(string &message);

private:
    int _result;
    int _code;
};

序列化和反序列化的具体操作我们不用自己做,我们使用Json::Value,它JsonCpp库(最流行的C++ JSON处理库之一)的核心数据类型。

JsonCpp库的安装:

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

Jsoncpp 提供了多种方式进行序列化方法,如下:

方法 优点 缺点 适用场景
FastWriter 体积最小 无定制能力 机器间数据传输
StyledWriter 可读性强 性能较差 调试/配置文件
StreamWriterBuilder 可定制性强,官方推荐 配置稍复杂 所有生产环境
直接写文件流 适合大文件处理 需管理文件流 持久化存储

这里我们简单一点使用FastWrite,数据是以key:value的方式存储。如下:

cpp 复制代码
string Serialize()
{
    Json::Value data;
    data["x"] = _x;
    data["y"] = _y;
    data["oper"] = _oper;
    Json::FastWriter writer;
    return writer.write(data);
}

如果传入 8,7,* 被序列化为:

cpp 复制代码
{"oper":42,"x":7,"y":8}

反序列化:

Json::Reader用来把字符串转化为Json::Value类型,再从Json::Value中提取到各个元素,这个过程和反序列化很类似,其中要指明数据类型,如.asInt()。反序列化相当于对Request成员变量初始化。

代码示例:

cpp 复制代码
void DeSerialize(string &message)
{
    Json::Value data;
    Json::Reader reader;
    reader.parse(message, data);
    _x = data["x"].asInt();
    _y = data["y"].asInt();
    _oper = data["oper"].asInt();
}

对于Response同样,如下:

cpp 复制代码
class Response
{
public:
    Response() {}
    Response(int result, int code)
        : _result(result), _code(code)
    {}
    string Serialize()
    {
        Json::Value data;
        data["result"] = _result;
        data["code"] = _code;
        Json::FastWriter writer;
        return writer.write(data);
    }
    void DeSerialize(string &message)
    {
        Json::Value data;
        Json::Reader reader;
        reader.parse(message, data);
        _result = data["result"].asInt();
        _code = data["code"].asBool();
    }
private:
    int _result;
    int _code;
};

2. 封包与解包

半包问题

解决了粘包问题,但我们还需要知道能够判断报文是否完整,即处理半包问题。

对于半包问题,这里我们选择在数据包前加上一个报头,这个报头存储的是这个数据包有效载荷的长度 ,然后报头与数据包用"\r\n"区分开,在报文尾加"\r\n"用来区分下一个报文。这样的话我们可以通过报头知道这个报文一个有多长,然后去看报文有没有到达对应的长度,如果是则是完整的,如果不是,则就是不完整。这个过程我们称为封包和解包。

我们创建一个类Protocol来封装 封包、解包、请求处理、获取响应、请求构建等。

  • 封包:添加报头,即有效载荷的长度,用"\r\n"与报文分开。方便接收方识别报文的完整性。
  • 解包:判断报文的完整性,并移除报头。
  • 请求构建:对数据进行序列化,封包。
  • 获取响应:接收数据,解包,反序列化。
  • 请求处理:接收数据,解包,反序列化,业务处理(回调),序列化,封包,发送。

数据的处理实质就是传入一个Request,得到一个Response,所以我们定义一个函数类型:

  • using func_t = function<Response(Request &)>
cpp 复制代码
const string sep = "\r\n";
using func_t = function<Response(Request &)>;
class Protocol
{
public:
    Protocol() {};
    Protocol(func_t func)
        : _func(func)
    {}
    string EnCode(string &jsonstr);//封包
    bool DeCode(string &buffer, string *package);//解包
    string BuildRequestString(int x, int y, char oper);//构建请求报文
    void GetRequest(shared_ptr<Socket> &sock, InetAddr &addr);//请求处理
    bool GetResponse(shared_ptr<Socket> &sock, string *buffer, Response *rsp);//获取响应
private:
    func_t _func;
};
  • GetRequest:给服务器用的,即请求处理(业务处理),涉及数据收发,所以传入Socket指针和客户端地址信息InetAddr。
  • GetResponse:给客户端使用,用来获取数据处理结果,涉及数据接收,所以传入客户端的Socket指针,输出型参数buffer(缓冲区)和rsp。注意因为接收到的报文可能不完整,不能一次取到报文,所以需要缓冲区来保留数据。

还记得在TcpServer里我们缺少的成员变量数据处理函数吗?现在我们知道它是谁了,即:

  • void GetRequest(shared_ptr<Socket> &sock, InetAddr &addr);

在TcpServer.hpp中声明一个类型:

  • using func_t = function<void(shared_ptr<Socket> &sock, InetAddr)>;

然后添加成员变量func_t _func,并在构造函数的参数列表进行初始化。

EnCode

封包 = 报文长度+"\r\n"+报文+"\r\n"。如下:

cpp 复制代码
string EnCode(string &jsonstr)
{
    size_t len = jsonstr.size();
    return to_string(len) + sep + jsonstr + sep;
}

DeCode

  1. 从缓冲区找到标识符"\r\n",如果找不到,说明报文不完整,返回false。
  2. 从缓冲区找到标识符"\r\n"后,提取报头并算出完整报文的长度。如果大于缓冲区长度,说明缓冲区不够一个完整报文的长度,返回false。
  3. 走到这里说明能取到一个完整的报文,然后把有效载荷提取出来,为方便下次提取,删除缓冲区一个报文的长度。返回true。

如下:

cpp 复制代码
bool DeCode(string &buffer, string *package)
{
    int pos = buffer.find(sep);
    if (pos == string::npos)
        return false;
    string lenStr = buffer.substr(0, pos);
    int lenTarget = lenStr.size() + stoi(lenStr) + 2 * sep.size();
    if (buffer.size() < lenTarget)
        return false;
    *package = buffer.substr(pos + sep.size(), stoi(lenStr));
    buffer.erase(0, lenTarget);
    return true;
}

BuildRequestString

构建请求报文,即对数据进行序列化和封包,如下:

cpp 复制代码
string BuildRequestString(int x, int y, char oper)
{
    Request req(x, y, oper);
    string json_str = req.Serialize();
    return EnCode(json_str);
}

GetRequest

得到并处理请求,对我们刚才写的方法进行组合,即接收数据,解包,反序列化,业务处理,序列化,封包,发送数据。如下:

cpp 复制代码
void GetRequest(shared_ptr<Socket> &sock, InetAddr &addr)
{
    while (true)
    {
        string json_package; //接收数据
        int n = sock->Recv(&json_package);
        if (n == 0)
        {
            LOG(Level::INFO) << "client " << addr.tostring_ip() << " exit";
            break;
        }
        else if (n < 0)
        {
            LOG(Level::WARING) << "Recv fail";
            break;
        }
        else
        {
            // 解报包
            string json_str;
            while (DeCode(json_package, &json_str))
            {
                // 反序列化
                Request req;
                req.DeSerialize(json_str);
                // 业务处理
                Response resp = _func(req);
                // 序列化
                string send_str = resp.Serialize();
                // 加报头
                send_str = EnCode(send_str);
                // 发送
                sock->Send(send_str);
            }
        }
    }
}

注意把解包过程写成循环,因为一次性也有可能读到多个完整报文,需要把它们都读取出来。

GetResponse

接收数据,解包,反序列化,最后数据是通过输出型参数带回的。

cpp 复制代码
bool GetResponse(shared_ptr<Socket> &sock, string *buffer, Response *rsp)
{
    while (true)
    {
        int n = sock->Recv(buffer);
        if (n == 0)
        {
            LOG(Level::WARING) << "server exit";
            return false;
        }
        else if (n < 0)
        {
            LOG(Level::WARING) << "client Recv fail";
            return false;
        }
        else
        {
            // 解包
            string json_str;
            if (!DeCode(*buffer, &json_str)) continue;
            // 反序列化
            rsp->DeSerialize(json_str);
            return true;
        }
    }
}

三、业务

业务处理部分大家可以自行设定,即实现一个**function<Response(Request &)>**类型的函数,最好封装一个类来维护。这里做一个简单的计算器来充当一个,如下:

cpp 复制代码
class NetCal
{
public:
    Response Execute(Request &req);
private:
};

具体实现在文末源码给出。

TcpServer.cc

做完上面的一切我们就可以来完成服务器主函数main了。

首先需要程序外部传入端口号,所以main函数需要传入命令行参数。需要检测格式的正确性。

其次我们创建业务类对象,协议类对象,通信类对象,并++把回调方法一层一层的往下传++,如下:

cpp 复制代码
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        LOG(Level::FATAL) << "Usage server port";
        exit(USAGE_ERRO);
    }
    // 务业
    unique_ptr<NetCal> nc(make_unique<NetCal>());
    // 协议
    unique_ptr<Protocol> pt(make_unique<Protocol>([&](Request &req) -> Response
                                                  { return nc->Execute(req); }));
    // 通通信
    unique_ptr<TcpServer> ts(make_unique<TcpServer>(stoi(argv[1]), [&](shared_ptr<Socket> sock, InetAddr addr)
                                                    { pt->GetRequest(sock, addr); }));
    return 0;
}

客户端

  1. 同样的需要传入命令行参数来指定服务器的IP和端口号,需要检查格式。
  2. 创建Socket类对象,打开网络文件和与服务器进行连接。
  3. 创建协议对象和用来接收返回结果的缓冲区。
  4. 做一个死循环,进行构建请求,发送请求,接收响应,输出结果。如下:
cpp 复制代码
inline void GetDatafromstdin(int *x, int *y, char *oper)
{
    cout << "Please Enter x:";
    cin >> *x;
    cout << "Please Enter oper:";
    cin >> *oper;
    cout << "Please Enter y:";
    cin >> *y;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        LOG(Level::FATAL) << "Usage serever's ip and port";
        exit(USAGE_ERRO);
    }
    //构建Socket类,并创建套接字,与服务器连接。
    shared_ptr<Socket> client = make_shared<TcpSocket>();
    client->SocketOrDie();
    client->Connect(argv[1], stoi(argv[2]));
    //创建协议类对象
    unique_ptr<Protocol> ptl = make_unique<Protocol>();
    string buffer;
    while (true)
    {
        //读取输入并构建请求
        int x, y;
        char oper;
        GetDatafromstdin(&x, &y, &oper);
        string send_str = ptl->BuildRequestString(x, y, oper);
        //发送请求
        client->Send(send_str);
        //接收响应
        Response rsp;
        if (!ptl->GetResponse(client, &buffer, &rsp))
            break;
        //结果展示
        rsp.ShowResult();
    }
    return 0;
}

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!💕💕

四、源码

TcpServer.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <unistd.h>
#include <memory>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
using namespace my_log;
using namespace SocketMoudule;
using ioservice_t = function<void(shared_ptr<Socket> &socket, InetAddr &addr)>;
class TcpServer
{
public:
    TcpServer(uint16_t port, ioservice_t service)
        :_service(service),_listensockptr(make_unique<TcpSocket>())
    {
        _listensockptr->BuildTcpServerMoudule(port);
        Start();
    }
    void Start()
    {
        while(true)
        {
            InetAddr addr;
            auto sock = _listensockptr->Accept(&addr);
            if(sock == nullptr) continue;
            LOG(Level::INFO)<<addr.stringaddr()<<"accept success...";
            pid_t pid = fork();
            if(pid < 0)
            {
                LOG(Level::FATAL)<<"fork fail";
                exit(FORK_ERRO);
            }
            if(pid == 0)
            {
                if(fork()>0)
                    exit(SUCCESS);
                _service(sock,addr);
                sock->Close();
                exit(SUCCESS);
            }
            else
            {
                sock->Close();
            }
        }
    }

private:
    ioservice_t _service;
    unique_ptr<Socket> _listensockptr;
};

TcpServer.cc

cpp 复制代码
#include "TcpServer.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"
#include "NetCal.hpp"
using namespace SocketMoudule;
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        LOG(Level::FATAL) << "Usage server port";
        exit(USAGE_ERRO);
    }
    // 务业
    unique_ptr<NetCal> nc(make_unique<NetCal>());
    // 协议
    unique_ptr<Protocol> pt(make_unique<Protocol>([&](Request &req) -> Response
                                                  { return nc->Execute(req); }));
    // 通通信
    unique_ptr<TcpServer> ts(make_unique<TcpServer>(stoi(argv[1]), [&](shared_ptr<Socket> sock, InetAddr addr)
                                                    { pt->GetRequest(sock, addr); }));
    return 0;
}

TcpClient.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "Common.hpp"
#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"
using namespace my_log;
using namespace SocketMoudule;
inline void GetDatafromstdin(int *x, int *y, char *oper)
{
    cout << "Please Enter x:";
    cin >> *x;
    cout << "Please Enter oper:";
    cin >> *oper;
    cout << "Please Enter y:";
    cin >> *y;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        LOG(Level::FATAL) << "Usage serever's ip and port";
        exit(USAGE_ERRO);
    }
    shared_ptr<Socket> client = make_shared<TcpSocket>();
    unique_ptr<Protocol> ptl = make_unique<Protocol>();
    client->SocketOrDie();
    client->Connect(argv[1], stoi(argv[2]));
    string buffer;

    while (true)
    {

        int x, y;
        char oper;
        GetDatafromstdin(&x, &y, &oper);
        string send_str = ptl->BuildRequestString(x, y, oper);
        client->Send(send_str);
        Response rsp;
        if (!ptl->GetResponse(client, &buffer, &rsp))
            break;
        rsp.ShowResult();
    }
    return 0;
}

Socket.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <memory>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace my_log;
static const int gbacklog = 8;
namespace SocketMoudule
{
    class Socket
    {
    public:
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t) = 0;
        virtual void ListenOrDie(int) = 0;
        virtual shared_ptr<Socket> Accept(InetAddr *) = 0;
        virtual void Close() = 0;
        virtual int Recv(string *) = 0;
        virtual int Send(string &) = 0;
        virtual void Connect(const std::string &, uint16_t) = 0;

    public:
        void BuildTcpServerMoudule(uint16_t port, int backlog = gbacklog)
        {
            SocketOrDie();
            BindOrDie(port);
            ListenOrDie(backlog);
        }
        void BuildUdpServerMoudule()
        {
            void SocketOrDie();
            void BindOrDie();
        }
    };
    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int socketfd = -1)
            : _socketfd(socketfd)
        {
        }
        virtual void SocketOrDie() override
        {
            _socketfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_socketfd < 0)
            {
                LOG(Level::FATAL) << "Socket fail";
                exit(SOCKET_ERRO);
            }
            LOG(Level::INFO) << "socket success";
        }
        virtual void BindOrDie(uint16_t port) override
        {
            InetAddr addr(port);
            int n = bind(_socketfd, addr.getaddr(), addr.getlen());
            if (n < 0)
            {
                LOG(Level::FATAL) << "bind fail";
                exit(BIND_ERRO);
            }
            LOG(Level::INFO) << "Bind success";
        }
        virtual void ListenOrDie(int backlog) override
        {
            int n = listen(_socketfd, backlog);
            if (n < 0)
            {
                LOG(Level::FATAL) << "Listen fail";
                exit(LISTEN_ERRO);
            }
            LOG(Level::INFO) << "Listen success";
        }
        virtual shared_ptr<Socket> Accept(InetAddr *addr) override
        {
            // 为什么不直接用addr,因为构造不了IP。
            socklen_t len = sizeof(sockaddr_in);
            sockaddr_in peer;
            int n = accept(_socketfd, (sockaddr *)&peer, &len);
            if (n < 0)
            {
                LOG(Level::WARING) << addr->tostring_ip() << "accept fail";
                return nullptr;
            }
            *addr = InetAddr(peer);
            return make_shared<TcpSocket>(n);
        }
        virtual void Close() override
        {
            close(_socketfd);
        }
        virtual int Recv(string *out) override
        {
            char buffer[1024];
            int n = read(_socketfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = '\0';
                *out += buffer;
            }
            return n;
        }
        virtual int Send(string &message) override
        {
            return write(_socketfd, message.c_str(), message.size());
        }
        virtual void Connect(const std::string &ip, uint16_t port) override
        {
            InetAddr addr(port, ip);
            int n = connect(_socketfd, addr.getaddr(), addr.getlen());
            if (n < 0)
            {
                LOG(Level::FATAL) << "connect fail";
                exit(CONNECT_ERRO);
            }
            LOG(Level::INFO) << "connect success";
        }

    private:
        int _socketfd;
    };
    
    class UdpSocket : public Socket
    {
    public:
        //......
    private:
        int _socketfd;
    };
}

InteAddr.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
using namespace std;
class InetAddr
{
public:
    InetAddr() {}
    InetAddr(sockaddr_in &peer)
        : _addr(peer)
    {
        _port = ntohs(peer.sin_port);
        char buffer[32];
        inet_ntop(AF_INET, &peer.sin_addr, buffer, sizeof(peer));
        _ip = buffer;
    }
    InetAddr(uint16_t port)
        : _port(port), _ip(to_string(INADDR_ANY))
    {
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = INADDR_ANY;
    }
    InetAddr(uint16_t port, string ip)
        : _port(port), _ip(ip)
    {
        _addr.sin_family = AF_INET;
        //主机序->网络序
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        //_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }
    string tostring_port()
    {
        return to_string(_port);
    }
    string tostring_ip()
    {
        return _ip;
    }
    bool operator==(InetAddr addr)
    {
        return _port == addr._port && _ip == addr._ip;
    }
    sockaddr *getaddr()
    {
        return (sockaddr *)&_addr;
    }
    socklen_t getlen()
    {
        return sizeof(_addr);
    }
    string stringaddr()
    {
        return tostring_ip() + ":" + tostring_port() + " ";
    }

private:
    uint16_t _port;
    string _ip;
    sockaddr_in _addr;
};

Protocol.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
#include <functional>
#include "Socket.hpp"
#include "InetAddr.hpp"
using namespace SocketMoudule;
using namespace std;
class Request
{
public:
    Request() {}
    Request(int x, int y, char oper)
        : _x(x), _y(y), _oper(oper)
    {
    }
    string Serialize()
    {
        Json::Value data;
        data["x"] = _x;
        data["y"] = _y;
        data["oper"] = _oper;
        Json::FastWriter writer;
        cout<<writer.write(data);
        return writer.write(data);
    }
    void DeSerialize(string &message)
    {
        Json::Value data;
        Json::Reader reader;
        reader.parse(message, data);
        _x = data["x"].asInt();
        _y = data["y"].asInt();
        _oper = data["oper"].asInt();
    }
    int X() { return _x; }
    int Y() { return _y; }
    char Oper() { return _oper; }

private:
    int _x;
    int _y;
    char _oper;
};
class Response
{
public:
    Response() {}
    Response(int result, int code)
        : _result(result), _code(code)
    {
    }
    string Serialize()
    {
        Json::Value data;
        data["result"] = _result;
        data["code"] = _code;
        Json::FastWriter writer;
        return writer.write(data);
    }
    void DeSerialize(string &message)
    {
        Json::Value data;
        Json::Reader reader;
        reader.parse(message, data);
        _result = data["result"].asInt();
        _code = data["code"].asBool();
    }
    void ShowResult()
    {
        cout << "result[" << _result << "]:code[" << _code << "]" << endl;
    }
    int Result() { return _result; }
    bool Code() { return _code; }
    void SetResult(int ret) { _result = ret; }
    void SetCode(int f) { _code = f; }

private:
    int _result;
    int _code;
};
const string sep = "\r\n";
using func_t = function<Response(Request &)>;
class Protocol
{
public:
    Protocol() {};
    Protocol(func_t func)
        : _func(func)
    {
    }
    string EnCode(string &jsonstr)
    {
        size_t len = jsonstr.size();
        return to_string(len) + sep + jsonstr + sep;
    }
    bool DeCode(string &buffer, string *package)
    {
        int pos = buffer.find(sep);
        if (pos == string::npos)
            return false;
        string lenStr = buffer.substr(0, pos);
        int lenTarget = lenStr.size() + stoi(lenStr) + 2 * sep.size();
        if (buffer.size() < lenTarget)
            return false;
        *package = buffer.substr(pos + sep.size(), stoi(lenStr));
        buffer.erase(0, lenTarget);
        return true;
    }
    void GetRequest(shared_ptr<Socket> &sock, InetAddr &addr)
    {
        while (true)
        {
            string json_package; //?
            int n = sock->Recv(&json_package);
            if (n == 0)
            {
                LOG(Level::INFO) << "client " << addr.tostring_ip() << " exit";
                break;
            }
            else if (n < 0)
            {
                LOG(Level::WARING) << "Recv fail";
                break;
            }
            else
            {
                // 解报包
                string json_str;
                while (DeCode(json_package, &json_str))
                {
                    // 反序列化
                    Request req;
                    req.DeSerialize(json_str);
                    // 处理
                    Response resp = _func(req);
                    // 序列化
                    string send_str = resp.Serialize();
                    // 加报头
                    send_str = EnCode(send_str);
                    // 发送
                    sock->Send(send_str);
                }
            }
        }
    }
    bool GetResponse(shared_ptr<Socket> &sock, string *buffer, Response *rsp)
    {
        while (true)
        {
            int n = sock->Recv(buffer);
            if (n == 0)
            {
                LOG(Level::WARING) << "server exit";
                return false;
            }
            else if (n < 0)
            {
                LOG(Level::WARING) << "client Recv fail";
                return false;
            }
            else
            {
                // 解包
                string json_str;
                if (!DeCode(*buffer, &json_str)) continue;
                // 反序列化
                rsp->DeSerialize(json_str);
                return true;
            }
        }
    }
    string BuildRequestString(int x, int y, char oper)
    {
        Request req(x, y, oper);
        string json_str = req.Serialize();
        return EnCode(json_str);
    }

private:
    func_t _func;
};

NetCal.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
#include "Protocol.hpp"
class NetCal
{
public:
    Response Execute(Request &req)
    {
        Response resp(0, 0);
        switch (req.Oper())
        {
        case '+':
            resp.SetResult(req.X() + req.Y());
            break;
        case '-':
            resp.SetResult(req.X() - req.Y());
            break;
        case '*':
            resp.SetResult(req.X() * req.Y());
            break;
        case '/':
            if (req.Y() == 0)
                resp.SetCode(1);
            else
                resp.SetResult(req.X() / req.Y());
            break;
        case '%':
            if (req.Y() == 0)
                resp.SetCode(2);
            else
                resp.SetResult(req.X() % req.Y());
        default:
            resp.SetCode(3);
            break;
        }
        return resp;
    }
private:
};
相关推荐
Herbig2 分钟前
服务器上安装node
linux·node.js
敖云岚23 分钟前
【Linux】Centos7 安装 Docker 详细教程
linux·运维·服务器
北陌宝宝36 分钟前
Jenkins:开启高效软件开发的魔法之门
运维·jenkins
JhonKI44 分钟前
【Linux网络】构建HTTP响应与请求处理系统 - HttpResponse从理解到实现
linux·网络·http
虾球xz1 小时前
游戏引擎学习第246天:将 Worker 上下文移到主线程创建
c++·学习·游戏引擎
猩猩—点灯1 小时前
《TCP/IP详解 卷1:协议》之第七、八章:Ping && Traceroute
网络·tcp/ip
CHTXRT1 小时前
2025第十六届蓝桥杯大赛(软件赛)网络安全赛 Writeup
c语言·网络·web安全·网络安全·蓝桥杯·wireshark
纪元A梦1 小时前
华为OD机试真题——绘图机器(2025A卷:100分)Java/python/JavaScript/C++/C/GO最佳实现
java·javascript·c++·python·华为od·go·华为od机试题
文牧之1 小时前
PostgreSQL 常用日志
运维·数据库·postgresql
蓝莓味柯基1 小时前
DevOps:概念与学习路径
运维·学习·devops