RPC原理以及GRPC详解
一、RPC原理
1、RPC框架由来
单体应用体量越来越大,代码不好维护和管理,所以就产生了微服务架构,按照公共或功能模块拆分为一个个独立的服务,然后各独立的服务之间可以相互调用。
微服务之间相互调用,该如何实现?
首先要解决下面5个问题:
1、如何规定远程调用的语法?
2、如何传递参数?
3、如何表示数据?
4、如何知道一个服务端都实现了哪些远程调用?从哪个端口可以访问这个远程调用?
5、发生了错误、重传、丢包、性能等问题怎么办?
大家可能都写过socket或则http通信,简单的client访问server的模式,认为通过这个就可以解决服务之间的相互调用了,但是考虑下上面5个问题,处理起来就不是那么容易的事情了,非个人可以完成的工作。
于是就诞生了RPC框架,让我们不用管底层实现,简单好用:
2、RPC框架原理
当客户端的应用想发起一个远程调用时,它实际是调用客户端的 Stub。它负责将调用的接口、方法和参数,通过约定的协议规范进行编码,并通过本地的 RPCRuntime 进行传输,将调用网络包发送到服务器。服务器端的 RPCRuntime 收到请求后,交给服务器端的 Stub 进行解码,然后调用服务端的方法,服务端执行方法,返回结果,服务器端的 Stub 将返回结果编码后,发送给客户端,客户端的 RPCRuntime 收到结果,发给客户端的 Stub 解码得到结果,返回给客户端。
1、对于客户端而言,这些过程是透明的,就像本地调用一样;对于服务端而言,专注于业务逻辑的处理就可以了。
2、对于 Stub 层,处理双方约定好的语法、语义、封装、解封装。
3、对于 RPCRuntime,主要处理高性能的传输,以及网络的错误和异常。
来看一下RPC框架是如何解决上面5个问题的:1、2、3的问题可以由Stub层解决,4的问题可以由服务注册和发布解决,5的问题可以由RPCRuntime解决。
个人理解Stub层的语法、语义约定现在常用的都是协议文件方式,流行的有xml、json、protocol buffers等等,它们各自都带有自己特有的封装解封装方法,封装解封装主要解决两个问题:
1、压缩数据,减少传输数据量;
2、数据结构中字段的映射关系;
二、GRPC原理
gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持。
本文以GO语言版本讲解
1、Golang安装GRPC
详细安装连接
2、protocol buffer原理文章
文章1
文章2
3、grpc-go github地址:
github仓库
godoc文档
grpc-go 的Stub层协议约定问题通过.proto文件约定好服务接口、参数等,通过工具protoc-gen-go生成客户端和服务端共用的对照表,想生成什么语言文件就用相应的插件,这样就实现了跨语言。
生成GO语言文件使用命令如下:
protoc --go_out=plugins=grpc:. *.proto
gRPC RPCRuntime层基于HTTP/2设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特性。
GRPC Server端启动
1、整体启动过程
func main() {
//解析运行参数
flag.Parse()
//配置监听协议、地址、端口
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
//grpc额外的服务配置,这里主要是需不需要加密
var opts []grpc.ServerOption
if *tls {
if *certFile == "" {
*certFile = testdata.Path("server1.pem")
}
if *keyFile == "" {
*keyFile = testdata.Path("server1.key")
}
creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)
if err != nil {
log.Fatalf("Failed to generate credentials %v", err)
}
opts = []grpc.ServerOption{grpc.Creds(creds)}
}
//grpc服务初始化,绑定一些配置参数
grpcServer := grpc.NewServer(opts...)
//把.proto文件中定义的接口API实现注册到grpc服务上,方便调用
pb.RegisterRouteGuideServer(grpcServer, newServer())
//grpc服务启动,开始监听
grpcServer.Serve(lis)
}
2、Serve函数
关键处理就是一个for循环。如果Accept() 返回错误,并且错误是临时性的,那么会有重试,重试时间以5ms翻倍增长,直到1s。
for {
rawConn, err := lis.Accept()
//错误处理
if err != nil {
if ne, ok := err.(interface {
Temporary() bool
}); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.mu.Lock()
s.printf("Accept error: %v; retrying in %v", err, tempDelay)
s.mu.Unlock()
timer := time.NewTimer(tempDelay)
select {
case <-timer.C:
case <-s.quit.Done():
timer.Stop()
return nil
}
continue
}
s.mu.Lock()
s.printf("done serving; Accept = %v", err)
s.mu.Unlock()
if s.quit.HasFired() {
return nil
}
return err
}
tempDelay = 0
// Start a new goroutine to deal with rawConn so we don't stall this Accept
// loop goroutine.
//
// Make sure we account for the goroutine so GracefulStop doesn't nil out
// s.conns before this conn can be added.
s.serveWG.Add(1)
//重新启动一个goroutine处理accept的连接
go func() {
s.handleRawConn(rawConn)
s.serveWG.Done()
}()
}
3、handleRawConn函数
主要作用就是获取一个服务端的Transport,并开一个goroutine等待处理stream,里面会涉及到调用注册的方法。
st := s.newHTTP2Transport(conn, authInfo)
if st == nil {
return
}
rawConn.SetDeadline(time.Time{})
if !s.addConn(st) {
return
}
go func() {
s.serveStreams(st)
s.removeConn(st)
}()
GRPC Client端启动
1、建立连接和绑定实现的接口
//解析运行参数
flag.Parse()
//连接的一些配置,主要是加密,安全、阻塞
var opts []grpc.DialOption
if *tls {
if *caFile == "" {
*caFile = testdata.Path("ca.pem")
}
creds, err := credentials.NewClientTLSFromFile(*caFile, *serverHostOverride)
if err != nil {
log.Fatalf("Failed to create TLS credentials %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithInsecure())
}
opts = append(opts, grpc.WithBlock())
//建立一个连接
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()
//创建一个实现了.proto文件定义的接口API的Client
client := pb.NewRouteGuideClient(conn)
2、Client调用方式
Unary RPC: 一元RPC
func (c *routeGuideClient) GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) {
out := new(Feature)
err := c.cc.Invoke(ctx, "/routeguide.RouteGuide/GetFeature", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// printFeature gets the feature for the given point.
func printFeature(client pb.RouteGuideClient, point *pb.Point) {
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
feature, err := client.GetFeature(ctx, point)
if err != nil {
log.Fatalf("%v.GetFeatures(_) = _, %v: ", client, err)
}
log.Println(feature)
}
// GetFeature returns the feature at the given point.
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
return feature, nil
}
}
// No feature was found, return an unnamed feature
return &pb.Feature{Location: point}, nil
}
Server-Side streaming RPC: 服务端流式RPC
func (c *routeGuideClient) ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error) {
stream, err := c.cc.NewStream(ctx, &_RouteGuide_serviceDesc.Streams[0], "/routeguide.RouteGuide/ListFeatures", opts...)
if err != nil {
return nil, err
}
x := &routeGuideListFeaturesClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// printFeatures lists all the features within the given bounding Rectangle.
func printFeatures(client pb.RouteGuideClient, rect *pb.Rectangle) {
log.Printf("Looking for features within %v", rect)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := client.ListFeatures(ctx, rect)
if err != nil {
log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
}
for {
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
}
log.Println(feature)
}
}
// ListFeatures lists all features contained within the given bounding Rectangle.
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
if inRange(feature.Location, rect) {
if err := stream.Send(feature); err != nil {
return err
}
}
}
return nil
}
Client-Side streaming RPC: 客户端流式RPC
func (c *routeGuideClient) RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error) {
stream, err := c.cc.NewStream(ctx, &_RouteGuide_serviceDesc.Streams[1], "/routeguide.RouteGuide/RecordRoute", opts...)
if err != nil {
return nil, err
}
x := &routeGuideRecordRouteClient{stream}
return x, nil
}
// runRecordRoute sends a sequence of points to server and expects to get a RouteSummary from server.
func runRecordRoute(client pb.RouteGuideClient) {
// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := client.RecordRoute(ctx)
if err != nil {
log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
if err := stream.Send(point); err != nil {
log.Fatalf("%v.Send(%v) = %v", stream, point, err)
}
}
reply, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil)
}
log.Printf("Route summary: %v", reply)
}
// RecordRoute records a route composited of a sequence of points.
//
// It gets a stream of points, and responds with statistics about the "trip":
// number of points, number of known features visited, total distance traveled, and
// total time spent.
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
var pointCount, featureCount, distance int32
var lastPoint *pb.Point
startTime := time.Now()
for {
point, err := stream.Recv()
if err == io.EOF {
endTime := time.Now()
return stream.SendAndClose(&pb.RouteSummary{
PointCount: pointCount,
FeatureCount: featureCount,
Distance: distance,
ElapsedTime: int32(endTime.Sub(startTime).Seconds()),
})
}
if err != nil {
return err
}
pointCount++
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
featureCount++
}
}
if lastPoint != nil {
distance += calcDistance(lastPoint, point)
}
lastPoint = point
}
}
Bidirectional streaming RPC : 双向流式RPC
func (c *routeGuideClient) RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error) {
stream, err := c.cc.NewStream(ctx, &_RouteGuide_serviceDesc.Streams[2], "/routeguide.RouteGuide/RouteChat", opts...)
if err != nil {
return nil, err
}
x := &routeGuideRouteChatClient{stream}
return x, nil
}
// runRouteChat receives a sequence of route notes, while sending notes for various locations.
func runRouteChat(client pb.RouteGuideClient) {
notes := []*pb.RouteNote{
{Location: &pb.Point{Latitude: 0, Longitude: 1}, Message: "First message"},
{Location: &pb.Point{Latitude: 0, Longitude: 2}, Message: "Second message"},
{Location: &pb.Point{Latitude: 0, Longitude: 3}, Message: "Third message"},
{Location: &pb.Point{Latitude: 0, Longitude: 1}, Message: "Fourth message"},
{Location: &pb.Point{Latitude: 0, Longitude: 2}, Message: "Fifth message"},
{Location: &pb.Point{Latitude: 0, Longitude: 3}, Message: "Sixth message"},
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := client.RouteChat(ctx)
if err != nil {
log.Fatalf("%v.RouteChat(_) = _, %v", client, err)
}
waitc := make(chan struct{})
go func() {
for {
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
log.Fatalf("Failed to receive a note : %v", err)
}
log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
}
}()
for _, note := range notes {
if err := stream.Send(note); err != nil {
log.Fatalf("Failed to send a note: %v", err)
}
}
stream.CloseSend()
<-waitc
}
// RouteChat receives a stream of message/location pairs, and responds with a stream of all
// previous messages at each of those locations.
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
s.mu.Lock()
s.routeNotes[key] = append(s.routeNotes[key], in)
// Note: this copy prevents blocking other clients while serving this one.
// We don't need to do a deep copy, because elements in the slice are
// insert-only and never modified.
rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
copy(rn, s.routeNotes[key])
s.mu.Unlock()
for _, note := range rn {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
Client 连接底层两个主要方法
1、Invoke函数
newClientStream:获取传输层 Trasport 并组合封装到 ClientStream 中返回,在这块会涉及负载均衡、超时控制、 Encoding、 Stream 的动作,与服务端基本一致的行为。
cs.SendMsg:发送 RPC 请求出去,但其并不承担等待响应的功能。
cs.RecvMsg:阻塞等待接受到的 RPC 方法响应结果。
// Invoke sends the RPC request on the wire and returns after response is
// received. This is typically called by generated code.
//
// All errors returned by Invoke are compatible with the status package.
func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error {
// allow interceptor to see all applicable call options, which means those
// configured as defaults from dial option as well as per-call options
opts = combine(cc.dopts.callOptions, opts)
if cc.dopts.unaryInt != nil {
return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
}
return invoke(ctx, method, args, reply, cc, opts...)
}
func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error {
cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)
if err != nil {
return err
}
if err := cs.SendMsg(req); err != nil {
return err
}
return cs.RecvMsg(reply)
}
2、NewStream函数
// NewStream creates a new Stream for the client side. This is typically
// called by generated code. ctx is used for the lifetime of the stream.
//
// To ensure resources are not leaked due to the stream returned, one of the following
// actions must be performed:
//
// 1. Call Close on the ClientConn.
// 2. Cancel the context provided.
// 3. Call RecvMsg until a non-nil error is returned. A protobuf-generated
// client-streaming RPC, for instance, might use the helper function
// CloseAndRecv (note that CloseSend does not Recv, therefore is not
// guaranteed to release all resources).
// 4. Receive a non-nil, non-io.EOF error from Header or SendMsg.
//
// If none of the above happen, a goroutine and a context will be leaked, and grpc
// will not call the optionally-configured stats handler with a stats.End message.
func (cc *ClientConn) NewStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error) {
// allow interceptor to see all applicable call options, which means those
// configured as defaults from dial option as well as per-call options
opts = combine(cc.dopts.callOptions, opts)
if cc.dopts.streamInt != nil {
return cc.dopts.streamInt(ctx, desc, cc, method, newClientStream, opts...)
}
return newClientStream(ctx, desc, cc, method, opts...)
}
总结
gRPC的优点和缺点:
优点:
1、protobuf二进制消息,性能好/效率高(空间和时间效率都很不错);
2、proto文件生成目标代码,简单易用;
3、序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式);
4、支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级;
5、支持多种语言(可以把proto文件看做IDL文件);
6、Netty等一些框架集成;
缺点:
1、GRPC尚未提供连接池,需要自行实现;
2、尚未提供“服务发现”、“负载均衡”机制;
3、因为基于HTTP2,绝大部多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx1.9版本已支持);
4、Protobuf二进制可读性差(貌似提供了Text_Fromat功能,没用过);
5、默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持);