什么是RCEP?对我们普通人来讲有什么影响?

zkbhj 发表了文章 • 0 个评论 • 182 次浏览 • 2020-11-17 16:44 • 来自相关话题

一、概念
 
 
RCEP,英文全称 Regional Comprehensive Economic Partnership ,中文全称 区域全面经济伙伴关系协定。是2012年由东盟发起,历时八年,成员包括中国、日本、韩国、澳大利亚、新西兰和东盟十国共15方而制定的协定。东盟十国分别是:印度尼西亚、马来西亚、菲律宾、泰国、新加坡、文莱、柬埔寨、老挝、缅甸、越南。
 
2020年11月15日,区域全面经济伙伴关系协定签署仪式以视频方式进行,15个RCEP成员国经贸部长将在仪式上正式签署该协定。标志着当前世界上人口最多、经贸规模最大、最具发展潜力的自由贸易区正式启航。
 
协议共含20个章节,分为四大板块,包括货物贸易协定、投资协定、21世纪新议题和争端解决机制。
 原本应该有16国,包括印度在内,但是最后印度2019年因“有重要问题尚未得到解决”而没有加入协定。
 




 
二、意义
 
在疫情肆虐、世界经济严重衰退、国际贸易投资萎缩、保护主义单边主义加剧的特殊背景下,各方能够就RCEP达成共识,宣告了多边主义和自由贸易的胜利,将有力提振各方对经济增长的信心。将为区域和全球经济增长注入强劲动力。
 
世界正面临百年未有之大变局,RCEP的达成为亚太自贸区(FTAAP)进程提供了实现路径,进一步提升亚太地区今后在全球发展格局中的分量。
 三、全球经济格局现状
 
在RCEP签署之前,全球从洲际合作角度,最大的三个自贸区为北美自贸区(USMCA),欧盟(EU)和中国-东盟自贸区(CAFTA)。RCEP的诞生意味着全球最大自贸区形成,全球贸易格局正式演化为北美、欧盟、亚洲三足鼎立。
 
四、对普通人来讲,有哪些影响
 
RCEP协议的签署,就意味着在15国之内,商品流动、技术流动、服务流动、资本流动,包括人员跨境流动都会更加流畅。将会有超九成商品或纳入零关税范围,会极大地降低各成员国内流通商品的销售价格。当然更顺畅的贸易往来,也会带动更多的就业和创业机会。
 
但这些影响实际发生也需要等到2年之后,因为协议签订之后2年内,各个国家需要完成批准程序,协议才正式生效。 查看全部
一、概念
 
 
RCEP,英文全称 Regional Comprehensive Economic Partnership ,中文全称 区域全面经济伙伴关系协定。是2012年由东盟发起,历时八年,成员包括中国、日本、韩国、澳大利亚、新西兰和东盟十国共15方而制定的协定。东盟十国分别是:印度尼西亚、马来西亚、菲律宾、泰国、新加坡、文莱、柬埔寨、老挝、缅甸、越南。
 
2020年11月15日,区域全面经济伙伴关系协定签署仪式以视频方式进行,15个RCEP成员国经贸部长将在仪式上正式签署该协定。标志着当前世界上人口最多、经贸规模最大、最具发展潜力的自由贸易区正式启航。
 
协议共含20个章节,分为四大板块,包括货物贸易协定、投资协定、21世纪新议题和争端解决机制。
 原本应该有16国,包括印度在内,但是最后印度2019年因“有重要问题尚未得到解决”而没有加入协定。
 
timg.jpeg

 
二、意义
 
在疫情肆虐、世界经济严重衰退、国际贸易投资萎缩、保护主义单边主义加剧的特殊背景下,各方能够就RCEP达成共识,宣告了多边主义和自由贸易的胜利,将有力提振各方对经济增长的信心。将为区域和全球经济增长注入强劲动力。
 
世界正面临百年未有之大变局,RCEP的达成为亚太自贸区(FTAAP)进程提供了实现路径,进一步提升亚太地区今后在全球发展格局中的分量。
 三、全球经济格局现状
 
在RCEP签署之前,全球从洲际合作角度,最大的三个自贸区为北美自贸区(USMCA),欧盟(EU)和中国-东盟自贸区(CAFTA)。RCEP的诞生意味着全球最大自贸区形成,全球贸易格局正式演化为北美、欧盟、亚洲三足鼎立。
 
四、对普通人来讲,有哪些影响
 
RCEP协议的签署,就意味着在15国之内,商品流动、技术流动、服务流动、资本流动,包括人员跨境流动都会更加流畅。将会有超九成商品或纳入零关税范围,会极大地降低各成员国内流通商品的销售价格。当然更顺畅的贸易往来,也会带动更多的就业和创业机会。
 
但这些影响实际发生也需要等到2年之后,因为协议签订之后2年内,各个国家需要完成批准程序,协议才正式生效。

什么是变量的深拷贝和浅拷贝?

zkbhj 发表了文章 • 0 个评论 • 227 次浏览 • 2020-07-24 10:41 • 来自相关话题

通用深拷贝和浅拷贝的区别
 

深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。

 假设B复制了A,修改A的时候,看B是否发生变化:

如果B跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值)
如果B没有改变,说明是深拷贝,自食其力!(修改堆内存中的不同的值)

 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
 
Go语言中的深浅拷贝
 
1、深拷贝(Deep Copy):

拷贝的是数据本身,创造一个样的新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。既然内存地址不同,释放内存地址时,可分别释放。

值类型的数据,默认全部都是深复制,Array、Int、String、Struct、Float,Bool。

2、浅拷贝(Shallow Copy):

拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。释放内存地址时,同时释放内存地址。

引用类型的数据,默认全部都是浅复制,Slice,Map。
 
深拷贝示例:
package main

import (
"fmt"
)

// 定义一个Robot结构体
type Robot struct {
Name string
Color string
Model string
}

func main() {
fmt.Println("深拷贝 内容一样,改变其中一个对象的值时,另一个不会变化。")
robot1 := Robot{
Name: "小白-X型-V1.0",
Color: "白色",
Model: "小型",
}
robot2 := robot1
fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, &robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, &robot2)

fmt.Println("修改Robot1的Name属性值")
robot1.Name = "小白-X型-V1.1"

fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, &robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, &robot2)

}深拷贝 内容一样,改变其中一个对象的值时,另一个不会变化。
Robot 1:{小白-X型-V1.0 白色 小型} 内存地址:0xc000072330
Robot 2:{小白-X型-V1.0 白色 小型} 内存地址:0xc000072360
修改Robot1的Name属性值
Robot 1:{小白-X型-V1.1 白色 小型} 内存地址:0xc000072330
Robot 2:{小白-X型-V1.0 白色 小型} 内存地址:0xc000072360浅拷贝示例2:
package main

import (
"fmt"
)

// 定义一个Robot结构体
type Robot struct {
Name string
Color string
Model string
}

func main() {

fmt.Println("浅拷贝 使用new方式")
//new方式返回的是一个指针,是引用类型,所以会触发浅拷贝
robot1 := new(Robot)
robot1.Name = "小白-X型-V1.0"
robot1.Color = "白色"
robot1.Model = "小型"

robot2 := robot1
fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, robot2)

fmt.Println("在这里面修改Robot1的Name和Color属性")
robot1.Name = "小蓝-X型-V1.2"
robot1.Color = "蓝色"

fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, robot2)
}浅拷贝 使用new方式
Robot 1:&{小白-X型-V1.0 白色 小型} 内存地址:0xc000068330
Robot 2:&{小白-X型-V1.0 白色 小型} 内存地址:0xc000068330
在这里面修改Robot1的Name和Color属性
Robot 1:&{小黑-X型-V1.2 黑色 小型} 内存地址:0xc000068330
Robot 2:&{小黑-X型-V1.2 黑色 小型} 内存地址:0xc000068330另外一种浅拷贝方式:&取指针赋值
package main

