网络序列化和反序列化

Posted 华丞臧.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络序列化和反序列化相关的知识,希望对你有一定的参考价值。

🥁作者 华丞臧.
📕​​​​专栏【网络】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站


文章目录


一、应用层

  • 程序员写的一个个解决实际问题满足日常需求的网络程序都是在应用层的。
  • 协议是一种“约定”,socket api的接口在读写数据时,都是按“字符串”的方式来发送接收的。
  • 如果我们要传输一些“结构化数据”,能直接传输吗?肯定是不能直接传输的,结构化数据存在一些问题,比如C语言中结构体需要结构体对齐,C++中类同样需要对齐,会浪费网络资源,并且不同操作系统的大小端在不同编译器下可能不同。

所以对于需要传输结构化数据,我们可以进行约定:

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照约定的规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转回结构体。
  • 序列化:将结构体数据转换成字符串的过程。
  • 反序列化:将字符串数据转换回结构化数据的过程。

二、网络版本计算器

在进行结构化数据的网络通信时,需要将数据序列化成字符串再发送给对方,但是对方并不知道传输数据的长度,也不知道如何从字符串的数据中读取结构化的数据;因此在进行网络通信之间,通信的双发需要进行约定,比如:约定如何确定序列化数据的长度以及用什么格式来反序列化字符串。
在这里我们先手写一个定制协议,约定如下:

  • 计算器格式通常是:1+2、 2*3、9/3;
  • 序列化字符串首部几个字节表示长度;
  • 有效载荷通过空格隔开;
  • 表示长度的报头和有效载荷通过\\r\\n隔开;
  • 报文之间也是用\\r\\n隔开

2.1 协议定制


客户端和服务端代码请看👉序列化和反序列化

#pragma once
#include <iostream>
#include <string>
#include <assert.h>

#include "Log.hpp"

#define CRLF "\\r\\n"
#define CRLF_LEN strlen(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define OPS "+-*/%"

// encode,对整个序列化之后的字符串进行添加长度
//"strlen\\r\\nXXXXXX\\r\\n"
std::string encode(std::string &in, uint32_t len)


// decode,整个序列化之后的字符串进行提取长度
// 1. 必须具有完整的长度
// 2. 必须具有和len相符合的有效载荷
// 我们才返回有效载荷和len
// 否则,我们就是一个检测函数!
// 9\\r\\n100 + 200\\r\\n    9\\r\\n112 / 200\\r\\n
std::string decode(std::string &in, uint32_t *len)



// 定制请求 
class Request

public:
    Request()
    
    
    ~Request()
    
    
    int& getX()
     return x_;
    
    int& getY()
     return y_;
    
    char& getOp()
     return op_;
    
    // 序列化
    void serialization(std::string *out)
    

    // 反序列化
    bool deserialization(std::string &in)
    
    
    void debug()
    
private:
    // 需要计算的数据
    int x_; 
    int y_;
    // 需要计算的种类,"+ - * / %"
    char op_;
;

// 定制响应
class Response

public:
    Response()
    :result_(0)
    ,exitCode_(0)
    
    ~Response()
    
    // 序列化
    void serialization(std::string *out)
    
    // 反序列化
    bool deserialization(std::string &in)
    
public:
    // 退出状态,0标识运算结果合法,非0标识运算结果是非法的
    int exitCode_; 
    // 运算结果
    int result_;
;

bool makeReuquest(const std::string &str, Request *req)

2.2 协议实现

encode

按照约定需要在序列化之后的字符串首部加上有效载荷的长度并用\\r\\n隔开,并且结尾也需要加上\\r\\n

// encode,对整个序列化之后的字符串进行添加长度
//"strlen\\r\\nXXXXXX\\r\\n"
std::string encode(std::string &in, uint32_t len)

    // "exitCode_ result_"
    // "len\\r\\n" "exitCode_ result_\\r\\n"
    std::string encodeStr = std::to_string(len);
    encodeStr += CRLF;
    encodeStr += in;
    encodeStr += CRLF;

    //std::cout << "debug->encode-> " << encodeStr << std::endl;
    return encodeStr;

