RPC 漫谈:序列化问题

news2024/11/25 6:33:26

RPC 漫谈:序列化问题

何为序列

对于计算机而言,一切数据皆为二进制序列。但编程人员为了以人类可读可控的形式处理这些二进制数据,于是发明了数据类型和结构的概念,数据类型用以标注一段二进制数据的解析方式,数据结构用以标注多段(连续/不连续)二进制数据的组织方式。

例如以下程序结构体:

type User struct {
	Name  string
	Email string
}

Name 和 Email 分别表示两块独立(或连续,或不连续)的内存空间(数据),结构体变量本身也有一个内存地址。

在单进程中,我们可以通过分享该结构体地址来交换数据。但如果要将该数据通过网络传输给其他机器的进程,我们需要现将该 User 对象中不同的内存空间,编码成一段连续二进制表示,此即为「序列化」。而对端机器收到了该二进制流以后,还需要能够认出该数据为 User 对象,解析为程序内部表示,此即为「反序列化」。

序列化和反序列化,就是将同一份数据,在人的视角和机器的视角之间相互转换。

序列化过程

在这里插入图片描述

定义接口描述(IDL)

为了传递数据描述信息,同时也为了多人协作的规范,我们一般会将描述信息定义在一个由 IDL(Interface Description Languages) 编写的定义文件中,例如下面这个 Protobuf 的 IDL 定义:

message User {
  string name  = 1;
  string email = 2;
}

生成 Stub 代码

无论使用什么样的序列化方法,最终的目的是要变成程序中里的一个对象,虽然序列化方法往往是语言无关的,但这段将内存空间与程序内部表示(如 struct/class)相绑定的过程却是语言相关的,所以很多序列化库才会需要提供对应的编译器,将 IDL 文件编译成目标语言的 Stub 代码。

Stub 代码内容一般分为两块:

类型结构体生成(即目标语言的 Struct[Golang]/Class[Java] )
序列化/反序列化代码生成(将二进制流与目标语言结构体相转换)
下面是一段 Thrift 生成的的序列化 Stub 代码:

type User struct {
  Name string `thrift:"name,1" db:"name" json:"name"`
  Email string `thrift:"email,2" db:"email" json:"email"`
}

//写入 User struct
func (p *User) Write(oprot thrift.TProtocol) error {
  if err := oprot.WriteStructBegin("User"); err != nil {
    return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) }
  if p != nil {
    if err := p.writeField1(oprot); err != nil { return err }
    if err := p.writeField2(oprot); err != nil { return err }
  }
  if err := oprot.WriteFieldStop(); err != nil {
    return thrift.PrependError("write field stop error: ", err) }
  if err := oprot.WriteStructEnd(); err != nil {
    return thrift.PrependError("write struct stop error: ", err) }
  return nil
}

