当前位置: 首页 > news >正文

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

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

目录

服务器

一、通信

二、协议

1.序列化与反序列化

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文件,实现类的声明,如下:

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等相关信息的封装,如下:

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解决,如下:

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分别对这两类数据进行序列化和反序列化,如下:

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库的安装:

ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

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

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

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

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

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

{"oper":42,"x":7,"y":8}

 反序列化:

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

代码示例:

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同样,如下:

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 &)>
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”。如下:

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。

如下:

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

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

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

 GetRequest

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

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

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

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 &)>类型的函数,最好封装一个类来维护。这里做一个简单的计算器来充当一个,如下:

class NetCal
{
public:Response Execute(Request &req);
private:
};

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

TcpServer.cc

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

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

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

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. 做一个死循环,进行构建请求,发送请求,接收响应,输出结果。如下:
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;
}

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

四、源码

TcpServer.hpp

#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

#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

#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

#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

#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

#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

#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);elseresp.SetResult(req.X() / req.Y());break;case '%':if (req.Y() == 0)resp.SetCode(2);elseresp.SetResult(req.X() % req.Y());default:resp.SetCode(3);break;}return resp;}
private:
};

相关文章:

  • Flask + ajax上传文件(三)--图片上传与OCR识别
  • 安服实习面试面经总结(也适合hvv蓝初)
  • 坚果派已适配的鸿蒙版flutter库【持续更新】
  • 什么是Lua模块?你会如何使用NGINX的Lua模块来定制请求处理流程?
  • 从“拼凑”到“构建”:大语言模型系统设计指南!
  • 【开源】基于51单片机的温湿度检测报警系统
  • WPF实现类似Microsoft Visual Studio2022界面效果及动态生成界面技术
  • 矫平机终极指南:特殊材料处理、工艺链协同与全球供应链管理
  • AI日报 - 2025年04月26日
  • 嵌入式学习笔记 - HAL_xxx_MspInit(xxx);函数
  • Prometheus、Zabbix和Nagios针对100个节点的部署设计架构图
  • Python基于Django的全国二手房可视化分析系统【附源码】
  • 2025第十六届蓝桥杯大赛(软件赛)网络安全赛 Writeup
  • 推荐三款GitHub上高星开源的音乐搜索平台
  • proxychains4系统代理for linux(加速国内github下载速度,pip安装)
  • Arm GICv3中断处理模型解析
  • Linux网络编程 原始套接字与ARP协议深度解析——从数据包构造到欺骗攻防
  • browser-use:AI驱动的浏览器自动化工具使用指南
  • 二叉树的遍历(深度优先搜索)
  • 基于AI技术的高速公路交通引流系统设计与应用研究
  • 热点问答|第三轮间接谈判结束,美伊分歧还有多大?
  • 哈马斯官员:只要以军持续占领,哈马斯就不会放下武器
  • 伊朗港口爆炸事件已致195人受伤
  • 新城市志|中国消费第一城,迎来“补贴力度最大”购物节
  • 俄总统助理:普京与美特使讨论了恢复俄乌直接谈判的可能性
  • 金隅集团:今年拿地将选择核心热门地块,稳健审慎投资