decode

  • 第一步要先提取有效载荷的长度,再根据长度检查字符串中是否含有一个完整的有效载荷;
  • 如果具有完整的有效载荷,可以通过长度来提取有效载荷。
  • 注意如果需要进行多次提取,需要将读取出的字符串删除。
// decode,整个序列化之后的字符串进行提取长度
// 1. 必须具有完整的长度
// 2. 必须具有和len相符合的有效载荷
// 我们才返回有效载荷和len
// 否则,我们就是一个检测函数!
// 9\\r\\n100 + 200\\r\\n    9\\r\\n112 / 200\\r\\n
std::string decode(std::string &in, uint32_t *len)

    assert(len);

    // 1. 确认是否是一个包含len的有效字符串
    *len = 0;
    size_t pos = in.find(CRLF);
    if(pos == std::string::npos)
        return "";

    // 2. 提取长度
    std::string strLen = in.substr(0, pos);
    int intLen = atoi(strLen.c_str());

    // 3. 确认有效载荷也是符合要求的
    int surplus = in.size() - 2 * CRLF_LEN - pos;
    if(surplus < intLen)
    
        return "";
    

    // 4. 确认有完整的报文结构
    std::string package = in.substr(pos + CRLF_LEN, intLen);
    *len = intLen;

    // 5. 将当前报文完整的从in中全部移除掉
    int remLen = strLen.size() + 2 *CRLF_LEN + package.size();
    in.erase(0, remLen);

    // 6. 正常返回
    return package;

定制请求 – Requset

定制请求,就是给请求方一个存放结构化数据的空间,请求方可以通过定制好的协议进行序列化得到字符串,然后就可以与服务端进行网络通信了;Request中的反序列化通常是给服务端提取请求方的结构化数据,所以服务端可以根据结构化数据向请求方进行响应。

序列化:

  • 序列化出的数据属于有效载荷,因此按照协议需要使用空格隔开;

反序列化:

  • 按照协议规定,结构化的数据在字符串中是通过空格隔开的,所以我们可以根据字符串中的两个空格将数据结构化提取出来。
#define CRLF "\\r\\n"
#define CRLF_LEN strlen(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)

// 定制请求 
class Request

public:
    Request()
    
    
    ~Request()
    

    int& getX()
     return x_;

    int& getY()
     return y_;
    
    char& getOp()
     return op_;

    // 序列化
    void serialization(std::string *out)
    
        // 100 + 200 
        // "100 + 200"
        std::string dateOne = std::to_string(x_);
        std::string dateTwo = std::to_string(y_);
        std::string dateOp = std::to_string(op_);

        *out = dateOne;
        *out += SPACE;
        *out += op_;
        *out += SPACE;
        *out += dateTwo;
        //std::cout << *out << std::endl;
    

    // 反序列化
    bool deserialization(std::string &in)
    
        //std::cout << 0 << std::endl;
        // 100 + 200 
        size_t spaceOne = in.find(SPACE);
        if(spaceOne == std::string::npos) return false;
        //std::cout << 1 << std::endl;

        size_t spaceTwo = in.rfind(SPACE);
        if(spaceTwo == std::string::npos) return false;
        //std::cout << 2 << std::endl;


        std::string dateOne = in.substr(0, spaceOne);
        std::string dateTwo = in.substr(spaceTwo + SPACE_LEN);
        std::string dateOp = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
        if(dateOp.size() != 1) 
        
            printf("%d:%c\\n", dateOp.size(), dateOp.c_str());
            return false;
        
        //std::cout << 3 << std::endl;

        // 转成内部成员
        x_ = atoi(dateOne.c_str());
        y_ = atoi(dateTwo.c_str());
        op_ = atoi(dateOp.c_str());

        return true;
    

    void debug()
    
        std::cout << "x_" << x_ << std::endl;
        std::cout << "y_" << y_ << std::endl;
        std::cout << "op_" << op_ << std::endl;
    
private:
    // 需要计算的数据
    int x_; 
    int y_;
    // 需要计算的种类,"+ - * / %"
    char op_;
;

定制响应 – Response