import (
"fmt"
)

// 定义一个Robot结构体
type Robot struct {
Name string
Color string
Model string
}

func main() {

fmt.Println("浅拷贝 内容和内存地址一样,改变其中一个对象的值时,另一个同时变化。")
robot1 := Robot{
Name: "小白-X型-V1.0",
Color: "白色",
Model: "小型",
}
robot2 := &robot1
fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, &robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, robot2)

fmt.Println("在这里面修改Robot1的Name和Color属性")
robot1.Name = "小黑-X型-V1.1"
robot1.Color = "黑色"

fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, &robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, robot2)

}
参考文档:
https://www.cnblogs.com/mikeCao/p/8710837.html
https://www.cnblogs.com/guichenglin/p/12736203.html
  查看全部
通用深拷贝和浅拷贝的区别
 


深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用


 假设B复制了A,修改A的时候,看B是否发生变化:


如果B跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值)
如果B没有改变,说明是深拷贝,自食其力!(修改堆内存中的不同的值)


 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
 
Go语言中的深浅拷贝
 
1、深拷贝(Deep Copy):

拷贝的是数据本身,创造一个样的新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。既然内存地址不同,释放内存地址时,可分别释放。

值类型的数据,默认全部都是深复制,Array、Int、String、Struct、Float,Bool。

2、浅拷贝(Shallow Copy):

拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。释放内存地址时,同时释放内存地址。

引用类型的数据,默认全部都是浅复制,Slice,Map。
 
深拷贝示例:
package main

import (
"fmt"
)

// 定义一个Robot结构体
type Robot struct {
Name string
Color string
Model string
}

func main() {
fmt.Println("深拷贝 内容一样,改变其中一个对象的值时,另一个不会变化。")
robot1 := Robot{
Name: "小白-X型-V1.0",
Color: "白色",
Model: "小型",
}
robot2 := robot1
fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, &robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, &robot2)

fmt.Println("修改Robot1的Name属性值")
robot1.Name = "小白-X型-V1.1"

fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, &robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, &robot2)

}
深拷贝 内容一样,改变其中一个对象的值时,另一个不会变化。
Robot 1:{小白-X型-V1.0 白色 小型} 内存地址:0xc000072330
Robot 2:{小白-X型-V1.0 白色 小型} 内存地址:0xc000072360
修改Robot1的Name属性值
Robot 1:{小白-X型-V1.1 白色 小型} 内存地址:0xc000072330
Robot 2:{小白-X型-V1.0 白色 小型} 内存地址:0xc000072360
浅拷贝示例2:
package main

import (
"fmt"
)

// 定义一个Robot结构体
type Robot struct {
Name string
Color string
Model string
}

func main() {

fmt.Println("浅拷贝 使用new方式")
//new方式返回的是一个指针,是引用类型,所以会触发浅拷贝
robot1 := new(Robot)
robot1.Name = "小白-X型-V1.0"
robot1.Color = "白色"
robot1.Model = "小型"

robot2 := robot1
fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, robot2)

fmt.Println("在这里面修改Robot1的Name和Color属性")
robot1.Name = "小蓝-X型-V1.2"
robot1.Color = "蓝色"

fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, robot2)
}
浅拷贝 使用new方式
Robot 1:&{小白-X型-V1.0 白色 小型} 内存地址:0xc000068330
Robot 2:&{小白-X型-V1.0 白色 小型} 内存地址:0xc000068330
在这里面修改Robot1的Name和Color属性
Robot 1:&{小黑-X型-V1.2 黑色 小型} 内存地址:0xc000068330
Robot 2:&{小黑-X型-V1.2 黑色 小型} 内存地址:0xc000068330
另外一种浅拷贝方式:&取指针赋值
package main

import (
"fmt"
)

// 定义一个Robot结构体
type Robot struct {
Name string
Color string
Model string
}

func main() {

fmt.Println("浅拷贝 内容和内存地址一样,改变其中一个对象的值时,另一个同时变化。")
robot1 := Robot{
Name: "小白-X型-V1.0",
Color: "白色",
Model: "小型",
}
robot2 := &robot1
fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, &robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, robot2)

fmt.Println("在这里面修改Robot1的Name和Color属性")
robot1.Name = "小黑-X型-V1.1"
robot1.Color = "黑色"

fmt.Printf("Robot 1:%s\t内存地址:%p \n", robot1, &robot1)
fmt.Printf("Robot 2:%s\t内存地址:%p \n", robot2, robot2)

}

参考文档:
https://www.cnblogs.com/mikeCao/p/8710837.html
https://www.cnblogs.com/guichenglin/p/12736203.html
 

什么是RPC?

zkbhj 发表了文章 • 0 个评论 • 254 次浏览 • 2020-07-23 16:52 • 来自相关话题

1.1 基本概念
 
RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务本地过程调用:如果需要将本地student对象的age+1,可以实现一个addAge()方法,将student对象传入,对年龄进行更新之后返回即可,本地方法调用的函数体通过函数指针来指定。远程过程调用:上述操作的过程中,如果addAge()这个方法在服务端,执行函数的函数体在远程机器上,如何告诉机器需要调用这个方法呢?
 简单总结,RPC需要以下三个步骤来完成:

1、首先客户端需要告诉服务器,需要调用的函数,这里函数和进程ID存在一个映射,客户端远程调用时,需要查一下函数,找到对应的ID,然后执行函数的代码。
2、客户端需要把本地参数传给远程函数,本地调用的过程中,直接压栈即可,但是在远程调用过程中不再同一个内存里,无法直接传递函数的参数,因此需要客户端把参数转换成字节流,传给服务端,然后服务端将字节流转换成自身能读取的格式,是一个序列化和反序列化的过程。
3、数据准备好了之后,如何进行传输?网络传输层需要把调用的ID和序列化后的参数传给服务端,然后把计算好的结果序列化传给客户端,因此TCP层即可完成上述过程,gRPC中采用的是HTTP2协议。

总结一下上述过程:
// Client端
// Student student = Call(ServerAddr, addAge, student)
1. 将这个调用映射为Call ID。
2. 将Call ID,student(params)序列化,以二进制形式打包
3. 把2中得到的数据包发送给ServerAddr,这需要使用网络传输层
4. 等待服务器返回结果
5. 如果服务器调用成功,那么就将结果反序列化,并赋给student,年龄更新

// Server端
1. 在本地维护一个Call ID到函数指针的映射call_id_map,可以用Map<String, Method> callIdMap
2. 等待服务端请求
3. 得到一个请求后,将其数据包反序列化,得到Call ID
4. 通过在callIdMap中查找,得到相应的函数指针
5. 将student(params)反序列化后,在本地调用addAge()函数,得到结果
6. 将student结果序列化后通过网络返回给Client





在微服务的设计中,一个服务A如果访问另一个Module下的服务B,可以采用HTTP REST传输数据,并在两个服务之间进行序列化和反序列化操作,服务B把执行结果返回过来。






由于HTTP在应用层中完成,整个通信的代价较高,远程过程调用中直接基于TCP进行远程调用,数据传输在传输层TCP层完成,更适合对效率要求比较高的场景,RPC主要依赖于客户端和服务端之间建立Socket链接进行,底层实现比REST更复杂。

1.2 rpc demo










 
完整源码:https://github.com/guangxush/wheel/tree/master/RPC/src
 
2. gRPC的使用

