Microservices & Cloud Native: Communication Principles of gRPC Clients and Servers

gRPC Hello World

insert image description here
protoc is the core tool of Protobuf, used to write .protofiles and generate protobuf code. Here, take the Go language code as an example to write gRPC-related code.

go install google.golang.org/protobuf/cmd/[email protected]
go install google.golang.org/grpc/cmd/[email protected]
  • Define the directory structure and write files in the proto directory helloworld.proto:
    insert image description here
syntax="proto3";
package proto;

option go_package = "./;proto";

service Greeter {
    
    
    rpc SayHello (HelloRequest) returns (HelloReply) {
    
    }
}

message HelloRequest {
    
    
    string name = 1;
}

message HelloReply {
    
    
    string message = 1;
}

The run protoc -I . helloworld.proto --go_out=:. --go-grpc_out=require_unimplemented_servers=false:.command is used to generate code related to Protocol Buffers and gRPC services.
insert image description here

  • Write the server and client codes in the server directory and client directory respectively, start the sever and client, and the client can see the running results: Hello: Alex.
// server.go
package main

import (
    "context"
    "net"

    "google.golang.org/grpc"

    "your_moudle/proto"
)

type Server struct {
    
    }

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
    
    
    return &proto.HelloReply{
    
    Message: "Hello " + request.GetName()}, nil
}

func main() {
    
    
    lis, err := net.Listen("tcp", "0.0.0.0:8000")
    if err != nil {
    
    
        panic("failed to listen: " + err.Error())
    }

    g := grpc.NewServer()
    proto.RegisterGreeterServer(g, &Server{
    
    })

    err = g.Serve(lis)
    if err != nil {
    
    
        panic("failed to start grpc: " + err.Error())
    }
}

// client.go

package main

import (
    "context"
    "fmt"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    "your_moudle/proto"
)

func main() {
    
    
    conn, err := grpc.Dial("127.0.0.1:8000", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
    
    
        panic(err)
    }

    defer conn.Close()

    c := proto.NewGreeterClient(conn)
    r, err := c.SayHello(context.Background(), &proto.HelloRequest{
    
    Name: "Alex"})
    if err != nil {
    
    
        panic(err)
    }

    fmt.Println(r.Message)
}

As can be seen from the above example, a complete client-server communication process is as follows:
insert image description here

Server:

  • The server implements the interface defined in the proto file:
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
    
    
    return &proto.HelloReply{
    
    Message: "Hello " + request.GetName()}, nil
}
  • Listening TCP port:
lis, _ := net.Listen("tcp", "0.0.0.0:8000")
  • Register and start the service:
g := grpc.NewServer()
proto.RegisterGreeterServer(g, &Server{
    
    })
_ = g.Serve(lis)

client:

  • Establish a TCP connection:
conn, _ := grpc.Dial("127.0.0.1:8000", grpc.WithTransportCredentials(insecure.NewCredentials()))
  • Create Client:
c := proto.NewGreeterClient(conn)
  • Execute an RPC call and receive information:
r, _ := c.SayHello(context.Background(), &proto.HelloRequest{
    
    Name: "Alex"})

How to implement method calls inside gRPC

Server

NewServer()Initialize a gRPC server through the method for subsequent registration services and accepting requests

Create a gRPC server structure object with default values, initialize various parameter options describing the protocol, including message size sent and received, buffer size, etc., similar to http Headers.

func NewServer(opt ...ServerOption) *Server {
    
    
    // 描述协议的各种参数选项,包括发送和接收的消息大小、buffer大小等等各种,类似于 http Headers
    opts := defaultServerOptions
    for _, o := range globalServerOptions {
    
    
        o.apply(&opts)
    }
    for _, o := range opt {
    
    
        o.apply(&opts)
    }

    // 创建带默认值的 gRPC server 结构体对象
    s := &Server{
    
    
        lis:      make(map[net.Listener]bool),
        opts:     opts,
        conns:    make(map[string]map[transport.ServerTransport]bool),
        services: make(map[string]*serviceInfo),
        quit:     grpcsync.NewEvent(),
        done:     grpcsync.NewEvent(),
        czData:   new(channelzData),
    }

    // 配置拦截器
    chainUnaryServerInterceptors(s)
    chainStreamServerInterceptors(s)
    s.cv = sync.NewCond(&s.mu)

    // 配置简单的链路工具
    if EnableTracing {
    
    
        _, file, line, _ := runtime.Caller(1)
        s.events = trace.NewEventLog("grpc.Server", fmt.Sprintf("%s:%d", file, line))
    }

    if s.opts.numServerWorkers > 0 {
    
    
        s.initServerWorkers()
    }

    s.channelzID = channelz.RegisterServer(&channelzServer{
    
    s}, "")
    channelz.Info(logger, s.channelzID, "Server created")
    return s
}

