第五章——使用一元GRPC传递pcbook
5.1、定义一个 proto 服务和一个一元 RPC
创建新文件——proto/laptop_service.proto 定义一条CreateLaptopRequest消息,它只包含一个字段:我们要创建的笔记本电脑
message CreateLaptopRequest {
Laptop laptop = 1;
}
定义CreateLaptopResponse消息也只有 1 个字段:创建的笔记本电脑的 ID
message CreateLaptopResponse {
string id = 1 ;
}
定义一元GRPC
service LaptopService {
rpc CreateLaptop ( CreateLaptopRequest) returns ( CreateLaptopResponse) { } ;
}
5.2、配置服务端
创建service文件夹,创建service/laptop_server.go文件 声明一个LaptopServer结构和一个NewLaptopServer()函数来返回它的一个新实例
type LaptopServer struct {
pb. UnimplementedLaptopServiceServer
}
func NewLaptopServer ( ) * LaptopServer {
return & LaptopServer{ }
}
在laptop_service.proto中定义了一个CreateLaptop一元服务,在此go文件中需要去定义一个CreateLaptop函数实现服务
func ( server * LaptopServer) CreateLaptop ( ctx context. Context, req * pb. CreateLaptopRequest) ( * pb. CreateLaptopResponse, error ) {
return nil , nil
}
5.3、完善服务端代码
5.3.1、通过req获取Laptop结构体
由于在proto中定义的Request消息携带Laptop结构体,所以可以在函数中调用GetLaptop函数从请求中获取笔记本电脑对象
func ( server * LaptopServer) CreateLaptop ( ctx context. Context, req * pb. CreateLaptopRequest) ( * pb. CreateLaptopResponse, error ) {
laptop := req. GetLaptop ( )
log. Printf ( "receive a create-laptop request with id: %s" , laptop. Id)
return nil , nil
}
5.3.2、如果客户端已经生成了笔记本电脑ID,检测UUID是否有效
使用Google UUID包,命令:go get github.com/google/uuid, 使用uuid.Parse()函数来解析笔记本电脑的 ID。
if len ( laptop. Id) > 0 {
_ , err := uuid. Parse ( laptop. Id)
if err != nil {
return nil , status. Errorf ( codes. InvalidArgument, "laptop ID is not a valid UUID: %v" , err)
}
}
5.3.3、如果客户端没有发送笔记本电脑 ID,我们将在服务器上使用uuid.NewRandom()命令生成它。
else {
id, err := uuid. NewRandom ( )
if err != nil {
return nil , status. Errorf ( codes. Internal, "cannot generate a new laptop ID: %v" , err)
}
laptop. Id = id. String ( )
}
5.3.4、将获取到的Laptop进行存储
创建新文件service/laptop_store.go 定义一个LaptopStore接口,此接口需要实现一个save方法,这样后面可以定义存储在文件中的结构体与存储在数据库中的结构体
type LaptopStore interface {
save ( laptop * pb. Laptop) error
}
实现接口,定义为存储在磁盘上的结构体
需要一个读写互斥体来处理多个并发请求以保存笔记本电脑
type InMemoryLaptopStore struct {
mutex sync. RWMutex
data map [ string ] * pb. Laptop
}
声明一个函数NewInMemoryLaptopStore来返回一个新的 InMemoryLaptopStore,并初始化其中的数据映射
func NewInMemoryLaptopStore ( ) * InMemoryLaptopStore {
return & InMemoryLaptopStore{
data: make ( map [ string ] * pb. Laptop) ,
}
}
实现InMemoryLaptopStore结构体的save功能
需要在添加新对象之前获取写锁 我们检查笔记本电脑 ID 是否已经存在于地图中 如果笔记本电脑不存在,先深度拷贝这个Laptop结构体后将此结构体进行存储
func ( store * InMemoryLaptopStore) Save ( laptop * pb. Laptop) error {
store. mutex. Lock ( )
defer store. mutex. Unlock ( )
if store. data[ laptop. Id] != nil {
return errors. New ( "record already exists" )
}
other, _ := deepCopy ( laptop)
store. data[ other. Id] = other
return nil
}
实现deepCopy函数
使用包,命令:go get github.com/jinzhu/copier
func deepCopy ( laptop * pb. Laptop) ( * pb. Laptop, error ) {
other := & pb. Laptop{ }
err := copier. Copy ( other, laptop)
if err != nil {
return nil , fmt. Errorf ( "cannot copy laptop data: %w" , err)
}
return other, nil
}
5.3.5、在server端使用存储结构体进行存储
在laptop_server.go文件中的LaptopServer结构体添加新字段LaptopServer
type LaptopServer struct {
pb. UnimplementedLaptopServiceServer
laptopStore LaptopStore
}
在CreateLaptop()函数中,我们可以调用server.laptopStore.Save()将输入的笔记本电脑保存到商店
使用codes.Internal获取保存错误 调用errors.Is()函数检查是否存在
err := server. laptopStore. save ( laptop)
if err != nil {
code := codes. Internal
if errors. Is ( err, errors. New ( "record already exists" ) ) {
code = codes. AlreadyExists
}
return nil , status. Errorf ( code, "cannot save laptop to the store: %v" , err)
}
最后,如果没有发生错误,我们可以使用笔记本电脑 ID 创建一个新的响应对象并将其返回给调用者
log. Printf ( "saved laptop with id: %s" , laptop. Id)
res := & pb. CreateLaptopResponse{
Id: laptop. Id,
}
5.3.6、完整的server端代码
func ( server * LaptopServer) CreateLaptop ( ctx context. Context, req * pb. CreateLaptopRequest) ( * pb. CreateLaptopResponse, error ) {
laptop := req. GetLaptop ( )
log. Printf ( "receive a create-laptop request with id: %s" , laptop. Id)
if len ( laptop. Id) > 0 {
_ , err := uuid. Parse ( laptop. Id)
if err != nil {
return nil , status. Errorf ( codes. InvalidArgument, "laptop ID is not a valid UUID: %v" , err)
}
} else {
id, err := uuid. NewRandom ( )
if err != nil {
return nil , status. Errorf ( codes. Internal, "cannot generate a new laptop ID: %v" , err)
}
laptop. Id = id. String ( )
}
err := server. laptopStore. save ( laptop)
if err != nil {
code := codes. Internal
if errors. Is ( err, errors. New ( "record already exists" ) ) {
code = codes. AlreadyExists
}
return nil , status. Errorf ( code, "cannot save laptop to the store: %v" , err)
}
log. Printf ( "saved laptop with id: %s" , laptop. Id)
res := & pb. CreateLaptopResponse{
Id: laptop. Id,
}
return res, nil
}
5.4、编写主服务器和客户端
创建一个新cmd文件夹,创建cmd/server/main.go文件与cmd/client/main.go文件 编写Makefile文件
server:
go run cmd/server/main.go
client:
go run cmd/client/main.go
编写服务端代码
func main ( ) {
laptopServer := service. NewLaptopServer ( service. NewInMemoryLaptopStore ( ) )
grpcServer := grpc. NewServer ( )
pb. RegisterLaptopServiceServer ( grpcServer, laptopServer)
listener, _ := net. Listen ( "tcp" , ":8888" )
grpcServer. Serve ( listener)
}