2.1. gRPC与REST 
REST通常以业务为导向,将业务对象上执行的操作映射到HTTP动词,格式非常简单,可以使用浏览器进行扩展和传输,通过JSON数据完成客户端和服务端之间的消息通信,直接支持请求/响应方式的通信。不需要中间的代理,简化了系统的架构,不同系统之间只需要对JSON进行解析和序列化即可完成数据的传递。但是REST也存在一些弊端,比如只支持请求/响应这种单一的通信方式,对象和字符串之间的序列化操作也会影响消息传递速度,客户端需要通过服务发现的方式,知道服务实例的位置,在单个请求获取多个资源时存在着挑战,而且有时候很难将所有的动作都映射到HTTP动词。正是因为REST面临一些问题,因此可以采用gRPC作为一种替代方案,gRPC 是一种基于二进制流的消息协议,可以采用基于Protocol Buffer的IDL定义grpc API,这是Google公司用于序列化结构化数据提供的一套语言中立的序列化机制,客户端和服务端使用HTTP/2以Protocol Buffer格式交换二进制消息。gRPC的优势是,设计复杂更新操作的API非常简单,具有高效紧凑的进程通信机制,在交换大量消息时效率高,远程过程调用和消息传递时可以采用双向的流式消息方式,同时客户端和服务端支持多种语言编写,互操作性强;不过gRPC的缺点是不方便与JavaScript集成,某些防火墙不支持该协议。注册中心:当项目中有很多服务时,可以把所有的服务在启动的时候注册到一个注册中心里面,用于维护服务和服务器之间的列表,当注册中心接收到客户端请求时,去找到该服务是否远程可以调用,如果可以调用需要提供服务地址返回给客户端,客户端根据返回的地址和端口,去调用远程服务端的方法,执行完成之后将结果返回给客户端。这样在服务端加新功能的时候,客户端不需要直接感知服务端的方法,服务端将更新之后的结果在注册中心注册即可,而且当修改了服务端某些方法的时候,或者服务降级服务多机部署想实现负载均衡的时候,我们只需要更新注册中心的服务群即可。






参考文档:
https://www.jianshu.com/p/7d6853140e13
https://www.jianshu.com/p/eb66b0c4113d
 
  查看全部
1.1 基本概念
 
  • RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务
  • 本地过程调用:如果需要将本地student对象的age+1,可以实现一个addAge()方法,将student对象传入,对年龄进行更新之后返回即可,本地方法调用的函数体通过函数指针来指定。
  • 远程过程调用:上述操作的过程中,如果addAge()这个方法在服务端,执行函数的函数体在远程机器上,如何告诉机器需要调用这个方法呢?

 简单总结,RPC需要以下三个步骤来完成:

1、首先客户端需要告诉服务器,需要调用的函数,这里函数和进程ID存在一个映射,客户端远程调用时,需要查一下函数,找到对应的ID,然后执行函数的代码。
2、客户端需要把本地参数传给远程函数,本地调用的过程中,直接压栈即可,但是在远程调用过程中不再同一个内存里,无法直接传递函数的参数,因此需要客户端把参数转换成字节流,传给服务端,然后服务端将字节流转换成自身能读取的格式,是一个序列化和反序列化的过程。
3、数据准备好了之后,如何进行传输?网络传输层需要把调用的ID和序列化后的参数传给服务端,然后把计算好的结果序列化传给客户端,因此TCP层即可完成上述过程,gRPC中采用的是HTTP2协议。

总结一下上述过程:
// Client端 
// Student student = Call(ServerAddr, addAge, student)
1. 将这个调用映射为Call ID。
2. 将Call ID,student(params)序列化,以二进制形式打包
3. 把2中得到的数据包发送给ServerAddr,这需要使用网络传输层
4. 等待服务器返回结果
5. 如果服务器调用成功,那么就将结果反序列化,并赋给student,年龄更新

// Server端
1. 在本地维护一个Call ID到函数指针的映射call_id_map,可以用Map<String, Method> callIdMap
2. 等待服务端请求
3. 得到一个请求后,将其数据包反序列化,得到Call ID
4. 通过在callIdMap中查找,得到相应的函数指针
5. 将student(params)反序列化后,在本地调用addAge()函数,得到结果
6. 将student结果序列化后通过网络返回给Client

QQ截图20200723155851.jpg


在微服务的设计中,一个服务A如果访问另一个Module下的服务B,可以采用HTTP REST传输数据,并在两个服务之间进行序列化和反序列化操作,服务B把执行结果返回过来。

QQ截图20200723155953.jpg


由于HTTP在应用层中完成,整个通信的代价较高,远程过程调用中直接基于TCP进行远程调用,数据传输在传输层TCP层完成,更适合对效率要求比较高的场景,RPC主要依赖于客户端和服务端之间建立Socket链接进行,底层实现比REST更复杂。

1.2 rpc demo

QQ截图20200723160048.jpg


QQ截图20200723160056.jpg

 
完整源码:https://github.com/guangxush/wheel/tree/master/RPC/src
 
2. gRPC的使用

2.1. gRPC与REST 
  • REST通常以业务为导向,将业务对象上执行的操作映射到HTTP动词,格式非常简单,可以使用浏览器进行扩展和传输,通过JSON数据完成客户端和服务端之间的消息通信,直接支持请求/响应方式的通信。不需要中间的代理,简化了系统的架构,不同系统之间只需要对JSON进行解析和序列化即可完成数据的传递。
  • 但是REST也存在一些弊端,比如只支持请求/响应这种单一的通信方式,对象和字符串之间的序列化操作也会影响消息传递速度,客户端需要通过服务发现的方式,知道服务实例的位置,在单个请求获取多个资源时存在着挑战,而且有时候很难将所有的动作都映射到HTTP动词。
  • 正是因为REST面临一些问题,因此可以采用gRPC作为一种替代方案,gRPC 是一种基于二进制流的消息协议,可以采用基于Protocol Buffer的IDL定义grpc API,这是Google公司用于序列化结构化数据提供的一套语言中立的序列化机制,客户端和服务端使用HTTP/2以Protocol Buffer格式交换二进制消息
  • gRPC的优势是,设计复杂更新操作的API非常简单,具有高效紧凑的进程通信机制,在交换大量消息时效率高,远程过程调用和消息传递时可以采用双向的流式消息方式,同时客户端和服务端支持多种语言编写,互操作性强;不过gRPC的缺点是不方便与JavaScript集成,某些防火墙不支持该协议。
  • 注册中心:当项目中有很多服务时,可以把所有的服务在启动的时候注册到一个注册中心里面,用于维护服务和服务器之间的列表,当注册中心接收到客户端请求时,去找到该服务是否远程可以调用,如果可以调用需要提供服务地址返回给客户端,客户端根据返回的地址和端口,去调用远程服务端的方法,执行完成之后将结果返回给客户端。这样在服务端加新功能的时候,客户端不需要直接感知服务端的方法,服务端将更新之后的结果在注册中心注册即可,而且当修改了服务端某些方法的时候,或者服务降级服务多机部署想实现负载均衡的时候,我们只需要更新注册中心的服务群即可。


QQ截图20200723160510.jpg


参考文档:
https://www.jianshu.com/p/7d6853140e13
https://www.jianshu.com/p/eb66b0c4113d
 
 

怎么理解软件开发中的上游和下游?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1375 次浏览 • 2020-07-22 09:35 • 来自相关话题

IaaS,PaaS、SaaS分别指什么?有什么区别?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 421 次浏览 • 2020-05-25 14:48 • 来自相关话题

数据结构:堆(Heap)

zkbhj 发表了文章 • 0 个评论 • 305 次浏览 • 2020-05-20 12:23 • 来自相关话题

堆就是用数组实现的二叉树,所有它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。

堆的常用方法:
 
构建优先队列支持堆排序快速找出一个集合中的最小值(或者最大值)在朋友面前装逼

堆属性

堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。

在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。

例子:






这是一个最大堆,,因为每一个父节点的值都比其子节点要大。10 比 7 和 2 都大。7 比 5 和 1都大。

