序列化和反序列化

一 概念理解

        先前已经可以利用sock套接字通信了,但是数据如何处理就是我们应用层协议的内容了,之前都是发送一些字符串,但是实际上我们发送的消息可能是个结构化的数据。

        那我们能不能直接发结构体呢? 可以但是会浪费空间,你想想我们平时写的作文有固定格式和缩进,但是对于网络来说这些缩进是浪费空间,所以我们序列化是为了压缩发送的数据大小。 

        将结构化的数据转为一个大字符串,称为序列化,然后发给服务端,服务端解析字符串(这就是反序列化),然后服务端构建响应,又序列化发给客户端。

        我们是凭什么对一个结构体序列化,对字符串反序列的,就是双方约定好了一个格式,这样才能解析发来的数据,我们在下面约定的格式就是一种协议,而且是应用层协议。

        之前我们只是用了一下socket接口,根本没有对数据做序列化和反序列,也没有对数据做处理,因为无场景。接下来我们实现一个网络版本的计算器,从中我们会设计序列化和反序列化,本质是设计一个应用层协议。

二 编码实现

        计算器客户端和服务端实现放在两个.cc文件,通信客户端和服务端实现放在了头文件中。

1 makefile

        makefile:一同编译client.cc和server.cc。

.PHONY:all
all:client server
client:CalculatorClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
server:CalculatorServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -rf server client

2 封装系统调用

        由于我们要进程调用和套接字相关的接口,所以就对这些接口做了封装。

class Sock
{

public:

    int socket_;
    Log log_;
};

         不用cout,而是用日志打印,日志打印是我们之前封装的模块,下面直接展示代码,使用的时候我们直接用log_调用仿函数就可以了。

enum ErrorLevel
{
    Info = 1,
    Warning,
    Fatal,
    Debug
};
//接收输出的文件
enum PMethod
{
    Screen = 1,//输出到屏幕
    OneFile ,//输出到一个文件上
    ClassFile//分类输出到多个文件中
};

class Log
{
public:
    Log(int method = Screen)
    :printmethod(method)
    {
        ;
    }
    string leveltostring(int level)
    {
        switch (level)
        {
            case Info:
                return "Info";
            case Warning:
                return "Warning";
            case Fatal:
                return "Fatal";
            case Debug:
                return "Debug";
            default: 
                return "None";    
        }
    }
    //日志信息
    void operator()(int level, const char *format, ...)
    {
        char leftbuffer[SIZE];
        time_t t = time(NULL);
        struct tm * ltime = localtime(&t);
        //默认部分 事件等级和时间
        snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",leveltostring(level).c_str(),
        ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);

        //可变部分
        char rightbuffer[SIZE];
        va_list s;
        va_start(s,format);
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
        // printf("%s %s\n",leftbuffer,rightbuffer);
        char Logbuffer[SIZE*2];
        snprintf(Logbuffer,sizeof(Logbuffer),"%s %s",leftbuffer,rightbuffer);
        LogPrint(level,Logbuffer);
    }

    void PrintOnefile( const char *filename,string& lbuffer)
    {
        lbuffer+='\n';
        int fd = open(filename, O_CREAT|O_APPEND|O_WRONLY,0666);
        if(fd < 0)
            return;
         write(fd,lbuffer.c_str(),lbuffer.size());   
    }
    void PrintClassFile(int level,string& lbuffer)
    {
        string filename = Logname;//将不同错误信息分流到对应的文件
        filename += ".";
        filename += leveltostring(level);
        PrintOnefile(filename.c_str(),lbuffer);
    }
    void LogPrint(int level,string lbuffer)
    {
        switch(printmethod)
        {
            case Screen://输出到屏幕
               cout<<lbuffer<<endl;
                break;
            case OneFile: //输出到一个文件上
                PrintOnefile(Logname,lbuffer);
                break;
            case ClassFile:
                PrintClassFile(level,lbuffer);
                break;
        }
    }
private:
    int printmethod;
};

        之前创建套接字通信的时候,我们定义了许多和错误信息相关的宏,现在直接拿来用。

        接下来才到封装的实现。

创建套接字

绑定

监听