Method RegisterServiceto achieve service registration

When using proto.RegisterGreeterServer(g, &Server{})to register a service, it implements service registration xxx_grpc.pb.gothrough the method in .RegisterService

func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
    
    
    // 第二个参数为我们自定义实现了相应接口的实现类。
    s.RegisterService(&Greeter_ServiceDesc, srv)
}
// Greeter_ServiceDesc 是 greter 服务的 grpc.ServiceDesc,不能被自省或修改(即使作为副本)。
var Greeter_ServiceDesc = grpc.ServiceDesc{
    
    
    // 声明了名称、路由、方法及其他元数据属性
    ServiceName: "proto.Greeter",
    HandlerType: (*GreeterServer)(nil),
    Methods: []grpc.MethodDesc{
    
    
        {
    
    
            MethodName: "SayHello",
            Handler:    _Greeter_SayHello_Handler,
        },
    },
    Streams:  []grpc.StreamDesc{
    
    },
    Metadata: "helloworld.proto",
}

RegisterServiceThe specific implementation of the method is as follows:

func (s *Server) RegisterService(sd *ServiceDesc, ss interface{
    
    }) {
    
    
    // 判断 ServiceServer 是否实现 ServiceDesc 中描述的 HandlerType,如果实现了则调用 s.register 方法注册
    if ss != nil {
    
    
        ht := reflect.TypeOf(sd.HandlerType).Elem()
        st := reflect.TypeOf(ss)
        if !st.Implements(ht) {
    
    
            logger.Fatalf("grpc: Server.RegisterService found the handler of type %v that does not satisfy %v", st, ht)
        }
    }
    s.register(sd, ss)
}

First make some pre-judgments, then register creates the corresponding map according to the Method in sd, and adds the name as the key and the method description (pointer) as the value to the corresponding map. Finally, according to the service name key, the serviceInfo information Inject into the services map of Server.

func (s *Server) register(sd *ServiceDesc, ss interface{
    
    }) {
    
    
    s.mu.Lock()
    defer s.mu.Unlock()
    // 一些前置性判断,注册服务必须要在 server() 方法之前调用
    s.printf("RegisterService(%q)", sd.ServiceName)
    if s.serve {
    
    
        logger.Fatalf("grpc: Server.RegisterService after Server.Serve for %q", sd.ServiceName)
    }
    if _, ok := s.services[sd.ServiceName]; ok {
    
    
        logger.Fatalf("grpc: Server.RegisterService found duplicate service registration for %q", sd.ServiceName)
    }

    // 在 Server 结构体中,services 字段存放的是 {service name -> service info} map
    // serviceInfo 中有两个重要的属性:methods 和 streams
    info := &serviceInfo{
    
    
        serviceImpl: ss,
        methods:     make(map[string]*MethodDesc),
        streams:     make(map[string]*StreamDesc),
        mdata:       sd.Metadata,
    }

    // register 根据 sd 中的 Method 创建对应的 map,并将名称作为键,方法描述(指针)作为值,添加到相应的 map 中
    for i := range sd.Methods {
    
    
        d := &sd.Methods[i]
        info.methods[d.MethodName] = d
    }
    for i := range sd.Streams {
    
    
        d := &sd.Streams[i]
        info.streams[d.StreamName] = d
    }

    // 按照服务名为 key,将 serviceInfo 信息注入到 Server 的 services map 中
    s.services[sd.ServiceName] = info
}