根据这一属性,那么最大堆总是将其中的最大值存放在树的根节点。而对于最小堆,根节点中的元素总是树中的最小值。堆属性非常的有用,因为堆常常被当做优先队列使用,因为可以快速的访问到“最重要”的元素。

注意:堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于 index 0 的位置,但是最小的元素则未必是最后一个元素。--唯一能够保证的是最小的元素是一个叶节点,但是不确定是哪一个。


堆和普通树的区别

堆并不能取代二叉搜索树,它们之间有相似之处也有一些不同。我们来看一下两者的主要差别:

节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。

内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配额为是我内存。堆仅仅使用一个数据来存储数组,且不使用指针。

平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足堆属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(log n) 的性能。

搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入、删除操作。

来自数组的树

用数组来实现树相关的数据结构也许看起来有点古怪,但是它在时间和空间山都是很高效的。

我们准备将上面的例子中的树这样存储:[ 10, 7, 2, 5, 1 ]就这多!我们除了一个简单的数组以外,不需要任何额外的空间。

如果我们不允许使用指针,那么我们怎么知道哪一个节点是父节点,哪一个节点是它的子节点呢?问得好!节点在数组中的位置index 和它的父节点以及子节点的索引之间有一个映射关系。

如果 i 是节点的索引,那么下面的公式就给出了它的父节点和子节点在数组中的位置:parent(i) = floor((i - 1)/2)
left(i) = 2i + 1
right(i) = 2i + 2注意 right(i) 就是简单的 left(i) + 1。左右节点总是处于相邻的位置。
 
我们将写公式放到前面的例子中验证一下。





 
 

注意:根节点(10)没有父节点,因为 -1 不是一个有效的数组索引。同样,节点 (2),(5)和(1) 没有子节点,因为这些索引已经超过了数组的大小,所以我们在使用这些索引值的时候需要保证是有效的索引值。

 
复习一下,在最大堆中,父节点的值总是要大于(或者等于)其子节点的值。这意味下面的公式对数组中任意一个索引 i都成立:
 
array[parent(i)] >= array[i]
可以用上面的例子来验证一下这个堆属性。

如你所见,这些公式允许我们不使用指针就可以找到任何一个节点的父节点或者子节点。事情比简单的去掉指针要复杂,但这就是交易:我们节约了空间,但是要进行更多计算。幸好这些计算很快并且只需要O(1)的时间。

理解数组索引和节点位置之间的关系非常重要。这里有一个更大的堆,它有15个节点被分成了4层:






图片中的数字不是节点的值,而是存储这个节点的数组索引!这里是数组索引和树的层级之间的关系:





 
由上图可以看到,数组中父节点总是在子节点的前面。

注意这个方案与一些限制。你可以在普通二叉树中按照下面的方式组织数据,但是在堆中不可以:






在堆中,在当前层级所有的节点都已经填满之前不允许开是下一层的填充,所以堆总是有这样的形状:






注意:你可以使用普通树来模拟堆,但是那对空间是极大的浪费。

 
更多操作可以参考原文链接:
 
作者:唐先僧
链接:https://www.jianshu.com/p/6b526aa481b1
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 查看全部
堆就是用数组实现的二叉树,所有它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。

堆的常用方法:
 
  1. 构建优先队列
  2. 支持堆排序
  3. 快速找出一个集合中的最小值(或者最大值)
  4. 在朋友面前装逼


堆属性

堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。

在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。

例子:

QQ截图20200520120758.jpg


这是一个最大堆,,因为每一个父节点的值都比其子节点要大。10 比 7 和 2 都大。7 比 5 和 1都大。

根据这一属性,那么最大堆总是将其中的最大值存放在树的根节点。而对于最小堆,根节点中的元素总是树中的最小值。堆属性非常的有用,因为堆常常被当做优先队列使用,因为可以快速的访问到“最重要”的元素。


注意:堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于 index 0 的位置,但是最小的元素则未必是最后一个元素。--唯一能够保证的是最小的元素是一个叶节点,但是不确定是哪一个。



堆和普通树的区别

堆并不能取代二叉搜索树,它们之间有相似之处也有一些不同。我们来看一下两者的主要差别:

节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。

内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配额为是我内存。堆仅仅使用一个数据来存储数组,且不使用指针。

平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足堆属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(log n) 的性能。

搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入、删除操作。

来自数组的树

用数组来实现树相关的数据结构也许看起来有点古怪,但是它在时间和空间山都是很高效的。

我们准备将上面的例子中的树这样存储:
[ 10, 7, 2, 5, 1 ]
就这多!我们除了一个简单的数组以外,不需要任何额外的空间。

如果我们不允许使用指针,那么我们怎么知道哪一个节点是父节点,哪一个节点是它的子节点呢?问得好!节点在数组中的位置index 和它的父节点以及子节点的索引之间有一个映射关系。

如果 i 是节点的索引,那么下面的公式就给出了它的父节点和子节点在数组中的位置:
parent(i) = floor((i - 1)/2)
left(i) = 2i + 1
right(i) = 2i + 2
注意 right(i) 就是简单的 left(i) + 1。左右节点总是处于相邻的位置。
 
我们将写公式放到前面的例子中验证一下。

QQ截图20200520121220.jpg

 
 


注意:根节点(10)没有父节点,因为 -1 不是一个有效的数组索引。同样,节点 (2),(5)和(1) 没有子节点,因为这些索引已经超过了数组的大小,所以我们在使用这些索引值的时候需要保证是有效的索引值。


 
复习一下,在最大堆中,父节点的值总是要大于(或者等于)其子节点的值。这意味下面的公式对数组中任意一个索引 i都成立:
 
array[parent(i)] >= array[i]
可以用上面的例子来验证一下这个堆属性。

如你所见,这些公式允许我们不使用指针就可以找到任何一个节点的父节点或者子节点。事情比简单的去掉指针要复杂,但这就是交易:我们节约了空间,但是要进行更多计算。幸好这些计算很快并且只需要O(1)的时间。

理解数组索引和节点位置之间的关系非常重要。这里有一个更大的堆,它有15个节点被分成了4层:

QQ截图20200520121352.jpg


图片中的数字不是节点的值,而是存储这个节点的数组索引!这里是数组索引和树的层级之间的关系:

QQ截图20200520121418.jpg

 
由上图可以看到,数组中父节点总是在子节点的前面。

注意这个方案与一些限制。你可以在普通二叉树中按照下面的方式组织数据,但是在堆中不可以:

QQ截图20200520121555.jpg


在堆中,在当前层级所有的节点都已经填满之前不允许开是下一层的填充,所以堆总是有这样的形状:

QQ截图20200520121602.jpg


注意:你可以使用普通树来模拟堆,但是那对空间是极大的浪费。


 
更多操作可以参考原文链接:
 
作者:唐先僧
链接:https://www.jianshu.com/p/6b526aa481b1
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

数据结构之:平衡树

zkbhj 发表了文章 • 0 个评论 • 232 次浏览 • 2020-05-08 11:47 • 来自相关话题

一.概念:

   平衡树是二叉搜索树和堆合并构成的数据结构,它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

二.优势

   对一棵查找树(search tree)进行查询/新增/删除 等动作, 所花的时间与树的高度h 成比例, 并不与树的容量 n 成比例。如果可以让树维持矮矮胖胖的好身材, 也就是让h维持在O(lg n)左右, 完成上述工作就很省时间。能够一直维持好身材, 不因新增删除而长歪的搜寻树, 叫做balanced search tree(平衡树)。

三.种类:

  平衡二叉树的常用实现方法有红黑树(rbt)、AVL、替罪羊树、Treap、伸展树(spaly)、SBT
 
四、为何引入平衡树?解决什么问题?
 
   一个长度为n的有序序列,从中查找一个指定的值,要花多少时间?
   一个最简单的做法就是一个个去试。如果你运气好,第一个就碰上了;如果你运气不好,最后一个才是你要查的值,那就需要把n个值都检查一遍。时间复杂度O(n)。
