网络计算器(2)增加客户端运行+接口总结

🎬 胖咕噜的稞达鸭个人主页
🔥 个人专栏 : 《数据结构《C++初阶高阶》
《Linux系统学习》
《算法日记》

⛺️技术的杠杆,撬动整个世界!


完整代码请移步我的gitee:
网络计算器完整代码

protocol.hpp中解决序列反序列化问题,数据粘报

应用在网络计算器的项目中:

定义在协议中:protocol.hpp

cpp 复制代码
std::string Serialize()//序列化
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        std::string s = writer.write(root);
        return s;

    }
    //{"x":10, "y" : 20}
    bool Deserialize(std::string &in)//反序列化
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in,root);//将收到in的数据反序列到root中
        if(ok)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();//作为一个整数
        }
        return ok;
    }
cpp 复制代码
std::string Serialize()//序列化
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        std::string s = writer.write(root);
        return s;

    }
    //{"x":10, "y" : 20}
bool Deserialize(std::string &in)//反序列化
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in,root);//将收到in的数据反序列到root中
        if(ok)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asString();
        }
        return ok;
    }

数据粘报问题---> 由程序员解决。

定义一个jsonstring串:数据长度+数据信息

bash 复制代码
content_len jsonstring
//50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n
//50
//{"x" : 10, "y" : 20, "oper" : '+'}

完整的报文:一个jsonstring串+jsonstring串的长度,长度报头和有效载荷之间用特殊字符串区分开。

jsonstring串的长度,我们就知道了Tcp面向字节流,发送的数据有多少个,我们应当接收多少个,不至于拿到不完整的报文。

如何具体解决:

以下面代码为例具体实现:

cpp 复制代码
   std::string Encode(const std::string jsonstr)//这一个函数用途:把一个jsonstring字符串分为包含长度和有效载荷的数据
    {
        std::string len = std::to_string(jsonstr.size());//计算数据报文有效载荷的长度
        //50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n
        /* std::string package =  len + sep + jsonstr + sep; */
        return len + sep + jsonstr + sep;//应用层封装报头
    }

    //50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n     :     package_len_str.size() + package_len_int + 2 * sep.size();
    //用途:1.判断报文的完整性
    //      2.如果包含至少一个完整请求,提取出来,方便处理下一个
    //提取到的数据放到buffer中
    bool Decode(std::string &buffer, std::string *package)
    {
        ssize_t pos = buffer.find(sep);//找到第一个分隔符
        if(pos == std::string::npos)//报文中没有找到sep分隔符
        {
            return false;//不完整报文,让调用方继续从内核中取出数据
        }
        //走到这一步就说明:有长度+分隔符
        std::string package_len_str = buffer.substr(0,pos);
        int package_len_int = std::stoi(package_len_str);//计算长度
        //buffer一定有长度,不一定有完整的报文
        int target_len = package_len_str.size() + package_len_int + 2 * sep.size();//整个buffer的长度必须大于target_len
        if(buffer.size() < target_len)
        {
            //buffer不完整
            return false;
        }
        //走到这一步说明BUffer中一定至少有一个完整的报文
        *package = buffer.substr(pos + sep.size(),package_len_int);//从buffer串中提取出来我们想要的jsonstring
        buffer.erase(0,target_len);
        return true;

    }

获取请求:在协议中要实现:接收到信息

  1. 读取:在解析报文,获取到完整的json请求(*out += buffer,socket.hpp中封装的含义,就是不断提取,直到完整且符合要求的字符串才开始执行下面的代码)。
  2. 拿到完整报文之后需要反序列化。
  3. 拿取完了而且也反序列化解析出来了,此时就需要执行业务。业务不属于协议,在外面定义一个NetCal.hpp来实现我们的网络计算器的代码。
