RPC
RPC
是远程过程调用,是一个节点向请求另一个节点提供的服务,像调用本地函数一样去调用远程函数
远程过程调用有很多问题
Call ID
映射:如何知道远程机器上的函数名json/xml/protobuf/msgpack
- 客户端
- 建立连接
tcp/http
- 序列化
- 向服务端发送数据 -> 这是二进制的数据
- 等待服务端响应 -> 这是二进制数据
- 反序列化
- 建立连接
- 服务端:
- 监听端口
- 读取客服端发送过来的数据 -> 这是二进制的数据
- 反序列化
- 处理业务逻辑
- 将客户端需要的数据序列化
- 返回给客户端 -> 这是二进制的数据
- 在大型分布式系统中,使用
json
作为数据格式协议几乎不可维护
http/tcp
- 对于
http
协议来说,它是一次性的,一旦对方有了结果,连接就断开了 http1.x
在微服务这块应用有性能问题http2.0
可以解决这个问题
如果不使用 RPC
框架,如何实现远程调用呢?
利用 go 内置的 rpc 实现
go
内置 rpc
实现源码:源码
新建 server
包,提供一个 HelloService
服务,这个函数的作用传进来一个 string
类型的值 xxx
,返回 hello, xxx
import (
"net"
"net/rpc"
)
type HelloService struct{}
func main() {
// 监听 tcp 服务的 1234 端口
listener, _ := net.Listen("tcp", ":1234")
// 注册一个 HelloService 服务
_ = rpc.RegisterName("HelloService", &HelloService{})
for {
//启动服务
conn, _ := listener.Accept()
rpc.ServeConn(conn)
}
}
// Hello 方法
func (s *HelloService) Hello(request string, reply *string) error {
*reply = "hello, " + request
return nil
}
新建 client
包,用来调用 server
包中的 HelloService
的 Hello
方法
import (
"fmt"
"net/rpc"
)
func main() {
// 与 server 建立连接
conn, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
panic(err)
}
var require *string = new(string)
// 调用 HelloService 的 Hello 方法,传入参数 uccs,返回值赋值给 require
err = conn.Call("HelloService.Hello", "uccs", require)
if err != nil {
panic(err)
}
fmt.Println(*require)
}
我们可以看到在不使用任何框架前,我们写的 rpc
代码是非常冗余的:
HelloService
server
端注册服务,启动服务client
端建立连接,调用方法,返回结果这些是非常繁琐的,我们将这些步骤进行简化
利用 go 内置的 rpc 实现优化版本
利用 go
内置的 rpc
实现优化版本:源码
我们最先能够想到的优化点是将 HelloService
的定义提出来,放在一个单独的包中
新建包 handler
type HelloService struct{}
client
我们现在在调用 Hello
时,需要写成 conn.Call("HelloService.Hello", xxx, xxx)
,但我们想要的调用方式是 conn.Hello(xxx, xxx)
在 go
中一个变量不可能凭空多出 Hello
方法
我们怎么才能实现这种方法呢?
可以通过 go
的结构体来实现
新建包 client_proxy
type HelloServiceStub struct {
*rpc.Client
}
// 建立连接,返回一个 HelloServiceStub
func NewHelloServiceClient(protocol string, address string) HelloServiceStub {
conn, err := rpc.Dial(protocol, address)
if err != nil {
panic("连接失败")
}
return HelloServiceStub{
conn,
}
}
// 在结构体 HelloServiceStub 中定义 Hello 方法
func (c *HelloServiceStub) Hello(request string, reply *string) error {
return c.Client.Call(handler.HelloServiceName+".Hello", request, reply)
}
在 client
中,我们调用就简单了,直接调用 NewHelloServiceClient
传入 protocol
和 address
然后在返回的值中调用 Hello
方法
import (
"fmt"
"go-rpc/optimized-rpc/client_proxy"
)
func main() {
client := client_proxy.NewHelloServiceClient("tcp", "localhost:1234")
var require *string = new(string)
err := client.Hello("uccs", require)
if err != nil {
panic(err)
}
fmt.Println(*require)
}
server
服务端要做的事情是将注册服务的函数提取出来
但这里有个问题:我们需要在注册服务时接受一个包含 Hello
方法的结构体
这个结构体是在 server
中定义的,但我们在 server_proxy
包中是无法引用的
这就可以使用接口来解决,就是下面定义的 HelloServicer
接口
新建包 server_proxy
import (
"go-rpc/optimized-rpc/handler"
"net/rpc"
)
type HelloServicer interface {
Hello(request string, reply *string) error
}
func RegisterHelloService(srv *HelloServicers) error {
return rpc.RegisterName(handler.HelloServiceName, srv)
}
在 server
中,我们调用 RegisterService
方法,传入 HelloService
,就可以注册服务了
import (
"go-rpc/optimized-rpc/handler"
"go-rpc/optimized-rpc/server_proxy"
"net"
"net/rpc"
)
type HelloService struct{}
func main() {
listener, _ := net.Listen("tcp", ":1234")
_ = server_proxy.RegisterHelloService(&HelloService{})
for {
conn, _ := listener.Accept()
go rpc.ServeConn(conn)
}
}
func (s *HelloService) Hello(request string, reply *string) error {
*reply = "hello, " + request
return nil
}
最终它的结构如下图所示:
使用 grpc 重写 rpc
使用 grpc
重写 rpc
:源码
新建 proto
包
定义 rpc
类型的 SayHello
syntax = "proto3";
option go_package ="./;proto";
service Greeter{
rpc SayHello(HelloRequest) returns (HelloReply); // hello 接口
}
message HelloRequest {
string name = 1;
}
message HelloReply{
string message = 1;
}
运行命令:protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. helloworld.proto
会生成两个文件:helloworld.pb.go
和 helloworld_grpc.pb.go
client
新建 client
包
使用 grpc
和 server
建立连接,然后就可以调用 SayHello
方法了
import (
"context"
"fmt"
"go-rpc/grpc/proto"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure())
if err != nil {
panic(err)
}
defer conn.Close()
client := proto.NewGreeterClient(conn)
r, err := client.SayHello(context.Background(), &proto.HelloRequest{Name: "uccs"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
server
新建 server
包
定义 SayHello
方法,入参:proto.HelloRequest
出参:proto.HelloReply
,并启动服务
import (
"context"
"go-rpc/grpc/proto"
"net"
"google.golang.org/grpc"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
return &proto.HelloReply{Message: "Hello " + request.Name}, nil
}
func main() {
g := grpc.NewServer()
proto.RegisterGreeterServer(g, &Server{})
lis, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
if err := g.Serve(lis); err != nil {
panic(err)
}
}
grpc
grpc
是谷歌开源的 rpc
框架,底层通信协议是 http2.0
使用 apt 安装
linux
环境下,使用 apt
安装:apt install -y protoc-gen-go protoc-gen-go-grpc
不需要额外安装 protoc
,因为他们自带了 protoc
protoc --version
# libprotoc 3.21.12
protoc-gen-go --version
# protoc-gen-go v1.28.1
protoc-gen-go-grpc --version
# protoc-gen-go-grpc 1.0
使用 wget 远程下载
linux
系统版本(我这里是 x86_64)uname -a
# Linux 667711fd2ac3 5.10.104-linuxkit #1 SMP Thu Mar 17 17:08:06 UTC 2022 x86_64 GNU/Linux
# 下载
wget https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip
# 解压
unzip protoc-23.3-linux-x86_64.zip
proto
放到环境变量弘mv -f bin/proto /usr/local/bin
include
目录中的内容放到 /usr/local/include
中mv -f include/google /usr/local/include
go
生成 proto
文件,需要安装 protoc-gen-go
和 protoc-gen-go-grpc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
protoc --version
# libprotoc 23.3
protoc-gen-go --version
# protoc-gen-go v1.31.0
protoc-gen-go-grpc --version
# protoc-gen-go-grpc v1.3.0
生成 go 文件
helloWorld.proto
文件syntax = "proto3";
// 在最新的 protoc 版本中,option 要像下面这样写,否则会报错
option go_package ="./;proto";
message HelloRequest {
string name = 1;
}
helloWorld.proto
同级目录下生成 helloWorld.pb.go
文件protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. helloWorld.proto
序列化和反序列化
go-rpc/proto
是 grpc
生成的文件用来定义数据格式
google.golang.org/protobuf/proto
进行序列化和反序列化
import (
proto2 "go-rpc/proto"
"google.golang.org/protobuf/proto"
)
func main() {
// 序列化
req := proto2.HelloRequest{
Name: "uccs",
}
rsp, _ := proto.Marshal(&req)
fmt.Println(rsp)
// 反序列化
req2 := proto2.HelloRequest{}
_ = proto.Unmarshal(rsp, &req2)
fmt.Println(req2.Name)
}