11 Go RPC
一、RPC 概述
RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
通俗地讲,一般在一个程序内,一个函数的调用栈是在程序结构内部完成的,即一个函数内调用另一个函数,只需简单的Call函数名。但有没有可能让你在调用外部的功能函数像调用内部函数那么简单直观?这就需要远程过程调用,要实现跨网络的外部调用,你需要构建一个C/S架构的程序,而远程通信的双方都遵守相同的通信协议,这就是RPC。
二、Go标准库提供基本的RPC
1. net/rpc
Go标准库提供net/rpc包支持构建RPC架构的程序
rpc包提供了通过网络或其他I/O连接对一个对象的导出方法的访问。服务端注册一个对象,使它作为一个服务被暴露,服务的名字是该对象的类型名。注册之后,对象的导出方法就可以被远程访问。服务端可以注册多个不同类型的对象(服务),但注册具有相同类型的多个对象是错误的。
只有满足如下标准的方法才能用于远程访问,其余方法会被忽略:
- 方法是导出的
- 方法有两个参数,都是导出类型或内建类型
- 方法的第二个参数是指针
- 方法只有一个error接口类型的返回值
编写服务端:rpc/server.go
package main
import (
"fmt"
"net"
"net/rpc"
"os"
)
func ErrorHandler(err error, where string) {
if err != nil {
fmt.Println("发现错误,", err, "出错位置:", where)
os.Exit(1)
}
}
type HelloService struct{}
func (hs *HelloService) Hello(request string, reply *string) error {
*reply = "Hello " + request
return nil
}
func main() {
fmt.Println("启动rpc服务...")
//注册一个RPC服务
err := rpc.RegisterName("HelloService", new(HelloService))
ErrorHandler(err, "rpc.RegisterName")
//监听一个端口并开始接收监听数据
listener, err := net.Listen("tcp", ":12138")
ErrorHandler(err, "net.Listen")
conn, err := listener.Accept()
ErrorHandler(err, "listener.Accept")
//rpc服务连接
rpc.ServeConn(conn)
fmt.Println("rpc进程退出!!!!")
}
编写客户端:rpc/client.go
package main
import (
"fmt"
"net/rpc"
)
func main() {
//rpc拨号连接
client, err := rpc.Dial("tcp", "localhost:12138")
ErrorHandler(err, "rpc.Dail")
//声明一个接收响应的数据
var reply string
err = client.Call("HelloService.Hello", "Fun", &reply)
ErrorHandler(err, "client.Call")
fmt.Println("请求rpc服务返回数据:", reply)
}
编译执行服务端和客户端生成可执行文件,先执行rpc服务端,在执行rpc客户端,即可发生简单的rpc通信。
2. net/rpc/jsonrpc
jsonrpc包实现了JSON-RPC的ClientCodec和ServerCodec接口,可用于rpc包。其只是将通信协议的消息结构转换成json传输。
编写服务端:jsonrpc/server.go
package main
import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"os"
)
func ErrorHandler(err error, where string) {
if err != nil {
fmt.Println("发现错误,", err, "出错位置:", where)
os.Exit(1)
}
}
type HelloService struct{}
func (hs *HelloService) Hello(request string, reply *string) error {
*reply = "Hello " + request
return nil
}
func main() {
fmt.Println("启动基于JSON通信的RPC服务...")
//注册RPC服务名
err := rpc.RegisterName("HelloService", new(HelloService))
ErrorHandler(err, "rpc.RegisterName")
//启动一个监听端口
listener, err := net.Listen("tcp", ":12138")
ErrorHandler(err, "net.Listen")
//每来一个连接开一个协程处理json通信数据
for {
conn, err := listener.Accept()
ErrorHandler(err, "listener.Accept")
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
编写客户端:jsonrpc/client.go
func main() {
//拨号RPC服务获取一个连接
conn, err := net.Dial("tcp", "localhost:12138")
ErrorHandler(err, "rpc.Dail")
//创建一个处理json通信数据的客户端
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
//使用客户端发送数据给服务端
var reply string
err = client.Call("HelloService.Hello", "Fuc", &reply)
ErrorHandler(err, "client.Call")
fmt.Println("请求rpc服务返回数据:", reply)
}
编译执行服务端和客户端生成可执行文件,先执行rpc服务端,在执行rpc客户端,即可发生简单的rpc通信。
三、gRPC与protobuf
1.gRPC概述
gRPC是Google开发的一套高性能RPC框架,默认使用protobuf协议,与现今流行的Thrift框架类型,它也有自己的一套IDL语法和编译引擎,执行定义IDL文件就可生成相关的协议结构和服务。
- 官方文档:https://grpc.io/docs
- 中文文档及教程:http://doc.oschina.net/grpc?t=60133
2.protobuf概述
Protobuf是Google开发的一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构的协议,可以用于网络通信和数据存储。
官方介绍:https://developers.google.cn/protocol-buffers/
与XML和JSON格式相比,protobuf更小、更快、更便捷。protobuf是跨语言的,自带一套IDL协议定义语法和一个编译器(protoc),安装完protoc后,只需要用protoc进行编译,就可以编译成Java、Python、C++、C#、Go等多种语言代码,项目中可以直接引用生成的协议包,不需要编写传输协议相关的代码。
protobuf还可根据RPC框架的不同自定义协议的结构,如gRPC只需在protoc编译.proto文件是附带参数:--go_out=plugins=grpc: 即可。
相关的protoc安装、IDL语法不便展开,在此直接演示
3.一个简单演示:
1.基于protobuf定义传输协议:
syntax = "proto3"; //设置语法版本
package proto; //声明包名:通常为文件所在目录名
//service 关键字定义开放调用的接口
service UserInfoService{
//rpc关键字定义该服务内开放调用的方法
rpc GetUserInfo (UserRequest) returns (UserResponse){}
}
//message关键字定义请求/响应的通信数据结构
//请求的信息结构体
message UserRequest {
//[修饰符] 类型 字段名=标识符,标识符可视为属性的顺序号
string name = 1;
}
//响应的信息结构体
message UserResponse {
int32 id = 1;
string name = 2;
int32 age = 3;
//repeated 表示字段是可变数组,即slice类型
repeated string title = 4;
}
2.生成proto依赖包:
Makefile
build:
protoc --go_out=plugins=grpc:./ ./proto/*.proto
#生成适用于gRPC的代码,在生成时设置其插件为grpc,
#如有其它rpc运行库,可针对其定制proto插件
#使用protoc命令:protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto
3.编写gRPC服务端:grpc_demo/server/mian.go
package main
import (
"fmt"
"golang.org/x/net/context"
"google.golang.org/grpc"
pb "grpc_demo/proto" //引入proto协议生成的依赖包,别名pb
"net"
"os"
)
func ErrorHandler(err error, where string) {
if err != nil {
fmt.Println("发现错误,", err, "出错位置:", where)
os.Exit(1)
}
}
//实现proto生成的服务接口
type UserInfoService struct{}
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
name := req.Name
//模拟在数据库查找数据得到结果
if name == "fun" {
resp = &pb.UserResponse{
Id: 100,
Name: "fun",
Age: 30,
Title: []string{"Teacher", "Coder"},
}
}
err = nil
return
}
//声明以上的服务,等待在主函数注册
var us = UserInfoService{}
//服务函数
func main() {
//开一个监听端口
listener, err := net.Listen("tcp", ":12138")
ErrorHandler(err, "net.Listen")
fmt.Println("开启grpc监听服务,端口:12138")
//创建一个grpc服务
server := grpc.NewServer()
//将上面实现UserInfoService的服务注册到grpc生成的代码
pb.RegisterUserInfoServiceServer(server, &us)
//启动服务监听
server.Serve(listener)
}
4.编写gRPC客户端:grpc_demo/client/mian.go
package main
import (
"fmt"
"golang.org/x/net/context"
"google.golang.org/grpc"
pb "grpc_demo/proto"
"os"
)
func ErrorHandler(err error, where string) {
if err != nil {
fmt.Println("发现错误,", err, "出错位置:", where)
os.Exit(1)
}
}
func main() {
//拨号连接
clientConn, err := grpc.Dial(":12138", grpc.WithInsecure())
ErrorHandler(err, "grpc.Dial")
defer clientConn.Close()
//实例化微服务客户端
userInfoServiceClient := pb.NewUserInfoServiceClient(clientConn)
//封装请求数据
request := new(pb.UserRequest)
request.Name = "fun"
//发起rpc请求
userResponse, err := userInfoServiceClient.GetUserInfo(context.Background(), request)
ErrorHandler(err, "client.GetUserInfo")
//打印请求数据
fmt.Println("请求数据为:", userResponse)
}
只需分别编译服务端和客户端生成可执行文件,分别启动即可执行简单的通信。