文章目录
- 前言
- 编写proto
- 编写业务代码
- 编写服务端代码
- 编写客户端
- 调用
- 添加拦截器
- 踩坑环节
- command-line-arguments: ***: undefined: ***
- go mod初始化模块管理
- cannot take the address & cannot call pointer method
- protoc-gen-go failed :: The import path must contain at least one forward slash ('/') character."
- 写在最后
前言
上次发文章是什么时候的事儿了,笔者懒癌又发作了。最近这段时间公司进行项目重构,主要是从python往go方面迁移。理由是python项目在目前的web服务中遭遇了一些瓶颈,包括:
- 代码规范混乱(写python的都知道,一个人就是一个风格)
- 系统运行性能低下(这倒不一定就是python 的锅)
- 语言陈旧(原项目采用的是python2)
由于项目是本身是一个微服务的架构,各个服务件本身是通过rpc去调用。原项目本身使用的rpc框架是grpc,在新项目中,仍然继续选择grpc。
grpc本身是支持http2标准化协议的,添加了头信息,可以方便在框架层面对调用做拦截和控制(比如说限流,调用链分析,安全认证等),其protobuffer也被证明是一种十分高效的序列化技术。
当然历史包袱也是有一定原因(没有足够的理由去更换掉grpc)
本文的调用方式仅限于一元调用。
编写proto
这里不详细介绍protobuf的语法了,贴一个简单的data.proto
做演示
syntax = "proto3";
package GrpcDemo;
option go_package = "./pb";
/* 服务 */
service HelloService {
/* 单个请求配置 */
rpc HelloInfo(HelloInfoRequest) returns (HelloInfoReply) {} // 请求主体
}
/* 请求主体对应单个请求配置 */
// 请求主体结构
message HelloInfoRequest {
string name = 1;
}
message HelloInfoReply {
string name = 1;
}
在使用grpc之前,我们必须先通过编写的proto文件生成对应的服务端和客户端代码。对应到go中,自然就是生成go的代码喽。
在这之前,我们需要下载一个编译依赖包:
go get github.com/golang/protobuf/protoc-gen-go
下载完成之后protoc --version
检查一下是否安装成功,如果没有找到protoc
命令,检查一下对应的执行文件是否正确下载到GOBIN路径当中,以及执行文件所在路径是否正确添加到环境变量PATH
当中。
OK,我们进入到上述编写好的proto
文件我在路径,执行protoc --go_out=plugins=grpc:. data.proto
就可以生成go的grpc客户端和服务端代码,文件名称格式为xxx.pb.go
编写业务代码
package service
import (
"GrpcDemo/pb"
"context"
)
type Hello struct {}
func (cls *Hello) HelloInfo(ctx context.Context, pkg *pb.HelloInfoRequest) (*pb.HelloInfoReply, error) {
return &pb.HelloInfoReply{
Name: pkg.Name,
}, nil
}
ctx
中包含了此次请求的元信息,类似于python web框架的request实例,这段代码中的函数名HelloInfo
和proto文件
中的rpc方法名需要一一对应,方法里什么都没有做,只是拿到请求后直接返回了请求体中的Name
数据。
编写服务端代码
下一步就是编写服务端代码,直接上代码
package main
import (
"GrpcDemo/pb"
"GrpcDemo/service"
"google.golang.org/grpc"
"log"
"net"
)
func main() {
addres := "localhost:8889" // 服务地址
listener, err := net.Listen("tcp", addres) // 监听地址
if err != nil {
log.Println("net listen err ", err)
return
}
s := grpc.NewServer() // 初始化grpc服务
pb.RegisterHelloServiceServer(s, &service.Hello{}) // 注册服务
log.Println("start gRPC listen on addres " + addres)
if err := s.Serve(listener); err != nil { // 监听服务,如启动失败则抛出异常
log.Println("failed to serve...", err)
return
}
}
其中pb.RegisterHelloServiceServer
为通过proto
文件生成的方法。用于注册我们刚刚写的业务代码。方法的命名格式为Register + xxx + Server
,其中xxx
的部分对应proto中的服务名称。
编写客户端
服务端有了,我们需要一个客户端去调用服务端,同样,直接上代码
package main
import (
"GrpcDemo/pb"
"context"
"fmt"
"google.golang.org/grpc"
"log"
)
const (
address = "localhost:8889"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure()) // 建立链接
if err != nil {
log.Println("did not connect.", err)
return
}
defer conn.Close()
client := pb.NewHelloServiceClient(conn) // 初始化客户端
ctx := context.Background() // 初始化元数据
helloName := &pb.HelloInfoRequest{Name: "chengjian.liu"} // 构造请求体
info, err := client.HelloInfo(ctx, helloName) // 调用helloInfo rpc服务,就像调用普通函数一样
if err != nil {
return
}
fmt.Println("hello", info.Name)
}
调用
业务代码,服务端,客户端都写好了,我们直接尝试调用一下,首先,启动服务端
接着运行客户端进行调用
OK,调用完成,grpc的一个完整流程到此已经走通了。
添加拦截器
我们在使用rpc服务时,通常需要在进入业务主体之前进行一些前置操作,比如鉴权,加密解密等等,我们不希望这些通用逻辑嵌入到每个业务当中。幸运的是,grpc为我们提供了拦截器的功能,我们可以通过添加拦截器给服务加上上述功能,而不影响到业务本身。
下面就来看看拦截器的写法吧~我们直接在服务端添加一个拦截器
package main
import (
"GrpcDemo/pb"
"GrpcDemo/service"
"context"
"fmt"
"google.golang.org/grpc"
"log"
"net"
)
// interceptor 拦截器
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
fmt.Println("拦截到服务: ", info.FullMethod)
// 继续处理请求
return handler(ctx, req)
}
func main() {
... // 服务端中的重复代码省略
var opts []grpc.ServerOption
// 注册interceptor
opts = append(opts, grpc.UnaryInterceptor(interceptor))
s := grpc.NewServer(opts...)
... // 服务端中的重复代码省略
}
拦截器方法和业务代码本质上差不多,不过其触发时机在所有业务代码之前,并且拦截器可以添加多个。
按照之前的方法启动服务端,在用客户端调用,查看服务端的输出,结果如下
客户端的拦截器流成和服务端基本相似,区别在于注册方法和位置要换成客户端的
var opts []grpc.DialOption
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
conn, err := grpc.Dial(Address, opts...)
踩坑环节
在编写调试过程中遇到了一些问题,相应的解决方案总结在下面供参考。
command-line-arguments: ***: undefined: ***
同一个包下的两个文件,点击idea的运行按钮或者运行 go run main.go命令时,就会报错,该问题属于go的多文件加载问题,在go ide中以package模式运行代码可以解决,具体解决方案参见:Go常见问题:# command-line-arguments: ***: undefined: ***
go mod初始化模块管理
在初始化项目前,我们首先要了解go mod是如何进行模块管理的,否则就会出现连项目都无法运行的情况,go mod运用详情参见:go mod使用
cannot take the address & cannot call pointer method
详见:Go 挖坑指南: cannot take the address & cannot call pointer method
protoc-gen-go failed :: The import path must contain at least one forward slash (’/’) character."
笔者在用proto文件生成代码时出现的问题,笔者的原因是proto的package文件路径设置有问题,问题详情参见:github issues proto-gen-go failed
写在最后
通常我们在搭建微服务时,rpc经常被用来在服务间进行通信,通过了解grpc,我们迈出了通向微服务道路的坚实一步。