当然你可能注意到了有序这个有用的性质,所以可以采用二分查找的方式,具体就不赘述了。时间复杂度O(log(n))。
   但是如果要添加一个数据(及动态更新)怎么办?要保证序列的有序性,你必须要插入到适当的位置。这个位置同样可以通过二分查找在O(log(n))的时间中找出。
可是插入的过程呢?我们必须把后面的数据一个个顺次往后挪一格,而这需要O(n)的时间。 这也意味着删除的时间复杂度也就是O(n)。太慢了!无法满足大数据底线O(nlogn)左右的时间复杂度。所以我们需要快一点的方法(数据结构)。

下面介绍其中的一种实现:红黑树——这是一个在当今信息界公认的最快的平衡二叉树之一。STL(Standard Template Library,C++里的一个标准库)里的< set >与< map >就由其作为底层数据结构。
 
红黑树
 
  R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性:

(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

注意:

(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。





 
实际应用
 
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
 
————————————————
版权声明:本文为CSDN博主「zhengx辉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yiye2017zhangmu/java/article/details/81516337
更多参考文档:
https://blog.csdn.net/lemonoil/java/article/details/54405613
https://www.cnblogs.com/skywang12345/p/3245399.html
https://mp.weixin.qq.com/s/jz1ajDUygZ7sXLQFHyfjWA
 
  查看全部
一.概念:

   平衡树是二叉搜索树和堆合并构成的数据结构,它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

二.优势

   对一棵查找树(search tree)进行查询/新增/删除 等动作, 所花的时间与树的高度h 成比例, 并不与树的容量 n 成比例。如果可以让树维持矮矮胖胖的好身材, 也就是让h维持在O(lg n)左右, 完成上述工作就很省时间。能够一直维持好身材, 不因新增删除而长歪的搜寻树, 叫做balanced search tree(平衡树)。

三.种类:

  平衡二叉树的常用实现方法有红黑树(rbt)、AVL、替罪羊树、Treap、伸展树(spaly)、SBT
 
四、为何引入平衡树?解决什么问题?
 
   一个长度为n的有序序列,从中查找一个指定的值,要花多少时间?
   一个最简单的做法就是一个个去试。如果你运气好,第一个就碰上了;如果你运气不好,最后一个才是你要查的值,那就需要把n个值都检查一遍。时间复杂度O(n)。
当然你可能注意到了有序这个有用的性质,所以可以采用二分查找的方式,具体就不赘述了。时间复杂度O(log(n))。
   但是如果要添加一个数据(及动态更新)怎么办?要保证序列的有序性,你必须要插入到适当的位置。这个位置同样可以通过二分查找在O(log(n))的时间中找出。
可是插入的过程呢?我们必须把后面的数据一个个顺次往后挪一格,而这需要O(n)的时间。 这也意味着删除的时间复杂度也就是O(n)。太慢了!无法满足大数据底线O(nlogn)左右的时间复杂度。所以我们需要快一点的方法(数据结构)

下面介绍其中的一种实现:红黑树——这是一个在当今信息界公认的最快的平衡二叉树之一。STL(Standard Template Library,C++里的一个标准库)里的< set >与< map >就由其作为底层数据结构。
 
红黑树
 
  R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性:

(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

注意:

(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

251730074203156.jpg

 
实际应用
 
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
 
————————————————
版权声明:本文为CSDN博主「zhengx辉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yiye2017zhangmu/java/article/details/81516337
更多参考文档:
https://blog.csdn.net/lemonoil/java/article/details/54405613
https://www.cnblogs.com/skywang12345/p/3245399.html
https://mp.weixin.qq.com/s/jz1ajDUygZ7sXLQFHyfjWA
 
 

神经病和精神病是一个病吗?有什么区别?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 636 次浏览 • 2020-04-30 14:49 • 来自相关话题

序列化结构数据方法ProtoBuf使用(PHP和Go)

zkbhj 发表了文章 • 0 个评论 • 351 次浏览 • 2020-04-13 17:24 • 来自相关话题

一、在PHP下使用ProtoBuf
 
PHP如果要使用protobuf 需要安装 protoc+ 安装protobuf composer包
 
protoc用于根据protobuf数据结构文件(.proto)生成对应的类,用于生成protobuf文件安装 google/protobuf composer 包composer require google/protobuf
 
怎么用呢?开搞!
 
1、首先,需要定一个消息类型。创建一个关于Person的定义文件(以.proto为后缀),如示例为person.proto,文件内容如下:
syntax="proto3";
package test;
message Person{
string name=1;//姓名
int32 age=2;//年龄
bool sex=3;//性别

syntax="proto3":表明使用的是proto3格式,如果不指定则为proto2package test:定义包名为test,生成类时,会产生一个目录为testmessage Person:消息主体内容,里面为各个字段的定义
 
2、生成对应的PHP类
 
定义好Person的格式后,该格式如果不生成我们所需要的类库,其实是无任何意义的,还google提供一个工具protoc生成我们要的类库。也就是最开始说的 protoc!
 
proto 最新版的下载地址是:最新3.11.3。可以通过 官方包Release 进行有选择的下载。
tar -zxvf protobuf-php-3.11.3.tar.gz
cd protobuf-3.11.3
./configure --prefix=/usr/local/protobuf
make
make install解压安装完后,就可以通过下面的命令,生成对应的类库了:
/usr/
local/protobuf/bin/protoc --php_out=./ person.proto生成后将在当前目录产生如下文件:
 
GPBMetadata/Person.phpTest/Person.php
 
3、类库生成完了,就可以在PHP项目中使用ProtoBuf了:

在PHP中使用ProtoBuf依赖一个protobuf的扩展,目前提供两种方式进行使用: 
php的c扩展,php的lib扩展包,
 
这两者均可在刚才下载包里可以找到。
 
另外,也可以使用composer进行安装该依赖扩展:
composer require google/protobuf
这里我主要是使用composer安装,应该它可以帮我产生autoload。安装好依赖后,我们就可以开始在php环境下使用protobuf了。

序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();运行后,产生的data.bin,只有14Byte
 
反序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();PHP常用的使用方法:

序列化:
1、serializeToString:序列化成二进制字符串
2、serializeToJsonString:序列化成JSON字符串

反序列化:
1、mergeFromString:二进制字符串反序列化
2、mergeFromJsonString:Json字符串反序列化

二、在Golang下使用ProtoBuf
 
安装protoc的方法和PHP中类型,编译安装即可。
 
安装好之后,需要安装ProtoBuf的编译器插件:protoc-gen-go:
 
进入GOPATH目录,运行:
go get -u github.com/golang/protobuf/protoc-gen-go  如果成功,会在GOPATH/bin下生成protoc-gen-go.exe文件(Windows上)。
 
1、准备好之后,就可以开始写proto文件了。假设文件目录为:
$GOPATH/src/test/protobuf/pb/user.proto代码如下:
//指定版本
//注意proto3与proto2的写法有些不同
syntax = "proto3";

//包名,通过protoc生成时go文件时
package test;

//手机类型
//枚举类型第一个字段必须为0
enum PhoneType {
HOME = 0;
WORK = 1;
}

//手机
message Phone {
PhoneType type = 1;
string number = 2;
}

//人
message Person {
//后面的数字表示标识号
int32 id = 1;
string name = 2;
//repeated表示可重复
//可以有多个手机
repeated Phone phones = 3;
}

//联系簿
message ContactBook {
repeated Person persons = 1;
}然后运行如下命令:
protoc --go_out=. *.proto
会生成一个user.pb.go的文件。
 
2、使用:
package main;

import (
"github.com/golang/protobuf/proto"
"go_dev/kongji/proto/test"
"io/ioutil"
"os"
"fmt"
)

func write() {
p1 := &test.Person{
Id: 1,
Name: "小张",
Phones: []*test.Phone{
{test.PhoneType_HOME, "111111111"},
{test.PhoneType_WORK, "222222222"},
},
};
p2 := &test.Person{
Id: 2,
Name: "小王",
Phones: []*test.Phone{
{test.PhoneType_HOME, "333333333"},
{test.PhoneType_WORK, "444444444"},
},
};

//创建地址簿
book := &test.ContactBook{};
book.Persons = append(book.Persons, p1);
book.Persons = append(book.Persons, p2);

//编码数据
data, _ := proto.Marshal(book);
//把数据写入文件
ioutil.WriteFile("./test.txt", data, os.ModePerm);
}

func read() {
//读取文件数据
data, _ := ioutil.ReadFile("./test.txt");
book := &test.ContactBook{};
//解码数据
proto.Unmarshal(data, book);
for _, v := range book.Persons {
fmt.Println(v.Id, v.Name);
for _, vv := range v.Phones {
fmt.Println(vv.Type, vv.Number);
}
}
}

func main() {
write();
read();
}
 

内容整理自下面链接:
https://www.jianshu.com/p/ce098058edf0
https://developers.google.cn/protocol-buffers/docs/gotutorial
https://developers.google.cn/protocol-buffers/docs/reference/go-generated
  查看全部
一、在PHP下使用ProtoBuf
 
PHP如果要使用protobuf 需要安装 protoc+ 安装protobuf composer包
 
  • protoc用于根据protobuf数据结构文件(.proto)生成对应的类,用于生成protobuf文件
  • 安装 google/protobuf composer 包composer require google/protobuf

 
怎么用呢?开搞!
 
1、首先,需要定一个消息类型。创建一个关于Person的定义文件(以.proto为后缀),如示例为person.proto,文件内容如下:
syntax="proto3";
package test;
message Person{
string name=1;//姓名
int32 age=2;//年龄
bool sex=3;//性别
}
 
  1. syntax="proto3":表明使用的是proto3格式,如果不指定则为proto2
  2. package test:定义包名为test,生成类时,会产生一个目录为test
  3. message Person:消息主体内容,里面为各个字段的定义

 
2、生成对应的PHP类
 
定义好Person的格式后,该格式如果不生成我们所需要的类库,其实是无任何意义的,还google提供一个工具protoc生成我们要的类库。也就是最开始说的 protoc!
 
proto 最新版的下载地址是:最新3.11.3。可以通过 官方包Release 进行有选择的下载。
tar -zxvf protobuf-php-3.11.3.tar.gz
cd protobuf-3.11.3
./configure --prefix=/usr/local/protobuf
make
make install
解压安装完后,就可以通过下面的命令,生成对应的类库了:
/usr/
local/protobuf/bin/protoc --php_out=./ person.proto
生成后将在当前目录产生如下文件:
 
  • GPBMetadata/Person.php
  • Test/Person.php

 
3、类库生成完了,就可以在PHP项目中使用ProtoBuf了:

在PHP中使用ProtoBuf依赖一个protobuf的扩展,目前提供两种方式进行使用: 
  1. php的c扩展,
  2. php的lib扩展包,

 
这两者均可在刚才下载包里可以找到。
 
另外,也可以使用composer进行安装该依赖扩展:
composer require google/protobuf

这里我主要是使用composer安装,应该它可以帮我产生autoload。安装好依赖后,我们就可以开始在php环境下使用protobuf了。

序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();
运行后,产生的data.bin,只有14Byte
 
反序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();
PHP常用的使用方法:

序列化:
1、serializeToString:序列化成二进制字符串
2、serializeToJsonString:序列化成JSON字符串

反序列化:
1、mergeFromString:二进制字符串反序列化
2、mergeFromJsonString:Json字符串反序列化

二、在Golang下使用ProtoBuf
 
安装protoc的方法和PHP中类型,编译安装即可。
 
安装好之后,需要安装ProtoBuf的编译器插件:protoc-gen-go:
 
进入GOPATH目录,运行:
go get -u github.com/golang/protobuf/protoc-gen-go
  如果成功,会在GOPATH/bin下生成protoc-gen-go.exe文件(Windows上)。
 
1、准备好之后,就可以开始写proto文件了。假设文件目录为:
$GOPATH/src/test/protobuf/pb/user.proto
代码如下:
//指定版本
//注意proto3与proto2的写法有些不同
syntax = "proto3";

//包名,通过protoc生成时go文件时
package test;

//手机类型
//枚举类型第一个字段必须为0
enum PhoneType {
HOME = 0;
WORK = 1;
}

//手机
message Phone {
PhoneType type = 1;
string number = 2;
}

//人
message Person {
//后面的数字表示标识号
int32 id = 1;
string name = 2;
//repeated表示可重复
//可以有多个手机
repeated Phone phones = 3;
}

//联系簿
message ContactBook {
repeated Person persons = 1;
}
然后运行如下命令:
 protoc --go_out=. *.proto
会生成一个user.pb.go的文件。
 
2、使用:
package main;

import (
"github.com/golang/protobuf/proto"
"go_dev/kongji/proto/test"
"io/ioutil"
"os"
"fmt"
)

func write() {
p1 := &test.Person{
Id: 1,
Name: "小张",
Phones: []*test.Phone{
{test.PhoneType_HOME, "111111111"},
{test.PhoneType_WORK, "222222222"},
},
};
p2 := &test.Person{
Id: 2,
Name: "小王",
Phones: []*test.Phone{
{test.PhoneType_HOME, "333333333"},
{test.PhoneType_WORK, "444444444"},
},
};

//创建地址簿
book := &test.ContactBook{};
book.Persons = append(book.Persons, p1);
book.Persons = append(book.Persons, p2);

//编码数据
data, _ := proto.Marshal(book);
//把数据写入文件
ioutil.WriteFile("./test.txt", data, os.ModePerm);
}

func read() {
//读取文件数据
data, _ := ioutil.ReadFile("./test.txt");
book := &test.ContactBook{};
//解码数据
proto.Unmarshal(data, book);
for _, v := range book.Persons {
fmt.Println(v.Id, v.Name);
for _, vv := range v.Phones {
fmt.Println(vv.Type, vv.Number);
}
}
}

func main() {
write();
read();
}

 

内容整理自下面链接:
https://www.jianshu.com/p/ce098058edf0
https://developers.google.cn/protocol-buffers/docs/gotutorial
https://developers.google.cn/protocol-buffers/docs/reference/go-generated
 

序列化结构数据方法ProtoBuf源码解读

zkbhj 发表了文章 • 0 个评论 • 438 次浏览 • 2020-04-13 16:53 • 来自相关话题

在上一篇 《序列化结构数据方法ProtoBuf编码》 中,我们详细解析了 ProtoBuf 的编码原理。

有了这个知识储备,我们就可以深入 ProtoBuf 序列化、反序列化的源码,从代码的层面理解 ProtoBuf 具体是如何实现对数据的编码(序列化)和解码(反序列化)的。

我们重新复习一下, ProtoBuf 的序列化使用过程:
 
定义 .proto 文件protoc 编译器编译 .proto 文件生成一系列接口代码调用生成的接口实现对 .proto 定义的字段的读取以及 message 对象的序列化、反序列化方法

具体调用代码如下:
Example1 example1;
example1.set_int32val(val);
example1.set_stringval("hello,world");
example1.SerializeToString(&output);
调用 SerializeToString 函数将 example1 对象序列化(编码)成字符串。我们的目的就是了解 SerializeToString 函数里到底发生了什么,是怎么一步一步得到最终的序列化结果的。
 

注意:并非编码成字符串数据,string 只是作为编码结果的容器


我们在 .proto 文件中定义的 message 在最终生成的对应语言的代码中,例如在 C++ (xxxx.pb.h、xxxx.pb.cpp) 中每一个在 .proto 文件中定义的 message 字段都会在代码中构造成一个类,且这些 message 消息类继承于 ::google::protobuf::Message,而 ::google::protobuf::Message 继承于一个更为轻量的 MessageLite 类。其相关的类图如下所示:





 
而我们经常调用的序列化函数 SerializeToString 并定义在基类 MessageLite 中。
 
编码

当某个 Message 调用 SerializeToString 时,经过一层层调用最终会调用底层的关键编码函数 WriteVarint32ToArray 或 WriteVarint64ToArray,整个过程如下图所示:






WriteVarint32ToArray 函数可在源码目录下的 google.protobuf.io 包下的 coded_stream.h 中找到。在上一篇 深入 ProtoBuf - 编码 中我们解析了 Varint 编码原理和详细过程,WriteVarint32ToArray(以及 WriteVarint64ToArray)并是 Varint 编码的核心。

可以对照上一篇指出的 Varints 编码的几个关键点来阅读以下代码,可以看出编码实现确实优雅,代码如下:inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
// 0x80 -> 1000 0000
// 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节
// 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回
// 如果至少需要两个字节
while (value >= 0x80) {
// 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位
*target = static_cast<uint8>(value | 0x80);
// 处理完七位,后移,继续处理下一个七位
value >>= 7;
// 指针加一,(数组后移一位)
++target;
}
// 跳出循环,则表示已无后续字节,但还有最后一个字节

// 把最后一个字节放入数组
*target = static_cast<uint8>(value);
// 结束地址指向数组最后一个元素的末尾
return target + 1;
}

// Varint64 同理
inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,
uint8* target) {
while (value >= 0x80) {
*target = static_cast<uint8>(value | 0x80);
value >>= 7;
++target;
}
*target = static_cast<uint8>(value);
return target + 1;
}在上面已添加详细注释,这里再强调几个关键点。
 
value | 0x80:xxx ... xxxx xxxx | 000 ... 1000 0000 的结果其实就是将最后一个字节的第一个 bit(最高位) 置 1,其他位不变,即 xxx ... 1xxx xxxx。注意 target 是 uint8 类型的指针,这意味它只会截断获取最后一个字节,即 1xxx xxxx,这里的 1 意味着什么?这个 1 就是所谓的 msb 了,意味着后续还有字节。之后就是右移 7 位(去掉最后 7 位),处理下一个 7位。通过这里的代码应该可以体会到为什么 Varints 编码结果是低位排在前面了。

了解了最底层 IO 包中的编码函数,再结合上篇文章介绍的编码原理,对 ProtoBuf 的编码应该有了更深入的认识。

Varints 类型序列化实现

int32、int64、uint32、uint64

int32 类型编码函数对应为 WriteInt32ToArray,源码如下:// WriteTagToArray 函数将 Tag 部分写入
// WriteInt32NoTagToArray 函数将 Value 部分写入
// WriteTagToArray 和 WriteInt32NoTagToArray 底层
// 均调用 coded_stream.h 中的 WriteVarint32ToArray
//因为 ProtoBuf 中的 Tag 均采用 Varint 编码
// int32 的 Value 部分也采用 Varint 编码
inline uint8* WireFormatLite::WriteInt32ToArray(int field_number, int32 value,
uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
return WriteInt32NoTagToArray(value, target);
}int64、uint32、uint64 类型与 int32 类型同理,只是处理位数有所不同。

uint32 和 uint64 也是采用 Varint 编码,所以底层编码实现与 int32、int64 一致。

 sint32、sint64

这两种类型编码函数对应为 WriteSInt32ToArray 和 WriteSInt64ToArray 。

在上一篇文章 深入 ProtoBuf - 编码 中我们已经介绍过 Varint 编码在负数的情况下编码效率很低,固对于 sint32、sint64 类型我们会采用 ZigZag 编码将负数映射成正数然后再进行 Varint 编码,而这种映射并非采用存储的 Map,而是使用移位实现。sint32 的 ZigZag 源码实现如下:inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
// 右移为算数右移
// 左移时需要先将 n 转成 uint32 类型,防止溢出
// 当 n 为正数时 result = 2 * n
// 当 n 为负数时 result = - (2 * n + 1)
return (static_cast<uint32>(n) << 1) ^ static_cast<uint32>(n >> 31);
}经过 ZigZagEncode32 编码之后,数字成为一个正数,之后等同于 int32 或 int64 进行完全相同的编码处理。

bool 与 enum

bool 和 enum 本质就是整型,编码处理与 int32、int64 相同。

32-bit、64-bit
fixed32/fixed64

fixed32 类型对应 WriteFixed32ToArray 函数,32-bit、64-bit类型的字段比起上述 Varint 类型则要简单的多,因为每个数字均是固定字节,源码如下:inline uint8* WireFormatLite::WriteSFixed32ToArray(int field_number,
int32 value, uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
return WriteSFixed32NoTagToArray(value, target);
}其中 WriteSFixed32NoTagToArray 源码如下:
inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,
uint8* target) {
return io::CodedOutputStream::WriteLittleEndian32ToArray(
static_cast<uint32>(value), target);
}由此可知,对于位数固定的 sfixed32 是将其转成 uint32 类型,然后使用与 fixed32 相同的函数写入。

sfixed64 与 sfixed32 同理,不赘述。
 
Length delimited 字段序列化

因为其编码结构为 Tag - Length - Value,所以其字段完整的序列化会稍微多出一些过程,其中有一些需要我们进一步整理。现在以一个 string 类型字段的序列化为例,来看看其序列化的完整过程,画出其程序时序图(上文出现过)如下:






可对照上述时序图来阅读源码,其序列化实现的几个关键函数为:
 
ByteSizeLong:计算对象序列化所需要的空间大小,在内存中开辟相应大小的空间WriteTagToArray:将 Tag 值写入到之前开辟的内存中WriteStringWithSizeToArray:将 Length + Value 值写入到之前开辟的内存中

其序列化代码的重点过程在上图的右下角,先是调用 WriteTagToArray 函数将 Tag 值写入到内存,返回指向下一个字节的指针以便继续写入。调用 WriteStringWithSizeToArray 函数,这个函数主要又执行了两个函数,先是执行 WriteVarint32ToArray 函数(注意 WriteTagToArray 内部调用的也是这个函数,因为 Tag 和 Length 都采用 Varints 编码),此函数的作用是将 Length 写入。执行的第二个函数为 WriteStringToArray,此函数的作用是将 Value(一个 UTF-8 string 值) 写入到内存,其中底层调用了 memcpy() 函数。

综上,对于 Varint 类型的字段自然采用 Varint 编码。

而对于 Length delimited 类型的字段,Tag-Length-Value 中的 Tag 和 Length 依然采用 Varint 编码,Value 若为 String 等类型,则直接进行 memcpy。

另外对于 embedded message 或 packed repeated ,则套用上述规则。底层编码实现实际并是遍历字段下所有内嵌字段,然后递归调用编码函数即可。

作者:404_89_117_101
链接:https://www.jianshu.com/p/62f0238beec8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 查看全部
在上一篇 《序列化结构数据方法ProtoBuf编码》 中,我们详细解析了 ProtoBuf 的编码原理。

有了这个知识储备,我们就可以深入 ProtoBuf 序列化、反序列化的源码,从代码的层面理解 ProtoBuf 具体是如何实现对数据的编码(序列化)和解码(反序列化)的。

我们重新复习一下, ProtoBuf 的序列化使用过程:
 
  • 定义 .proto 文件
  • protoc 编译器编译 .proto 文件生成一系列接口代码
  • 调用生成的接口实现对 .proto 定义的字段的读取以及 message 对象的序列化、反序列化方法


具体调用代码如下:
Example1 example1;
example1.set_int32val(val);
example1.set_stringval("hello,world");
example1.SerializeToString(&output);

调用 SerializeToString 函数将 example1 对象序列化(编码)成字符串。我们的目的就是了解 SerializeToString 函数里到底发生了什么,是怎么一步一步得到最终的序列化结果的。
 


注意:并非编码成字符串数据,string 只是作为编码结果的容器



我们在 .proto 文件中定义的 message 在最终生成的对应语言的代码中,例如在 C++ (xxxx.pb.h、xxxx.pb.cpp) 中每一个在 .proto 文件中定义的 message 字段都会在代码中构造成一个类,且这些 message 消息类继承于 ::google::protobuf::Message,而 ::google::protobuf::Message 继承于一个更为轻量的 MessageLite 类。其相关的类图如下所示:

QQ截图20200413164703.jpg

 
而我们经常调用的序列化函数 SerializeToString 并定义在基类 MessageLite 中。
 
编码

当某个 Message 调用 SerializeToString 时,经过一层层调用最终会调用底层的关键编码函数 WriteVarint32ToArray 或 WriteVarint64ToArray,整个过程如下图所示:

6009978-d3f8eaef64fe78e9.png


WriteVarint32ToArray 函数可在源码目录下的 google.protobuf.io 包下的 coded_stream.h 中找到。在上一篇 深入 ProtoBuf - 编码 中我们解析了 Varint 编码原理和详细过程,WriteVarint32ToArray(以及 WriteVarint64ToArray)并是 Varint 编码的核心。

可以对照上一篇指出的 Varints 编码的几个关键点来阅读以下代码,可以看出编码实现确实优雅,代码如下:
inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
// 0x80 -> 1000 0000
// 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节
// 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回
// 如果至少需要两个字节
while (value >= 0x80) {
// 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位
*target = static_cast<uint8>(value | 0x80);
// 处理完七位,后移,继续处理下一个七位
value >>= 7;
// 指针加一,(数组后移一位)
++target;
}
// 跳出循环,则表示已无后续字节,但还有最后一个字节

// 把最后一个字节放入数组
*target = static_cast<uint8>(value);
// 结束地址指向数组最后一个元素的末尾
return target + 1;
}