// 写入 name 字段
func (p *User) writeField1(oprot thrift.TProtocol) (err error) {
  if err := oprot.WriteFieldBegin("name", thrift.STRING, 1); err != nil {
    return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:name: ", p), err) }
  if err := oprot.WriteString(string(p.Name)); err != nil {
  return thrift.PrependError(fmt.Sprintf("%T.name (1) field write error: ", p), err) }
  if err := oprot.WriteFieldEnd(); err != nil {
    return thrift.PrependError(fmt.Sprintf("%T write field end error 1:name: ", p), err) }
  return err
}

// 写入 email 字段
func (p *User) writeField2(oprot thrift.TProtocol) (err error) {
  if err := oprot.WriteFieldBegin("email", thrift.STRING, 2); err != nil {
    return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:email: ", p), err) }
  if err := oprot.WriteString(string(p.Email)); err != nil {
  return thrift.PrependError(fmt.Sprintf("%T.email (2) field write error: ", p), err) }
  if err := oprot.WriteFieldEnd(); err != nil {
    return thrift.PrependError(fmt.Sprintf("%T write field end error 2:email: ", p), err) }
  return err
}

可以看到,为了把 User 对象给序列化成二进制,它 hard code 了整个结构体在内存中的组织方式和顺序,并且分别对每个字段去做强制类型转换。如果我们新增了一个字段,就需要重新编译 Stub 代码并要求所有 Client 进行升级更新(当然不需要用到新字段可以不用更新)。反序列化的步骤也是类似。

上述这段冗长的代码还只是我们用于演示的一个最简单的消息结构,对于生产环境中的真实消息类型,这段 Stub 代码会更加复杂。

Stub 代码生成只是为了解决跨语言调用的问题,并不是必须项。如果你的调用方与被调用方都是同一种语言,且未来一定能够保证都是同一种语言,这种情况也会选择直接用目标语言去写 IDL 定义,跳过编译的步骤,例如 Thrift 里的 drift 项目就是利用 Java 直接去写定义文件:

@ThriftStruct
public class User
{
    private final String name;
    private final String email;

    @ThriftConstructor
    public User(String name, String email)
    {
        this.name = name;
        this.email = email;
    }

    @ThriftField(1)
    public String getName()
    {
        return name;
    }

    @ThriftField(2)
    public String getEmail()
    {
        return email;
    }
}

我们前面说了,序列化本身的意义就在于提供人和机器视角对数据认识的一种转换。传统的思路是通过一个中间结构体,而这类方式是通过提供操作函数。

不过这类方式有一个通病就是仅仅只是提供了操作数据的能力,但是牺牲了程序编写者自己去管理数据的便利性。比如如果我们想知道这个 User 结构有哪些字段,除非序列化编译后的代码提供给了你这个能力,否则你将对一串二进制无从下手。比如你想直接把这个 User 对象和一些 ORM 工具组合存进数据库,你必须自己手写一个新的 User struct,然后挨个字段赋值。

这类序列化框架大多用在那些数据定义不怎么变化的核心基础设施服务,例如数据库,消息队列这类。如果用在日常业务开发,或许性价比不是很高。

最后

我们经常听到网上有人讨论,哪个序列化协议性能更好。其实如果我们真的认真去研究各类序列化方案,很容易会发现,序列化协议本身只是一份文档,它的性能优劣取决于你怎么去实现。不同语言实现,同语言不同方式方法的实现,都会对最终的易用性和性能产生巨大的影响。你完全可以把 Protobuf 的协议用 Flatbuffer 的方式去实现,能够提升非常多的性能,但未必就是你想要的。

与性能相比更为重要的是先弄清楚我们在序列化的各种问题中,希望解决哪些,愿意放弃哪些,有了明确的需求才能选择到适合的序列化方案,并且真的遇到问题时也能快速知道这个问题是否是可解的,如何解。

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

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

相关文章

echarts formatter如何自定义百分比小数位置,比如取整数。{b} {d}%

echarts formatter如何自定义百分比小数位置,比如取整数。{b} {d}% 一、现状 我有一个 pie 的图表,option 中的 formatter 是这样的: label: {show: true,position: outside,fontSize: 12,formatter: {b} {d}% },图表数据是这样的 二、需…

获取本地电脑连接的所有WIFI密码(适合Windows 11/10/8/7)

背景 如果你的心入职同事问你公司WIFI密码是多少,恰好这时你也忘记密码,用次方法可以实现得到WIFI密码。 如果你忘记现在在WIFI密码,也可以用此方法获取。 实现 1. 使用管理员权限打开 cmd.exe 2. 获取本机所有连接的 WIFI 用户配置 ne…

如何交叉编译程序:以freetype为例

【记录所学】 本博客为学习Linux开发时的笔记。主要记录如何交叉编译程序。 内容会首先介绍程序运行的一些基础知识,其次介绍常见错误的解决方法,然后介绍交叉编译程序的万能命令,最后以一个实际例子介绍如何交叉编译程序。 简要说明&#…

使用篇丨链路追踪(Tracing)很简单:链路实时分析、监控与告警

作者:涯海 前文回顾: 基础篇|链路追踪(Tracing)其实很简单 使用篇|链路追踪(Tracing)其实很简单:请求轨迹回溯与多维链路筛选 在前面文章里面,我们介绍了…

快排非递归 归并排序

递归深度太深会栈溢出 程序是对的&#xff0c;但是递归个10000层就是栈溢出 int fun(int n) {if (n < 1){return n;}return fun(n - 1) n; }所以需要非递归来搞快排和归并&#xff0c;在效率方面没什么影响&#xff0c;只是解决递归深度太深的栈溢出问题 有的能直接改&am…

2023年Android开发现状~

随着Android 开发行业的快速发展&#xff0c;市场需求也在不断提升&#xff0c;导致低端Android 开发市场就业大环境不好、行业趋势下滑&#xff0c;使得不少初中级的Android开发开始失业&#xff0c;找不到工作。 为什么这么说&#xff1f; 现在不像2012年——2018年的这段期…

性能调优通用逻辑

调优准备 定目标&#xff1a;根据线上预估访问量评估单场景QPS及混合场景QPS&#xff0c;和对应的RT值 环境区分&#xff1a; 测试环境单机压测进行链路问题排查问题&#xff0c;通常需要把单机打到CPU到100%&#xff0c;如果CPU到不了100%且请求已经各种超时或RT高于目标值…

Voting_Averaging算法预测银行客户流失率

Voting_Averaging算法预测银行客户流失率 描述 为了防止银行的客户流失&#xff0c;通过数据分析&#xff0c;识别并可视化哪些因素导致了客户流失&#xff0c;并通过建立一个预测模型&#xff0c;识别客户是否会流失&#xff0c;流失的概率有多大。以便银行的客户服务部门更…

【大型互联网应用轻量级架构实战の一】轻量级架构概述

1、轻量级架构概述 1.1.1、前言 当下&#xff0c;互联网应用呈高速发展的趋势&#xff0c;要想不被市场淘汰&#xff0c;就必须与时间赛跑&#xff0c;故而&#xff0c;快 就成了所有互联网公司产品的特征&#xff0c;只有率先推出产品&#xff0c;才能获取主动权。 1.1.2、…

大模型时代下的paper生存= =

第一类&#xff1a;PEFT类论文 &#xff08;我还挺喜欢的&#xff0c;不知道自己什么时候可以搞出这种工作 &#xff08;为什么中英文穿插&#xff0c;利于自己写论文&#xff1a;&#xff09; COMPOSITIONAL P ROMPT T UNING WITH M OTIONC UES FOR O PEN - VOCABULARY V ID…

构建数字时代下的必要防线 消除医疗行业数据安全建设“盲区”

4月7日&#xff0c;由厦门市卫生健康信息学会和厦门大学附属第一医院、厦门服云信息科技有限公司举办的医疗数据安全学术研讨会顺利开展。 作为国内云原生安全领导厂商&#xff0c;安全狗除了协助举办此次活动&#xff0c;还以数据安全治理专家的身份参与演讲分享。 厦门服云…

全网最详细,Jmeter性能测试-性能进阶, 无界面命令运行CLI模式(六)

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 如果使用jmeter.bat…

代码随想录算法训练营第四十一天-动态规划3|343. 整数拆分 ,96.不同的二叉搜索树

343整数拆分&#xff0c;有两种解法&#xff0c;一种是数学的方法&#xff0c;利用当f>4时&#xff0c;2*&#xff08;f - 2&#xff09;2f - 4 > f的性质&#xff0c;将所有的因子都拆成3&#xff0c;最后的余数再乘进去。另外一种是动态规划&#xff0c;把前面的数拆了…

算法---文件的最长绝对路径

题目 假设有一个同时存储文件和目录的文件系统。下图展示了文件系统的一个示例&#xff1a; 这里将 dir 作为根目录中的唯一目录。dir 包含两个子目录 subdir1 和 subdir2 。subdir1 包含文件 file1.ext 和子目录 subsubdir1&#xff1b;subdir2 包含子目录 subsubdir2&…

PHP快速入门11-文件操作,附写入文件、文件重命名等20个高频使用案例

文章目录前言一、文件操作介绍二、 20个文件操作的例子2.1 打开文件并写入数据2.2 读取文件中的一行数据2.3 读取文件中的一个字符2.4 读取整个文件内容2.5 向文件写入内容2.6 将整个文件读入一个数组中2.7 删除文件2.8 重命名文件2.9 复制文件2.10 判断是否为文件2.11 判断是否…

【致敬未来的攻城狮计划】RA2E1环境搭建点亮发光二极管

开启攻城狮的成长之旅&#xff01;这是我参与的由 CSDN博客专家 架构师李肯和 瑞萨MCU &#xff08;瑞萨电子 (Renesas Electronics Corporation) &#xff09; 联合发起的「 致敬未来的攻城狮计划 」的第 2 天&#xff0c;点击查看活动计划详情 &#xff01; 开发环境搭建 开…

React styled-components(三)—— 高级特性

styled-components 高级特性样式继承嵌套设置主题样式继承 新建 Demo.js 文件&#xff1a; import React, { Component } from react import styled from styled-components;const CustomStyle styled.divp { color: red;} const ContextBox styled(CustomStyle)width:…

Tableau-创建环状图:使用2个饼图

步骤 1&#xff1a;创建饼图 在“标记”下面&#xff0c;选择“饼图”标记类型。将分类拖到颜色。将任务总数拖到角度。再拖动一次任务总数&#xff0c;放到标签。根据需要调整饼图大小。 步骤 2&#xff1a;切换到双轴图表 右键点击任意一个字段&#xff0c;创建-->计算…

3年功能测试无情被裁,3个月学习自动化测试重新开始........

前言 不知不觉在软件测试行业工作了3年之久&#xff0c;虽然说我是主做的功能测试&#xff0c;但是我也一直是兢兢业业的呀&#xff0c;不曾想去年7月份无情被辞的消息让我感到一阵沉重。我曾经一直坚信自己的技能和经验足以支撑我在这个领域的未来&#xff0c;但现实却告诉我&…

考研数据结构——表达式的转换用栈实现表达式的概述

一、用表达式实现中缀表达式转后缀表达式 把括号里的符号移到括号外 二、用栈实现中缀表达式转后缀表达式 1、遇到字母写下来 2、遇到符号加入栈中 3、遇到成对括号才出栈 4、当前读取运算符要小于等于栈顶运算符优先级则出栈 从左向右扫描 三、表达式方法实现中缀表达式转…