It can be seen from registerthe method that in fact, for different RPC requests, according to different serviceNames in services, different handlers are taken out from the services map for processing.

Serve()Start the service by

Realize monitoring on a certain port through an infinite loop, then the client initiates a connection request to this port, establishes a connection after the handshake is successful, and then the server processes the request data sent by the client, calls different handlers for processing according to the request parameters, and writes back response data.

func (s *Server) Serve(lis net.Listener) error {
    
    
    // ...
    for {
    
    
        rawConn, err := lis.Accept()
        // ...
        s.serveWG.Add(1)
        go func() {
    
    
            // gRPC 是基于 HTTP2.0 实现
            // handleRawConn 实现了 http 的 handshake
            s.handleRawConn(lis.Addr().String(), rawConn)
            s.serveWG.Done()
        }()
    }
    //...
}
func (s *Server) handleRawConn(lisAddr string, rawConn net.Conn) {
    
    
    // 。。。
    // Finish handshaking (HTTP2)
    st := s.newHTTP2Transport(rawConn)
    rawConn.SetDeadline(time.Time{
    
    })
    // 。。。
    go func() {
    
    
        // 继续调用 serveStreams 方法
        s.serveStreams(st)
        s.removeConn(lisAddr, st)
    }()
}

func (s *Server) serveStreams(st transport.ServerTransport) {
    
    
    // 。。。
    st.HandleStreams(func(stream *transport.Stream) {
    
    
        // 。。。
        go func() {
    
    
            defer wg.Done()
            // 根据 serviceName 取 server 中的 services map
            s.handleStream(st, stream, s.traceInfo(st, stream))
        }()
    }, func(ctx context.Context, method string) context.Context {
    
    
        // 。。。
    })
    wg.Wait()
}

func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Stream, trInfo *traceInfo) {
    
    
    // ...
    service := sm[:pos]
    method := sm[pos+1:]
    // ...
    // 取出 handler 进行处理
    srv, knownService := s.services[service]
    if knownService {
    
    
        if md, ok := srv.methods[method]; ok {
    
    
            // 在该方法中实现 handler 对 rpc 的处理,以及处理后的 response 
            s.processUnaryRPC(t, stream, srv, md, trInfo)
            return
        }
        if sd, ok := srv.streams[method]; ok {
    
    
            s.processStreamingRPC(t, stream, srv, sd, trInfo)
            return
        }
    }
    // ...
}

client

Dial()Establish a connection by dialing

func Dial(target string, opts ...DialOption) (*ClientConn, error) {
    
    
    return DialContext(context.Background(), target, opts...)
}

The call DialContext()method mainly returns an initialized ClientConn{}structure object:

// 压缩解压缩、是否需要认证、超时时间、是否重试等信息
cc := &ClientConn{
    
    
    target: target,
    // 连接的状态管理器,每个连接具有 "IDLE"、"CONNECTING"、"READY"、"TRANSIENT_FAILURE"、"SHUTDOW N"、"Invalid-State" 这几种状态
    csMgr:  &connectivityStateManager{
    
    },
    conns:  make(map[*addrConn]struct{
    
    }),
    dopts:  defaultDialOptions(),
    // 监测 server 和 channel 的状态
    czData: new(channelzData),
    // 。。。
}

NewGreeterClientCreate a client by

proto.NewGreeterClient(conn)Create a Client object by creating a client xxx_grpc.pb.goinNewGreeterClient

func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
    
    
    return &greeterClient{
    
    cc}
}

Call InvokeInitiate an RPC call

Execute c.SayHello(context.Background(), &proto.HelloRequest{Name: "Alex"})the call and receive information, and the method initiates an RPC call SayHelloby callingInvoke

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
    
    
    out := new(HelloReply)

    // Invoke 中调用 invoke
    err := c.cc.Invoke(ctx, "/proto.Greeter/SayHello", in, out, opts...)
    if err != nil {
    
    
        return nil, err
    }
    return out, nil
}

func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{
    
    }, opts ...CallOption) error {
    
    
    // 。。。
    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 := cs.SendMsg(req); err != nil {
    
    
        return err
    }
    return cs.RecvMsg(reply)
}