响应是服务端在对客户端的请求提供服务之后给客户端返回的结果,所以Response需要能够存放服务结果以及发生错误时的状态码;而结果和错误码是数据化结构,所以需要序列化之后在传输给客户端,客户端在拿到响应后需要进行反序列化拿到结果和状态码。序列胡和反序列化过程与定制请求类似。

#define CRLF "\\r\\n"
#define CRLF_LEN strlen(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
// 定制响应
class Response

public:
    Response()
    :result_(0)
    ,exitCode_(0)
    
    
    ~Response()
    

    // 序列化
    void serialization(std::string *out)
    
        // "exitCode_ result_"

        std::string ec = std::to_string(exitCode_);
        std::string res = std::to_string(result_);

        *out = ec;
        *out += SPACE;
        *out += res;
    

    // 反序列化
    bool deserialization(std::string &in)
    
        // "nextiCode_ result_"
        size_t pos = in.find(SPACE);
        if(pos == std::string::npos) return false;

        std::string ec = in.substr(0, pos);
        std::string res = in.substr(pos + SPACE_LEN);

        exitCode_ = atoi(ec.c_str());
        result_ = atoi(res.c_str());

        return true;
    

public:
    // 退出状态,0标识运算结果合法,非0标识运算结果是非法的
    int exitCode_; 
    // 运算结果
    int result_;
;

输入参数读取

我们在客户端输入时是按照字符串的格式输入的,所以为了能够使用定制协议,需要进行反序列化即将输入的字符串数据按照协议中的结构化数据提取出来。

#define OPS "+-*/%"

bool makeReuquest(const std::string &str, Request *req)

    // 123+1  1*1 1/1
    char strtmp[1024];
    snprintf(strtmp, sizeof strtmp, "%s", str.c_str());
    char *left = strtok(strtmp, OPS);
    if (!left)
        return false;
    char *right = strtok(nullptr, OPS);
    if (!right)
        return false;
    char mid = str[strlen(left)];

    req->getX() = atoi(left);
    req->getY() = atoi(right);
    req->getOp() = mid;
    return true;

测试

  • IP:127.0.0.1 --》测试
  • IP:119.91.213.117 --》测试

2.3 使用第三方库

大佬也写了第三方库来支持序列化和反序列化,在这里我使用的jsoncpp,安装方式如下:

sudo yum install -y jsoncpp-devel

第三方库的使用较之我们自己实现无疑方便了很多,不用自己造轮子。

#pragma once
#include <iostream>
#include <string>
#include <assert.h>
#include <jsoncpp/json/json.h>

#include "Log.hpp"

#define CRLF "\\r\\n"
#define CRLF_LEN strlen(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define OPS "+-*/%"

// 定制请求 
class Request

public:
    Request()
    
    
    ~Request()
    

    int& getX()
     return x_;

    int& getY()
     return y_;
    
    char& getOp()
     return op_;

    // 序列化
    void serialization(std::string *out)
    
         //json
        // 1. Value对象,万能对象
        // 2. json是基于KV
        // 3. json有两套操作方法
        // 4. 序列化的时候,会将所有的数据内容,转换成为字符串
        Json::Value root;  //,万能对象,任意类型
        root["x"] = x_;
        root["y"] = y_;
        root["op"] = op_;
			
        Json::FastWriter fw;  
        //Json::StyledWriter fw;
        *out = fw.write(root); //序列化
    

    // 反序列化
    bool deserialization(std::string &in)
    
    Json::Value root;
    Json::Reader rd;
    rd.parse(in, root);

    x_ = root["x"].asInt();
    y_ = root["y"].asInt();
    op_ = root["op"].asInt();

    return true;
    

    void debug()
    
        std::cout << "x_" << x_ << std::endl;
        std::cout << "y_" << y_ << std::endl;
        std::cout << "op_" << op_ << std::endl;
    
private:
    // 需要计算的数据
    int x_; 
    int y_;
    // 需要计算的种类,"+ - * / %"
    char op_;
;

// 定制响应
class Response

public:
    Response()
    :result_(0)
    ,exitCode_(0)
    
    
    ~Response()
    

    // 序列化
    void serialization(std::string *out)
    
    Json::Value root;
    root["exitCode"] = exitCode_;
    root["result"] = result_;

    Json::FastWriter fw;
    *out = fw.write(root); //序列化
    

    // 反序列化
    bool deserialization(std::string &in)
    
    Json::Value root;
    Json::Reader rd;
    rd.parse(in, root);

    exitCode_ = root["exitCode"].asInt();
    result_ = root["result"].

