文章目录

  • 前言
  • 编写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服务中遭遇了一些瓶颈,包括:

  1. 代码规范混乱(写python的都知道,一个人就是一个风格)
  2. 系统运行性能低下(这倒不一定就是python 的锅)
  3. 语言陈旧(原项目采用的是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实例,这段代码中的函数名HelloInfoproto文件中的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)
}

调用

业务代码,服务端,客户端都写好了,我们直接尝试调用一下,首先,启动服务端

gRPC 的官方文档 grpc django_服务端


接着运行客户端进行调用

gRPC 的官方文档 grpc django_服务端_02


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...)

	... // 服务端中的重复代码省略
}

拦截器方法和业务代码本质上差不多,不过其触发时机在所有业务代码之前,并且拦截器可以添加多个。

按照之前的方法启动服务端,在用客户端调用,查看服务端的输出,结果如下

gRPC 的官方文档 grpc django_grpc_03


客户端的拦截器流成和服务端基本相似,区别在于注册方法和位置要换成客户端的

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,我们迈出了通向微服务道路的坚实一步。

gRPC 的官方文档 grpc django_服务端_04