基于TCP的网络计算器实现

news2024/11/15 19:26:57

目录

一.   重新理解协议

二.   序列化与反序列化

2.1   概念

2.2   重新理解 read、write、recv、send 和 tcp 为什么支持全双工 

2.3   理解TCP面向字节流

三.   请求应答模块实现

3.1   添加与解析报头

3.2   定制协议:

3.3   Json

四.   计算模块实现

五.   Socket自主实现封装

六.   服务端模块实现

七.   服务端调用模块实现

八.   客户端模块实现

九.   效果展示


此篇博客我们来讲解基于TCP的网络计算器的实现,其实重点是讲解序列化和反序列化。话不多说,开始今日份学习吧。

一.   重新理解协议

        前面说过, 在网络层面,协议 是一组规则、标准或约定,它们定义了在网络环境中,计算机、服务器、路由器、交换机等网络设备之间如何相互通信和交换信息。这些规则涵盖了数据格式、数据交换的顺序、速度、以及数据同步方式等各个方面,以确保数据在网络中的传输是可靠、有效和安全的。

        但在此节,我们要加深对协议的理解:在语言层面,协议就是通信双方定制一样的结构化的数据。

二.   序列化与反序列化

2.1   概念

        在网络发送消息过程中,我们不方便直接传输结构化数据。而是将结构化数据转化为字符串,进行网络传输,将字符串发送给对方。到达目的地后再转化为结构化数据。这就是序列化与反序列化

序列化:将数据结构或对象状态转换成可以存储或传输的格式的过程。

反序列化:序列化的逆过程,将之前序列化后的中间格式转换回原始的数据结构或对象状态。

那为什么要这么做呢?

为什么需要序列化

  1. 数据持久化:将对象的状态保存到文件或数据库中,以便程序可以在未来的某个时间点重新创建或恢复对象。
  2. 网络通信:在网络中传输对象时,由于网络协议基于字节流,所以需要将对象序列化为字节流才能在网络上传输。
  3. 对象传输:在进程间通信(IPC)或远程过程调用(RPC)中,对象需要被序列化后才能在不同的内存地址空间中传输。
  4. 安全传输:在发送敏感数据前,序列化后可以进行加密处理,增加数据传输的安全性。

 为什么需要反序列化

  1. 数据恢复:从文件或数据库中读取序列化的数据,并将其恢复为原始的对象或数据结构,以便在程序中进一步使用。
  2. 接收网络数据:在网络通信中,接收到的序列化数据需要被反序列化为对象,以便程序能够理解和处理这些数据。
  3. 对象重构:在进程间通信或远程过程调用中,接收到的序列化数据需要被反序列化为对象,以便在当前进程中使用。

总的来说:

  1. 跨平台兼容性:不同的编程语言和平台可能有不同的内存布局、字节序(endianess)和数据类型表示方式。直接发送结构化数据可能会导致接收方无法正确解析这些数据,因为它们可能不遵循接收方平台上的数据表示规则。序列化将结构化数据转换为一种平台无关的格式(如JSON、XML、Protobuf等),从而确保了数据可以在不同的平台之间无缝传输。
  2. 网络传输效率:直接发送结构化数据可能包含大量的冗余信息(如对齐填充、指针等),这些信息对于网络传输来说是不必要的,并且会增加传输的数据量。序列化过程可以去除这些冗余信息,只保留必要的数据,从而提高网络传输的效率。

2.2   重新理解 read、write、recv、send 和 tcp 为什么支持全双工 

       何为全双工,即可以发送数据也能接收数据。在之前TCP的相关代码实现中,我们发现也确实是这样的。客户端可以给服务端发送消息,而客户端也能接收来自服务端发送的消息。 

       其实read、write、recv和send函数本质是拷贝函数。就拿read函数来讲。

ssize_t read(int fd, void *buf, size_t count);

        read函数的作用就是将文件描述符 fd 对应的数据拷贝到对应的缓冲区 buf 中。也就是将数据从内核空间拷贝到了用户空间。那这和全双工有什么关系呢?

        我们知道tcp套接字的本质也是一个文件描述符,但是tcp套接字在设计的时候其实设计了两个缓冲区,一个用来发送数据一个用来接受数据,在通信时客户端和服务端会各自创建一个套接字,此时其实一共存在四个缓冲区。当客户端发送数据时其实是将客户端发送缓冲区的数据拷贝到服务端的接收缓冲区,而客户端接收数据时其实是将服务端的发送缓冲区的数据拷贝到客户端的接收缓冲区,这两个缓冲区相对独立互不干扰。

