什么是序列化和反序列化?
- 场景:用在两个客户端和服务端通信,或者两个服务器之间的通信。
- 举例:比如服务器A需要调用服务器B上的一个函数。
- 服务器A的代码中可能是一个对象的形式存储的数据信息。首先它得把对象序列化成json格式,使用二进制数据的方式发送给服务器B。
- 服务器B接收到json格式的二进制数据。首先得进行反序列化存储到对象模型中,然后进行业务处理得到结果。结果同样也得先序列化成json格式发送给服务器A。
- 只能序列化成json格式吗?
- 不是。除了json,还有xm、protobuf、msgpack等数据编码协议。
- 不同的数据传入格式有什么不同?
- 传输性能不同。protobuf和msgpack格式的传输性能更好。go微服务项目中就是使用的protobuf。
protobuf序列化编码协议和上手实操
- protobuf是什么?
- 是一种IDL(Interface Defintion Language 接口定义语言),还有Thrift也是。
- window上安装protobuf:
- go中安装protobuf编译插件、安装grpc框架::
go get -u google.golang.org/grpc
go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
- 创建项目目录:proto、server、client
-
server:存放服务端代码。
client:存放客户端代码。
- 编写proto文件。
- protoc3.10之后要添加
option go_package参数
- 执行命令根据proto文件生成go文件(两个go文件):
protoc --go_out=. --go-grpc_out=. hello.proto
如果不加--go-grpc_out=.就不会生成service对应的go文件
- 编写server代码和client代码
- 运行代码
- 运行服务端代码:
go run server/server.go
- 运行客户端代码:
go run client/client.go
- 成功的话客户端会打印
Greeting: Hello, World!
代码实践
代码编写思路
- 定义服务接口和服务消息
- 编写proto文件
- protoc命令根据proto文件生成go代码
- 为什么使用proto的方式定义接口和消息服务?
- proto是跨语言的协议,不仅可以生成go语言的代码,还能生成其他语言的。
- protobuf的传输格式非常高效,能够被序列化成非常高效的二进制数据进行传输,速度快,空间小。
- 为什么要通过定义接口的方式,而不是直接使用结构体?
- 使用接口的方式,客户端在调用服务时,直接调用接口中指定的方法就行了,不用管是哪个具体的实现类实现的这个接口。
- 这样比如具体的服务方法逻辑改变了,只用更换/修改实现类就行了。客户端调用方法的代码不用做任何改变,因为它只知道调用的是接口的方法。
- 而如果使用结构体的方式。服务方法的逻辑改变后,更换/修改实现类后,客户端由于是通过具体的实现类调用的方法,所以它也得替换成新实现类实例。服务代码和客户端代码就会强耦合,修改一个,其他一堆客户端代码也得跟着改。
- 启动服务(server负责)
- 创建服务实例
- 创建监听器。
- 端口
- 启动服务。
- 连接服务(client负责)
- 建立连接
- 创建一个客户端实例
- 调用方法(client中调用server的方法)
- 发送请求数据
- 返回响应数据
编写proto文件
- protoc3.10之后要添加
option go_package参数,用于指定生成的go文件目录
// proto/hello.proto
syntax = "proto3";
option go_package = ".;proto";
package hello;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
根据proto文件生成go文件
protoc --go_out=. --go-grpc_out=. hello.proto
- 如果不加
--go-grpc_out=.就不会生成service对应的go文件
- 会生成两个文件:
hello.pb.go,hello_grpc.pb.go
helloworld.pb.go:包含了消息类型的定义。
helloworld_grpc.pb.go:包含了 gRPC 服务的接口定义。
编写server.go和client.go
- 定义一个 server 结构体,嵌入了生成的 GreeterServer 接口的默认未实现版本。
- 实现 Greeter 服务中的 SayHello 方法
- SayHello 方法会接收 HelloRequest 请求,并返回 HelloReply 响应
- 创建问候消息,使用请求中的 Name 字段
- 返回 HelloReply 响应,包含生成的问候消息
- 创建一个监听器,监听端口 50051 lis, err := net.Listen("tcp", ":50051")
- 创建一个 gRPC 服务器的实例
- 将我们的 server 结构体注册到 gRPC 服务器中, 这样 gRPC服务器 就知道如何处理 Greeter 服务的请求了
- 启动 gRPC 服务器,开始监听客户端的请求
package main
import (
"context"
"fmt" "google.golang.org/grpc" pb "grpc-demo-proj/proto"
"log" "net")
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
replyMessage := fmt.Sprintf("Hello, %s!", req.Name)
return &pb.HelloReply{Message: replyMessage}, nil
}
func main() {
if err != nil {
log.Fatalf("监听端口失败: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, &server{})
log.Println("gRPC 服务器正在运行,监听端口 50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("gRPC 服务器启动失败: %v", err)
}
}
package main
import (
"context"
"log" "time"
"google.golang.org/grpc" pb "grpc-demo-proj/proto"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("无法连接到服务端: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
req := &pb.HelloRequest{Name: "World"}
res, err := client.SayHello(ctx, req)
if err != nil {
log.Fatalf("请求失败: %v", err)
}
log.Printf("收到服务端响应: %s", res.Message)
}