// Varint64 同理
inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,
uint8* target) {
while (value >= 0x80) {
*target = static_cast<uint8>(value | 0x80);
value >>= 7;
++target;
}
*target = static_cast<uint8>(value);
return target + 1;
}
在上面已添加详细注释,这里再强调几个关键点。
 
  • value | 0x80:xxx ... xxxx xxxx | 000 ... 1000 0000 的结果其实就是将最后一个字节的第一个 bit(最高位) 置 1,其他位不变,即 xxx ... 1xxx xxxx。注意 target 是 uint8 类型的指针,这意味它只会截断获取最后一个字节,即 1xxx xxxx,这里的 1 意味着什么?这个 1 就是所谓的 msb 了,意味着后续还有字节。之后就是右移 7 位(去掉最后 7 位),处理下一个 7位。
  • 通过这里的代码应该可以体会到为什么 Varints 编码结果是低位排在前面了。


了解了最底层 IO 包中的编码函数,再结合上篇文章介绍的编码原理,对 ProtoBuf 的编码应该有了更深入的认识。

Varints 类型序列化实现

int32、int64、uint32、uint64

int32 类型编码函数对应为 WriteInt32ToArray,源码如下:
// WriteTagToArray 函数将 Tag 部分写入
// WriteInt32NoTagToArray 函数将 Value 部分写入
// WriteTagToArray 和 WriteInt32NoTagToArray 底层
// 均调用 coded_stream.h 中的 WriteVarint32ToArray
//因为 ProtoBuf 中的 Tag 均采用 Varint 编码
// int32 的 Value 部分也采用 Varint 编码
inline uint8* WireFormatLite::WriteInt32ToArray(int field_number, int32 value,
uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
return WriteInt32NoTagToArray(value, target);
}
int64、uint32、uint64 类型与 int32 类型同理,只是处理位数有所不同。