所以:

  • 在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工。
  • 这就是为什么一个 tcp sockfd 读写都是它的原因。
  • 实际数据什么时候发,发多少,出错了怎么办,由 TCP 控制,所以 TCP 叫做传输控制协议。

2.3   理解TCP面向字节流

        由于TCP是面向字节流,所以他不像UDP一样发送与接收的都是一个完整的数据报,我们TCP在接收时,可能接收到的是半个请求,也有可能是一个半个请求。这就是TCP的粘报问题。类似于我们平时蒸包子,蒸好后你去拿,可能拿到的是半个包子,也有可能是多个包子粘在一起。

为了解决这个问题,我们采用在报文前面添加报头的方式,来检测我们是否收到一个完整的请求。当检测到不足一个时,可以继续往下读,读到下一个报头就读到完整的报文。具体实现我们通过代码实现。

三.   请求应答模块实现

3.1   添加与解析报头

此处为了与如今网络上一致,我们采用 \r\n 作为行与行之间的分隔符。

首先解决粘报问题,在发送数据之前添加报头:

const string SEP="\r\n";

string Encode(const string& json_str)
{
    int json_str_len=json_str.size();
    string proto_str=to_string(json_str_len);
    proto_str+=SEP;
    proto_str+=json_str;
    proto_str+=SEP;
    return proto_str;
}

 可以看到我们添加的报头是字符串的长度,添加报头之后得到的报文就像这样:

"len"\r\n"{             }"\r\n;//大括号里面是正文部分。

在接收数据之后解析报头:

const string SEP="\r\n";

string Decode(string& inbuffer)
{
    auto pos=inbuffer.find(SEP);
    if(pos==string::npos)//报文没有\r\n,报文不完整
    {
        return string();
    }
    //计算报文的总长度
    string len_str=inbuffer.substr(0,pos);
    if(len_str.empty())
    {
        return string();
    }
    int packlen=stoi(len_str);
    int total=packlen+len_str.size()+2*SEP.size();
    if(inbuffer.size()<total)
    {
        return string();
    }

    //到这说明收到的报文流一定存在一个完整的报文
    string package=inbuffer.substr(pos+SEP.size(),packlen);
    inbuffer.erase(0,total);
    return package;
}

3.2   定制协议:

        定制协议就是定制一个双方需要的结构化字段,对于网络计算器来说,我们需要设计一个类包含操作数和操作符,而结果的返回我们也需要设计一个类,包含计算结果、返回码及相关描述等信息。

class Request
{
public:
    Request()
    {}

    Request(int x, int y, char oper)
        : _x(x), _y(y), _oper(oper)
    {}

    bool Serialize(string *out) // 序列化
    {}

    bool Deserialize(string &in) // 反序列化
    {}

    ~Request()
    {}

public:
    int _x;
    int _y;
    char _oper; // "+-*/%" _x _oper _y
};

class Response
{
public:
    Response()
    {}

    Response(int result, int code)
        : _result(result), _code(code)
    {}

    bool Serialize(string *out) // 序列化
    {}

    bool Deserialize(string &in) // 反序列化
    {}

public:
    int _result;//结果
    int _code;//0:success 1:除0 2:非法操作 3. 4. 5
};

有了结构化数据大体框架之后,我们来看看如何序列化与反序列化,此处我们采用Json来实现。

3.3   Json

要能使用Json,必须包含 jsoncpp 这个库。

#include<jsoncpp/json/json.h>

        Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。

安装:

C++

ubuntu:sudo apt-get install libjsoncpp-dev

Centos: sudo yum install jsoncpp-devel

  • 序列化

 首先我们需要创建一个Json::Value对象,如代码所示,我们将p对象的成员赋值给了Json::Value 的root对象,于此同时我们还给这些成员各自起了一个名字,这样我们就可以通过键值对的方式来找到对应的数据了。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
  people p("joe","男");
  Json::Value root;
  root["name"] = p.name;
  root["sex"] = p.sex;
  return 0;
}

