RPC和REST协议的区别,这些协议都是建立在TCP基础上的可靠传输协议。
传输协议 HTTP协议 规定传输方式:
Method:方法:GET PUT POST DELETE …
URL:地址
Data:具体数据或者二进制data
Header:
RPC 借用HTTP协议,携带RPC数据。
REST 在HTTP协议,根据HTTP框架设计的一种接口方式,用于CRUD
C:创建 POST xxx.xxx.com/user,内容放在Data中,Data以json的形式
R:获取 GET xxx.xxx.com/user/{id}
U:更新 PUT xxx.xxx.com/user/{id},更新内容放在Data中,以json的形式
D:删除 DELETE xxx.xxx.com/user/{id}
选择RPC或者REST需要根据服务工程化来决定,微服务下,服务之间通过RPC访问可以不用对外暴露接口,统一由RPC Gateway做反向代理转发。
RPC Remote Procedure Call
:远程过程调用。可以将远端的函数、方法当做本地的函数或方法使用,通过Protobuf
实现跨语言通信。
RPC中解决的问题:
Call的id映射(需要确定调用的远程函数具体是哪个函数)(在HTTP协议上,通过path代表Call ID)
序列化和反序列化(如何传输数据,使用rpc默认的序列化协议还是json
还是protobuf
)(在HTTP协议上,通过header头指定的序列化协议)
网络传输(通过tcp
还是http
传输数据,确保数据传输以及传输格式)(HTTP1协议默认短链接,过程中要经历三次握手和四次挥手)(gRPC框架基于http2.0,使用长连接流式传输,无论是网络连接还是序列化反序列化,性能更高)
RPC通信的四个部分:
服务端
服务端存根
客户端
客户端存根
类似于一个HTTP服务
1 2 3 4 5 6 7 8 9 10 11 http.HandleFunc("/hello" , func (writer http.ResponseWriter, request *http.Request) { _ = request.ParseForm() a := request.Form["a" ] b := request.Form["b" ] res := map [string ]string { "a + b" : a[0 ] + b[0 ], } writer.Header().Add("content-type" , "application/json" ) datas, _ := json.Marshal(res) writer.Write(datas) })
请求时,通过/hello
确定call ID,通过writer.Header().Add("content-type", "application/json")
确定序列化方式,通过HTTP网络传输,这也是一种RPC。
通过RPC框架进行封装,则可以将Call ID进行封装,序列化和反序列化的方式进行封装,通信方式也进行封装,例如RPC协议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type RPC struct {} func (r *RPC) Hello(req string , reply *string ) error { *reply = req + req return nil } func main () { err := rpc.RegisterName("hello" , &RPC{}) if err != nil { log.Fatal(err) } listener, _ := net.Listen("tcp" , ":8080" ) rpc.Accept(listener) }
客户端调用
1 2 3 dial, _ := rpc.Dial("tcp" , ":8080" ) var reply string err := dial.Call("hello.Hello" , "nihao" , &reply)
RPC
有标准的用法,步骤如下:
服务端注册带名称的服务,第一个入参是请求,第二个入参是指针,代表返回内容
服务端通过TCP
套接字层监听并且接收请求
客户端建立TCP
连接
客户端通过服务名称和两个参数调用连接
因此可以看到,rpc协议的序列化和反序列化,可以通过rpc自带的协议实现,也可以通过json实现,也可以通过http协议实现。
rpc协议的通信,可以继续tcp的socket变成实现,也可以通过http协议实现。具体例子可以看另外一篇文档:golang中的网络编程
Protobuf 全称是Protocl Buffers
,是一种数据描述语言,类似json
、xml
,可通过附带工具生成代码并实现将结构化数据序列化的功能。例如通过proto
文件生成golang
文件
1 2 3 4 5 6 7 8 9 10 syntax = "proto3" ; package gostudy; option go_package = "gostudy/proto/gen/go;userpb" ; message User { int64 id = 1 ; string name = 2 ; int64 duty_d = 3 ; }
通过protoc
生成go
代码
1 protoc --go_out=plugins=grpc:. hello.proto
生成之后的结构体
1 2 3 4 5 6 7 8 9 type User struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` DutyD int64 `protobuf:"varint,3,opt,name=duty_d,json=dutyD,proto3" json:"duty_d,omitempty"` }
通过proto
序列化和反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import ( "encoding/json" userpb "gostudy/proto/gen/go" ) func main () { user := userpb.User{ Id: 1 , Name: "mitaka" , DutyD: 5 , } fmt.Println(&user) bytes, err := proto.Marshal(&user) if err != nil { panic (err) } fmt.Printf("%X\n" , bytes) user1 := userpb.User{} err = proto.Unmarshal(bytes, &user1) if err != nil { panic (err) } fmt.Println(&user1) b, err := json.Marshal(&user) if err != nil { panic (err) } fmt.Printf("%s\n" , b) user2 := userpb.User{} err = json.Unmarshal(b, &user2) if err != nil { panic (err) } fmt.Println(&user2) }
复合类型和枚举
1 2 3 4 5 6 7 8 9 10 11 12 message User { int64 id = 1 ; Book now_book = 4 ; repeated Book carry_books =5 ; string name = 2 ; int64 duty_d = 3 ; } message Book { int64 id = 1 ; string name = 2 ; }
生成代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type Book struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` } type User struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` NowBook *Book `protobuf:"bytes,4,opt,name=now_book,json=nowBook,proto3" json:"now_book,omitempty"` CarryBooks []*Book `protobuf:"bytes,5,rep,name=carry_books,json=carryBooks,proto3" json:"carry_books,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` DutyD int64 `protobuf:"varint,3,opt,name=duty_d,json=dutyD,proto3" json:"duty_d,omitempty"` }
可以看到复合类型和枚举都是指针。
字段可选性
1 2 3 4 5 6 7 8 9 10 11 12 message User { ... BookStatus status = 6 ; } enum BookStatus { BS_NOT_SPECIFIED = 0 ; NOT_STARTED = 1 ; IN_PROCESS = 2 ; FINISHED = 3 ; PAID = 4 ; }
限定User
的status
,只能在这些状态。
结构体中所有的字段都是可选的,当这个字段不填,则是零值。如果要区分是零值还是没有填写,那么需要增加一个字段,is_xxx bool
,需要确保false
的含义,与老版本匹配。
gRPC 为了解决RPC协议中,对客户端存根和服务端存根的封装,而且可以实现跨语言,则需要通过一种框架实现,RPC框架中,使用的最为广泛的,就是gRPC框架。
gRPC
是基于ProtoBuf
开发的跨语言的开源RPC
框架。
客户端和服务端通信,中间经过网络,需要确定网络协议(tcp
、udp
),确定服务端地址(域名、ip和端口),服务路径(url
)服务参数(body
,query
,path
),数据类型(string
,int
),数据编码(json
、xml
),安全性(ssl
加密,token
),错误处理(error
response
)。使用HTTP
通信可以满足上述所有条件,使用gRPC
协议可以满足。gRPC
使用HTTP/2
协议,数据传输使用二进制,流式传输streaming,多路复用一个TCP
连接。
定义服务方法、请求参数、返回参数
1 2 3 4 5 6 7 8 9 10 11 12 message GetUserRequest { int64 id = 1 ; } message GetUserResponse { int64 id = 1 ; Book book = 2 ; } service UserService { rpc GetUser (GetUserRequest) returns (GetUserResponse) ; }
生成代码,通过plugins=grpc
指定plgins
1 protoc -I=. --go_out=plugins=grpc,paths=source_relative:gen/go user.proto
生成的方法
1 2 3 type UserServiceServer interface { GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error ) }
服务端实现interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Service struct {} func (*Service) GetUser(ctx context.Context, in *userpb.GetUserRequest) (*userpb.GetUserResponse, error ) { user := userpb.User{ Id: 1 , Name: "mitaka" , DutyD: 5 , } return &userpb.GetUserResponse{ Id: in.Id, User: &user, }, nil }
服务端开启socket监听
1 2 3 4 listen, err := net.Listen("tcp" , ":8081" ) s := grpc.NewServer() userpb.RegisterUserServiceServer(s, &userservice.Service{}) err = s.Serve(listen)
客户端连接
1 2 3 dial, err := grpc.Dial("localhost:8081" , grpc.WithInsecure()) userclient := userpb.NewUserServiceClient(dial) user, err := userclient.GetUser(context.Background(), &userpb.GetUserRequest{Id: 1 })
GRPC GATEWAY 为了对外兼容gRPC请求和HTTP请求,而且不需要写两份代码,维护两份逻辑,可以通过gRPC-gatewag实现一种代理功能,在gRPC协议外层代理一层HTTP协议。
通过grpc-gateway反向代理,服务器内部通信使用grpc,外部访问通过http
user.yaml
1 2 3 4 5 6 7 type: google.api.Service config_version: 3 http: rules: - selector: gostudy.UserService.GetUser get: /user/{id}
生成gateway文件
1 protoc -I=. --grpc-gateway_out=paths=source_relative,grpc_api_configuration=user.yaml:gen/go user.proto
生成的文件
1 2 3 4 5 /* Package userpb is a reverse proxy. // 反向代理 It translates gRPC into RESTful JSON APIs. // 将gRPC转换成RESTful风格的JSON APIs */
这个过程其实是内部调用grpc,并且启动一个http服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel()mux := runtime.NewServeMux() err = userpb.RegisterUserServiceHandlerFromEndpoint( ctx, mux, "0.0.0.0:8081" , []grpc.DialOption{grpc.WithInsecure()}) if err != nil { log.Fatalf("register handler error: %v" , err) } err = http.ListenAndServe(":8080" , mux) if err != nil { log.Fatalf("grpc gateway listen error: %v" , err) }
1 2 curl http://127.0.0.1:8080/user/1 {"id":"1", "user":{"id":"1", "nowBook":null, "carryBooks":[], "name":"mitaka", "dutyD":"5", "status":"BS_NOT_SPECIFIED"}}
从结果可以看到:
"dutyD":"5"
,int64转换成string,需要改成int32
status
的value不是指定的常量
1 2 3 4 5 6 7 8 9 mux := runtime.NewServeMux( runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{ MarshalOptions: protojson.MarshalOptions{ UseEnumNumbers: true , }, UnmarshalOptions: protojson.UnmarshalOptions{}, }, ), )
引申
grpc-gateway
grpc-gateway wiki