什么叫序列化和反序列化?

序列化:对象转化为字节序列的过程叫做对象的序列化

反序列化:字节序列恢复为对象的过程叫做对象的反序列化

 

为啥要序列化?(序列化的用途)

-需要将对象永久的保存在硬盘中(比如Session对象)

-网络传输对象的序列(网络中的二进制序列)

 

过程:

序列化的过程:创建一个对象输出流对象,调用对象的writeObject()函数

反序列化的过程,创建一个对象输入流,调用readObject()函数

 

例子:

package Note;

import java.io.*;

/**
 * @author yintianhao
 * @createTime 20190218 21:46
 * @description 序列化
 */
public class People implements Serializable {
    //序列化id
    private static final long serialVersionUID = -5809782578272943999L;

    private int age;
    private String name;


    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) throws IOException,ClassNotFoundException {
        //序列化
        People one = new People();
        one.setAge(5);
        one.setName("张三");
        ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream(new File("G:/test.txt")));
        stream.writeObject(one);
        stream.close();

        //反序列化
        ObjectInputStream stream1 = new ObjectInputStream(new FileInputStream(new File("G:/test.txt")));
        People two = (People) stream1.readObject();
        System.out.println("Name = "+two.getName()+"--age = "+two.getAge());
        stream1.close();
    }
}
Code

检验一下G盘是否有这个test.txt

 再看Console

 

 

 

那么问题来了:serialVersionUID是干啥的? 

看字面上的意思就是版本号嘛,那么这个版本号是干啥的呢

那么就有必要了解一下序列化的过程了:

序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。

当然这里是我自己指定的serialVersionUID,如果这里我们不写也是没问题的,因为如果我们自己没定义serialVersionUID

jdk会自行给我们生成一个serialVersionUID,但是一般情况IDE会报个警告,不知道为啥IDEA没报,但是Eclipse是报了...

第一句是生成默认的版本ID,值为

第二句是根据类的类名信息等生成的,值为

 

但是如果我们不指定ID,那么,如果我们修改了类中的信息,比如增加或者删除某个属性,那么我们在反序列化之前的

对象的时候就会报错,因为这个修改后的类的serialVersionUID已经变了,所以不能反序列化第一次的对象

打个比方增加一个性别sex的选项,然后为了验证把之前的序列化的过程去掉,看改完后的这个能不能反序列化之前的

代码如下:

package Note;

import java.io.*;

/**
 * @author yintianhao
 * @createTime 20190218 21:46
 * @description 序列化
 */
public class People implements Serializable {
    //序列化id

    private int age;
    private String name;
    private String sex;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) throws IOException,ClassNotFoundException {
       

        //反序列化
        ObjectInputStream stream1 = new ObjectInputStream(new FileInputStream(new File("G:/test.txt")));
        People two = (People) stream1.readObject();
        System.out.println("Name = "+two.getName()+"--age = "+two.getAge());
        stream1.close();
    }
}
Code

运行,就报错了

Exception in thread "main" java.io.InvalidClassException: Note.People; local class incompatible: stream classdesc serialVersionUID = 8716394284034362776, local class serialVersionUID = 5389077117487999490
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at Note.People.main(People.java:37)

 

这里这个ID是由JDK的生成的,根据类的一些细节信息生成,具体怎么生成的我们不管,但是像刚才那样我们是不愿看见的,因为不同的Java编译器可能生成的

ID就不一样,所以还是强烈建议自定义一个明确的serialVersionUID

 

参考博客:https://www.cnblogs.com/xdp-gacl/p/3777987.html

https://www.cnblogs.com/duanxz/p/3511695.html

 

以上是关于网络序列化和反序列化的主要内容,如果未能解决你的问题,请参考以下文章

序列化和反序列化

(JSON) 序列化和反序列化,这个是啥意思呀?

java序列化反序列化深入探究

在Java中进行序列化和反序列化

Java序列化和反序列化

序列化和反序列化