我们可以创建一个Json::FastWriter对象,调用它的write方法:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
  people p("joe","男");
  Json::Value root;
  root["name"] = p.name;
  root["sex"] = p.sex;
 
  Json::FastWriter writer;
  std::string s = writer.write(root);
  return 0;
}

序列化出来的字符串就是这样的:

{"name":"joe","sex":"男"}
  • 反序列化:

 使用Json::Reader

int main()
{
    std::string str={"name":"joe","sex":"男"}
    Json::Value root;
    Json::Reader read;
    //将Json字符串写到Json对象中
    bool ret = read.parse(str, root);
    //将Json对象中的数据写到数据化结构中
    People p;
    p.name= root["x"].asString();
    p.sex= root["y"].asString();
 
}

那我们该如何去实现请求与应答的序列化与反序列化呢?仿照上面的就可写出:

class Request
{
public:
    Request()
    {}

    Request(int x, int y, char oper)
        : _x(x), _y(y), _oper(oper)
    {}

    bool Serialize(string *out) // 序列化
    {
        // 转化成为字符串
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        *out = writer.write(root);

        return true;
    }

    bool Deserialize(string &in) // 反序列化
    {
        Json::Value root;
        Json::Reader reader;
        bool res = reader.parse(in, root);
        if(!res) return false;

        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();

        return res;
    }

    ~Request()
    {}

public:
    int _x;
    int _y;
    char _oper; // "+-*/%" _x _oper _y
};

class Response
{
public:
    Response()
    {}

    Response(int result, int code)
        : _result(result), _code(code)
    {}

    bool Serialize(string *out) // 序列化
    {
        // 转化成为字符串
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter writer;
        *out = writer.write(root);

        return true;
    }

    bool Deserialize(string &in) // 反序列化
    //你怎么保证读到的in是完整的呢
    {
        Json::Value root;
        Json::Reader reader;
        bool res = reader.parse(in, root);
        if(!res) return false;

        _result = root["result"].asInt();
        _code = root["code"].asInt();

        return true;
    }

public:
    int _result;//结果
    int _code;//0:success 1:除0 2:非法操作 3. 4. 5
};

最后我们再封装一个类,用来实现创建出请求与应答:

class Factory
{
public:
    Factory()
    {
        srand(time(nullptr)^getpid());
        opers="+/*/%^&|";
    }

    shared_ptr<Request> BuildRequest()
    {
        int x=rand()%10+1;
        usleep(x*10);
        int y=rand()%5;
        usleep(y*x*5);
        char oper=opers[rand()%opers.size()];
        shared_ptr<Request> req=make_shared<Request>(x,y,oper);
        return req;
    }

    shared_ptr<Response> BuildResponse()
    {
        return make_shared<Response>();
    }

    ~Factory()
    {}
private:
    string opers;
};

由此我们的请求应答模块就实现完整了,代码总体如下:

#pragma once

#include <iostream>
#include <string>
#include<jsoncpp/json/json.h>
#include<sys/types.h>
#include<unistd.h>
using namespace std;

namespace protocol_ns
{
    const string SEP="\r\n";

    //我们把tcp中读到的报文,可能读到半个,也可能读到1个半等-->TCP 粘报问题
    //解决TCP粘报问题
    string Encode(const string& json_str)
    {
        int json_str_len=json_str.size();
        string proto_str=to_string(json_str_len);
        proto_str+=SEP;
        proto_str+=json_str;
        proto_str+=SEP;
        return proto_str;
    }

    // "len"\r\n"{
    // "len"\r\n"{             }"
    // "len"\r\n"{             }"\r\n;
    // "len"\r\n"{             }"\r\n"len";
    // "len"\r\n"{             }"\r\n"len"\r\n"{             }";
    // "len"\r\n"{             }"\r\n"len"\r\n"{             }"\r\n
    string Decode(string& inbuffer)
    {
        auto pos=inbuffer.find(SEP);
        if(pos==string::npos)//不完整
        {
            return string();
        }
        string len_str=inbuffer.substr(0,pos);
        if(len_str.empty())
        {
            return string();
        }
        int packlen=stoi(len_str);
        int total=packlen+len_str.size()+2*SEP.size();
        if(inbuffer.size()<total)
        {
            return string();
        }

        string package=inbuffer.substr(pos+SEP.size(),packlen);
        inbuffer.erase(0,total);
        return package;
    }
    // 我们协议的样子
    // 报文 = 报头 + 有效载荷
    //"有效载荷的长度""\r\n""有效载荷""\r\n"
    //"len""\r\n"_x _op _y""\r\n" ->len:有效载荷的长度,约定\r\n是分隔符,不参与统计

