go微服务学习(二)-从rpc到grpc
写在开头
仅用于自己学习
回顾上节代码
client/client.go
func main() {
con, err := rpc.Dial("tcp", ":8080")
if err != nil {
log.Fatal(err)
return
}
defer con.Close()
var str string
err = con.Call("hello.Hello", "zjb", &str)
if err != nil {
log.Fatal(err)
return
}
fmt.Println(str)
}
server/server.go
type HelloServer struct {
}
func (s HelloServer) Hello(str string, res *string) error {
*res = "hello" + str
return nil
}
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
return
}
_ = rpc.RegisterName("hello", HelloServer{})
for {
con, err := ln.Accept()
if err != nil {
log.Fatal(err)
return
}
go rpc.ServeConn(con)
}
}
1.原生rpc的弊端
无非就是如何解耦
1.约定问题如何解决
最大的弊端不用想就知道,通常客户端和服务端都不在一台服务器上,那么客户端如何知道服务端结构体的结构?
直接把服务端代码也拷贝一份下来?看安全性显然不行,那么我们只需要把客户端需要的部分给拷贝下来不就行了?
抽象出handler逻辑
handler/handler.go
const (
//这里包名+结构体名保证唯一性
HelloServiceName string = "handler/HelloServer"
)
那么在服务端修改后客户端只需要引用改变了即可
//server.go
_ = rpc.RegisterName(handler.HelloServiceName, HelloServer{})
//client.go
err = con.Call(handler.HelloServiceName+".Hello", "zjb", &str)
还是不够爽,我连这个方法名我都想让handler告诉我,那就使用interface进行约定
handler.go
type HelloService interface {
Hello(str string, res *string) error
}
那我们看一眼就知道有哪些方法了,服务器具体的实现类随便它
1.2独立业务逻辑
此时的client.go我们只想写自己的业务逻辑,什么连接啊,访问哪个啊我都不关心,那么我们就把它抛给一个代理或管理器来执行
clent_proxy/manger.go
type Client struct {
*rpc.Client
}
var _ handler.HelloService = (*Client)(nil)
func (c *Client) Hello(str string, res *string) error {
return c.Call(handler.HelloServiceName+".Hello", str, res)
}
func (c *Client) Close() {
c.Close()
}
func MakeClient(addr string) (*Client, error) {
con, err := rpc.Dial("tcp", ":8080")
if err != nil {
return nil, err
}
return &Client{con}, nil
}
那么客户端只需要调用代理的Hello方法即可,同理服务端
server_proxy/manger.go
type HelloServer struct {
}
var _ handler.HelloService = (*HelloServer)(nil)
func (s HelloServer) Hello(str string, res *string) error {
*res = "hello" + str
return nil
}
func RegisterName(name string, srv any) {
rpc.RegisterName(name, srv)
}
综上,我们看到核心是通过handler文件来进行双方的约束,并利用代理类让横向业务不入侵到主业务当中
问题又来了,这些玩意手写维护都很麻烦,有没有好用的工具帮我们自动生成server_proxy、client_proxy和handler?
就算go能生成,那能为其他语言也生成一份吗(这样不就其他语言能互相调用函数了吗)?
这就引入了下文protobuf 和grpc
2.Proto 和 grpc
2.1环境安装
proto工具安装比较简单,网上都能百度到就不再过多赘述
执行命令查看是否安装成功
protoc --version
安装grpc比较麻烦
我自己的安装环境是windows
官方给的命令不起作用
go get google.golang.org/grpc
原因是这个代码已经转移到github上了,但代码里包的依赖还是没有修改还是google.golang.org,所以我更推荐下面这种办法。
这里提供一种像我一样无妨访问goolg官网安装grpc办法
需要注意对应的目录
git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net
git clone https://github.com/golang/text.git $GOPATH/src/golang.org/x/text
git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto
最后进入到gopath目录的src下执行以下命令即可
go install google.golang.org/grpc
如果你像我一样克隆仓库都办不到,那直接去github仓库下载zip包来解压到$GOPATH的对应文件夹即可
2.2 proto 3语法
- 第一行必须声明是3的语法
syntax = "proto3";
- option这里用的最多是go_package 来声明包名,如下会自己创建pb/proto_demo文件夹下的go文件,文件的包名为“proto_demo”
option go_package = "pb/proto_demo";
- 定义消息数据结构message,各个类型对应什么字段翻阅文档就行
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
数据结构的字段后面跟的编号需要保证唯一性,这些编号不是默认值,而是用于二进制格式标识字段,就好比数组哪个内存单元占什么数据,这也意味着一旦消息类型被使用,编号就不应被更改
- 保留字段。即使我们要更新消息的数据结构,如删除某个字段,也不能重用该数字编号。如果以后加载旧版本的相同.proto文件,会导致数据损坏、隐私泄露等问题。用保留字段reserved标注即可,当这个变量名被重新使用时编译器报错
message Foo {
// 注意,同一个 reserved 语句不能同时包含变量名和 Tag
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
- 定义服务。service会使proto生成对应的客户端和服务端代码,对应第一节的两个代理代码
service UserServiceRpc
{
rpc Login(LoginRequest) returns(LoginResponse);
}
2.3proto文件编译
protoc \
-I=xxx // .proto文件所在的目录
--go_out=. //文件在哪里生成
[--go_out=plugins=grpc:.] //涉及到grpc代码的生成需要使用插件
2.4快速体验grpc
编写hello.proto文件,提供名为My的服务,该服务具体提供Hello服务
syntax = "proto3";
option go_package = "pb/proto_demo";
message HelloRequest {
string name = 1; //1是编号
}
message Response {
string reply = 1;
}
service My {
rpc Hello(HelloRequest) returns (Response); //hello接口
}
编译文件生成约束规则
protc -I . --go_out=plugin=grpc:.
我么可以观察生成的go文件多出了myclient结构体和myserver结构体,这不就类似上文的两个代理
服务端server/main.go编写服务的具体内容
type Server struct {
}
var _ proto_demo.MyServer = (*Server)(nil)
// 逻辑就是返回 hello+name
func (s *Server) Hello(ctx context.Context, r *proto_demo.HelloRequest) (*proto_demo.Response, error) {
return &proto_demo.Response{
Reply: "hello" + r.Name,
}, nil
}
func main() {
g := grpc.NewServer()
proto_demo.RegisterMyServer(g, &Server{})
ln, err := net.Listen("tcp", ":8080")
if err != nil {
panic("you die!")
}
log.Fatal(g.Serve(ln))
}
client/main.go客户端发起请求
func main() {
con, err := grpc.Dial(":8080", grpc.WithInsecure())
if err != nil {
panic("you die!")
}
defer con.Close()
var req = &proto_demo.HelloRequest{
Name: "zjb",
}
client := proto_demo.NewMyClient(con)
res, err := client.Hello(context.Background(), req)
if err != nil {
panic("you die!")
}
fmt.Println(res.Reply)
}
结果
hellozjb