cs.SendMsg(req)method, first prepare the data:

hdr, payload, data, err := prepareMsg(m, cs.codec, cs.cp, cs.comp)

Then call the method csAttemptin this structure sendMsg:

op := func(a *csAttempt) error {
    
    
    // 这个 sendMsg 方法中通过 a.t.Write(a.s, hdr, payld, &transport.Options{Last: !cs.desc.ClientStreams}) 发出的数据写操作
    return a.sendMsg(m, hdr, payload, data)
}

cs.RecvMsg(reply)csAttemptmethod called in method recvMsg:

func (cs *clientStream) RecvMsg(m interface{
    
    }) error {
    
    
    // 。。。
    err := cs.withRetry(func(a *csAttempt) error {
    
    
        return a.recvMsg(m, recvInfo)
    }, cs.commitAttemptLocked)
    // 。。。
}

Through layer-by-layer calls, the data is finally read through a pr of type io.Reader.

func (a *csAttempt) recvMsg(m interface{
    
    }, payInfo *payloadInfo) (err error) {
    
    
    // ...
    err = recv(a.p, cs.codec, a.s, a.dc, m, *cs.callInfo.maxReceiveMessageSize, payInfo, a.decomp)
    // ...
}

func recv(p *parser, c baseCodec, s *transport.Stream, dc Decompressor, m interface{
    
    }, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor) error {
    
    
    // 通过 recvAndDecompress 方法,接收数据并解压缩
    d, err := recvAndDecompress(p, s, dc, maxReceiveMessageSize, payInfo, compressor)
    // 。。。
}

func recvAndDecompress(p *parser, s *transport.Stream, dc Decompressor, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor) ([]byte, error) {
    
    
    pf, d, err := p.recvMsg(maxReceiveMessageSize)
    // 。。。
}

func (p *parser) recvMsg(maxReceiveMessageSize int) (pf payloadFormat, msg []byte, err error) {
    
    
    if _, err := p.r.Read(p.header[:]); err != nil {
    
    
        return 0, nil, err
    }
    // 。。。
}

Proxy mode

Definition: Provide a proxy object, and the proxy object controls the reference to the original object.

There are three types of roles in proxy mode:

  • Abstract role Subject: Declare business methods through interfaces or abstract classes, which need proxy roles and real roles to implement.
  • Proxy role Proxy: exposed to the client, is the proxy of the real role, implements the abstract method through the business logic method of the real role, and can add additional methods.
  • Real role RealSubject: Realize the business logic of the real role, which can be invoked by the proxy role.
    insert image description here

As shown in the figure above, the Client initiates a request through toRequest. Abstract role Subject as an interface, which has a Request method. There are two implementation classes below, RealSubject is the actual implementation class, and Proxy is a proxy class, which directly calls the request method of RealSubject. Of course, in Proxy, it can add its own preRequest and afterRequest processing logic.

Without proxy mode, Client will directly request RealSubject. In the proxy mode, the Proxy directly called by the Client is processed by the Proxy first, and then the RealSubject is called by the Proxy, and finally returned to the Client. During the whole process, the Client only sees the Proxy, and for it, the Proxy is the real Subject implementation class. RealSubject used to serve many clients, but now it only needs to be exposed to Proxy, and its risk is much smaller, because it is enough to trust Proxy.

To sum up, the proxy mode has the effect of intermediary isolation, avoiding direct exposure of RealSubject, and it also conforms to the principle of opening and closing, which is open to extension and closed to modification. Proxy is an extension, and new requirements can be implemented in Proxy, thereby reducing the modification of RealSubject. In this way, RealSubject can focus more on its core capabilities, and put some marginal and frequently changing scalability requirements in Proxy to achieve.

As a supplement, common gRPC Proxy principles:

  • Start a gRPC proxy server;
  • Intercept the gRPC service and forward it to a function of the agent for execution;
  • Receive the client's request, complete the relevant processing and forward it to the server;
  • Receive the response from the server and forward it to the client after completing relevant processing;

Guess you like

Origin blog.csdn.net/by6671715/article/details/131690041