    // 结构化数据,不能向网络中直接发送,需转化为字符串
    //其他问题:struct Request req(10,20,"*"),为什么不直接发送req对象?可以但不建议,跨平台性差
    class Request
    {
    public:
        Request()
        {}

        Request(int x, int y, char oper)
            : _x(x), _y(y), _oper(oper)
        {}

        bool Serialize(string *out) // 序列化
        {
            // 转化成为字符串
            Json::Value root;
            root["x"] = _x;
            root["y"] = _y;
            root["oper"] = _oper;

            Json::FastWriter writer;
            *out = writer.write(root);

            return true;
        }

        bool Deserialize(string &in) // 反序列化
        {
            Json::Value root;
            Json::Reader reader;
            bool res = reader.parse(in, root);
            if(!res) return false;

            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();

            return res;
        }

        ~Request()
        {}

    public:
        int _x;
        int _y;
        char _oper; // "+-*/%" _x _oper _y
    };

    class Response
    {
    public:
        Response()
        {}

        Response(int result, int code)
            : _result(result), _code(code)
        {}

        bool Serialize(string *out) // 序列化
        {
            // 转化成为字符串
            Json::Value root;
            root["result"] = _result;
            root["code"] = _code;

            Json::FastWriter writer;
            *out = writer.write(root);

            return true;
        }

        bool Deserialize(string &in) // 反序列化
        //你怎么保证读到的in是完整的呢
        {
            Json::Value root;
            Json::Reader reader;
            bool res = reader.parse(in, root);
            if(!res) return false;

            _result = root["result"].asInt();
            _code = root["code"].asInt();

            return true;
        }

    public:
        int _result;//结果
        int _code;//0:success 1:除0 2:非法操作 3. 4. 5
    };

    class Factory
    {
    public:
        Factory()
        {
            srand(time(nullptr)^getpid());
            opers="+/*/%^&|";
        }

        shared_ptr<Request> BuildRequest()
        {
            int x=rand()%10+1;
            usleep(x*10);
            int y=rand()%5;
            usleep(y*x*5);
            char oper=opers[rand()%opers.size()];
            shared_ptr<Request> req=make_shared<Request>(x,y,oper);
            return req;
        }

        shared_ptr<Response> BuildResponse()
        {
            return make_shared<Response>();
        }

        ~Factory()
        {}
    private:
        string opers;
    };
}

四.   计算模块实现

我们产生了请求之后,需要根据请求里面的操作数以及操作符,计算出结果,如果操作数与操作符不符合计算习惯,还需要填写应答里面的状态。

仍然封装成一个类:

class Calculate
{
public:
    Calculate()
    {
    }

    Response Excute(const Request &req)
    {
        Response resp{0, 0};
        switch (req._oper)
        {
        case '+':
            resp._result = req._x + req._y;
            break;
        case '-':
            resp._result = req._x - req._y;
            break;
        case '*':
            resp._result = req._x * req._y;
            break;
        case '/':
        {
            if(req._y==0)
            {
                resp._code=1;
            }
            else
            {
                resp._result=req._x/req._y;
            }
        }
        break;
        case '%':
        {
            if(req._y==0)
            {
                resp._code=2;
            }
            else
            {
                resp._result=req._x%req._y;
            }
        }
        break;
        default:
            resp._code=3;
            break;
        }
        return resp;
    }

    ~Calculate()
    {}

private:
};

五.   Socket自主实现封装

为了更好的代码独立性,我们自己封装一下Socket的相关操作。

我们设计了一个基类Socket,并在基类里面设计一些虚函数。让TcpSocket继承这个基类,只需在TcpSocket里面重写虚函数,实现具体操作即可。此处由于只涉及TCP,所以我们之封装了TcpSocket。读者可以自行封装UdpSocket。

#include "InetAddr.hpp"
#include "Log.hpp"