uint32 和 uint64 也是采用 Varint 编码,所以底层编码实现与 int32、int64 一致。


 sint32、sint64

这两种类型编码函数对应为 WriteSInt32ToArray 和 WriteSInt64ToArray 。

在上一篇文章 深入 ProtoBuf - 编码 中我们已经介绍过 Varint 编码在负数的情况下编码效率很低,固对于 sint32、sint64 类型我们会采用 ZigZag 编码将负数映射成正数然后再进行 Varint 编码,而这种映射并非采用存储的 Map,而是使用移位实现。sint32 的 ZigZag 源码实现如下:
inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
// 右移为算数右移
// 左移时需要先将 n 转成 uint32 类型,防止溢出
// 当 n 为正数时 result = 2 * n
// 当 n 为负数时 result = - (2 * n + 1)
return (static_cast<uint32>(n) << 1) ^ static_cast<uint32>(n >> 31);
}
经过 ZigZagEncode32 编码之后,数字成为一个正数,之后等同于 int32 或 int64 进行完全相同的编码处理。

bool 与 enum

bool 和 enum 本质就是整型,编码处理与 int32、int64 相同。

32-bit、64-bit
fixed32/fixed64

fixed32 类型对应 WriteFixed32ToArray 函数,32-bit、64-bit类型的字段比起上述 Varint 类型则要简单的多,因为每个数字均是固定字节,源码如下:
inline uint8* WireFormatLite::WriteSFixed32ToArray(int field_number,
int32 value, uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
return WriteSFixed32NoTagToArray(value, target);
}
其中 WriteSFixed32NoTagToArray 源码如下:
inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,
uint8* target) {
return io::CodedOutputStream::WriteLittleEndian32ToArray(
static_cast<uint32>(value), target);
}
由此可知,对于位数固定的 sfixed32 是将其转成 uint32 类型,然后使用与 fixed32 相同的函数写入。