cpp 复制代码
//获取请求
    void GetRequest(std::shared_ptr<Socket> &sock,InetAddr &client)
    {
        //读取:需要通过套接字读取
        //定义一个缓冲区
        std::string inbuffer;
        while(true)
        {
            int n = sock->Recv(&inbuffer);//用Recv读取
            if(n > 0)
            {
                //1.解析报文,提取完整的json请求,不完整就让服务器继续读取
                bool ret = Decode(inbuffer, &json_package);
                if(!ret)//流式字符串中不存在一个完整的报文,不做处理,继续读报文:所以socket.hpp中封装的是*out += buffer
                    continue;
                
                //2.此时走到这里,一定拿到了一个完整的报文,提取出来了一个完整的json串,而且也从队列中移除了
                //50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n   此时拿到了字符串,对方发给我们的是一个请求字符串,我们要做反序列化
                Request req;
                bool ok = req.Deserialize(json_package);
                if(!ok)
                    continue;//报文不符合要求,继续拿取

                //3.此时我们拿到的是一个内部属性已经被设置了的req了
                //通过req 得到 resp,此时要完成计算功能
                //这一步的计算功能属于业务级别了,不属于协议的内容,所以就在外面实现一个函数
            }
            else if(n == 0)
            {
                LOG(LogLevel::INFO) << "client: " << client.StringAddr() << "Quit!";
                break;
            }
            else 
            {
                LOG(LogLevel::WARNING) << "client: " << client.StringAddr() << "recv error!";
                break;
            }
            /* sock->Recv(&inbuffer);//用Recv读取
            //此时要对读取到的数据进行处理,不然会越来越长
            std::cout << "inbuffer:\n" << inbuffer << std::endl; */  
        }
        sock->Close();
    }

socket.hpp中封装实现数据读取和发送

当实现了序列化反序列化,将数据封装解包,此时就需要读取数据。读取的时候我们用套接字读取。所以在socket.hpp中封装。

cpp 复制代码
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string out) = 0;

recv接口:接收信息

recv是专门用来做套接字的读取封装的。与read使用方式多了一个int flags。此处我们设置为0.
send接口:发送信息

cpp 复制代码
int Send(const std::string &message)override{
   return send(_sockfd,message.c_str(),message.size(),0);
   //在套接字中写,写message.c_str(),写入message.size()个长度,标记位为0
}

独立封装一个文件实现计算器业务NetCal.hpp:+ - * / %

cpp 复制代码
#pragma once

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

class Cal 
{
public:
    Response Execute(Request &req)
    {
        Response resp(0,0);//code 表示成功
        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());
            }  
        }
        break;
        default:
            resp.SetCode(3);//非法错误
            break;    
        }

        return resp;
    }
};

增加客户端TcpClient.cc

客户端代码的雏形:终端输入+套接字建立+链接+发送给服务端

cpp 复制代码
class Socket内public:
    virtual int Connect(const std::string &server_ip, uint16_t port ) = 0;//客户端用,发起连接的请求
    void BuildTcpClientSocketMethod()//创建套接字
    {
        SocketOrDie();
    }
Tcpsocket派生类:
virtual int Connect(const std::string &server_ip, uint16_t port )override //实现客户端连接服务端
{
    InetAddr server(server_ip,port);
    return ::connet(_sockfd,server.NetAddrPtr(),server.NetAddrLen());
}
cpp 复制代码
include <iostream>
#include "Common.hpp"
#include "Socket.hpp"
#include <string>
#include <memory>

void Usage()
{
    std::cerr << "Usage: " << proc << "server_ip server_port" << std::endl;
}
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    //创建一个套接字
    std::unique_ptr<Socket> client = std::make_unique<TcpSocket>();
    client->BuildTcpClientSocketMethod();

    /* client->Connect(server_ip,server_port);//发起建立链接的请求 */
    if(client->Connect(server_ip,server_port) == 0)
    {
        //成功
        client->Send()
    }
}

从标准输入中获取数据