namespace socket_ns
{
    class Socket;
    const static int gbacklog = 8;
    using socket_sptr = shared_ptr<Socket>;
    enum
    {
        SOCKET_ERROR = 1,
        BIND_ERROR,
        LISTEN_ERROR,
        USAGE_ERROR
    };

    class Socket
    {
    public:
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(InetAddr &addr) = 0;
        virtual void ListenSocketOrDie() = 0;
        virtual socket_sptr Accepter(InetAddr *addr) = 0;
        virtual bool Connectcor(InetAddr &addr) = 0;
        virtual int Sockfd() = 0;
        virtual int Recv(string *out) = 0;
        virtual int Send(const string &in)=0;

    public:
        void BuildListenSocket(InetAddr &addr)
        {
            CreateSocketOrDie();
            BindSocketOrDie(addr);
            ListenSocketOrDie();
        }

        bool  BuildClientSocket(InetAddr &addr)
        {
            CreateSocketOrDie();
            return Connectcor(addr);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int fd=-1)
        :_sockfd(fd)
        {}

        void CreateSocketOrDie() override
        {
            // 1.创建流式套接字
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(FATAL, "socket error\n");
                exit(SOCKET_ERROR);
            }
            LOG(DEBUG, "socket create success, sockfd is: %d\n", _sockfd);
        }

        void BindSocketOrDie(InetAddr &addr)
        {
            // 2.bind
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(addr.Port());
            local.sin_addr.s_addr = inet_addr(addr.Ip().c_str());
            int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
            if (n < 0)
            {
                LOG(FATAL, "bind error\n");
                exit(BIND_ERROR);
            }
            LOG(DEBUG, "bind success,sockfd is: %d\n", _sockfd);
        }

        void ListenSocketOrDie() override
        {
            int n = ::listen(_sockfd, gbacklog);
            if (n < 0)
            {
                LOG(FATAL, "listen error\n");
                exit(LISTEN_ERROR);
            }
            LOG(DEBUG, "listen success,sockfd is: %d\n", _sockfd);
        }

        socket_sptr Accepter(InetAddr *addr)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                return nullptr;
            }
            *addr = peer;
            socket_sptr sock = make_shared<TcpSocket>(sockfd);
            return sock;
        }

        bool Connectcor(InetAddr &addr)
        {
            // 构建目标主机的socket信息
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(addr.Port());
            server.sin_addr.s_addr = inet_addr(addr.Ip().c_str());

            int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                cerr << "connect error"<< endl;
                return false;
            }
            return true;
        }

        int Sockfd() override
        {
            return _sockfd;
        }

        int Recv(string *out) override
        {
            char inbuffer[1024];
            ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1,0); 
            if (n > 0)
            {
                inbuffer[n] = 0;
                *out+=inbuffer;//??? +=
            }
            return n;
        }

        int Send(const string& in) override
        {
            int n=::send(_sockfd,in.c_str(),in.size(),0);
            return n;
        }
    private:
        int _sockfd;
    };
}

对于,InetAddr.hpp与Log.hpp两个文件,前面几篇博客均提到了,可以去看看(点此查看)。

六.   服务端模块实现

服务端我设计了一个类TcpServer来实现具体的代码实现。

并且我们为了保证代码的低耦合,服务端我们只需调用服务即可,并不需要执行具体服务,采用回调函数的方式让上层实现,这也是我们上面将计算服务封装起来的原因。

using namespace std;
using namespace socket_ns;

class TcpServer;

using io_service_t=function<void(socket_sptr sockfd,InetAddr client)>;//处理方法

struct ThreadData
{
public:
    ThreadData(socket_sptr fd,InetAddr addr,TcpServer* s)
    :sockfd(fd)
    ,clientaddr(addr)
    ,self(s)
    {}
public:
    socket_sptr sockfd;
    InetAddr clientaddr;
    TcpServer* self;
};


class TcpServer
{
public:
    TcpServer(int port,io_service_t service)
    :_localaddr("0",port)
    ,_listensock(make_unique<TcpSocket>())
    ,_service(service)
    ,_isrunning(false)
    {
        _listensock->BuildListenSocket(_localaddr);
    }

    static void* HandlerSock(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData* td=static_cast<ThreadData*>(args);
        td->self->_service(td->sockfd,td->clientaddr);
        ::close(td->sockfd->Sockfd());
        delete td;
        return nullptr;
    }