sfixed64 与 sfixed32 同理,不赘述。
 
Length delimited 字段序列化

因为其编码结构为 Tag - Length - Value,所以其字段完整的序列化会稍微多出一些过程,其中有一些需要我们进一步整理。现在以一个 string 类型字段的序列化为例,来看看其序列化的完整过程,画出其程序时序图(上文出现过)如下:

6009978-d3f8eaef64fe78e9.png


可对照上述时序图来阅读源码,其序列化实现的几个关键函数为:
 
  • ByteSizeLong:计算对象序列化所需要的空间大小,在内存中开辟相应大小的空间
  • WriteTagToArray:将 Tag 值写入到之前开辟的内存中
  • WriteStringWithSizeToArray:将 Length + Value 值写入到之前开辟的内存中


其序列化代码的重点过程在上图的右下角,先是调用 WriteTagToArray 函数将 Tag 值写入到内存,返回指向下一个字节的指针以便继续写入。调用 WriteStringWithSizeToArray 函数,这个函数主要又执行了两个函数,先是执行 WriteVarint32ToArray 函数(注意 WriteTagToArray 内部调用的也是这个函数,因为 Tag 和 Length 都采用 Varints 编码),此函数的作用是将 Length 写入。执行的第二个函数为 WriteStringToArray,此函数的作用是将 Value(一个 UTF-8 string 值) 写入到内存,其中底层调用了 memcpy() 函数。

综上,对于 Varint 类型的字段自然采用 Varint 编码。

而对于 Length delimited 类型的字段,Tag-Length-Value 中的 Tag 和 Length 依然采用 Varint 编码,Value 若为 String 等类型,则直接进行 memcpy。

另外对于 embedded message 或 packed repeated ,则套用上述规则。底层编码实现实际并是遍历字段下所有内嵌字段,然后递归调用编码函数即可。

作者:404_89_117_101
链接:https://www.jianshu.com/p/62f0238beec8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。