cpp 复制代码
void GetDataFromStdin(int *x,int *y,char *oper)
{
    std::cout << "Please Enter x:";
    std::cin >> *x;
    std::cout << "Please Enter y:";
    std::cin >> *y;
    std::cout << "Please Enter oper:";
    std::cin >> *oper;
}

从标准输入中获取到数据,我们还想这个数据是可以直接使用的,所以最好将请求端的字符串实现到协议中:Protocol.hpp

cpp 复制代码
//获取请求
    void GetRequest(std::shared_ptr<Socket> &sock,InetAddr &client)
    {
        //读取:需要通过套接字读取
        //定义一个缓冲区
        std::string inbuffer;
        while(true)
        {
            int n = sock->Recv(&inbuffer);//用Recv读取
            if(n > 0)
            {
                std::cout << "..............request_buffer......" << std::endl;
                std::cout << inbuffer << std::endl;
                std::cout << "..................................." << std::endl;

                std::string json_package;
                //1.解析报文,提取完整的json请求,不完整就让服务器继续读取
                bool ret = Decode(inbuffer, &json_package);
                if(!ret)//流式字符串中不存在一个完整的报文,不做处理,继续读报文:所以socket.hpp中封装的是*out += buffer
                    continue;
                
                //2.此时走到这里,一定拿到了一个完整的报文,提取出来了一个完整的json串,而且也从队列中移除了
                //50\r\n{"x" : 10, "y" : 20, "oper" : '+'}\r\n   此时拿到了字符串,对方发给我们的是一个请求字符串,我们要做反序列化
                Request req;
                bool ok = req.Deserialize(json_package);
                if(!ok)
                    continue;//报文不符合要求,继续拿取

                //3.此时我们拿到的是一个内部属性已经被设置了的req了
                //通过req 得到 resp,此时要完成计算功能
                Response resp = _func(req);//计算完成之后有resp接收

                //4.返回给客户端:序列化
                std::string json_str = resp.Serialize();

                //5.添加自定义长度
                std::string send_str = Encode(json_str);//此时就是一个携带长度的报文了

                //6.直接发送
                sock->Send(send_str);
            }
            else if(n == 0)
            {
                LOG(LogLevel::INFO) << "client: " << client.StringAddr() << "Quit!";
                break;
            }
            else 
            {
                LOG(LogLevel::WARNING) << "client: " << client.StringAddr() << "recv error!";
                break;
            }
            /* sock->Recv(&inbuffer);//用Recv读取
            //此时要对读取到的数据进行处理,不然会越来越长
            std::cout << "inbuffer:\n" << inbuffer << std::endl; */  
        }
        
    }

总结

1. 基础封装模块(Socket.hpp)

核心接口及逻辑
  • Socket基类纯虚接口 :定义统一通信规范,屏蔽底层差异
    • SocketOrDie():创建套接字(调用::socket),失败则日志打印并退出,确保套接字创建成功。
    • BindOrDie(uint16_t port):绑定端口(调用::bind),关联本地地址与套接字,失败则日志+退出。
    • ListenOrDie(int backlog):开启监听(调用::listen),设置等待连接队列大小,失败则日志+退出。
    • Accept(InetAddr *client):接收客户端连接(调用::accept),获取客户端地址并返回新连接套接字的智能指针。
    • Recv(std::string *out):纯虚接口,从套接字读取数据,将字节流存入out缓冲区(本质是内核缓冲区到用户内存的拷贝)。
    • Send(const std::string out):纯虚接口,发送数据(调用::send),将out中的数据拷贝到内核发送缓冲区。
    • Connect(const std::string &server_ip, uint16_t port):客户端专属接口,发起TCP连接(调用::connect),关联服务端IP和端口。
派生类(TcpSocket)实现逻辑
  • 继承Socket基类,实现所有纯虚接口,内部维护_sockfd(套接字文件描述符),统一管理连接生命周期。
  • 提供BuildTcpSocketMethod()(服务端)和BuildTcpClientSocketMethod()(客户端),封装套接字创建、绑定、监听/连接的流程,简化调用。