    void Loop()
    {
        _isrunning=true;
        //4.不能直接接收数据,应该先获取连接
        while(_isrunning)
        {
            InetAddr peeraddr;
            socket_sptr normalsock=_listensock->Accepter(&peeraddr);
            if(normalsock==nullptr) continue;

            //Version 2:采用多线程
            //此处不能像多进程一样关闭文件描述符,因为多线程文件描述符表是共享的
            pthread_t t;
            ThreadData* td=new ThreadData(normalsock,peeraddr,this);
            pthread_create(&t,nullptr,HandlerSock,td);//将线程分离
        }
        _isrunning=false;
    }

    ~TcpServer()
    {
        
    }
private:
    InetAddr _localaddr;
    unique_ptr<Socket> _listensock;
    bool _isrunning;

    io_service_t _service;
};

七.   服务端调用模块实现

服务端调用就要实现结构化数据与字符串的转换。

具体思路就是:

  1. 读取数据
  2. 分析数据,确认完整报文
  3. 反序列化
  4. 业务处理
  5. 数据响应,序列化,添加报头
  6. 发送数据
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " serverport\n"
         << endl;
}

using callback_t = function<Response(const Request &req)>;

class Service
{
public:
    Service(callback_t cb)
        : _cb(cb)
    {
    }

    void ServiceHelper(socket_sptr sockptr, InetAddr client)
    {
        int sockfd = sockptr->Sockfd();
        LOG(DEBUG, "get a new link,info %s:%d,fd: %d\n", client.Ip().c_str(), client.Port(), sockfd);
        string clientaddr = "[" + client.Ip() + ":" + to_string(client.Port()) + "]# ";

        string inbuffer;
        while (true)
        {
            Request req;
            // 1.读取数据
            //  你怎么保证一个大的字符串里面有没有完整的请求
            int n = sockptr->Recv(&inbuffer); // 你怎么保证读到的是完整的request->有效载荷的长度
            if (n <= 0)
            {
                LOG(DEBUG, "client %s quit\n", clientaddr.c_str());
                break;
            }

            // 2.分析数据,确认完整报文
            string package;
            while(true)
            {
                cout<<"inbuffer: "<<inbuffer<<endl;
                package = Decode(inbuffer);
                if (package.empty())
                    break;
                cout<<"------------------begin--------------------"<<endl;
                cout<<"resq string:\n"<<package<<endl;
                // 到这,可以保证一定读到了一个完整的json串

                // 3.反序列化
                req.Deserialize(package);

                // 4.业务处理
                Response resp = _cb(req);

                // 5.对应答做序列化
                string send_str;
                resp.Serialize(&send_str);

                cout<<"resp Serialize:"<<endl;
                cout<<send_str<<endl;

                // 6.添加长度报头
                send_str = Encode(send_str); //"len"\r\n""{         }""\r\n"
                cout<<"resp Encode:"<<endl;
                cout<<send_str<<endl;

                //"len"\r\n""{         }""\r\n"
                sockptr->Send(send_str);
            }
        }
    }

private:
    callback_t _cb;
};

// ./tcpserver serverport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    uint16_t port = stoi(argv[1]);

    //daemon(0,0);

    //EnableScreen();
    Calculate cal;
    Service calservice(bind(&Calculate::Excute, &cal, placeholders::_1));
    io_service_t service = bind(&Service::ServiceHelper, &calservice, placeholders::_1, placeholders::_2);
    unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port, service);

    tsvr->Loop();
    return 0;
}

八.   客户端模块实现

客户端就需要实现具体请求的关创建,并向服务端发送请求,然后接收服务端返回的应答。

具体思路是:

  1. 创建请求
  2. 将数据序列化
  3. 添加报头
  4. 发送数据
  5. 读取响应
  6. 检测报头
  7. 反序列化
using namespace std;
using namespace socket_ns;
using namespace protocol_ns;

void Usage(string proc)
{
    cout<<"Usage:\n\t"<<proc<<" serverip serverport\n"<<endl;
}