接收链接

     int Accept(string* ip,uint16_t* port) // 名字不能为accept
    {
        // 获取链接
        struct sockaddr_in sock; // 头文件<netinet/in.h>
        bzero(&sock, sizeof(sock));
        socklen_t len = sizeof(sock);
        int socket = accept(socket_, (sockaddr *)&sock, &len);
        if (socket < 0)
        {
            log_(ErrorLevel::Info, "accept err");
            exit(SOCKET_ERR);
        }
        else
        {
            *ip = inet_ntoa(sock.sin_addr);
            *port = ntohs(sock.sin_port);
        }
        return socket;
    }

        connect,这个connect是客户端要用的,也是sock接口,就一同封装了。

        Sock类中的套接字socket_含义由使用者来定义,可以是监听套接字,也可以是直接写的套接字。

三 服务端实现

        我们是对服务端实现做了封装,封装在了该头文件中。

        服务端main函数在该文件中。

        我们在main函数中给服务类传端口号和可调用对象,服务端ip不用绑定,我这是云服务器,一绑定就会出错,然后初始化和启动服务端。

        接下来看看服务器内部实现。成员如下,func_是接收可调用对象的。

1 初始化

        初始化显然就是创建和监听套接字,显然我们此时的通信是基于tcp协议的。

2 初识start

        暂时没链接时会链接失败,此时我们不能直接退出,要继续链接。

    class Tcpserver;
    class ThreadData
    {
    public:
        ThreadData(std::string ip, uint16_t port,int socket,Tcpserver* ts)
        :ip_(ip)
        ,port_(port)
        ,socket_(socket)
        ,ts_(ts)
        {
            ;
        }
        std::string ip_;
        uint16_t port_;
        int socket_;
        Tcpserver* ts_;
    }; 

     class Tcpserver
    {
    public:
        using func_t = std::function<Response(const Request)>;
        Tcpserver(func_t func, uint16_t port)
            : port_(port), func_(func)
        {
            ;
        }
    
        void start() // 接收链接,获取客户端端口号+ip,创建线程执行
        {
            while (true)
            {
                string ClientIp;
                uint16_t clientport;
                int socket_ = sock_.Accept(&ClientIp, &clientport);
                if (socket_ < 0)
                    continue;
                log_(ErrorLevel::Debug, "get a new client,client info:            
              [%s:%d]",ClientIp.c_str(),clientport);

               创建线程,传递客户端端口号和ip

                pthread_t id;
                ThreadData *td = new ThreadData(ClientIp, clientport, socket_, this);
                pthread_create(&id, nullptr, threadRoutine, td);
                
            }
        }

    private:
        func_t func_;
        Log log_;
        Sock sock_;
        uint16_t port_;
    };
       

        我们给执行函数传了个ThreadData类对象。在threadRoutie掉用serverio函数,方便后续如果服务端收到请求要做其它处理,此时就将serverio函数换成其它函数即可。

        static void* threadRoutine(void*arg)
        {
            ThreadData* td = static_cast<ThreadData*>(arg);
            td->ts_->ServerIO(td->ip_,td->port_,td->socket_);
        }

         我们知道如果ServerIO函数要和客户端通信,肯定需要客户端ip,端口和套接字,所以我们先把这些参数合并传给该函数。写完后,但是客户端还没写好,如何测试,我们可以用可以指令向服务器发起一个链接。

         void ServerIO(std::string ip, uint16_t port,int socket)
        {
  
                ;
        }

        可以测试目前代码是否可以跑通,也就是套接字创建是否会问题。

        在实现ServerIO函数前,我们先定协议,先记住我们要在这个函数读客户端消息,处理数据并返回,这个大致步骤方便我们实现完协议来理解调用逻辑。

3 设计协议

        数据的流动前面提过:客户端发起request,序列化转为字符串发给服务端,服务端收到后反序列化,处理返回responce,将结果序列化再发给客户端,客户端收到后再反序列化转为Response对象,客户端直接读写对象成员就可以获得结果了。

        所以我们要将request和Response序列化和反序列化。封装在下面这个头文件中。

Request序列化

        协议就是规定:我们首先规定Request请求就是x + y,两个操作数,一个操作符,然后序列化的字符串必须是"x + y",有时候我们想让字符串变成"x  +  y",操作符之间间隔增加一个空格,这里要体会一下不用宏的话,如果要变更格式有多麻烦。

 #define SEP " "
 #define SEP_LEN strlen(SEP) 
class Request
    {
    public:
        Request()
        {
            ;
        }
        Request(int x, int y, char op)
            : x_(x), y_(y), op_(op)
        {
            ;
        }

        // class -> string 序列化
        bool serialize(std::string *res)
        {
            *res += std::to_string(x_);
            *res += SEP;
            *res += op_;
            *res += SEP;
            *res += std::to_string(y_);
            return true;
        }
        
        ~Request()
        {
            ;
        }
        int x_;
        int y_;
        char op_;
    };

反序列化

        既然前面已经规定操作数之间,操作符和操作数之间是有间隔符的,那我们就用string的find接口查找间隔符SEP,把一个个操作数截取下来。

class util
{
public:
      //"10 + 10"
    static void StringSplit(const std::string res,const std::string sep,std::vector<std::string>*vs)
    {
        int pos = 0;
        while(pos != -1)
        {
            int nextpos = res.find(sep.c_str(),pos);
            if(nextpos == -1)
                break;
            vs->push_back(res.substr(pos,nextpos - pos));
            pos = nextpos + sep.size();
        }
    截取最后一个操作数
        vs->push_back(res.substr(pos,-1));
    }
};

        将"x + y"截取成"x","+","y"保存到vector中,此时我们可以更深刻的意识到协议就是规定。

         string -> class "x + y" 反序列化

        bool Deserialize(const std::string &res)
        {
            std::vector<std::string> vs;
            util::StringSplit(res, SEP, &vs);
            if (vs.size() != 3)
                return false;
            x_ = atoi(vs[0].c_str());
            if (vs[1].size() != 1)
                return false;
            op_ = vs[1][0];
            y_ = atoi(vs[2].c_str());
        }

        做判断,操作符大小必须是1,操作符和操作数的大小和为3,这,都是基于我们的规定的来的。

Response 序列化

 class Response
    {
    public:
        Response()
        {
            ;
        }
        Response(int result, int exitcode)
            : result_(result), exitcode_(exitcode)
        {
            ;
        }
        // class -> string
        bool serialize(std::string *msg)
        {
            *msg += std::to_string(result_);
            *msg += SEP;
            *msg += std::to_string(exitcode_);
            return true;
        }
       
        ~Response()
        {
            ;
        }
        int result_;
        int exitcode_;
    };

        我们在序列化Respnse的时候规定了,必须是"结果 + 退出码",这是反序列的时候截取字符串的基础。

反序列化,还可以复用Reuest实现的截取字符串,封装的妙处总是在不经意中体现。

           string -> class
        bool Deserialize(const std::string &res)
        {
            std::vector<std::string> vs;
            if (vs.size() != 2)
                return false;
            util::StringSplit(res, SEP, &vs);
            result_ = atoi(vs[0].c_str());
            exitcode_ = atoi(vs[1].c_str());
            return true;
        }

        可是设计了协议,怎么调用呢?调用顺序是什么呢? 我们前面说了,我们在threadRoutine内调用ServerIO函数来读写,所以接下来就在该函数内将上述实现用起来。

4 start完善

       先读数据,既然是读数据,自然是从套接字中读取,有意思的是我们怎么保证一次读一个完整报文呢,也就是说我们怎么保证一次读出"x + y"这样的完整报文,我们调用read每次读固定大小,肯定会出现读取多个报文的情况,所以我们对协议做了修改,原先规定请求是"x + y",为了方便读取,我们添加了一个报头,5"\n"x + y",然后设计一个函数,保证能切割出一个完整报文给下一步执行。这个还和tcp面向字节流有些关系,导致tcp向上向下交付以字节为单位,而不是一个一个数据报交付,需要我们手动切割。

        我们来看看函数内部实现。inbuffer保存了recv读到的所有数据。

        这个是后面经常用的宏。

     从socket读取数据,保存到inbuffer中,并解析出数据报放到package中
    int Readpackage(int socket, std::string *inbuffer, std::string *package)
    {
        cout << "读取数据前:" << *inbuffer << endl;
        Log log_;
        char buffer[1024] = {0};
        int n = recv(socket, buffer, sizeof(buffer), 0);

        *inbuffer += buffer; 保存读到的数据

        cout << "读取数据中:" << endl
             << *inbuffer;

        if (n < 0) // 出错后返回
        {
            return -1;
        }

        截取一个数据报 "5\n"x + y"\n"

        int pos = inbuffer->find(HEAD, 0);
        if (pos == -1)
        {
            return -1;
        }

        // 截取的是记录着有效载荷长的字符串
        std::string Size = inbuffer->substr(0, pos);
        int lensize = atoi(Size.c_str()); 有效载荷长度

        完整报文长度
        int packagelen = lensize + 2 * HEAD_LEN + Size.size();

        if ((*inbuffer).size() < packagelen) 读取的数据不够一个数据报
            return 0;

        截取有效载荷
        *package = inbuffer->substr(pos + HEAD_LEN, lensize);
        inbuffer->erase(0, packagelen);

        cout << "读取数据后:" << endl
             << *inbuffer;
        return lensize;
    }

        此时我们回到外部函数逻辑中。

        void ServerIO(std::string ip, uint16_t port, int socket)
        {
            std::string inbuffer;
            while (true)
            {
                    1 读取数据
                std::string package;
                int n = Readpackage(socket,&inbuffer,&package);
                if (n < 0)
                {    
                    close(socket);
                    exit(READ_ERR);
                }
                else if (n == 0)
                    continue;
                
                到了这里就已经读取到了完整的数据报,先去除报头
                其实已经不用去除了,因为前面我们读取的就是一个有效载荷
                package = RemoveHead(package,n);

                2 将字符串反序列化
                Request rq;
                rq.Deserialize(package);

                3 处理一个请求,并返回结果
                Response rp = func_(rq);

                4 将结果序列化
                string send_string;
                rp.serialize(&send_string);

                添加报头发送,响应也要有报头
                send_string = AddHead(send_string);
                //发送到网络中
                write(socket,send_string.c_str(),send_string.size());
            }
        }
   

       添加报头。

        移除报头。

         在第3步的时候我们用func回调了一个函数,这个函数是一开始外部传入的请求处理函数。

func_函数实现。

Response calculate(const Request &rq)
{
    Response rp(0,0);
    switch (rq.op_)
    {
    case '+':
        rp.result_ = rq.x_ + rq.y_;
        break;
    case '-':
        rp.result_ = rq.x_ - rq.y_;
        break;
    case '*':
        rp.result_ = rq.x_ * rq.y_;
        break;
    case '/':
        if (rq.y_ == 0)
        {
            rp.exitcode_ = 1;
            break;
        }
        rp.result_ = rq.x_ / rq.y_;
        break;
    case '%':
        if (rq.y_ == 0)
        {
            rp.exitcode_ = 2;
            break;
        }
        rp.result_ = rq.x_ + rq.y_;
        break;
    default:
        rp.exitcode_ = 3;
        break;
    }
    return rp;
}

四 客户端编写

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        exit(USAGE_ERR);
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);


    Log logs;
    Sock socks;
    socks.Socket();
    socks.Connect(ip, port);
    logs(ErrorLevel::Debug, "init success: sockt:%d", socks.socket_);

    std::string inbuffer;
    while (true)
    {
        std::cout << "data1# ";
        Request rq;

        std::cin >> rq.x_;

        std::cout << "data2# ";
        std::cin >> rq.y_;

        std::cout << "data3# ";
        std::cin >> rq.op_;
        // 序列化
        std::string ret;
        rq.serialize(&ret);

        // 添加报头
        ret = AddHead(ret);

        // 开始发送
        send(socks.socket_, ret.c_str(), ret.size(), 0);

        // 开始读取
        std::string package;
START:
        int n = Readpackage(socks.socket_, &inbuffer, &package);
        if (n < 0)
        {
            close(socks.socket_);
            exit(READ_ERR);
        }
        else if (n == 0)
            goto START;
        // 到了这里就已经读取到了完整的数据报,先去除报头
        package = RemoveHead(package, n);

        //将字符串反序列化
        Response rp;
        rp.Deserialize(package);    
        cout<<"result: "<<rp.result_<<endl;
        cout<<"exitcode: "<<rp.exitcode_<<endl;
    }
    return 0;
}

        首先获取到服务端ip和端口。

        创建套接字,并且链接服务器。

                开始获取构建一个请求,这个请求是关于x和y的计算,所以我们首先输入x和y的值,然后把操作数也输入进来,因为这个请求可能是x+y,或者x-y。

        输入完后,要开始准备发送了。当然要先序列化了,把请求转成字符串,但是我们还要添加报头,这个报头是服务端读取一个完整报文的关键。

        此时我们才可以发送,发送完我们还可以接收服务端的响应。

        这个接收服务端的响应的实现就和服务端那边的实现差不多,都是调用Readpackage读取一个完整报文,然后就是去除报头,再反序列化,我们最终可以打印显示响应了,这个响应包括计算结果和错误码。

        由于调用链比较长,我们增加了一些日志,一步步看看我们的序列化和反序列是否符合逻辑。

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小何只露尖尖角

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值