2. 协议模块(Protocol.hpp)

核心结构与接口
  • Request类(请求端):封装客户端计算请求数据

    • Serialize():将请求参数(_x_y_oper)序列化为JSON字符串(通过Json库的FastWriter),便于网络传输。
    • Deserialize(std::string &in):将接收的JSON字符串反序列化为Request对象,提取xyoper赋值给成员变量,失败返回false
  • Response类(响应端):封装服务端计算结果

    • Serialize():将计算结果(_x_y_oper_result_code)序列化为JSON字符串,用于回传客户端。
    • Deserialize(std::string &in):将服务端返回的JSON字符串反序列化为Response对象,提取结果和状态码。
  • 粘包解决方案接口

    • Encode(const std::string jsonstr):给JSON字符串添加长度报头和分隔符(格式:长度+sep+JSON串+sep),明确报文边界。
    • Decode(std::string &buffer, std::string *package):解析缓冲区数据,通过分隔符提取长度报头,判断是否为完整报文;完整则提取JSON串,移除缓冲区中已处理部分,不完整则返回false

3. 业务逻辑模块(NetCal.hpp)

核心接口及逻辑
  • Cal类的Execute(Request &req):实现计算器核心业务
    • 接收Request对象,根据oper+-*/%)执行对应运算。
    • 处理异常情况:除零/模零错误设对应状态码(1/2),非法运算符设状态码(3),正常则设置计算结果。
    • 返回Response对象,包含原始参数、运算结果和状态码。

4. 服务端模块(TcpServer.hpp)

核心接口及逻辑
  • ioservice_t回调函数:std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>
    • 实现业务解耦,TcpServer框架不关心具体业务,通过该回调将连接和客户端地址传递给业务逻辑(如GetRequest)。
  • GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client):服务端请求处理流程
    1. 循环调用sock->Recv(&inbuffer)读取数据,存入缓冲区。
    2. 调用Decode解析缓冲区,提取完整JSON报文。
    3. 调用Request::Deserialize将JSON串转为请求对象。
    4. 调用Cal::Execute执行业务计算,获取Response对象。
    5. 序列化Response并通过sock->Send回传客户端。
    6. 处理客户端断开(n==0)或接收错误(n<0)的情况,打印日志并退出循环。

5. 客户端模块(Tcpclient.cc

核心接口及逻辑
  • BuildTcpClientSocketMethod():调用SocketOrDie创建客户端套接字。
  • Connect(const std::string &server_ip, uint16_t port):连接服务端IP和端口,成功则进入数据交互流程。
  • GetDataFromStdin(int *x, int *y, char *oper):从终端读取用户输入的运算数(xy)和运算符(oper)。
  • 数据发送流程:读取输入后,通过Request::Serialize生成JSON请求串,调用Encode添加报头,再通过Send发送给服务端;接收服务端响应后,反序列化并解析结果。
关键依赖与设计原则
  1. 序列化/反序列化 :依赖Json库(Json::ValueFastWriterReader),实现结构化数据与字节流的转换,适配网络传输。
  2. 解耦设计 :通过回调函数(ioservice_t)分离框架与业务,Socket封装屏蔽底层API差异,协议模块独立处理粘包和数据格式。
  3. TCP适配 :通过"长度报头+分隔符"解决TCP面向字节流的粘包问题,确保报文完整性;Recv循环读取缓冲区,适配"多次写、一次读"或"一次写、多次读"的场景。
相关推荐
安科士andxe7 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
寻寻觅觅☆9 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio9 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
fpcc10 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
l1t10 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
CTRA王大大10 小时前
【网络】FRP实战之frpc全套配置 - fnos飞牛os内网穿透(全网最通俗易懂)
网络
小白同学_C10 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖10 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
2601_9491465310 小时前
Shell语音通知接口使用指南:运维自动化中的语音告警集成方案
运维·自动化
儒雅的晴天10 小时前
大模型幻觉问题
运维·服务器