// ./tcpclient serverip serverport
int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }

    string serverip=argv[1];
    uint16_t serverport=stoi(argv[2]);

    InetAddr serveraddr(serverip,serverport);

    Factory factory;
    unique_ptr<Socket> cli=make_unique<TcpSocket>();
    bool res=cli->BuildClientSocket(serveraddr);
    string inbuffer;
    while(res)
    {
        sleep(1);
        //1.构建一个请求
        auto req=factory.BuildRequest();

        //2.对请求进行序列化
        string send_str;
        req->Serialize(&send_str);

        cout<<"Serialize:\n"<<send_str<<endl;

        //3.添加长度报头
        send_str =Encode(send_str);

        cout<<"Encode:\n"<<send_str<<endl;

        //4."len"\r\n""{}"\r\n"
        cli->Send(send_str);

        //5.读取应答
        int n=cli->Recv(&inbuffer);
        if(n<=0) break;

        string package=Decode(inbuffer);
        if(package.empty()) continue;

        //6.我能保证package一定是一个完整的应答
        auto resp=factory.BuildResponse();
        //6.1 反序列化
        resp->Deserialize(package);

        //7.拿到了结构化的应答吗
        cout<<resp->_result<<"["<<resp->_code<<"]"<<endl;
        
    }

    return 0;
}

九.   效果展示


总结:

好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。

祝大家越来越好,不用关注我(疯狂暗示)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2138006.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

字典转换(根据字典转换、根据id转换)

一、三种转换方式 翻译场景:序列化时候转换 适用类型: 字典type转中文用户id转用户名部门id转名称附件id转url路径1.1 根据另一个映射字段 翻译保存到此字段 根据创建者createBy的id,查询名称设置到 createName 1.2 直接根据此字段值翻译后替换此字段值 ossId 替换为 url …

医疗监测数据检测系统源码分享

医疗监测数据检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer…

vue3透传、注入

属性透传 传递给子组件时&#xff0c;没有被子组件消费的属性或事件&#xff0c;常见的如id、class 注意1 1.class、style是合并的&#xff0c;style中如果出现重复的样式&#xff0c;以透传属性为准2.id属性是以透传属性为准&#xff0c;其他情况透传属性名相同&#xff0c…

深度学习云服务器免费使用教程

#云服务器# #深度学习# #人工智能# #计算机视觉# 本文为各位学习深度学习的入门选手而创建&#xff0c;降低深度学习的入门门槛。 谷歌云服务器Colab&#xff1a; T4GPU。限额&#xff0c;需要科学上网&#xff0c;不能使用终端。 谷歌云服务器地址&#xff1a;欢迎使用 C…

C语言 | Leetcode C语言题解之第405题数字转换为十六进制数

题目&#xff1a; 题解&#xff1a; char * toHex(int num){int i0;char *nums(char*)malloc(sizeof(char)*32);unsigned int newnum(unsigned int)num;if(num0){nums[0]0;nums[1]\0;return nums;}while(newnum>1){int flagnewnum%16;newnum/16;if(flag<9){nums[i]flag0…

华为OD机试 - 计算误码率(Python/JS/C/C++ 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试真题&#xff08;Python/JS/C/C&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加入华为OD刷题交流群&#xff0c;…

Acwing 队列

模拟队列 主要思想&#xff1a;先进先出&#xff08;注意与栈做区分&#xff09;&#xff0c;队尾插入&#xff0c;队头删除。设置一个数组存储数据&#xff0c;队头指针指向队列第一个元素&#xff08;初始为0&#xff09;&#xff0c;队尾指针指向最后一个元素&#xff08;初…

Qt常用控件——QSpinBox

文章目录 QSpinBox核心属性及信号点餐示例 QSpinBox核心属性及信号 QSpinBox或者QDoubleSpinBox表示微调框&#xff0c;带有按钮的输入框&#xff0c;可以用来输入整数/浮点数或者通过点击按钮调整数值大小 QSpinBox和QDoubleSpinBox用法基本相同&#xff0c;本篇以QSpinBox为…

Ubuntu 安装包下载(以20版本 阿里镜像站为例子)

Ubuntu安装包下载 上一篇文章写了一些国内常用的镜像站&#xff0c;这篇以阿里云镜像站Ubuntu20版本为例。 https://mirrors.aliyun.com/ubuntu-releases/ 1.点击自己想要下载的版本 2.点击以amd64.iso为结尾的文件&#xff0c;这个是安装文件&#xff0c;如果是桌面端&…

C++初阶学习——探索STL奥秘——vector的模拟实现

vector的结构比较特殊&#xff0c;成员变量为三个指针 #pragma once #include <iostream> using std::cin; using std::cout; using std::endl;#include <string> using std::string;namespace Yohifo {template<class T>class vector{public:typedef T val…

使用jmeter做性能测试实践过程中需要注意什么

前言 在驾驭Apache JMeter进行性能测试之旅中&#xff0c;深刻理解其特性和限制是至关重要的。以下是提升JMeter效能的关键策略&#xff0c;旨在挖掘其潜力&#xff0c;克服局限&#xff0c;实现精准测试。 1.精确调控线程数 推荐阈值&#xff1a;将线程数控制在300以内&…

模拟视频推到WVP推流列表

效果 1. wvp创建RTMP 2. 使用ffmpeg将本地的视频转为rtmp ffmpeg -re -i F:rtsp\123.mp4 -c copy -f flv rtmp://192.168.1.237:1935/cd/10001?sign=Z4Y3eYeSg

HTML5之canvas绘图

介绍 <canvas> 是 HTML5 引入的一个强大元素&#xff0c;允许直接在网页上进行矢量图形和位图图像的绘制&#xff0c;为网页提供了一个动态图形渲染的平台。这一特性极大丰富了网页的表现力&#xff0c;特别是在数据可视化、游戏开发、交互式图表和动画制作等领域发挥着…

第311题| 超好用!二重积分求旋转体体积公式|武忠祥老师每日一题

第一步&#xff1a; &#xff08;1&#xff09;找渐近线&#xff0c;先看水平渐近线&#xff0c;看x趋于无穷时&#xff0c;y有没有趋于一个有限的值。 , 得出水平渐近线y1。因为左右两边都是水平渐近线&#xff0c;所以没有斜渐近线。 第二步&#xff1a; 画出图像&#…

Spring Boot环境下的学生读书笔记共享

第4章 系统设计 4.1系统结构设计 读书笔记共享平台的设计主要是为了满足用户的实际需求。 因此&#xff0c;它需要通过Internet实现&#xff0c;因此它必须具备硬件和软件基础。该平台最终可以通过科学技术和各种方式达到支持智能化的信息管理的目的。因此&#xff0c;它必须具…

深入理解Python中的生成器:高效迭代与延迟计算的艺术

在处理大量数据时&#xff0c;如何有效地管理内存成为了一个关键问题。Python中的生成器&#xff08;Generator&#xff09;提供了一种优雅的解决方案&#xff0c;它允许你在迭代过程中按需生成数据&#xff0c;而不是一次性加载所有数据到内存中。本文将详细探讨生成器的工作原…

看Threejs好玩示例,学习创新与技术(三)

本文接上篇内容&#xff0c;继续挖掘应用ThreeJS的一些创新算法。 1、获得鼠标移动对应的地理位置 这个算法如果放在几年前&#xff0c;那肯定会难倒一帮人的。因为是三维投影涉及矩阵变换及求逆&#xff0c;而且还是投影模式下的。在Project Texture这个示例中&#xff0c;作…

Sentaurus TCAD的sdevice求解中选择Math求解方法

目录 并行迭代线性求解器&#xff08;ILS&#xff09;并行超节点直接求解器&#xff08;ParDiSo&#xff09;超节点直接求解器&#xff08;Super&#xff09;详细解释1. 并行迭代线性求解器&#xff08;ILS&#xff09;2. 并行超节点直接求解器&#xff08;ParDiSo&#xff09;…

windows安装docker、elasticsearch、kibana、cerebro、logstash

文章目录 1. 安装docker1.1. 两大要点1.1.1. 安装启用hyper-v电脑不存在hyper-v的情况 1.1.2. 下载安装docker 2. 在docker里面安装elasticSearch&#xff0c;kibana&#xff0c;cerebro3. 安装logstash-将数据导入到elasticSearch3.1 安装logstash3.1.1 注意事项3.1.1.1. 等了…

vue3中把封装svg图标为全局组件

在vue3中我们使用svg图标是下面这样子的 <svg style="width:30px;height:30px;"><use xlink:href="#icon-phone" fill="red"></use></svg>第次使用图标都要写这么多重复的代码,很不方便,所以,如果我们把它封装成全局…