├── README.md ├── 1-grpc concepts & http2.md ├── 2-grpc hello world.md ├── 11-grpc 协议解包过程全剖析.md ├── 10-grpc 协议编解码器.md ├── 7-grpc 认证鉴权——tls认证.md ├── 12-grpc 数据流转.md ├── 9-grpc 拦截器实现.md ├── 3-grpc hello world server 解析.md ├── 8-grpc 认证鉴权——oauth2认证.md ├── 5-grpc 服务发现.md ├── 6-grpc 负载均衡.md └── 4-grpc hello world client 解析.md /README.md: -------------------------------------------------------------------------------- 1 | # grpc_read 2 | grpc 源码解读 3 | 4 | > 本文原发表于:https://diu.life/lessons/grpc-read/ 5 | 6 | > 最新版本请访问原文链接 7 | -------------------------------------------------------------------------------- /1-grpc concepts & http2.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-concepts-http2/ 2 | > 最新版本请访问原文链接 3 | 4 | ### 写在前面 5 | 6 | #### grpc 介绍 7 | 8 | grpc 是 google 开源的一款高性能的 rpc 框架。github 上介绍如下: 9 | 10 | gRPC is a modern, open source, high-performance remote procedure call (RPC) framework that can run anywhere 11 | 12 | 市面上的 rpc 框架数不胜数,包括 alibaba dubbo 和微博的 motan 等。grpc 能够在众多的框架中脱颖而出是跟其高性能是密切相关的。 13 | 14 | #### CONCEPTS 15 | 16 | 阅读 grpc 源码之前,我们不妨先了解一些 concepts,github 上也有一些 concepts 介绍 17 | 18 | https://github.com/grpc/grpc/blob/master/CONCEPTS.md 19 | 20 | ##### 1.接口设计 21 | 22 | Developers using gRPC start with a language agnostic description of an RPC service (a collection of methods). From this description, gRPC will generate client and server side interfaces in any of the supported languages. The server implements the service interface, which can be remotely invoked by the client interface. 23 | 24 | By default, gRPC uses Protocol Buffers as the Interface Definition Language (IDL) for describing both the service interface and the structure of the payload messages. It is possible to use other alternatives if desired. 25 | 26 | 对一个远程服务 service 的调用,grpc 约定 client 和 server 首先需要约定好 service 的结构。包括一系列方法的组合,每个方法定义、参数、返回体等。对这个结构的描述,grpc 默认是用 protocol buffer 去实现的。 27 | 28 | ##### 2. Streaming 29 | 30 | streaming 在 http/1.x 已经出现了,http2 实现了 streaming 的多路复用。grpc 是基于 http2 实现的。所以 grpc 也实现了 streaming 的多路复用,所以 grpc 的请求有四种模式:Simple RPC、Client-side streaming RPC、Server-side streaming RPC、Bidirectional streaming RPC 。也就是说同时支持单边流和双向流 31 | 32 | ##### 3. Protocol 33 | 34 | grpc 的协议层是基于 http2 设计的,所以你如果想了解 grpc 的话,可以先深入了解 http2 35 | 36 | ##### 4. Flow Control 37 | 38 | grpc 的协议支持流量控制,这里也是采用了 http2 的 flow control 机制。 39 | 40 | 41 | 通过上面的介绍可以看到,grpc 的高性能很大程度上依赖了 http2 的能力,所以要了解 grpc 之前,我们需要先了解一下 http 2 的特性。 42 | 43 | 44 | 45 | #### http2 特性 46 | 47 | 48 | 1. 二进制协议 49 | 50 | 众所周知,二进制协议比文本形式的协议,发送的数据量肯定是更小,传输效率更高的。所以 http2 比 http/1.x 更高效,因为二进制是不可读的,所以会损失部分可读性。 51 | 52 | 2. 多路复用的流 53 | 54 | http/1.x 一个 stream 是需要一个 tcp 连接的,其实从性能上来说是比较浪费的。http2 可以复用 tcp 连接,实现一个 tcp 连接可以处理多个 stream,同时可以为每一个 stream 设置优先级,可以用来告诉对端哪个流更重要。当资源有限的时候,服务器会根据优先级来选择应该先发送哪些流 55 | 56 | 3. 头部压缩 57 | 58 | 由于 http 协议是一个无状态的协议,导致了很多相同或者类似的 http 请求重复发送时,带的头部信息很多时候是完全一样的。http2 对头部进行压缩,可以减少数据包大小,提高协议性能 59 | 60 | 4. 请求 reset 61 | 62 | 在 http/1.x 中,当一个含有确切值的 Content-Length 的 http 消息发出之后,需要断开 tcp 连接才能中断。这样会导致需要通过三次握手来重新建立一个新的 tcp 连接,代价是比较大的。在 http2 里面,我们可以通过发送 RST_STREAM 帧来终止当前消息发送,从而避免了浪费带宽和中断已有的连接。 63 | 64 | 5. 服务器推送 65 | 66 | 如果一个 client 请求资源 A,而 server 知道 client 可能也会需要资源 B, 所以在 client 发起请求前,server 提前将 B 推送给 A 缓存起来,从而可以缩短资源 A 这个请求的响应时间。 67 | 68 | 6. flow control 69 | 70 | 在 http2 中,每个 http stream 都有自己公示的流量窗口,对于每个 stream 来说,client 和 server 都必须互相告诉对方自己能够处理的窗口大小,stream 中的数据帧大小不得超过能处理的窗口值。 71 | 72 | 73 | 74 | 这是题主简单了解,并总结了一下,对 http2,网上有很多文章介绍的非常详细,大家可以自行深入研究下。 75 | -------------------------------------------------------------------------------- /2-grpc hello world.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-hello-world/ 2 | > 最新版本请访问原文链接 3 | 4 | ### grpc quick start 5 | 6 | 我们分析 go 版本的 grpc 实现,所以这里主要讲解 grpc-go 的安装和使用 7 | 8 | #### 1、安装 9 | 10 | go 语言版本的 grpc 安装需要 1.6 以上的 go 版本,所以你需要先执行 go version 查看 go 版本,假如版本低于 1.6 则需要先升级。 11 | 12 | 官网上给的安装方式是 13 | 14 | go get -u google.golang.org/grpc 15 | 16 | 因为 google.golang.org 这个域名在国内会被墙,所以假如可以翻墙的计算机可以用这种方式,不能翻墙的可以通过以下方式安装 17 | 18 | git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc 19 | git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net 20 | git clone https://github.com/golang/text.git $GOPATH/src/golang.org/x/text 21 | go get -u github.com/golang/protobuf/{proto,protoc-gen-go} 22 | git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto 23 | 24 | cd $GOPATH/src/ 25 | go install google.golang.org/grpc 26 | 27 | 假如安装不成功则说明你 GOPATH 没有配置,可以先配置 GOPATH 后再执行 28 | 29 | // 配置 GOPATH 30 | export GOPATH=/Users/delvin/go 31 | 32 | #### 2、hello world 33 | 34 | grpc-go 官方提供了一些 examples ,都放在 examples 目录下,examples 目录下有三个目录,features 目录主要是 grpc 的一些特写使用,包括路由寻址、keep-alive、负载均衡等。helloworld 目录下主要是提供了一个 helloworld demo。route_guide 目录主要提供了对 grpc 四种调用方式:Simple RPC、Client-side streaming RPC、Server-side streaming RPC、Bidirectional streaming RPC 的模拟调用 demo。 35 | 36 | 我们通过 grpc 提供的 helloworld demo 来简单了解下如何使用 grpc 。 37 | 38 | 之前我们说到了 grpc 是通过 protocol buffer 来实现接口的定义的。helloworld 包下已经给我们定义好了一个 helloworld.proto 如下: 39 | 40 | syntax = "proto3"; 41 | 42 | option java_multiple_files = true; 43 | option java_package = "io.grpc.examples.helloworld"; 44 | option java_outer_classname = "HelloWorldProto"; 45 | 46 | package helloworld; 47 | 48 | // The greeting service definition. 49 | service Greeter { 50 | // Sends a greeting 51 | rpc SayHello (HelloRequest) returns (HelloReply) {} 52 | } 53 | 54 | // The request message containing the user's name. 55 | message HelloRequest { 56 | string name = 1; 57 | } 58 | 59 | // The response message containing the greetings 60 | message HelloReply { 61 | string message = 1; 62 | } 63 | 64 | 可以看到 SayHello 这个结构体里面已经定义好了一个 rpc Service 调用, 65 | 66 | rpc SayHello (HelloRequest) returns (HelloReply) {} 67 | 68 | 69 | 在 Service 中包含一个方法,SayHello, 支持传入一个 HelloRequest 的参数,返回一个 HelloReply 的响应。这两个结构体分别也在 proto 文件里面定义了。 70 | 71 | 通过 protoc 可以生成一个 pb.go 文件(这里 demo )里面已经生成好了。然后编写一个 client 和 server 即可实现一个完整的 rpc 调用链路,这里 demo 里面也提供了,我们先直接运行一下: 72 | 73 | cd $GOPATH/src/google.golang.org/grpc/examples/helloworld 74 | 75 | 先在一个 terminal 下执行: 76 | 77 | go run greeter_server/main.go 78 | 79 | 然后在另一个 terminal 下执行: 80 | 81 | go run greeter_client/main.go 82 | 83 | 此时 client 会输出 Greeting: Helloworld 84 | 85 | 2019/08/03 15:57:46 Greeting: Helloworld 86 | 87 | 88 | #### 3、自定义一个 service 89 | 90 | 这里也是 grpc docs 上一个相同的例子,我贴上来介绍一下,为了让本节内容更完整。 91 | 92 | (1)在 pb 文件里面添加一个 rpc 方法 93 | 94 | // The greeting service definition. 95 | service Greeter { 96 | // Sends a greeting 97 | rpc SayHello (HelloRequest) returns (HelloReply) {} 98 | // Sends another greeting 99 | rpc SayHelloAgain (HelloRequest) returns (HelloReply) {} 100 | } 101 | 102 | // The request message containing the user's name. 103 | message HelloRequest { 104 | string name = 1; 105 | } 106 | 107 | // The response message containing the greetings 108 | message HelloReply { 109 | string message = 1; 110 | } 111 | 112 | (2)生成 pb.go 文件 113 | 114 | protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld 115 | 116 | (3)在 greeter_server/main.go 下添加一个方法,入参和返回值和 SayHello 完全相同,使用已经定义过的结构体 HelloRequest 和 HelloReply 117 | 118 | func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { 119 | return &pb.HelloReply{Message: "Hello again " + in.Name}, nil 120 | } 121 | 122 | (4)在 greeter_client/main.go 下添加一个方法调用,调用 SayHelloAgain 这个方法 123 | 124 | r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name}) 125 | if err != nil { 126 | log.Fatalf("could not greet: %v", err) 127 | } 128 | log.Printf("Greeting: %s", r.Message) 129 | 130 | (5)执行 131 | 132 | 先在一个 terminal 下执行: 133 | 134 | go run greeter_server/main.go 135 | 136 | 然后在另一个 terminal 下执行: 137 | 138 | go run greeter_client/main.go 139 | 140 | 可以看到 client 的 terminal 输出如下: 141 | 142 | DEVLINTANG-MB0:helloworld delvin$ go run greeter_client/main.go 143 | 2019/08/03 16:18:24 Greeting: Hello world 144 | 2019/08/03 16:18:24 Greeting: Hello again world 145 | 146 | 这篇内容主要介绍了 grpc 的安装以及基本使用。分析了通过 grpc 输出 hello world 的基本要素和过程。 147 | -------------------------------------------------------------------------------- /11-grpc 协议解包过程全剖析.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-protocol-unpacking-analysis/ 2 | > 最新版本请访问原文链接 3 | 4 | ### http2 协议帧格式 5 | 6 | 我们知道网络传输都是以二进制的形式,所以所有的协议底层的传输也是二进制。那么问题来了,client 往 server 发一个数据包,server 如何知道数据包是完成还是还在发送呢?又或者,假如一个数据包过大,client 需要拆成几个包发送,或者数据包过小,client 需要合成一个包发送,server 如何识别呢?为了解决这些问题,client 和 server 都会约定好一些双方都能理解的“规则“,这就是协议。 7 | 8 | 我们知道 grpc 传输层是基于 http2 协议规范,所以我们先要了解 http2 协议帧的格式。http 2 协议帧格式如下: 9 | 10 | ```http2 11 | Frame Format 12 | All frames begin with a fixed 9-octet header followed by a variable-length payload. 13 | 14 | +-----------------------------------------------+ 15 | | Length (24) | 16 | +---------------+---------------+---------------+ 17 | | Type (8) | Flags (8) | 18 | +-+-------------+---------------+-------------------------------+ 19 | |R| Stream Identifier (31) | 20 | +=+=============================================================+ 21 | | Frame Payload (0...) ... 22 | +---------------------------------------------------------------+ 23 | ``` 24 | 对于一个网络包而言,首先要知道这个包的格式,然后才能按照约定的格式解析出这个包。那么 grpc 的包是什么样的格式呢? 看了源码后,先直接揭晓出来,它其实是这样的 25 | 26 | ![](https://images.xiaozhuanlan.com/photo/2019/ad81643a987d5ae267f3ea2dc4cd3434.png) 27 | 28 | http 帧格式为:length (3 byte) + type(1 byte) + flag (1 byte) + R (1 bit) + stream identifier (31 bit) + paypoad,payload 是消息具体内容 29 | 30 | 前 9 个字节是 http 包头,length 表示消息长度,type 表示 http 帧的类型,http 一共规定了 10 种帧类型: 31 | 32 | - HEADERS帧 头信息,对应于HTTP HEADER 33 | - DATA帧 对应于HTTP Response Body 34 | - PRIORITY帧 用于调整流的优先级 35 | - RST_STREAM帧 流终止帧,用于中断资源的传输 36 | - SETTINGS帧 用户客户服务器交流连接配置信息 37 | - PUSH_PROMISE帧 服务器向客户端主动推送资源 38 | - GOAWAY帧 通知对方断开连接 39 | - PING帧 心跳帧,检测往返时间和连接可用性 40 | - WINDOW_UPDATE帧 调整帧大小 41 | - CONTINUATION帧 HEADERS太大时的续帧 42 | 43 | flag 表示标志位,http 一共三种标志位: 44 | 45 | - END_STREAM 流结束标志,表示当前帧是流的最后一个帧 46 | - END_HEADERS 头结束表示,表示当前帧是头信息的最后一个帧 47 | - PADDED 填充标志,在数据Payload里填充无用信息,用于干扰信道监听 48 | 49 | R 是 1bit 的保留位,stream identifier 是流 id,http 会为每一个数据流分配一个 id 50 | 51 | 具体可以参考:[http frame](https://http2.github.io/http2-spec/#FramingLayer) 52 | 53 | 54 | ### 解析 http 帧头 55 | 56 | 回到 examples 目录的 helloworld 目录, 在 server main 函数中跟踪 s.Serve 方法: s.Serve() ——> s.handleRawConn(rawConn) ——> s.serveStreams(st) 57 | 58 | 来看一下 serveStreams 这个方法 59 | 60 | ```go 61 | func (s *Server) serveStreams(st transport.ServerTransport) { 62 | defer st.Close() 63 | var wg sync.WaitGroup 64 | st.HandleStreams(func(stream *transport.Stream) { 65 | wg.Add(1) 66 | go func() { 67 | defer wg.Done() 68 | s.handleStream(st, stream, s.traceInfo(st, stream)) 69 | }() 70 | }, func(ctx context.Context, method string) context.Context { 71 | if !EnableTracing { 72 | return ctx 73 | } 74 | tr := trace.New("grpc.Recv."+methodFamily(method), method) 75 | return trace.NewContext(ctx, tr) 76 | }) 77 | wg.Wait() 78 | } 79 | ``` 80 | 这里调用了 transport 的 HandleStreams 方法, 这个方法就是 http 帧的处理的具体实现。它的底层直接调用的 http2 包的 ReadFrame 方法去读取一个 http 帧数据。 81 | 82 | ```go 83 | type framer struct { 84 | writer *bufWriter 85 | fr *http2.Framer 86 | } 87 | 88 | func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) { 89 | defer close(t.readerDone) 90 | for { 91 | frame, err := t.framer.fr.ReadFrame() 92 | atomic.StoreUint32(&t.activity, 1) 93 | 94 | ... 95 | 96 | switch frame := frame.(type) { 97 | case *http2.MetaHeadersFrame: 98 | if t.operateHeaders(frame, handle, traceCtx) { 99 | t.Close() 100 | break 101 | } 102 | case *http2.DataFrame: 103 | t.handleData(frame) 104 | case *http2.RSTStreamFrame: 105 | t.handleRSTStream(frame) 106 | case *http2.SettingsFrame: 107 | t.handleSettings(frame) 108 | case *http2.PingFrame: 109 | t.handlePing(frame) 110 | case *http2.WindowUpdateFrame: 111 | t.handleWindowUpdate(frame) 112 | case *http2.GoAwayFrame: 113 | // TODO: Handle GoAway from the client appropriately. 114 | default: 115 | errorf("transport: http2Server.HandleStreams found unhandled frame type %v.", frame) 116 | } 117 | } 118 | } 119 | ``` 120 | 121 | 通过 http2 包的 ReadFrame 直接读取出一个帧的数据。 122 | 123 | ```go 124 | func (fr *Framer) ReadFrame() (Frame, error) { 125 | fr.errDetail = nil 126 | if fr.lastFrame != nil { 127 | fr.lastFrame.invalidate() 128 | } 129 | fh, err := readFrameHeader(fr.headerBuf[:], fr.r) 130 | if err != nil { 131 | return nil, err 132 | } 133 | if fh.Length > fr.maxReadSize { 134 | return nil, ErrFrameTooLarge 135 | } 136 | payload := fr.getReadBuf(fh.Length) 137 | if _, err := io.ReadFull(fr.r, payload); err != nil { 138 | return nil, err 139 | } 140 | f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, payload) 141 | if err != nil { 142 | if ce, ok := err.(connError); ok { 143 | return nil, fr.connError(ce.Code, ce.Reason) 144 | } 145 | return nil, err 146 | } 147 | if err := fr.checkFrameOrder(f); err != nil { 148 | return nil, err 149 | } 150 | if fr.logReads { 151 | fr.debugReadLoggerf("http2: Framer %p: read %v", fr, summarizeFrame(f)) 152 | } 153 | if fh.Type == FrameHeaders && fr.ReadMetaHeaders != nil { 154 | return fr.readMetaFrame(f.(*HeadersFrame)) 155 | } 156 | return f, nil 157 | } 158 | ``` 159 | fh, err := readFrameHeader(fr.headerBuf[:], fr.r) 这一行代码读取了 http 的包头数据,我们来看一下 headerBuf 的长度,发现果然是 9 个字节。 160 | 161 | ```go 162 | const frameHeaderLen = 9 163 | 164 | func readFrameHeader(buf []byte, r io.Reader) (FrameHeader, error) { 165 | _, err := io.ReadFull(r, buf[:frameHeaderLen]) 166 | if err != nil { 167 | return FrameHeader{}, err 168 | } 169 | return FrameHeader{ 170 | Length: (uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])), 171 | Type: FrameType(buf[3]), 172 | Flags: Flags(buf[4]), 173 | StreamID: binary.BigEndian.Uint32(buf[5:]) & (1<<31 - 1), 174 | valid: true, 175 | }, nil 176 | } 177 | ``` 178 | 179 | ### 解析业务数据 180 | 181 | 经过上面的过程,我们终于将 http 包头给读出来了。前面说到了,读出 http 包体之后,还需要解析 grpc 协议头。那这部分是怎么去解析的呢? 182 | 183 | 回到 http2 读帧的部分,当发现帧的格式是 MetaHeadersFrame,也就是第一个帧时,会调用 operateHeaders 方法 184 | 185 | ```go 186 | case *http2.MetaHeadersFrame: 187 | if t.operateHeaders(frame, handle, traceCtx) { 188 | t.Close() 189 | break 190 | } 191 | ``` 192 | 193 | 看一下 operateHeaders ,里面会去调用 handle(s) , 这个handle 其实是 之前 s.Serve() ——> s.handleRawConn(rawConn) ——> s.serveStreams(st) 这个路径下的 HandleStreams 方法传入的,也就是会去调用 handleStream 这个方法 194 | 195 | ``` 196 | st.HandleStreams(func(stream *transport.Stream) { 197 | wg.Add(1) 198 | go func() { 199 | defer wg.Done() 200 | s.handleStream(st, stream, s.traceInfo(st, stream)) 201 | }() 202 | }, func(ctx context.Context, method string) context.Context { 203 | if !EnableTracing { 204 | return ctx 205 | } 206 | tr := trace.New("grpc.Recv."+methodFamily(method), method) 207 | return trace.NewContext(ctx, tr) 208 | }) 209 | ``` 210 | 211 | s.handleStream(st, stream, s.traceInfo(st, stream)) ——> s.processUnaryRPC(t, stream, srv, md, trInfo) ———> d, err := recvAndDecompress(&parser{r: stream}, stream, dc, s.opts.maxReceiveMessageSize, payInfo, decomp) ,进入 recvAndDecompress 这个函数,里面调用了 212 | 213 | ``` 214 | pf, d, err := p.recvMsg(maxReceiveMessageSize) 215 | ``` 216 | 217 | 进入 recvMsg,发现它就是解析 grpc 协议 的函数,先把协议头读出来,用了 5 个字节。从协议头中得知协议体消息的长度,然后用一个相应长度的 byte 数组把协议体读出来 218 | 219 | ```go 220 | type parser struct { 221 | r io.Reader 222 | 223 | header [5]byte 224 | } 225 | 226 | func (p *parser) recvMsg(maxReceiveMessageSize int) (pf payloadFormat, msg []byte, err error) { 227 | if _, err := p.r.Read(p.header[:]); err != nil { 228 | return 0, nil, err 229 | } 230 | 231 | pf = payloadFormat(p.header[0]) 232 | length := binary.BigEndian.Uint32(p.header[1:]) 233 | 234 | ... 235 | msg = make([]byte, int(length)) 236 | if _, err := p.r.Read(msg); err != nil { 237 | if err == io.EOF { 238 | err = io.ErrUnexpectedEOF 239 | } 240 | return 0, nil, err 241 | } 242 | 243 | return pf, msg, nil 244 | } 245 | ``` 246 | 247 | 继续回到这个图,前面说到了 grpc 协议头是 5个字节。 248 | ![](https://images.xiaozhuanlan.com/photo/2019/ad81643a987d5ae267f3ea2dc4cd3434.png) 249 | compressed-flag 表示是否压缩,值为 1 是压缩消息体数据,0 不压缩。 250 | length 表示消息体数据长度。现在终于知道了这个数据结构的由来! 251 | 252 | 读取出来的数据是二进制的,读出来原数据之后呢,我们就可以针对相应的数据做解包操作了。这里可以参考我的另一篇文章 :[10-grpc 协议编解码器](https://github.com/lubanproj/grpc_read/blob/master/10-grpc%20%E5%8D%8F%E8%AE%AE%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8.md) 253 | -------------------------------------------------------------------------------- /10-grpc 协议编解码器.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-protocol-codec/ 2 | > 最新版本请访问原文链接 3 | 4 | ### 协议编解码器 5 | 6 | 一般的协议都会包括协议头和协议体,对于业务而言,一般只关心需要发送的业务数据。所以,协议头的内容一般是框架自动帮忙填充。将业务数据包装成指定协议格式的数据包就是编码的过程,从指定协议格式中的数据包中取出业务数据的过程就是解码的过程。 7 | 8 | 每个 rpc 框架基本都有自己的编解码器,下面我们就来说说 grpc 的编解码过程。 9 | 10 | ### grpc 解码 11 | 我们还是从我们的 examples 目录下的 helloworld demo 中 server 的 main 函数入手 12 | 13 | func main() { 14 | lis, err := net.Listen("tcp", port) 15 | if err != nil { 16 | log.Fatalf("failed to listen: %v", err) 17 | } 18 | s := grpc.NewServer() 19 | pb.RegisterGreeterServer(s, &server{}) 20 | if err := s.Serve(lis); err != nil { 21 | log.Fatalf("failed to serve: %v", err) 22 | } 23 | } 24 | 25 | 在 s.Serve(lis) ——> s.handleRawConn(rawConn) —— > s.serveStreams(st) ——> s.handleStream(st, stream, s.traceInfo(st, stream)) ——> s.processUnaryRPC(t, stream, srv, md, trInfo) 方法中有一段代码: 26 | 27 | sh := s.opts.statsHandler 28 | ... 29 | df := func(v interface{}) error { 30 | if err := s.getCodec(stream.ContentSubtype()).Unmarshal(d, v); err != nil { 31 | return status.Errorf(codes.Internal, "grpc: error unmarshalling request: %v", err) 32 | } 33 | if sh != nil { 34 | sh.HandleRPC(stream.Context(), &stats.InPayload{ 35 | RecvTime: time.Now(), 36 | Payload: v, 37 | WireLength: payInfo.wireLength, 38 | Data: d, 39 | Length: len(d), 40 | }) 41 | } 42 | if binlog != nil { 43 | binlog.Log(&binarylog.ClientMessage{ 44 | Message: d, 45 | }) 46 | } 47 | if trInfo != nil { 48 | trInfo.tr.LazyLog(&payload{sent: false, msg: v}, true) 49 | } 50 | return nil 51 | } 52 | 53 | 这段代码的逻辑先调 getCodec 获取解包类,然后调用这个类的 Unmarshal 方法进行解包。将业务数据取出来,然后调用 handler 进行处理。 54 | 55 | func (s *Server) getCodec(contentSubtype string) baseCodec { 56 | if s.opts.codec != nil { 57 | return s.opts.codec 58 | } 59 | if contentSubtype == "" { 60 | return encoding.GetCodec(proto.Name) 61 | } 62 | codec := encoding.GetCodec(contentSubtype) 63 | if codec == nil { 64 | return encoding.GetCodec(proto.Name) 65 | } 66 | return codec 67 | } 68 | 69 | 我们来看 getCodec 这个方法,它是通过 contentSubtype 这个字段来获取解包类的。假如不设置 contentSubtype ,那么默认会用名字为 proto 的解码器。 70 | 71 | 我们来看看 contentSubtype 是如何设置的。之前说到了 grpc 的底层默认是基于 http2 的。在 serveHttp 时调用了 NewServerHandlerTransport 这个方法来创建一个 ServerTransport,然后我们发现,其实就是根据 content-type 这个字段去生成的。 72 | 73 | func NewServerHandlerTransport(w http.ResponseWriter, r *http.Request, stats stats.Handler) (ServerTransport, error) { 74 | ... 75 | 76 | contentType := r.Header.Get("Content-Type") 77 | // TODO: do we assume contentType is lowercase? we did before 78 | contentSubtype, validContentType := contentSubtype(contentType) 79 | if !validContentType { 80 | return nil, errors.New("invalid gRPC request content-type") 81 | } 82 | if _, ok := w.(http.Flusher); !ok { 83 | return nil, errors.New("gRPC requires a ResponseWriter supporting http.Flusher") 84 | } 85 | 86 | st := &serverHandlerTransport{ 87 | rw: w, 88 | req: r, 89 | closedCh: make(chan struct{}), 90 | writes: make(chan func()), 91 | contentType: contentType, 92 | contentSubtype: contentSubtype, 93 | stats: stats, 94 | } 95 | } 96 | 97 | 我们来看看 contentSubtype 这个方法 。 98 | 99 | ... 100 | baseContentType = "application/grpc" 101 | ... 102 | func contentSubtype(contentType string) (string, bool) { 103 | if contentType == baseContentType { 104 | return "", true 105 | } 106 | if !strings.HasPrefix(contentType, baseContentType) { 107 | return "", false 108 | } 109 | // guaranteed since != baseContentType and has baseContentType prefix 110 | switch contentType[len(baseContentType)] { 111 | case '+', ';': 112 | // this will return true for "application/grpc+" or "application/grpc;" 113 | // which the previous validContentType function tested to be valid, so we 114 | // just say that no content-subtype is specified in this case 115 | return contentType[len(baseContentType)+1:], true 116 | default: 117 | return "", false 118 | } 119 | } 120 | 121 | 可以看到 grpc 协议默认以 application/grpc 开头,假如不一这个开头会返回错误,假如我们想使用 json 的解码器,应该设置 content-type = application/grpc+json 。下面是一个基于 grpc 协议的请求 request : 122 | 123 | HEADERS (flags = END_HEADERS) 124 | :method = POST 125 | :scheme = http 126 | :path = /google.pubsub.v2.PublisherService/CreateTopic 127 | :authority = pubsub.googleapis.com 128 | grpc-timeout = 1S 129 | content-type = application/grpc+proto 130 | grpc-encoding = gzip 131 | authorization = Bearer y235.wef315yfh138vh31hv93hv8h3v 132 | 133 | DATA (flags = END_STREAM) 134 | 135 | 136 | 详细可参考 [proto-http2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) 137 | 138 | 怎么拿的呢,再看一下 encoding.getCodec 方法 139 | 140 | func GetCodec(contentSubtype string) Codec { 141 | return registeredCodecs[contentSubtype] 142 | } 143 | 144 | 它其实取得是 registeredCodecs 这个 map 中的 codec,这个 map 是 RegisterCodec 方法注册进去的。 145 | 146 | var registeredCodecs = make(map[string]Codec) 147 | 148 | func RegisterCodec(codec Codec) { 149 | if codec == nil { 150 | panic("cannot register a nil Codec") 151 | } 152 | if codec.Name() == "" { 153 | panic("cannot register Codec with empty string result for Name()") 154 | } 155 | contentSubtype := strings.ToLower(codec.Name()) 156 | registeredCodecs[contentSubtype] = codec 157 | } 158 | 159 | 毫无疑问, encoding 目录的 proto 包下肯定在初始化时调用注册方法了。果然 160 | 161 | func init() { 162 | encoding.RegisterCodec(codec{}) 163 | } 164 | 165 | 绕了一圈,调用的其实是 proto 的 Unmarshal 方法,如下: 166 | 167 | func (codec) Unmarshal(data []byte, v interface{}) error { 168 | protoMsg := v.(proto.Message) 169 | protoMsg.Reset() 170 | 171 | if pu, ok := protoMsg.(proto.Unmarshaler); ok { 172 | // object can unmarshal itself, no need for buffer 173 | return pu.Unmarshal(data) 174 | } 175 | 176 | cb := protoBufferPool.Get().(*cachedProtoBuffer) 177 | cb.SetBuf(data) 178 | err := cb.Unmarshal(protoMsg) 179 | cb.SetBuf(nil) 180 | protoBufferPool.Put(cb) 181 | return err 182 | } 183 | 184 | ### grpc 编码 185 | 在剖析解码代码的基础上,编码代码就很轻松了,其实直接找到 encoding 目录的 proto 包,看 Marshal 方法在哪儿被调用就行了。 186 | 187 | 于是我们很快就找到了调用路径,也是这个路径: 188 | 189 | s.Serve(lis) ——> s.handleRawConn(rawConn) —— > s.serveStreams(st) ——> s.handleStream(st, stream, s.traceInfo(st, stream)) ——> s.processUnaryRPC(t, stream, srv, md, trInfo) 190 | 191 | processUnaryRPC 方法中有一段 server 发送响应数据的代码。其实也就是这一行: 192 | 193 | if err := s.sendResponse(t, stream, reply, cp, opts, comp); err != nil { 194 | 195 | 我们其实也能猜到,发送数据给 client 之前肯定要编码。果然调用了 encode 方法 196 | 197 | func (s *Server) sendResponse(t transport.ServerTransport, stream *transport.Stream, msg interface{}, cp Compressor, opts *transport.Options, comp encoding.Compressor) error { 198 | data, err := encode(s.getCodec(stream.ContentSubtype()), msg) 199 | if err != nil { 200 | grpclog.Errorln("grpc: server failed to encode response: ", err) 201 | return err 202 | } 203 | ... 204 | } 205 | 206 | 来看一下 encode 207 | 208 | func encode(c baseCodec, msg interface{}) ([]byte, error) { 209 | if msg == nil { // NOTE: typed nils will not be caught by this check 210 | return nil, nil 211 | } 212 | b, err := c.Marshal(msg) 213 | if err != nil { 214 | return nil, status.Errorf(codes.Internal, "grpc: error while marshaling: %v", err.Error()) 215 | } 216 | if uint(len(b)) > math.MaxUint32 { 217 | return nil, status.Errorf(codes.ResourceExhausted, "grpc: message too large (%d bytes)", len(b)) 218 | } 219 | return b, nil 220 | } 221 | 222 | 它调用了 c.Marshal 方法, Marshal 方法其实是 baseCodec 定义的一个通用抽象方法 223 | 224 | type baseCodec interface { 225 | Marshal(v interface{}) ([]byte, error) 226 | Unmarshal(data []byte, v interface{}) error 227 | } 228 | 229 | proto 实现了 baseCodec,前面说到了通过 s.getCodec(stream.ContentSubtype(),msg) 获取到的其实是 contentType 里面设置的协议名称,不设置的话默认取 proto 的编码器。所以最终是调用了 proto 包下的 Marshal 方法,如下: 230 | 231 | func (codec) Marshal(v interface{}) ([]byte, error) { 232 | if pm, ok := v.(proto.Marshaler); ok { 233 | // object can marshal itself, no need for buffer 234 | return pm.Marshal() 235 | } 236 | 237 | cb := protoBufferPool.Get().(*cachedProtoBuffer) 238 | out, err := marshal(v, cb) 239 | 240 | // put back buffer and lose the ref to the slice 241 | cb.SetBuf(nil) 242 | protoBufferPool.Put(cb) 243 | return out, err 244 | } 245 | 246 | ok,那么至此,grpc 的整个编解码的流程我们就已经剖析完了 247 | -------------------------------------------------------------------------------- /7-grpc 认证鉴权——tls认证.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-auth-tls/ 2 | > 最新版本请访问原文链接 3 | 4 | ## grpc 认证鉴权 5 | 在了解 grpc 认证鉴权之前,我们有必要先梳理一下认证鉴权方面的知识。 6 | 7 | ### 1、单体模式下的认证鉴权 8 | 9 | 在单体模式下,整个应用是一个进程,应用一般只需要一个统一的安全认证模块来实现用户认证鉴权。例如用户登陆时,安全模块验证用户名和密码的合法性。假如合法,为用户生成一个唯一的 Session。将 SessionId 返回给客户端,客户端一般将 SessionId 以 Cookie 的形式记录下来,并在后续请求中传递 Cookie 给服务端来验证身份。为了避免 Session Id被第三者截取和盗用,客户端和应用之前应使用 TLS 加密通信,session 也会设置有过期时间。 10 | 11 | 客户端访问服务端时,服务端一般会用一个拦截器拦截请求,取出 session id,假如 id 合法,则可判断客户端登陆。然后查询用户的权限表,判断用户是否具有执行某次操作的权限。 12 | 13 | ### 2、微服务模式下的认证鉴权 14 | 在微服务模式下,一个整体的应用可能被拆分为多个微服务,之前只有一个服务端,现在会存在多个服务端。对于客户端的单个请求,为保证安全,需要跟每个微服务都要重复上面的过程。这种模式每个微服务都要去实现相同的校验逻辑,肯定是非常冗余的。 15 | 16 | #### 用户身份认证 17 | 为了避免每个服务端都进行重复认证,采用一个服务进行统一认证。所以考虑一个单点登录的方案,用户只需要登录一次,就可以访问所有微服务。一般在 api 的 gateway 层提供对外服务的入口,所以可以在 api gateway 层提供统一的用户认证。 18 | 19 | #### 用户状态保持 20 | 由于 http 是一个无状态的协议,前面说到了单体模式下通过 cookie 保存用户状态, cookie 一般存储于浏览器中,用来保存用户的信息。但是 cookie 是有状态的。客户端和服务端在一次会话期间都需要维护 cookie 或者 sessionId,在微服务环境下,我们期望服务的认证是无状态的。所以我们一般采用 token 认证的方式,而非 cookie。 21 | 22 | token 由服务端用自己的密钥加密生成,在客户端登录或者完成信息校验时返回给客户端,客户端认证成功后每次向服务端发送请求带上 token,服务端根据密钥进行解密,从而校验 token 的合法,假如合法则认证通过。token 这种方式的校验不需要服务端保存会话状态。方便服务扩展 23 | 24 | ### 3、grpc 认证鉴权 25 | grpc-go 官方对于认证鉴权的介绍如下:https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-auth-support.md 26 | 27 | 通过官方介绍可知, grpc-go 认证鉴权是通过 tls + oauth2 实现的。这里不对 tls 和 oauth2 进行详细介绍,假如有不清楚的可以参考阮一峰老师的教程,介绍得比较清楚 28 | 29 | tls :http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html 30 | oauth2 :http://www.ruanyifeng.com/blog/2019/04/oauth_design.html 31 | 32 | 下面我们就来具体看看 grpc-go 是如何实现认证鉴权的 33 | 34 | grpc-go 官方 doc 说了这里关于 auth 的部分有 demo 放在 examples 目录下的 features 目录下。但是 demo 没有包括证书生成的步骤,这里我们自建一个 demo,从生成证书开始一步步进行 grpc 的认证讲解。 35 | 36 | 我们先创建一个文件夹 helloauth,然后把之前examples 目录下 helloworld demo 中的 client 和 server 的 go 文件全部 copy 过来,先执行 go mod init helloauth 来生成 go.mod 文件。由于 google.golang.org 被墙,所以执行 go mod edit -replace=google.golang.org/grpc=github.com/grpc/grpc-go@latest, 接着 37 | 注意把 替换成 pb "google.golang.org/grpc/examples/helloworld/helloworld" 替换成 pb "helloauth/helloworld" 来引用我们新生成的 pb 文件 38 | 39 | #### 生成证书 40 | 生成私钥 41 | 42 | openssl ecparam -genkey -name secp384r1 -out server.key 43 | 44 | 使用私钥生成证书 45 | 46 | openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650 47 | 48 | 填写信息(注意 Common Name 要填写服务名) 49 | 50 | Country Name (2 letter code) []: 51 | State or Province Name (full name) []: 52 | Locality Name (eg, city) []: 53 | Organization Name (eg, company) []: 54 | Organizational Unit Name (eg, section) []: 55 | Common Name (eg, fully qualified host name) []:helloauth 56 | Email Address []: 57 | 58 | 生成完毕后,将证书文件放到 keys 目录下,整个项目目录结构如下: 59 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190824164530540.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2RpdWJyb3RoZXI=,size_16,color_FFFFFF,t_70) 60 | #### 使用证书进行 TLS 通信认证 61 | 我们之前的 helloworld demo 中,client 在创建 DialContext 指定非安全模式通信,如下: 62 | 63 | conn, err := grpc.Dial(address, grpc.WithInsecure()) 64 | 65 | 这种模式下,client 和 server 都不会进行通信认证,其实是不安全的。下面我们来看看安全模式下应该如何通信 66 | 67 | #### server 68 | 69 | package main 70 | 71 | import ( 72 | "context" 73 | "log" 74 | "net" 75 | 76 | "google.golang.org/grpc" 77 | "google.golang.org/grpc/credentials" 78 | 79 | pb "google.golang.org/grpc/examples/helloworld/helloworld" 80 | ) 81 | 82 | const ( 83 | port = ":50051" 84 | ) 85 | 86 | // server is used to implement helloworld.GreeterServer. 87 | type server struct{} 88 | 89 | // SayHello implements helloworld.GreeterServer 90 | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { 91 | log.Printf("Received: %v", in.Name) 92 | return &pb.HelloReply{Message: "Hello " + in.Name}, nil 93 | } 94 | 95 | func main() { 96 | c, err := credentials.NewServerTLSFromFile("../keys/server.pem", "../keys/server.key") 97 | if err != nil { 98 | log.Fatalf("credentials.NewServerTLSFromFile err: %v", err) 99 | } 100 | 101 | lis, err := net.Listen("tcp", port) 102 | if err != nil { 103 | log.Fatalf("failed to listen: %v", err) 104 | } 105 | s := grpc.NewServer(grpc.Creds(c)) 106 | pb.RegisterGreeterServer(s, &server{}) 107 | if err := s.Serve(lis); err != nil { 108 | log.Fatalf("failed to serve: %v", err) 109 | } 110 | } 111 | 112 | #### client 113 | package main 114 | 115 | import ( 116 | "context" 117 | "log" 118 | "os" 119 | "time" 120 | 121 | "google.golang.org/grpc" 122 | "google.golang.org/grpc/credentials" 123 | 124 | pb "helloauth/helloworld" 125 | ) 126 | 127 | const ( 128 | address = "localhost:50051" 129 | defaultName = "world" 130 | ) 131 | 132 | func main() { 133 | cred, err := credentials.NewClientTLSFromFile("../keys/server.pem", "helloauth") 134 | if err != nil { 135 | log.Fatalf("credentials.NewClientTLSFromFile err: %v", err) 136 | } 137 | 138 | // Set up a connection to the server. 139 | conn, err := grpc.Dial(address, grpc.WithTransportCredentials(cred)) 140 | if err != nil { 141 | log.Fatalf("did not connect: %v", err) 142 | } 143 | defer conn.Close() 144 | c := pb.NewGreeterClient(conn) 145 | 146 | // Contact the server and print out its response. 147 | name := defaultName 148 | if len(os.Args) > 1 { 149 | name = os.Args[1] 150 | } 151 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 152 | defer cancel() 153 | r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) 154 | if err != nil { 155 | log.Fatalf("could not greet: %v", err) 156 | } 157 | log.Printf("Greeting: %s", r.Message) 158 | } 159 | 160 | 这里的代码已经上传 github 了,详见:https://github.com/diubrother/helloauth 161 | 162 | ### 4、grpc 认证鉴权源码解读 163 | #### server 164 | 先来看 server 端,server 端根据 server 的公钥和私钥生成了一个 TransportCredentials ,如下: 165 | 166 | c, err := credentials.NewServerTLSFromFile("../keys/server.pem", "../keys/server.key") 167 | 168 | func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) { 169 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 170 | if err != nil { 171 | return nil, err 172 | } 173 | return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil 174 | } 175 | 176 | 看一下 NewTLS 这个方法,他其实就返回了一个 tlsCreds 的结构体,这个结构体实现了 TransportCredentials 这个接口,包括 ClientHandshake 和 ServerHandshake 。 177 | 178 | func NewTLS(c *tls.Config) TransportCredentials { 179 | tc := &tlsCreds{cloneTLSConfig(c)} 180 | tc.config.NextProtos = appendH2ToNextProtos(tc.config.NextProtos) 181 | return tc 182 | } 183 | 184 | 来看一下服务端握手的方法 ServerHandshake,可以发现其底层还是调用 go 的 tls 包去实现 tls 认证鉴权。 185 | 186 | func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, AuthInfo, error) { 187 | conn := tls.Server(rawConn, c.config) 188 | if err := conn.Handshake(); err != nil { 189 | return nil, nil, err 190 | } 191 | return internal.WrapSyscallConn(rawConn, conn), TLSInfo{conn.ConnectionState()}, nil 192 | } 193 | 194 | #### client 195 | 和 server 端类似,client 端也是通过公钥和服务名先创建一个 TransportCredentials 196 | 197 | cred, err := credentials.NewClientTLSFromFile("../keys/server.pem", "helloauth") 198 | 199 | 看一下 NewClientTLSFromFile 这个方法,发现它也是调用了相同的 NewTLS 方法返回了一个 tlsCreds 结构体,跟 server 简直一模一样。 200 | 201 | func NewTLS(c *tls.Config) TransportCredentials { 202 | tc := &tlsCreds{cloneTLSConfig(c)} 203 | tc.config.NextProtos = appendH2ToNextProtos(tc.config.NextProtos) 204 | return tc 205 | } 206 | 207 | 接下来在创建客户端连接时,将 tlsCreds 这个结构体传了进去。 208 | 209 | conn, err := grpc.Dial(address, grpc.WithTransportCredentials(cred)) 210 | 211 | Dial —— > DialContext 方法中有这么一段代码,将我们传入的 serverName 也就是 “helloauth" 赋值给了 clientConn 的 authority 这个字段。 212 | 213 | creds := cc.dopts.copts.TransportCredentials 214 | if creds != nil && creds.Info().ServerName != "" { 215 | cc.authority = creds.Info().ServerName 216 | } else if cc.dopts.insecure && cc.dopts.authority != "" { 217 | cc.authority = cc.dopts.authority 218 | } else { 219 | // Use endpoint from "scheme://authority/endpoint" as the default 220 | // authority for ClientConn. 221 | cc.authority = cc.parsedTarget.Endpoint 222 | } 223 | 224 | #### 认证过程 225 | #### client 226 | 那什么时候开始认证呢?先来说说 client。 227 | 228 | client 的认证其实是在调用 connect 方法的时候,在之前讲述负载均衡时降到了,在 acBalancerWrapper 里面有一个 UpdateAddresses 方法,调用 ac.connect() ——> ac.resetTransport() ——> ac.tryAllAddrs ——> ac.createTransport ——> transport.NewClientTransport ——> newHTTP2Client 方法时,有这么一段代码: 229 | 230 | transportCreds := opts.TransportCredentials 231 | perRPCCreds := opts.PerRPCCredentials 232 | 233 | if b := opts.CredsBundle; b != nil { 234 | if t := b.TransportCredentials(); t != nil { 235 | transportCreds = t 236 | } 237 | if t := b.PerRPCCredentials(); t != nil { 238 | perRPCCreds = append(perRPCCreds, t) 239 | } 240 | } 241 | if transportCreds != nil { 242 | scheme = "https" 243 | conn, authInfo, err = transportCreds.ClientHandshake(connectCtx, addr.Authority, conn) 244 | if err != nil { 245 | return nil, connectionErrorf(isTemporary(err), err, "transport: authentication handshake failed: %v", err) 246 | } 247 | isSecure = true 248 | } 249 | 250 | 这里即调用了tlsCreds 的 ClientHandshake 方法进行握手,实现客户端的认证。 251 | 252 | #### server 253 | 再来说说 server 254 | 255 | server 的认证其实是在调用 Serve ——> handleRawConn ——> useTransportAuthenticator 方法,调用了 s.opts.creds.ServerHandshake(rawConn) 方法,其底层也是调用 tlsCreds ServerHandshake 方法进行服务端握手。 256 | 257 | func (s *Server) useTransportAuthenticator(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { 258 | if s.opts.creds == nil { 259 | return rawConn, nil, nil 260 | } 261 | return s.opts.creds.ServerHandshake(rawConn) 262 | } 263 | -------------------------------------------------------------------------------- /12-grpc 数据流转.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-data-flow/ 2 | > 最新版本请访问原文链接 3 | 4 | # grpc 数据流转 5 | 6 | 阅读本文的前提是你对 grpc 协议的编解码和 协议打解包过程都比较清楚了,假如不是很了解可以先去阅读 [《10 - grpc 协议编解码器》](https://github.com/lubanproj/grpc_read/blob/master/10-grpc%20%E5%8D%8F%E8%AE%AE%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8.md) 和 [《11 - grpc 协议解包过程全剖析》](https://github.com/lubanproj/grpc_read/blob/master/11-grpc%20%E5%8D%8F%E8%AE%AE%E8%A7%A3%E5%8C%85%E8%BF%87%E7%A8%8B%E5%85%A8%E5%89%96%E6%9E%90.md) 7 | 8 | ## 再谈协议 9 | 我们知道协议是一款 rpc 框架的基础。协议里面定义了一次客户端需要携带的信息,包括请求的后端服务名 ServiceName,方法名 Method、超时时间 Timeout、编码 Encoding、认证信息 Authority 等等。 10 | 11 | 前面我们已经说到了,grpc 是基于 http2 协议的,我们来看看 grpc 协议里面的一些关键信息: 12 | 13 | ![grpc 协议](https://images.xiaozhuanlan.com/photo/2020/211eb35fa812fda032a342bc60611cb9.png) 14 | 15 | 可以看到,一次请求需要携带这么多信息,server 会根据 client 携带的这些信息来进行相应的处理。那么这些协议里面定义的内容要如何被传递下去呢? 16 | 17 | ## 数据承载体 18 | 为了回答上面的问题,我们需要一个数据承载体结构,来保存协议里面的一些需要透传的一些重要信息,比如 Method 等。在 grpc 中,这个结构就是 Stream, 我们来看一下 Stream 的定义。 19 | 20 | // Stream represents an RPC in the transport layer. 21 | type Stream struct { 22 | id uint32 23 | st ServerTransport // nil for client side Stream 24 | ctx context.Context // the associated context of the stream 25 | cancel context.CancelFunc // always nil for client side Stream 26 | done chan struct{} // closed at the end of stream to unblock writers. On the client side. 27 | ctxDone <-chan struct{} // same as done chan but for server side. Cache of ctx.Done() (for performance) 28 | method string // the associated RPC method of the stream 29 | recvCompress string 30 | sendCompress string 31 | buf *recvBuffer 32 | trReader io.Reader 33 | fc *inFlow 34 | wq *writeQuota 35 | 36 | // Callback to state application's intentions to read data. This 37 | // is used to adjust flow control, if needed. 38 | requestRead func(int) 39 | 40 | headerChan chan struct{} // closed to indicate the end of header metadata. 41 | headerChanClosed uint32 // set when headerChan is closed. Used to avoid closing headerChan multiple times. 42 | 43 | // hdrMu protects header and trailer metadata on the server-side. 44 | hdrMu sync.Mutex 45 | // On client side, header keeps the received header metadata. 46 | // 47 | // On server side, header keeps the header set by SetHeader(). The complete 48 | // header will merged into this after t.WriteHeader() is called. 49 | header metadata.MD 50 | trailer metadata.MD // the key-value map of trailer metadata. 51 | 52 | noHeaders bool // set if the client never received headers (set only after the stream is done). 53 | 54 | // On the server-side, headerSent is atomically set to 1 when the headers are sent out. 55 | headerSent uint32 56 | 57 | state streamState 58 | 59 | // On client-side it is the status error received from the server. 60 | // On server-side it is unused. 61 | status *status.Status 62 | 63 | bytesReceived uint32 // indicates whether any bytes have been received on this stream 64 | unprocessed uint32 // set if the server sends a refused stream or GOAWAY including this stream 65 | 66 | // contentSubtype is the content-subtype for requests. 67 | // this must be lowercase or the behavior is undefined. 68 | contentSubtype string 69 | } 70 | 71 | ### server 端 Stream 的构造 72 | 接下来我们来看看 server 端 Stream 的构造。前面的内容已经说过 server 的处理流程了。我们直接进入 serveStreams 这个方法。路径为:s.Serve(lis) ——> s.handleRawConn(rawConn) ——> s.serveStreams(st) 73 | 74 | func (s *Server) serveStreams(st transport.ServerTransport) { 75 | defer st.Close() 76 | var wg sync.WaitGroup 77 | st.HandleStreams(func(stream *transport.Stream) { 78 | wg.Add(1) 79 | go func() { 80 | defer wg.Done() 81 | s.handleStream(st, stream, s.traceInfo(st, stream)) 82 | }() 83 | }, func(ctx context.Context, method string) context.Context { 84 | if !EnableTracing { 85 | return ctx 86 | } 87 | tr := trace.New("grpc.Recv."+methodFamily(method), method) 88 | return trace.NewContext(ctx, tr) 89 | }) 90 | wg.Wait() 91 | } 92 | 93 | 最上层 HandleStreams 是对 http2 数据帧的处理。grpc 一共处理了 MetaHeadersFrame 、DataFrame、RSTStreamFrame、SettingsFrame、PingFrame、WindowUpdateFrame、GoAwayFrame 等 7 种帧。 94 | 95 | // HandleStreams receives incoming streams using the given handler. This is 96 | // typically run in a separate goroutine. 97 | // traceCtx attaches trace to ctx and returns the new context. 98 | func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) { 99 | defer close(t.readerDone) 100 | for { 101 | frame, err := t.framer.fr.ReadFrame() 102 | atomic.StoreUint32(&t.activity, 1) 103 | if err != nil { 104 | if se, ok := err.(http2.StreamError); ok { 105 | warningf("transport: http2Server.HandleStreams encountered http2.StreamError: %v", se) 106 | t.mu.Lock() 107 | s := t.activeStreams[se.StreamID] 108 | t.mu.Unlock() 109 | if s != nil { 110 | t.closeStream(s, true, se.Code, false) 111 | } else { 112 | t.controlBuf.put(&cleanupStream{ 113 | streamID: se.StreamID, 114 | rst: true, 115 | rstCode: se.Code, 116 | onWrite: func() {}, 117 | }) 118 | } 119 | continue 120 | } 121 | if err == io.EOF || err == io.ErrUnexpectedEOF { 122 | t.Close() 123 | return 124 | } 125 | warningf("transport: http2Server.HandleStreams failed to read frame: %v", err) 126 | t.Close() 127 | return 128 | } 129 | switch frame := frame.(type) { 130 | case *http2.MetaHeadersFrame: 131 | if t.operateHeaders(frame, handle, traceCtx) { 132 | t.Close() 133 | break 134 | } 135 | case *http2.DataFrame: 136 | t.handleData(frame) 137 | case *http2.RSTStreamFrame: 138 | t.handleRSTStream(frame) 139 | case *http2.SettingsFrame: 140 | t.handleSettings(frame) 141 | case *http2.PingFrame: 142 | t.handlePing(frame) 143 | case *http2.WindowUpdateFrame: 144 | t.handleWindowUpdate(frame) 145 | case *http2.GoAwayFrame: 146 | // TODO: Handle GoAway from the client appropriately. 147 | default: 148 | errorf("transport: http2Server.HandleStreams found unhandled frame type %v.", frame) 149 | } 150 | } 151 | } 152 | 153 | 对于每一次请求而言,client 一定会先发 HeadersFrame 这个帧,grpc 这里是直接使用 http2 工具包进行实现,直接处理的 MetaHeadersFrame 帧,这个帧的定义为: 154 | 155 | // A MetaHeadersFrame is the representation of one HEADERS frame and 156 | // zero or more contiguous CONTINUATION frames and the decoding of 157 | // their HPACK-encoded contents. 158 | // 159 | // This type of frame does not appear on the wire and is only returned 160 | // by the Framer when Framer.ReadMetaHeaders is set. 161 | type MetaHeadersFrame struct { 162 | *HeadersFrame 163 | Fields []hpack.HeaderField 164 | Truncated bool 165 | } 166 | 167 | 所以是在 MetaHeadersFrame 这个帧里去处理包头数据。所以会去执行 operateHeaders 这个方法,在这个方法里面会去构造一个 stream ,这个 stream 里面包含了传输层请求上下文的数据。包括方法名等。 168 | 169 | s := &Stream{ 170 | id: streamID, 171 | st: t, 172 | buf: buf, 173 | fc: &inFlow{limit: uint32(t.initialWindowSize)}, 174 | recvCompress: state.data.encoding, 175 | method: state.data.method, 176 | contentSubtype: state.data.contentSubtype, 177 | } 178 | 179 | 构造完 stream 后,接下来 tranport 对数据的处理都会将 stream 层层透传下去。所以整个请求内所需要的数据都从 stream 中可以得到,这样就实现了 server 端的数据流转。 180 | 181 | ### client 端数据流转 182 | 183 | 与 server 相对应,client 端也有一个 clientStream 结构,定义如下: 184 | 185 | // clientStream implements a client side Stream. 186 | type clientStream struct { 187 | callHdr *transport.CallHdr 188 | opts []CallOption 189 | callInfo *callInfo 190 | cc *ClientConn 191 | desc *StreamDesc 192 | 193 | codec baseCodec 194 | cp Compressor 195 | comp encoding.Compressor 196 | 197 | cancel context.CancelFunc // cancels all attempts 198 | 199 | sentLast bool // sent an end stream 200 | beginTime time.Time 201 | 202 | methodConfig *MethodConfig 203 | 204 | ctx context.Context // the application's context, wrapped by stats/tracing 205 | 206 | retryThrottler *retryThrottler // The throttler active when the RPC began. 207 | 208 | binlog *binarylog.MethodLogger // Binary logger, can be nil. 209 | // serverHeaderBinlogged is a boolean for whether server header has been 210 | // logged. Server header will be logged when the first time one of those 211 | // happens: stream.Header(), stream.Recv(). 212 | // 213 | // It's only read and used by Recv() and Header(), so it doesn't need to be 214 | // synchronized. 215 | serverHeaderBinlogged bool 216 | 217 | mu sync.Mutex 218 | firstAttempt bool // if true, transparent retry is valid 219 | numRetries int // exclusive of transparent retry attempt(s) 220 | numRetriesSincePushback int // retries since pushback; to reset backoff 221 | finished bool // TODO: replace with atomic cmpxchg or sync.Once? 222 | attempt *csAttempt // the active client stream attempt 223 | // TODO(hedging): hedging will have multiple attempts simultaneously. 224 | committed bool // active attempt committed for retry? 225 | buffer []func(a *csAttempt) error // operations to replay on retry 226 | bufferSize int // current size of buffer 227 | } 228 | 229 | client 的构造就更直接了,在 invoke 发起下游调用时, 直接在 sendMsg 之前就会提前构造 clientStream, 如下: 230 | 231 | func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error { 232 | cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...) 233 | if err != nil { 234 | return err 235 | } 236 | if err := cs.SendMsg(req); err != nil { 237 | return err 238 | } 239 | return cs.RecvMsg(reply) 240 | } 241 | 242 | 243 | stream 这个结构承载了数据流转之外,同时 grpc 流式传输的实现也是基于 stream 去实现的。 244 | -------------------------------------------------------------------------------- /9-grpc 拦截器实现.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-interceptor-implementation/ 2 | > 最新版本请访问原文链接 3 | 4 | ## grpc 源码解读 —— 从 0 到 1 实现拦截器 5 | 6 | 拦截器,通俗点说,就是在执行一段代码之前或者之后,去执行另外一段代码。 7 | 拦截器在业界知名框架中的运用非常普遍。包括 Spring 、Grpc 等框架中都有拦截器的实现。接下来我们想办法从 0 到 1 自己实现一个拦截器。以下的实现主要使用 go 语言讲解。 8 | 9 | 假设有一个方法 handler(ctx context.Context) ,我想要给这个方法赋予一个能力:允许在这个方法执行之前能够打印一行日志。 10 | 11 | #### 1、定义结构 12 | 13 | 于是我们轻而易举得想到了定义一个结构 interceptor 这个结构包含两个参数,一个 context 和 一个 handler 14 | 15 | type interceptor func(ctx context.Context, handler func(ctx context.Context) ) 16 | 17 | 为了能够更加方便,我们将 handler 单独定义成一种类型: 18 | 19 | type interceptor func(ctx context.Context, h handler) 20 | 21 | type handler func(ctx context.Context) 22 | 23 | 24 | #### 2、申明赋值 25 | 26 | 接下来,为了实现我们的目标,对 handler 的每个操作,我们都需要先经过 interceptor 。于是我们申明两个 interceptor 和 handler 的变量并赋值 27 | 28 | var h = func(ctx context.Context) { 29 | fmt.Println("do something ...") 30 | } 31 | 32 | var inter1 = func(ctx context.Context, h handler) { 33 | fmt.Println("interceptor1") 34 | h(ctx) 35 | } 36 | 37 | #### 3、编写执行函数 38 | 编写一个执行函数,看看效果 39 | 40 | func main() { 41 | 42 | var ctx context.Context 43 | 44 | var ceps []interceptor 45 | 46 | var h = func(ctx context.Context) { 47 | fmt.Println("do something ...") 48 | } 49 | 50 | var inter1 = func(ctx context.Context, h handler) { 51 | fmt.Println("interceptor1") 52 | h(ctx) 53 | } 54 | 55 | ceps = append(ceps, inter1) 56 | 57 | for _ , cep := range ceps { 58 | cep(ctx, h) 59 | } 60 | 61 | } 62 | 63 | 输出结果为 : 64 | 65 | interceptor1 66 | do something ... 67 | 68 | ok,我们已经完成了实现这个方法之前 输出一行内容。 69 | 70 | 是不是大功告成了呢? wait ... 我们再来加一个 interceptor 试试,于是我们又加了一个 interceptor 71 | 72 | var inter2 = func(ctx context.Context, h handler) { 73 | fmt.Println("interceptor2") 74 | h(ctx) 75 | } 76 | 77 | 同样,我们编写一个执行函数 78 | 79 | func main() { 80 | 81 | var ctx context.Context 82 | 83 | var ceps []interceptor 84 | 85 | var h = func(ctx context.Context) { 86 | fmt.Println("do something ...") 87 | } 88 | 89 | var inter1 = func(ctx context.Context, h handler) { 90 | fmt.Println("interceptor1") 91 | h(ctx) 92 | } 93 | var inter2 = func(ctx context.Context, h handler) { 94 | fmt.Println("interceptor2") 95 | h(ctx) 96 | } 97 | 98 | ceps = append(ceps, inter1, inter2) 99 | 100 | for _ , cep := range ceps { 101 | cep(ctx, h) 102 | } 103 | 104 | } 105 | 106 | 执行结果如下: 107 | 108 | interceptor1 109 | do something ... 110 | interceptor2 111 | do something ... 112 | 113 | 可以看到,在 handler 之前确实输出了两行内容。但是总感觉哪里不太对??? wait ... handler 竟然执行了两次。这可不是我们想要的效果,我们希望无论打印多少行内容,应该保证 handler 只执行一次。 114 | 115 | #### 4、借鉴 grpc-go 116 | 117 | 于是我们开始想办法,怎么才能让 handler 只执行一次呢? 想啊想,想了一会儿没想到,这个时候灵光一闪,可以借助前人的智慧啊...... grpc 中肯定有实现,我们先来 “借鉴” 一下,毕竟他山之石,可以攻玉嘛..... 118 | 119 | 翻开 grpc-go 的源码,直接找到 helloworld demo client 端的 main 函数,grpc.Dial ——> DialContext ,里面有一行 120 | 121 | chainUnaryClientInterceptors(cc) 122 | 123 | 别问我是怎么找到的,直接去看我之前关于 client 的源码解读 [grpc quick start](https://github.com/lubanproj/grpc_read/blob/master/4-grpc%20hello%20world%20client%20%E8%A7%A3%E6%9E%90.md) 就知道了(这广告貌似一点也不违合 hhh)。 124 | 125 | 来看看这个函数,这个函数就有点牛逼了。 126 | 127 | // chainUnaryClientInterceptors chains all unary client interceptors into one. 128 | func chainUnaryClientInterceptors(cc *ClientConn) { 129 | interceptors := cc.dopts.chainUnaryInts 130 | // Prepend dopts.unaryInt to the chaining interceptors if it exists, since unaryInt will 131 | // be executed before any other chained interceptors. 132 | if cc.dopts.unaryInt != nil { 133 | interceptors = append([]UnaryClientInterceptor{cc.dopts.unaryInt}, interceptors...) 134 | } 135 | var chainedInt UnaryClientInterceptor 136 | if len(interceptors) == 0 { 137 | chainedInt = nil 138 | } else if len(interceptors) == 1 { 139 | chainedInt = interceptors[0] 140 | } else { 141 | chainedInt = func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error { 142 | return interceptors[0](ctx, method, req, reply, cc, getChainUnaryInvoker(interceptors, 0, invoker), opts...) 143 | } 144 | } 145 | cc.dopts.unaryInt = chainedInt 146 | } 147 | 148 | chains all unary client interceptors into one. 这句话告诉我们,这个函数把所有拦截器串成了一个拦截器。这是怎么实现的呢?这不就是我们上面碰到的问题吗!来瞅瞅~~~ 149 | 150 | // getChainUnaryInvoker recursively generate the chained unary invoker. 151 | func getChainUnaryInvoker(interceptors []UnaryClientInterceptor, curr int, finalInvoker UnaryInvoker) UnaryInvoker { 152 | if curr == len(interceptors)-1 { 153 | return finalInvoker 154 | } 155 | return func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error { 156 | return interceptors[curr+1](ctx, method, req, reply, cc, getChainUnaryInvoker(interceptors, curr+1, finalInvoker), opts...) 157 | } 158 | } 159 | 160 | 原来是通过 getChainUnaryInvoker 这个方法,返回一个 UnaryInvoker ,这一个 UnaryInvoker 也是一个函数 161 | 162 | type UnaryInvoker func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error 163 | 164 | 在这个 UnaryInvoker 实例化时会去调用第 curr+1 个 interceptors。也就是最终会返回这样一个结构: 165 | 166 | ![请求流程](https://img-blog.csdnimg.cn/20190827163339853.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2RpdWJyb3RoZXI=,size_16,color_FFFFFF,t_70) 167 | 接下来将这个结构赋值给了 cc.dopts.unaryInt ,但此时并没有调用。什么时候开始调用的呢? 168 | 169 | 还记得我们之前 client SayHello 的时候调用的这一行代码吗? 170 | 171 | err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) 172 | 173 | 在 Invoke 这个函数里面进行了真正的调用 174 | 175 | func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error { 176 | // allow interceptor to see all applicable call options, which means those 177 | // configured as defaults from dial option as well as per-call options 178 | opts = combine(cc.dopts.callOptions, opts) 179 | 180 | if cc.dopts.unaryInt != nil { 181 | return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...) 182 | } 183 | return invoke(ctx, method, args, reply, cc, opts...) 184 | } 185 | 186 | 这一行代码 cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...) 才是入口,就如同多米诺骨牌一样,只要这里开始调用,然后就会一直层层调用下去,直到所有的 interceptor 调用完成。 187 | 188 | 这里的代码虽然不多,但其实设计得非常精妙。包括每一个结构的定义。前辈果然还是前辈 189 | 190 | #### 5、从 0 到 1 实现一个拦截器 191 | 192 | 仔细研究完 grpc 的经典实现,这里我们就可以自己来实现一个简化版的拦截器了。 193 | 194 | ##### 5.1 重新定义结构 195 | 196 | 首先,之前我们的疑问是如何让 handler 只执行一遍。这里我们将原来的 handler 升级一下,成为 Invoker , 重新定义一个 handler ,用于在 Invoker 执行之前处理某些事情。 197 | 198 | type invoker func(ctx context.Context, interceptors []interceptor2 , h handler) error 199 | type handler func(ctx context.Context) 200 | 201 | 之前的 interceptor 也需要更改一下,需要传入我们的 invoker 和 handler 202 | 203 | type interceptor2 func(ctx context.Context, h handler, ivk invoker) error 204 | 205 | ##### 5.2 串联所有 interceptor 206 | 207 | 接下来,我们需要把所有的 interceptor 串联起来 208 | 209 | func getInvoker(ctx context.Context, interceptors []interceptor2 , cur int, ivk invoker) invoker{ 210 | if cur == len(interceptors) - 1 { 211 | return ivk 212 | } 213 | return func(ctx context.Context, interceptors []interceptor2 , h handler) error{ 214 | return interceptors[cur+1](ctx, h, getInvoker(ctx,interceptors, cur+1, ivk)) 215 | } 216 | } 217 | 218 | ##### 5.3 返回第一个 interceptor 作为入口 219 | 220 | func getChainInterceptor(ctx context.Context, interceptors []interceptor2 , ivk invoker) interceptor2 { 221 | if len(interceptors) == 0 { 222 | return nil 223 | } 224 | if len(interceptors) == 1 { 225 | return interceptors[0] 226 | } 227 | return func(ctx context.Context, h handler, ivk invoker) error { 228 | return interceptors[0](ctx, h, getInvoker(ctx, interceptors, 0, ivk)) 229 | } 230 | } 231 | 232 | ##### 5.4 编写执行用例 233 | 234 | 完整的执行用例如下: 235 | 236 | package main 237 | 238 | import ( 239 | "context" 240 | "fmt" 241 | ) 242 | 243 | type interceptor2 func(ctx context.Context, h handler, ivk invoker) error 244 | 245 | type handler func(ctx context.Context) 246 | 247 | type invoker func(ctx context.Context, interceptors []interceptor2 , h handler) error 248 | 249 | func main() { 250 | 251 | var ctx context.Context 252 | var ceps []interceptor2 253 | var h = func(ctx context.Context) { 254 | fmt.Println("do something") 255 | } 256 | 257 | var inter1 = func(ctx context.Context, h handler, ivk invoker) error{ 258 | h(ctx) 259 | return ivk(ctx,ceps,h) 260 | } 261 | var inter2 = func(ctx context.Context, h handler, ivk invoker) error{ 262 | h(ctx) 263 | return ivk(ctx,ceps,h) 264 | } 265 | 266 | var inter3 = func(ctx context.Context, h handler, ivk invoker) error{ 267 | h(ctx) 268 | return ivk(ctx,ceps,h) 269 | } 270 | 271 | ceps = append(ceps, inter1, inter2, inter3) 272 | var ivk = func(ctx context.Context, interceptors []interceptor2 , h handler) error { 273 | fmt.Println("invoker start") 274 | return nil 275 | } 276 | 277 | cep := getChainInterceptor(ctx, ceps,ivk) 278 | cep(ctx, h,ivk) 279 | 280 | } 281 | 282 | func getChainInterceptor(ctx context.Context, interceptors []interceptor2 , ivk invoker) interceptor2 { 283 | if len(interceptors) == 0 { 284 | return nil 285 | } 286 | if len(interceptors) == 1 { 287 | return interceptors[0] 288 | } 289 | return func(ctx context.Context, h handler, ivk invoker) error { 290 | return interceptors[0](ctx, h, getInvoker(ctx, interceptors, 0, ivk)) 291 | } 292 | 293 | } 294 | 295 | 296 | func getInvoker(ctx context.Context, interceptors []interceptor2 , cur int, ivk invoker) invoker{ 297 | if cur == len(interceptors) - 1 { 298 | return ivk 299 | } 300 | return func(ctx context.Context, interceptors []interceptor2 , h handler) error{ 301 | return interceptors[cur+1](ctx, h, getInvoker(ctx,interceptors, cur+1, ivk)) 302 | } 303 | } 304 | 305 | 306 | ##### 5.5 执行结果 307 | 308 | do something 309 | do something 310 | do something 311 | invoker start 312 | 313 | 执行结果如上,可以看到每次 Invoker 执行前我们都调用了 handler,但是 Invoker 只被调用了一次,完美地实现了我们的诉求,一个简化版的拦截器诞生了。 314 | 315 | -------------------------------------------------------------------------------- /3-grpc hello world server 解析.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-hello-world-server-analysis/ 2 | > 最新版本请访问原文链接 3 | 4 | ### grpc hello world server 解析 5 | 6 | 我们介绍 grpc quick start 时,通过快速启动一个 grpc server 端和 client 端,然后以 rpc 调用的方式输出一个 hello world。那么输出 hello world 需要经过哪些方法的处理呢?.......这个我也不知道,所以我们先去瞅瞅源码,探究一下 hello world 的背后是连接是如何建立的,然后一起来解读这个问题哈哈。 7 | 8 | 这节内容我们先来研究一下 server 端连接建立过程。 9 | 10 | 先放上 server 端的 main 函数。 11 | 12 | func main() { 13 | lis, err := net.Listen("tcp", port) 14 | if err != nil { 15 | log.Fatalf("failed to listen: %v", err) 16 | } 17 | s := grpc.NewServer() 18 | pb.RegisterGreeterServer(s, &server{}) 19 | if err := s.Serve(lis); err != nil { 20 | log.Fatalf("failed to serve: %v", err) 21 | } 22 | } 23 | 24 | 我们发现其实 server 端连接的建立主要包括三步: 25 | 26 | (1)创建 server 27 | 28 | (2)server 的注册 29 | 30 | (3)调用 Serve 监听端口并处理请求 31 | 32 | ok,弄清楚主流程之后下面我们进入每个步骤里面去看一下代码实现。 33 | 34 | ####1、创建 server 35 | 36 | server 的创建比较简单,其实就下面一个方法: 37 | 38 | func NewServer(opt ...ServerOption) *Server { 39 | opts := defaultServerOptions 40 | for _, o := range opt { 41 | o.apply(&opts) 42 | } 43 | s := &Server{ 44 | lis: make(map[net.Listener]bool), 45 | opts: opts, 46 | conns: make(map[transport.ServerTransport]bool), 47 | m: make(map[string]*service), 48 | quit: grpcsync.NewEvent(), 49 | done: grpcsync.NewEvent(), 50 | czData: new(channelzData), 51 | } 52 | s.cv = sync.NewCond(&s.mu) 53 | if EnableTracing { 54 | _, file, line, _ := runtime.Caller(1) 55 | s.events = trace.NewEventLog("grpc.Server", fmt.Sprintf("%s:%d", file, line)) 56 | } 57 | 58 | if channelz.IsOn() { 59 | s.channelzID = channelz.RegisterServer(&channelzServer{s}, "") 60 | } 61 | return s 62 | } 63 | 这个方法的核心无非是创建了一个 server 结构体,然后为结构体的属性赋值。我们顺便来瞅瞅 server 的结构: 64 | 65 | // Server is a gRPC server to serve RPC requests. 66 | type Server struct { 67 | // serverOptions 就是描述协议的各种参数选项,包括发送和接收的消息大小、buffer大小等等各种,跟 http Headers 类似,我们这里就暂时先不管 68 | opts serverOptions 69 | 70 | // 一个互斥锁 71 | mu sync.Mutex // guards following 72 | // listener map 73 | lis map[net.Listener]bool 74 | // connections map 75 | conns map[transport.ServerTransport]bool 76 | // server 是否在处理请求的一个状态位 77 | serve bool 78 | drain bool 79 | cv *sync.Cond // signaled when connections close for GracefulStop 80 | // service map 81 | m map[string]*service // service name -> service info 82 | events trace.EventLog 83 | 84 | quit *grpcsync.Event 85 | done *grpcsync.Event 86 | channelzRemoveOnce sync.Once 87 | serveWG sync.WaitGroup // counts active Serve goroutines for GracefulStop 88 | 89 | channelzID int64 // channelz unique identification number 90 | czData *channelzData 91 | } 92 | 93 | 虽然 server 结构体里面各种乱起八糟的字段,但是我们可以先不管哈哈哈,比较重要的无非就是三个 map 表分别用来存放多个 listener 、connection 和 service。其他字段都是为了实现协议描述或者并发控制的功能。我们重点关注下 94 | 95 | m map[string]*service 96 | 97 | 这个结构,service 中主要包含了 MethodDesc 和 StreamDesc 这两个 map 98 | 99 | type service struct { 100 | server interface{} // the server for service methods 101 | md map[string]*MethodDesc 102 | sd map[string]*StreamDesc 103 | mdata interface{} 104 | } 105 | 106 | ![enter image description here](https://images.gitbook.cn/2c36f9d0-b69a-11e9-8dd9-33673ef07123) 107 | 108 | 109 | ####2、server 注册 110 | 111 | server 的注册调用了 RegisterGreeterServer 方法,这个方法是 pb.go 文件里面的,如下: 112 | 113 | func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { 114 | s.RegisterService(&_Greeter_serviceDesc, srv) 115 | } 116 | 117 | 这个方法调用了 server 的 RegisterService 方法,然后传入了一个 ServiceDesc 的数据结构,如下 : 118 | 119 | var _Greeter_serviceDesc = grpc.ServiceDesc{ 120 | ServiceName: "helloworld.Greeter", 121 | HandlerType: (*GreeterServer)(nil), 122 | Methods: []grpc.MethodDesc{ 123 | { 124 | MethodName: "SayHello", 125 | Handler: _Greeter_SayHello_Handler, 126 | }, 127 | { 128 | MethodName: "SayHelloAgain", 129 | Handler: _Greeter_SayHelloAgain_Handler, 130 | }, 131 | }, 132 | Streams: []grpc.StreamDesc{}, 133 | Metadata: "helloworld.proto", 134 | } 135 | 136 | 我们来看看 RegisterService 这个方法,可以看到主要是调用了 register 方法,register 方法则按照方法名为 key,将方法注入到 server 的 service map 中。看到这里我们其实可以预测一下,server 不同 rpc 请求的处理,也是根据 service 中不同的 serviceName 去 service map 中取出不同的 handler 进行处理 137 | 138 | func (s *Server) RegisterService(sd *ServiceDesc, ss interface{}) { 139 | ht := reflect.TypeOf(sd.HandlerType).Elem() 140 | st := reflect.TypeOf(ss) 141 | if !st.Implements(ht) { 142 | grpclog.Fatalf("grpc: Server.RegisterService found the handler of type %v that does not satisfy %v", st, ht) 143 | } 144 | s.register(sd, ss) 145 | } 146 | 147 | func (s *Server) register(sd *ServiceDesc, ss interface{}) { 148 | s.mu.Lock() 149 | defer s.mu.Unlock() 150 | s.printf("RegisterService(%q)", sd.ServiceName) 151 | if s.serve { 152 | grpclog.Fatalf("grpc: Server.RegisterService after Server.Serve for %q", sd.ServiceName) 153 | } 154 | if _, ok := s.m[sd.ServiceName]; ok { 155 | grpclog.Fatalf("grpc: Server.RegisterService found duplicate service registration for %q", sd.ServiceName) 156 | } 157 | srv := &service{ 158 | server: ss, 159 | md: make(map[string]*MethodDesc), 160 | sd: make(map[string]*StreamDesc), 161 | mdata: sd.Metadata, 162 | } 163 | for i := range sd.Methods { 164 | d := &sd.Methods[i] 165 | srv.md[d.MethodName] = d 166 | } 167 | for i := range sd.Streams { 168 | d := &sd.Streams[i] 169 | srv.sd[d.StreamName] = d 170 | } 171 | s.m[sd.ServiceName] = srv 172 | } 173 | 174 | ####3、Serve 过程 175 | 176 | 回想所有 C/S 模式下,client 和 server 的通信基本是类似的。大致过程无非是 server 通过死循环的方式在某一个端口实现监听,然后 client 对这个端口发起连接请求,握手成功后建立连接,然后 server 处理 client 发送过来的请求数据,根据请求类型和请求参数,调用不同的 handler 进行处理,回写响应数据。 177 | 178 | 所以,对 server 端来说,主要是了解其如何实现监听,如何为请求分配不同的 handler 和 回写响应数据。 179 | 180 | 上面我们得知 server 调用了 Serve 方法来进行处理,所以立马就想跟进去看看。 181 | 跳过前面一堆条件检查和控制代码,直接锁定了一个 for 循环,如下:(中间的一些代码已省略) 182 | 183 | 184 | for { 185 | rawConn, err := lis.Accept() 186 | 187 | ...... 188 | 189 | s.serveWG.Add(1) 190 | go func() { 191 | s.handleRawConn(rawConn) 192 | s.serveWG.Done() 193 | }() 194 | } 195 | 196 | ok,我们已经看到了监听过程,server 的监听果然是通过一个死循环 调用了 lis.Accept() 进行端口监听。 197 | 198 | 继续往下看,我们发现新起协程调用了 handleRawConn 这个方法,为了节约篇幅,我们直接看重点代码,如下: 199 | 200 | func (s *Server) handleRawConn(rawConn net.Conn) { 201 | 202 | ... 203 | 204 | conn, authInfo, err := s.useTransportAuthenticator(rawConn) 205 | 206 | ... 207 | 208 | // Finish handshaking (HTTP2) 209 | st := s.newHTTP2Transport(conn, authInfo) 210 | if st == nil { 211 | return 212 | } 213 | ... 214 | 215 | go func() { 216 | s.serveStreams(st) 217 | s.removeConn(st) 218 | }() 219 | } 220 | 221 | 可以看到 handleRawConn 里面实现了 http 的 handshake,还记得之前我们说过,grpc 是基于 http2 实现的吗?这里是不是实锤了....... 发现又通过一个新的协程调用了 serveStreams 这个方法,这个方法干了啥呢? 222 | 223 | func (s *Server) serveStreams(st transport.ServerTransport) { 224 | defer st.Close() 225 | var wg sync.WaitGroup 226 | st.HandleStreams(func(stream *transport.Stream) { 227 | wg.Add(1) 228 | go func() { 229 | defer wg.Done() 230 | s.handleStream(st, stream, s.traceInfo(st, stream)) 231 | }() 232 | }, func(ctx context.Context, method string) context.Context { 233 | if !EnableTracing { 234 | return ctx 235 | } 236 | tr := trace.New("grpc.Recv."+methodFamily(method), method) 237 | return trace.NewContext(ctx, tr) 238 | }) 239 | wg.Wait() 240 | } 241 | 242 | 其实它主要调用了 handleStream ,继续跟进 handleStream 方法,我们发现了重要线索,如下(省略了部分无关代码) 243 | 244 | func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Stream, trInfo *traceInfo) { 245 | sm := stream.Method() 246 | 247 | ... 248 | 249 | service := sm[:pos] 250 | method := sm[pos+1:] 251 | 252 | srv, knownService := s.m[service] 253 | if knownService { 254 | if md, ok := srv.md[method]; ok { 255 | s.processUnaryRPC(t, stream, srv, md, trInfo) 256 | return 257 | } 258 | if sd, ok := srv.sd[method]; ok { 259 | s.processStreamingRPC(t, stream, srv, sd, trInfo) 260 | return 261 | } 262 | } 263 | 264 | ... 265 | } 266 | 267 | 268 | 重要线索就是这一行 269 | 270 | srv, knownService := s.m[service] 271 | 272 | 还记得我们之前的预测吗?根据 serviceName 去 server 中的 service map,也就是 m 这个字段,里面去取出 handler 进行处理。我们 hello world 这个 demo 的请求不涉及到 stream ,所以直接取出 handler ,然后传给 processUnaryRPC 这个方法进行处理。 273 | 274 | 275 | if md, ok := srv.md[method]; ok { 276 | s.processUnaryRPC(t, stream, srv, md, trInfo) 277 | return 278 | } 279 | 280 | 再来看看 processUnaryRpc 这个方法 281 | 282 | func (s *Server) processUnaryRPC(t transport.ServerTransport, stream *transport.Stream, srv *service, md *MethodDesc, trInfo *traceInfo) (err error) { 283 | 284 | ... 285 | 286 | sh := s.opts.statsHandler 287 | if sh != nil { 288 | beginTime := time.Now() 289 | begin := &stats.Begin{ 290 | BeginTime: beginTime, 291 | } 292 | sh.HandleRPC(stream.Context(), begin) 293 | defer func() { 294 | end := &stats.End{ 295 | BeginTime: beginTime, 296 | EndTime: time.Now(), 297 | } 298 | if err != nil && err != io.EOF { 299 | end.Error = toRPCErr(err) 300 | } 301 | sh.HandleRPC(stream.Context(), end) 302 | }() 303 | } 304 | 305 | ... 306 | 307 | if err := s.sendResponse(t, stream, reply, cp, opts, comp); err != nil { 308 | if err == io.EOF { 309 | // The entire stream is done (for unary RPC only). 310 | return err 311 | } 312 | if s, ok := status.FromError(err); ok { 313 | if e := t.WriteStatus(stream, s); e != nil { 314 | grpclog.Warningf("grpc: Server.processUnaryRPC failed to write status: %v", e) 315 | } 316 | } else { 317 | switch st := err.(type) { 318 | case transport.ConnectionError: 319 | // Nothing to do here. 320 | default: 321 | panic(fmt.Sprintf("grpc: Unexpected error (%T) from sendResponse: %v", st, st)) 322 | } 323 | } 324 | if binlog != nil { 325 | h, _ := stream.Header() 326 | binlog.Log(&binarylog.ServerHeader{ 327 | Header: h, 328 | }) 329 | binlog.Log(&binarylog.ServerTrailer{ 330 | Trailer: stream.Trailer(), 331 | Err: appErr, 332 | }) 333 | } 334 | return err 335 | } 336 | 337 | ... 338 | } 339 | 340 | 我们终于看到了 handler 对 rpc 的处理: 341 | 342 | sh := s.opts.statsHandler 343 | sh.HandleRPC(stream.Context(), begin) 344 | sh.HandleRPC(stream.Context(), end) 345 | 346 | 同时也看到了 response 的回写 347 | 348 | s.sendResponse(t, stream, reply, cp, opts, comp) 349 | 350 | 至此,server 端我们的目标实现,追踪到了整个请求和监听、handler 处理 和 response 回写的过程。 351 | 352 | 353 | 354 | 355 | -------------------------------------------------------------------------------- /8-grpc 认证鉴权——oauth2认证.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-auth-oauth2/ 2 | > 最新版本请访问原文链接 3 | 4 | ## grpc 认证鉴权 —— oauth2 5 | 6 | 前面我们说了 tls 认证,tls 保证了 client 和 server 通信的安全性,但是无法做到接口级别的权限控制。例如有 A、B、C、D 四个系统,存在下面两个场景: 7 | 1、我们希望 A 可以访问 B、C 系统,但是不能访问 D 系统 8 | 2、B 系统提供了 b1、b2、b3 三个接口,我们希望 A 系统可以访问 b1、b2 接口,但是不能访问 b3 接口。 9 | 此时 tls 认证肯定是无法实现上面两个诉求的,对于这两个场景,grpc 提供了 oauth2 的认证方式。对 oauth2 不了解的同学可以参考 http://www.ruanyifeng.com/blog/2019/04/oauth_design.html 10 | 11 | ### oauth2 认证鉴权实现 12 | grpc 官方提供了对 oauth2 认证鉴权的实现 demo,放在 examples 目录的 features 目录的 authentication 目录下,我们来看一下源码实现 13 | 14 | #### server 15 | server 端源码实现如下: 16 | 17 | func main() { 18 | flag.Parse() 19 | fmt.Printf("server starting on port %d...\n", *port) 20 | 21 | cert, err := tls.LoadX509KeyPair(testdata.Path("server1.pem"), testdata.Path("server1.key")) 22 | if err != nil { 23 | log.Fatalf("failed to load key pair: %s", err) 24 | } 25 | opts := []grpc.ServerOption{ 26 | // The following grpc.ServerOption adds an interceptor for all unary 27 | // RPCs. To configure an interceptor for streaming RPCs, see: 28 | // https://godoc.org/google.golang.org/grpc#StreamInterceptor 29 | grpc.UnaryInterceptor(ensureValidToken), 30 | // Enable TLS for all incoming connections. 31 | grpc.Creds(credentials.NewServerTLSFromCert(&cert)), 32 | } 33 | s := grpc.NewServer(opts...) 34 | ecpb.RegisterEchoServer(s, &ecServer{}) 35 | lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) 36 | if err != nil { 37 | log.Fatalf("failed to listen: %v", err) 38 | } 39 | if err := s.Serve(lis); err != nil { 40 | log.Fatalf("failed to serve: %v", err) 41 | } 42 | } 43 | 44 | server 端先调用了 tls 包下的 LoadX509KeyPair,通过 server 的公钥和私钥生成了一个 Certificate 结构体来保存证书信息。然后注册了一个校验 token 的方法到拦截器中,并将证书信息设置到 serverOption 中,构造 server 的时候层层透传进去,最终会被设置到 Server 里面 ServerOptions 结构中的 credentials.TransportCredentials 和 UnaryServerInterceptor 中。 45 | 46 | 我们来看看这两个结构什么时候会被调用,先梳理调用链路,在 s.Serve ——> s.handleRawConn ——> s.serveStreams ——> s.handleStream ——> s.processUnaryRPC 方法中有一行 47 | 48 | reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt) 49 | 50 | 可以看到调用了 md.Handler 方法,将 s.opts.unaryInt 这个结构传入了进去。s.opts.unaryInt 就是我们之前注册的 UnaryServerInterceptor 拦截器。md 是一个 MethodDesc 这个结构,包括了 MethodName 和 Handler 51 | 52 | type MethodDesc struct { 53 | MethodName string 54 | Handler methodHandler 55 | } 56 | 57 | 这里会取出我们之前注册进去的结构,还记得我们介绍 helloworld 时 RegisterService 吗?至于如何取出 MethodName,源码中的设计非常复杂,经过了层层包装,这里不是本节重点就不赘述了。 58 | 59 | func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { 60 | s.RegisterService(&_Greeter_serviceDesc, srv) 61 | } 62 | 63 | var _Greeter_serviceDesc = grpc.ServiceDesc{ 64 | ServiceName: "helloworld.Greeter", 65 | HandlerType: (*GreeterServer)(nil), 66 | Methods: []grpc.MethodDesc{ 67 | { 68 | MethodName: "SayHello", 69 | Handler: _Greeter_SayHello_Handler, 70 | }, 71 | }, 72 | Streams: []grpc.StreamDesc{}, 73 | Metadata: "helloworld.proto", 74 | } 75 | 76 | 我们看到 md.Handler 其实是 _Greeter_SayHello_Handler 这个结构,它也是在 pb 文件中生成的。 77 | 78 | func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 79 | in := new(HelloRequest) 80 | if err := dec(in); err != nil { 81 | return nil, err 82 | } 83 | if interceptor == nil { 84 | return srv.(GreeterServer).SayHello(ctx, in) 85 | } 86 | info := &grpc.UnaryServerInfo{ 87 | Server: srv, 88 | FullMethod: "/helloworld.Greeter/SayHello", 89 | } 90 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 91 | return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) 92 | } 93 | return interceptor(ctx, in, info, handler) 94 | } 95 | 96 | 这里调用了我们传入的 interceptor 方法。回到我们的调用: 97 | 98 | reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt) 99 | 100 | 所以其实是调用了 s.opts.unaryInt 这个拦截器。这个拦截器是我们之前在 创建 server 的时候赋值的。 101 | 102 | opts := []grpc.ServerOption{ 103 | // The following grpc.ServerOption adds an interceptor for all unary 104 | // RPCs. To configure an interceptor for streaming RPCs, see: 105 | // https://godoc.org/google.golang.org/grpc#StreamInterceptor 106 | grpc.UnaryInterceptor(ensureValidToken), 107 | // Enable TLS for all incoming connections. 108 | grpc.Creds(credentials.NewServerTLSFromCert(&cert)), 109 | } 110 | s := grpc.NewServer(opts...) 111 | 112 | 看 grpc.UnaryInterceptor 这个方法,其实是将 ensureValidToken 这个函数赋值给了 s.opts.unaryInt 113 | 114 | func UnaryInterceptor(i UnaryServerInterceptor) ServerOption { 115 | return newFuncServerOption(func(o *serverOptions) { 116 | if o.unaryInt != nil { 117 | panic("The unary server interceptor was already set and may not be reset.") 118 | } 119 | o.unaryInt = i 120 | }) 121 | } 122 | 123 | 所以之前我们执行的这一行 124 | 125 | return interceptor(ctx, in, info, handler) 126 | 127 | 其实是执行了 ensureValidToken 这个函数,这个函数就是我们在 server 端定义的 token 校验的函数。先取出我们传入的 metadata 数据,然后校验 token 128 | 129 | func ensureValidToken(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 130 | md, ok := metadata.FromIncomingContext(ctx) 131 | if !ok { 132 | return nil, errMissingMetadata 133 | } 134 | // The keys within metadata.MD are normalized to lowercase. 135 | // See: https://godoc.org/google.golang.org/grpc/metadata#New 136 | if !valid(md["authorization"]) { 137 | return nil, errInvalidToken 138 | } 139 | // Continue execution of handler after ensuring a valid token. 140 | return handler(ctx, req) 141 | } 142 | 143 | 校验完 token 后,最终执行了 handler(ctx, req) 144 | 145 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 146 | return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) 147 | } 148 | return interceptor(ctx, in, info, handler) 149 | 150 | 可以看到最终其实执行了 GreeterServer 的 SayHello 这个函数,也就是我们在 main 函数中定义的,这个函数就是我们在 server 端定义的提供 SayHello 给客户端回消息的函数。 151 | 152 | // SayHello implements helloworld.GreeterServer 153 | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { 154 | log.Printf("Received: %v", in.Name) 155 | return &pb.HelloReply{Message: "Hello " + in.Name}, nil 156 | } 157 | 158 | 这里还可以额外说一下,md.Handler 执行完之后,其实 reply 就是 SayHello 的回包。 159 | 160 | reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt) 161 | 162 | 获取到回包之后 server 执行了 sendResponse 方法,将回包发送给 client,这个方法我们之前已经剖析过了,最终会调用 http2Server 的 Write 方法。 163 | 164 | if err := s.sendResponse(t, stream, reply, cp, opts, comp); err != nil { 165 | 166 | 看到这里,server 端对 token 的校验在哪里执行的我们已经清楚了。假如还没有被绕晕,那么恭喜你!可以继续完成 client 的挑战了。 167 | 168 | #### client 169 | 在 client 中,先看 main 函数 170 | 171 | // Set up the credentials for the connection. 172 | perRPC := oauth.NewOauthAccess(fetchToken()) 173 | creds, err := credentials.NewClientTLSFromFile(testdata.Path("ca.pem"), "x.test.youtube.com") 174 | if err != nil { 175 | log.Fatalf("failed to load credentials: %v", err) 176 | } 177 | opts := []grpc.DialOption{ 178 | // In addition to the following grpc.DialOption, callers may also use 179 | // the grpc.CallOption grpc.PerRPCCredentials with the RPC invocation 180 | // itself. 181 | // See: https://godoc.org/google.golang.org/grpc#PerRPCCredentials 182 | grpc.WithPerRPCCredentials(perRPC), 183 | // oauth.NewOauthAccess requires the configuration of transport 184 | // credentials. 185 | grpc.WithTransportCredentials(creds), 186 | } 187 | 188 | conn, err := grpc.Dial(*addr, opts...) 189 | 190 | 可以看到 client 首先通过 NewOauthAccess 方法生成了包含 token 信息的 PerRPCCredentials 结构 191 | 192 | func NewOauthAccess(token *oauth2.Token) credentials.PerRPCCredentials { 193 | return oauthAccess{token: *token} 194 | } 195 | 196 | 然后再将 PerRPCCredentials 通过 grpc.WithPerRPCCredentials(perRPC) 添加到了到了 client 的 DialOptions 中的 transport.ConnectOptions 结构中的 [] credentials.PerRPCCredentials 结构中。 197 | 198 | 那么这个结构什么时候被使用呢,我们来看看。先梳理下调用链 ,在 client 调用的 Invoke ——> invoke ——> newClientStream ——> cs.newAttemptLocked ——> cs.cc.getTransport ——> pick ——> acw.getAddrConn().getReadyTransport() ——> ac.connect() ——> ac.resetTransport() ——> ac.tryAllAddrs ——> ac.createTransport ——> transport.NewClientTransport ——> newHTTP2Client 这个方法里面,有这么一段代码,先取出 []credentials.PerRPCCredentials 中的所有 PerRPCCredentials 添加到 perRPCCreds 中。 199 | 200 | transportCreds := opts.TransportCredentials 201 | perRPCCreds := opts.PerRPCCredentials 202 | 203 | if b := opts.CredsBundle; b != nil { 204 | if t := b.TransportCredentials(); t != nil { 205 | transportCreds = t 206 | } 207 | if t := b.PerRPCCredentials(); t != nil { 208 | perRPCCreds = append(perRPCCreds, t) 209 | } 210 | } 211 | 212 | 然后再将 perRPCCreds 赋值给 http2Client 的 perRPCCreds 属性 213 | 214 | t := &http2Client{ 215 | 216 | ... 217 | 218 | perRPCCreds: perRPCCreds, 219 | 220 | ... 221 | } 222 | 223 | 那么 perRPCCreds 属性什么时候被用呢?来继续跟踪,newClientStream 方法中有一段代码 224 | 225 | op := func(a *csAttempt) error { return a.newStream() } 226 | if err := cs.withRetry(op, func() { cs.bufferForRetryLocked(0, op) }); err != nil { 227 | cs.finish(err) 228 | return nil, err 229 | } 230 | 231 | 这里调用了 csAttempt 的 newStream ——> a.t.NewStream (http2Client 的 NewStream) ——> createHeaderFields ——> getTrAuthData 方法 232 | 233 | func (t *http2Client) getTrAuthData(ctx context.Context, audience string) (map[string]string, error) { 234 | if len(t.perRPCCreds) == 0 { 235 | return nil, nil 236 | } 237 | authData := map[string]string{} 238 | for _, c := range t.perRPCCreds { 239 | data, err := c.GetRequestMetadata(ctx, audience) 240 | if err != nil { 241 | if _, ok := status.FromError(err); ok { 242 | return nil, err 243 | } 244 | 245 | return nil, status.Errorf(codes.Unauthenticated, "transport: %v", err) 246 | } 247 | for k, v := range data { 248 | // Capital header names are illegal in HTTP/2. 249 | k = strings.ToLower(k) 250 | authData[k] = v 251 | } 252 | } 253 | return authData, nil 254 | } 255 | 256 | 这个方法,通过调用 GetRequestMetadata 取出 token 信息,这里会调用 oauth 的 GetRequestMetadata 方法 ,按照指定格式拼装成一个 map[string]string{} 的形式 257 | 258 | func (s *serviceAccount) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { 259 | s.mu.Lock() 260 | defer s.mu.Unlock() 261 | if !s.t.Valid() { 262 | var err error 263 | s.t, err = s.config.TokenSource(ctx).Token() 264 | if err != nil { 265 | return nil, err 266 | } 267 | } 268 | return map[string]string{ 269 | "authorization": s.t.Type() + " " + s.t.AccessToken, 270 | }, nil 271 | } 272 | 273 | 然后将以 map[string]string{} 的形式组装成一个 string map 返回,如下: 274 | 275 | for k, v := range authData { 276 | headerFields = append(headerFields, hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)}) 277 | } 278 | 279 | 返回的 map 会被遍历每个 key,并设置到 headerFields 中,以 http 头部的形式发送出去。数据最终会被 metadata.FromIncomingContext(ctx) 获取到,然后被取出 map 数据。 280 | 281 | 282 | 至此,client 和 server 的数据流转过程被打通 283 | -------------------------------------------------------------------------------- /5-grpc 服务发现.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-service-discovery/ 2 | > 最新版本请访问原文链接 3 | 4 | ### 服务发现 5 | 在了解 grpc 服务发现之前,我们先来了解一下服务发现的路由方式。一般来说,我们有客户端路由和代理层路由两种方式。 6 | 7 | #### 客户端路由 8 | 客户端路由模式,也就是调用方负责获取被调用方的地址信息,并使用相应的负载均衡算法发起请求。调用方访问服务注册服务,获取对应的服务 IP 地址和端口,可能还包括对应的服务负载信息(负载均衡算法、服务实例权重等)。调用方通过负载均衡算法选取其中一个发起请求。如下: 9 | 10 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190817131529906.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2RpdWJyb3RoZXI=,size_16,color_FFFFFF,t_70) 11 | server 启动的时候向 config server 注册自身的服务地址,server 正常退出的时候调用接口移除自身地址,通过定时心跳保证服务是否正常以及地址的有效性。 12 | 13 | #### 代理层路由 14 | 代理层路由,不是由调用方去获取被调方的地址,而是通过代理的方式,由代理去获取被调方的地址、发起调用请求。如下: 15 | 16 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190817132105444.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2RpdWJyb3RoZXI=,size_16,color_FFFFFF,t_70) 17 | 代理层路由这种模式,对 server 的寻址不再是由 client 去实现,而是由代理去实现。client 只是会对代理层发起简单请求,代理层去进行 server 寻址、负载均衡等。 18 | 19 | grpc 官方介绍的服务发现流程如下: 20 | 21 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190817134928571.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2RpdWJyb3RoZXI=,size_16,color_FFFFFF,t_70) 22 | 由这张图可以看出,grpc 是使用客户端路由的方式。具体的过程我们在介绍负载均衡时再继续介绍 23 | 24 | 25 | ### grpc 服务发现 26 | 之前在介绍 grpc client 时谈到了 resolver 这个类,对下面这段代码并未做详细介绍,我们来详细看看 27 | 28 | if cc.dopts.resolverBuilder == nil { 29 | // Only try to parse target when resolver builder is not already set. 30 | cc.parsedTarget = parseTarget(cc.target) 31 | grpclog.Infof("parsed scheme: %q", cc.parsedTarget.Scheme) 32 | cc.dopts.resolverBuilder = resolver.Get(cc.parsedTarget.Scheme) 33 | if cc.dopts.resolverBuilder == nil { 34 | // If resolver builder is still nil, the parsed target's scheme is 35 | // not registered. Fallback to default resolver and set Endpoint to 36 | // the original target. 37 | grpclog.Infof("scheme %q not registered, fallback to default scheme", cc.parsedTarget.Scheme) 38 | cc.parsedTarget = resolver.Target{ 39 | Scheme: resolver.GetDefaultScheme(), 40 | Endpoint: target, 41 | } 42 | cc.dopts.resolverBuilder = resolver.Get(cc.parsedTarget.Scheme) 43 | } 44 | } else { 45 | cc.parsedTarget = resolver.Target{Endpoint: target} 46 | } 47 | 48 | 这段代码主要干了两件事情,parseTarget 和 resolver.Get 获取了一个 resolverBuilder 49 | 50 | parseTarget 其实就是将 target 赋值给了 resolver target 对象的 endpoint 属性,如下 51 | 52 | func parseTarget(target string) (ret resolver.Target) { 53 | var ok bool 54 | ret.Scheme, ret.Endpoint, ok = split2(target, "://") 55 | if !ok { 56 | return resolver.Target{Endpoint: target} 57 | } 58 | ret.Authority, ret.Endpoint, ok = split2(ret.Endpoint, "/") 59 | if !ok { 60 | return resolver.Target{Endpoint: target} 61 | } 62 | return ret 63 | } 64 | 65 | 这里来看 resolver.Get 方法 ,这里从一个 map 中取出了一个 Builder 66 | 67 | var ( 68 | // m is a map from scheme to resolver builder. 69 | m = make(map[string]Builder) 70 | // defaultScheme is the default scheme to use. 71 | defaultScheme = "passthrough" 72 | ) 73 | 74 | func Get(scheme string) Builder { 75 | if b, ok := m[scheme]; ok { 76 | return b 77 | } 78 | return nil 79 | } 80 | 81 | Builder 是在 resolver 中定义的,在了解 Builder 是啥之前,我们先来看看 resolver 这个结构体 82 | 83 | ### resolver 84 | 85 | // Package resolver defines APIs for name resolution in gRPC. 86 | // All APIs in this package are experimental. 87 | 88 | resolver 主要提供了一个名字解析的规范,所有的名字解析服务可以实现这个规范,包括 dns 解析类 dns_resolver 就是实现了这个规范的一个解析器。 89 | 90 | resolver 中定义了 Builder ,通过调用 Build 去获取一个 resolver 实例 91 | 92 | // Builder creates a resolver that will be used to watch name resolution updates. 93 | type Builder interface { 94 | // Build creates a new resolver for the given target. 95 | // 96 | // gRPC dial calls Build synchronously, and fails if the returned error is 97 | // not nil. 98 | Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error) 99 | // Scheme returns the scheme supported by this resolver. 100 | // Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md. 101 | Scheme() string 102 | } 103 | 104 | 我们在调用 Dial 方法发起 rpc 请求之前需要创建一个 ClientConn 连接,在 DialContext 这个方法中对 ClientConn 各属性进行了赋值,其中有一行代码就完成了 build resolver 的工作。 105 | 106 | // Build the resolver. 107 | rWrapper, err := newCCResolverWrapper(cc) 108 | 109 | func newCCResolverWrapper(cc *ClientConn) (*ccResolverWrapper, error) { 110 | rb := cc.dopts.resolverBuilder 111 | if rb == nil { 112 | return nil, fmt.Errorf("could not get resolver for scheme: %q", cc.parsedTarget.Scheme) 113 | } 114 | 115 | ccr := &ccResolverWrapper{ 116 | cc: cc, 117 | addrCh: make(chan []resolver.Address, 1), 118 | scCh: make(chan string, 1), 119 | } 120 | 121 | var err error 122 | ccr.resolver, err = rb.Build(cc.parsedTarget, ccr, resolver.BuildOption{DisableServiceConfig: cc.dopts.disableServiceConfig}) 123 | if err != nil { 124 | return nil, err 125 | } 126 | return ccr, nil 127 | } 128 | 129 | 不出意料,我们之前通过 get 去获取了一个 Builder, 这里调用了 Builder 的 Build 方法产生一个 resolver。 130 | 131 | ### register() 132 | 133 | 上面我们说到了,resolver 通过 get 方法,根据一个 string key 去一个 builder map 中获取一个 builder,这个 map 在 resolver 中初始化如下,那么是怎么进行赋值的呢? 134 | 135 | var ( 136 | // m is a map from scheme to resolver builder. 137 | m = make(map[string]Builder) 138 | // defaultScheme is the default scheme to use. 139 | defaultScheme = "passthrough" 140 | ) 141 | 142 | 我们猜测肯定会有一个服务注册的过程,果然看到了一个 Register 方法 143 | 144 | func Register(b Builder) { 145 | m[b.Scheme()] = b 146 | } 147 | 148 | 所有的 resolver 实现类通过 Register 方法去实现 Builder 的注册,比如 grpc 提供的 dnsResolver 这个类中调用了 init 方法,在服务初始化时实现了 Builder 的注册 149 | 150 | func init() { 151 | resolver.Register(NewBuilder()) 152 | } 153 | 154 | ### 获取服务地址 155 | resolver 和 builder 都是 interface,也就是说它们只是定义了一套规则。具体实现由实现他们的子类去完成。例如在 helloworld 例子中,默认是通过默认的 passthrough 这个 scheme 去获取的 passthroughResolver 和 passthroughBuilder,我们来看 passthroughBuilder 的 Build 方法返回了一个带有 address 的 resolver,这个地址就是 server 的地址列表。在 helloworld demo 中,就是 “localhost:50051”。 156 | 157 | func (*passthroughBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) { 158 | r := &passthroughResolver{ 159 | target: target, 160 | cc: cc, 161 | } 162 | r.start() 163 | return r, nil 164 | } 165 | 166 | func (r *passthroughResolver) start() { 167 | r.cc.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: r.target.Endpoint}}}) 168 | } 169 | 170 | 171 | 172 | ### dns_resolver 173 | grpc 支持自定义 resolver 实现服务发现。同时 grpc 官方提供了一个基于 dns 的服务发现 resolver,这就是 dns_resolver,dns_resolver 通过 Build() 创建一个 resolver 实例,我们来具体看一下 Build() 方法: 174 | 175 | // Build creates and starts a DNS resolver that watches the name resolution of the target. 176 | func (b *dnsBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) { 177 | host, port, err := parseTarget(target.Endpoint, defaultPort) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | // IP address. 183 | if net.ParseIP(host) != nil { 184 | host, _ = formatIP(host) 185 | addr := []resolver.Address{{Addr: host + ":" + port}} 186 | i := &ipResolver{ 187 | cc: cc, 188 | ip: addr, 189 | rn: make(chan struct{}, 1), 190 | q: make(chan struct{}), 191 | } 192 | cc.NewAddress(addr) 193 | go i.watcher() 194 | return i, nil 195 | } 196 | 197 | // DNS address (non-IP). 198 | ctx, cancel := context.WithCancel(context.Background()) 199 | d := &dnsResolver{ 200 | freq: b.minFreq, 201 | backoff: backoff.Exponential{MaxDelay: b.minFreq}, 202 | host: host, 203 | port: port, 204 | ctx: ctx, 205 | cancel: cancel, 206 | cc: cc, 207 | t: time.NewTimer(0), 208 | rn: make(chan struct{}, 1), 209 | disableServiceConfig: opts.DisableServiceConfig, 210 | } 211 | 212 | if target.Authority == "" { 213 | d.resolver = defaultResolver 214 | } else { 215 | d.resolver, err = customAuthorityResolver(target.Authority) 216 | if err != nil { 217 | return nil, err 218 | } 219 | } 220 | 221 | d.wg.Add(1) 222 | go d.watcher() 223 | return d, nil 224 | } 225 | 226 | 在 Build 方法中,我们没有看到对 server address 寻址的过程,仔细找找,发现了一个 watcher 方法,如下: 227 | 228 | go d.watcher() 229 | 230 | 看一下 watcher 方法,发现它其实是一个监控进程,顾名思义作用是监控我们产生的 resolver 的状态,这里使用了一个 for 循环无限监听,通过 chan 进行消息通知。 231 | 232 | func (d *dnsResolver) watcher() { 233 | defer d.wg.Done() 234 | for { 235 | select { 236 | case <-d.ctx.Done(): 237 | return 238 | case <-d.t.C: 239 | case <-d.rn: 240 | if !d.t.Stop() { 241 | // Before resetting a timer, it should be stopped to prevent racing with 242 | // reads on it's channel. 243 | <-d.t.C 244 | } 245 | } 246 | 247 | result, sc := d.lookup() 248 | // Next lookup should happen within an interval defined by d.freq. It may be 249 | // more often due to exponential retry on empty address list. 250 | if len(result) == 0 { 251 | d.retryCount++ 252 | d.t.Reset(d.backoff.Backoff(d.retryCount)) 253 | } else { 254 | d.retryCount = 0 255 | d.t.Reset(d.freq) 256 | } 257 | d.cc.NewServiceConfig(sc) 258 | d.cc.NewAddress(result) 259 | 260 | // Sleep to prevent excessive re-resolutions. Incoming resolution requests 261 | // will be queued in d.rn. 262 | t := time.NewTimer(minDNSResRate) 263 | select { 264 | case <-t.C: 265 | case <-d.ctx.Done(): 266 | t.Stop() 267 | return 268 | } 269 | } 270 | } 271 | 272 | 我们定位到里面的 lookup 方法。 273 | 274 | result, sc := d.lookup() 275 | 276 | 进入 lookup 方法,发现它调用了 lookupSRV 这个方法 277 | 278 | func (d *dnsResolver) lookup() ([]resolver.Address, string) { 279 | newAddrs := d.lookupSRV() 280 | // Support fallback to non-balancer address. 281 | newAddrs = append(newAddrs, d.lookupHost()...) 282 | if d.disableServiceConfig { 283 | return newAddrs, "" 284 | } 285 | sc := d.lookupTXT() 286 | return newAddrs, canaryingSC(sc) 287 | } 288 | 289 | 继续追踪,lookupSRV 这个方法最终其实调用了 go 源码包 net 包下的 的 lookupSRV 方法,这个方法实现了 dns 协议对指定的service服务,protocol协议以及name域名进行srv查询,返回server 的 address 列表。经过层层解剖,我们终于找到了返回 server 的 address list 的代码。 290 | 291 | _, srvs, err := d.resolver.LookupSRV(d.ctx, "grpclb", "tcp", d.host) 292 | 293 | ... 294 | 295 | func (r *Resolver) LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*SRV, err error) { 296 | return r.lookupSRV(ctx, service, proto, name) 297 | } 298 | 299 | ### 总结 300 | 301 | 总结一下, grpc 的服务发现,主要通过 resolver 接口去定义,支持业务自己实现服务发现的 resolver。 grpc 提供了默认的 passthrough_resolver,不进行地址解析,直接将 client 发起请求时指定的 address (例如 helloworld client 指定地址为 “localhost:50051” )当成 server address。同时,假如业务使用 dns 进行服务发现,grpc 提供了 dns_resolver,通过对指定的service服务,protocol协议以及name域名进行srv查询,来返回 server 的 address 列表。 302 | 303 | 304 | -------------------------------------------------------------------------------- /6-grpc 负载均衡.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-load-balancing/ 2 | > 最新版本请访问原文链接 3 | 4 | ### grpc负载均衡 5 | #### 负载均衡流程 6 | grpc 官方的 doc 中介绍了,grpc 的负载均衡是基于一次请求而不是一次连接的。也就是说,假如所有的请求都来自同一个客户端的连接,这些请求还是会被均衡到所有服务器。 7 | 8 | 整个 grpc 负载均衡流程如下图: 9 | 10 | ![ grpc 负载均衡](https://img-blog.csdnimg.cn/20190818144901158.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2RpdWJyb3RoZXI=,size_16,color_FFFFFF,t_70) 11 | 1、启动时,grpc client 通过服名字解析服务得到一个 address list,每个 address 将指示它是服务器地址还是负载平衡器地址,以及指示要哪个客户端负载平衡策略的服务配置(例如,round_robin 或 grpclb) 12 | 13 | 2、客户端实例化负载均衡策略 14 | 如果解析程序返回的任何一个地址是负载均衡器地址,则无论 service config 中定义了什么负载均衡策略,客户端都将使用grpclb策略。否则,客户端将使用 service config 中定义的负载均衡策略。如果服务配置未请求负载均衡策略,则客户端将默认使用选择第一个可用服务器地址的策略。 15 | 16 | 3、负载平衡策略为每个服务器地址创建一个 subchannel,假如是 grpclb 策略,客户端会根据名字解析服务返回的地址列表,请求负载均衡器,由负载均衡器决定请求哪个 subConn,然后打开一个数据流,对这个 subConn 中的所有服务器 adress 都建立连接,从而实现 client stream 的效果 17 | 18 | 4、当有rpc请求时,负载均衡策略决定哪个子通道即grpc服务器将接收请求,当可用服务器为空时客户端的请求将被阻塞。 19 | 20 | 21 | #### 源码实现 22 | 接下来我们来看一下源码里面关于 grpc 负载均衡的实现,这里主要分为初始化 balancer 和寻址两步 23 | 24 | #### 1. 初始化 balancer 25 | 26 | 之前介绍 grpc 服务发现时,我们知道了通过 dns_resolver 的 lookup 方法可以得到一个 address list,那么拿到地址列表之后具体干了什么事呢?下面我们来看看 27 | 28 | result, sc := d.lookup() 29 | // Next lookup should happen within an interval defined by d.freq. It may be 30 | // more often due to exponential retry on empty address list. 31 | if len(result) == 0 { 32 | d.retryCount++ 33 | d.t.Reset(d.backoff.Backoff(d.retryCount)) 34 | } else { 35 | d.retryCount = 0 36 | d.t.Reset(d.freq) 37 | } 38 | d.cc.NewServiceConfig(sc) 39 | d.cc.NewAddress(result) 40 | 41 | 这里调用了 NewAddress 方法,在 NewAddress 这个方法里面又调用了 updateResolverState 这个方法,对负载均衡器的初始化就是在这个方法中进行的,如下: 42 | 43 | if cc.dopts.balancerBuilder == nil { 44 | // Only look at balancer types and switch balancer if balancer dial 45 | // option is not set. 46 | var newBalancerName string 47 | if cc.sc != nil && cc.sc.lbConfig != nil { 48 | newBalancerName = cc.sc.lbConfig.name 49 | balCfg = cc.sc.lbConfig.cfg 50 | } else { 51 | var isGRPCLB bool 52 | for _, a := range s.Addresses { 53 | if a.Type == resolver.GRPCLB { 54 | isGRPCLB = true 55 | break 56 | } 57 | } 58 | if isGRPCLB { 59 | newBalancerName = grpclbName 60 | } else if cc.sc != nil && cc.sc.LB != nil { 61 | newBalancerName = *cc.sc.LB 62 | } else { 63 | newBalancerName = PickFirstBalancerName 64 | } 65 | } 66 | cc.switchBalancer(newBalancerName) 67 | } else if cc.balancerWrapper == nil { 68 | // Balancer dial option was set, and this is the first time handling 69 | // resolved addresses. Build a balancer with dopts.balancerBuilder. 70 | cc.curBalancerName = cc.dopts.balancerBuilder.Name() 71 | cc.balancerWrapper = newCCBalancerWrapper(cc, cc.dopts.balancerBuilder, cc.balancerBuildOpts) 72 | } 73 | 74 | cc.balancerWrapper.updateClientConnState(&balancer.ClientConnState{ResolverState: s, BalancerConfig: balCfg}) 75 | 76 | 之前说到了我们在 dns_resolver 中查找 address 时是通过 grpclb 去进行查找的,所以它返回的 resolver 的策略就是 grpclb 策略。这里会进入到 switchBalancer 方法,我们来看看这个方法干了啥 77 | 78 | func (cc *ClientConn) switchBalancer(name string) { 79 | 80 | builder := balancer.Get(name) 81 | 82 | ... 83 | 84 | cc.curBalancerName = builder.Name() 85 | cc.balancerWrapper = newCCBalancerWrapper(cc, builder, cc.balancerBuildOpts) 86 | } 87 | 88 | 这里通过 grpclb 这个 name 去获取到了 grpclb 策略的一个 balancer 实现,然后调用了 newCCBalancerWrapper 这个方法,继续跟踪 89 | 90 | func newCCBalancerWrapper(cc *ClientConn, b balancer.Builder, bopts balancer.BuildOptions) *ccBalancerWrapper { 91 | ccb := &ccBalancerWrapper{ 92 | cc: cc, 93 | stateChangeQueue: newSCStateUpdateBuffer(), 94 | ccUpdateCh: make(chan *balancer.ClientConnState, 1), 95 | done: make(chan struct{}), 96 | subConns: make(map[*acBalancerWrapper]struct{}), 97 | } 98 | go ccb.watcher() 99 | ccb.balancer = b.Build(ccb, bopts) 100 | return ccb 101 | } 102 | 103 | 来看一下这里的 Build 方法,去 grpclb 这个策略的实现类里面看,发现它返回了一个 lbBalancer 实例 104 | 105 | func (b *lbBuilder) Build(cc balancer.ClientConn, opt balancer.BuildOptions) balancer.Balancer { 106 | ... 107 | lb := &lbBalancer{ 108 | cc: newLBCacheClientConn(cc), 109 | target: opt.Target.Endpoint, 110 | opt: opt, 111 | fallbackTimeout: b.fallbackTimeout, 112 | doneCh: make(chan struct{}), 113 | 114 | manualResolver: r, 115 | subConns: make(map[resolver.Address]balancer.SubConn), 116 | scStates: make(map[balancer.SubConn]connectivity.State), 117 | picker: &errPicker{err: balancer.ErrNoSubConnAvailable}, 118 | clientStats: newRPCStats(), 119 | backoff: defaultBackoffConfig, // TODO: make backoff configurable. 120 | } 121 | ... 122 | return lb 123 | } 124 | 125 | #### 2. 寻址 126 | 之前我们说到了,helloworld demo 中 client 发送请求主要分为三步,对 balancer 的初始化其实是在第一步 grpc.Dial 时初始化 dialContext 时完成的。那么寻址过程,就是在第三步调用 sayHello 时完成的。 127 | 128 | 我们进入 sayHello ——> c.cc.Invoke ——> invoke ——> newClientStream 方法中,有下面一段代码: 129 | 130 | if err := cs.newAttemptLocked(sh, trInfo); err != nil { 131 | cs.finish(err) 132 | return nil, err 133 | } 134 | 135 | 进入 newAttemptLocked 方法,如下: 136 | 137 | func (cs *clientStream) newAttemptLocked(sh stats.Handler, trInfo *traceInfo) error { 138 | cs.attempt = &csAttempt{ 139 | cs: cs, 140 | dc: cs.cc.dopts.dc, 141 | statsHandler: sh, 142 | trInfo: trInfo, 143 | } 144 | 145 | if err := cs.ctx.Err(); err != nil { 146 | return toRPCErr(err) 147 | } 148 | t, done, err := cs.cc.getTransport(cs.ctx, cs.callInfo.failFast, cs.callHdr.Method) 149 | ... 150 | } 151 | 152 | 我们发现它调用了 getTransport 方法,进入这个方法,我们找到了 pick 方法的调用 153 | 154 | func (cc *ClientConn) getTransport(ctx context.Context, failfast bool, method string) (transport.ClientTransport, func(balancer.DoneInfo), error) { 155 | t, done, err := cc.blockingpicker.pick(ctx, failfast, balancer.PickOptions{ 156 | FullMethodName: method, 157 | }) 158 | if err != nil { 159 | return nil, nil, toRPCErr(err) 160 | } 161 | return t, done, nil 162 | } 163 | 164 | pick 方法即是具体寻址的方法,仔细看 pick 方法,它先 Pick 获取了一个 SubConn,SubConn 结构体中包含了一个 address list,然后它会对每一个 address 都会发送 rpc 请求。 165 | 166 | func (bp *pickerWrapper) pick(ctx context.Context, failfast bool, opts balancer.PickOptions) (transport.ClientTransport, func(balancer.DoneInfo), error) { 167 | var ch chan struct{} 168 | 169 | for { 170 | 171 | ... 172 | p := bp.picker 173 | ... 174 | subConn, done, err := p.Pick(ctx, opts) 175 | 176 | ... 177 | } 178 | } 179 | 180 | 它调用了 pickWrapper 中的 Pick 方法,在第一步初始化 balancer 的时候我们说到,它返回的其实是 lbBalancer 实例,所以这里去看 lbBalancer 实例的 Pick 实现 181 | 182 | func (p *lbPicker) Pick(ctx context.Context, opts balancer.PickOptions) (balancer.SubConn, func(balancer.DoneInfo), error) { 183 | p.mu.Lock() 184 | defer p.mu.Unlock() 185 | 186 | // Layer one roundrobin on serverList. 187 | s := p.serverList[p.serverListNext] 188 | p.serverListNext = (p.serverListNext + 1) % len(p.serverList) 189 | 190 | // If it's a drop, return an error and fail the RPC. 191 | if s.Drop { 192 | p.stats.drop(s.LoadBalanceToken) 193 | return nil, nil, status.Errorf(codes.Unavailable, "request dropped by grpclb") 194 | } 195 | 196 | // If not a drop but there's no ready subConns. 197 | if len(p.subConns) <= 0 { 198 | return nil, nil, balancer.ErrNoSubConnAvailable 199 | } 200 | 201 | // Return the next ready subConn in the list, also collect rpc stats. 202 | sc := p.subConns[p.subConnsNext] 203 | p.subConnsNext = (p.subConnsNext + 1) % len(p.subConns) 204 | done := func(info balancer.DoneInfo) { 205 | if !info.BytesSent { 206 | p.stats.failedToSend() 207 | } else if info.BytesReceived { 208 | p.stats.knownReceived() 209 | } 210 | } 211 | return sc, done, nil 212 | } 213 | 214 | 可以看到这其实是一个轮询实现。用一个指针表示这次取的位置,取过之后就更新这个指针为下一位。Pick 的返回是一个 SubConn 结构,SubConn 里面就包含了 server 的地址列表,此时寻址就完成了。 215 | 216 | 3、发起 request 217 | 218 | 寻址完成之后,我们得到了包含 server 地址列表的 SubConn,接下来是如何发送请求的呢?在 pick 方法中接着往下看,发现了这段代码。 219 | 220 | acw, ok := subConn.(*acBalancerWrapper) 221 | if !ok { 222 | grpclog.Error("subconn returned from pick is not *acBalancerWrapper") 223 | continue 224 | } 225 | if t, ok := acw.getAddrConn().getReadyTransport(); ok { 226 | if channelz.IsOn() { 227 | return t, doneChannelzWrapper(acw, done), nil 228 | } 229 | return t, done, nil 230 | } 231 | 232 | 这段代码先将 SubConn 转换成了一个 acBalancerWrapper ,然后获取其中的 addrConn 对象,接着调用 getReadyTransport 方法,如下: 233 | 234 | func (ac *addrConn) getReadyTransport() (transport.ClientTransport, bool) { 235 | ac.mu.Lock() 236 | if ac.state == connectivity.Ready && ac.transport != nil { 237 | t := ac.transport 238 | ac.mu.Unlock() 239 | return t, true 240 | } 241 | var idle bool 242 | if ac.state == connectivity.Idle { 243 | idle = true 244 | } 245 | ac.mu.Unlock() 246 | // Trigger idle ac to connect. 247 | if idle { 248 | ac.connect() 249 | } 250 | return nil, false 251 | } 252 | 253 | getReadyTransport 这个方法返回一个 Ready 状态的网络连接,假如连接状态是 IDLE 状态,会调用 connect 方法去进行客户端连接,connect 方法如下: 254 | 255 | func (ac *addrConn) connect() error { 256 | ac.mu.Lock() 257 | if ac.state == connectivity.Shutdown { 258 | ac.mu.Unlock() 259 | return errConnClosing 260 | } 261 | if ac.state != connectivity.Idle { 262 | ac.mu.Unlock() 263 | return nil 264 | } 265 | // Update connectivity state within the lock to prevent subsequent or 266 | // concurrent calls from resetting the transport more than once. 267 | ac.updateConnectivityState(connectivity.Connecting) 268 | ac.mu.Unlock() 269 | 270 | // Start a goroutine connecting to the server asynchronously. 271 | go ac.resetTransport() 272 | return nil 273 | } 274 | 275 | 通过 go ac.resetTransport() 这一行可以看到 connect 方法新起协程异步去与 server 建立连接。resetTransport 方法中有一行调用了 tryAllAddrs 方法,如下: 276 | 277 | newTr, addr, reconnect, err := ac.tryAllAddrs(addrs, connectDeadline) 278 | 279 | 我们猜测是在这个方法中去轮询 address 与 每个 address 的 server 建立连接。 280 | 281 | func (ac *addrConn) tryAllAddrs(addrs []resolver.Address, connectDeadline time.Time) (transport.ClientTransport, resolver.Address, *grpcsync.Event, error) { 282 | for _, addr := range addrs { 283 | ... 284 | 285 | newTr, reconnect, err := ac.createTransport(addr, copts, connectDeadline) 286 | if err == nil { 287 | return newTr, addr, reconnect, nil 288 | } 289 | ac.cc.blockingpicker.updateConnectionError(err) 290 | } 291 | 292 | // Couldn't connect to any address. 293 | return nil, resolver.Address{}, nil, fmt.Errorf("couldn't connect to any address") 294 | } 295 | 296 | 一看这个方法,果然如此,遍历所有地址,然后调用了 createTransport 方法,为每个地址的服务器建立连接,看到这里,我们也明白了 Stream 的实现。传统的 client 实现是对某个 server 地址发起 connect,Stream 的实质无非是对一批 server 的 address 进行轮询并建立 connect 297 | 298 | 299 | #### 总结 300 | grpc 负载均衡的实现是通过客户端路由的方式,先通过服务发现获取一个 resolver.Address 列表,resolver.Address 中包含了服务器地址和负载均衡服务名字,通过这个名字去初始化响应的 balancer,dns_resolver 中默认是使用的 grpclb 这个负载均衡器,寻址方式是轮询。通过调用 picker 去 生成一个 SubConn,SubConn 中包括服务器的地址列表,采用异步的方式对地址列表进行轮询,然后为每一个服务端地址都进行 connect 。 301 | 302 | 其实关于 resolver、balancer、picker 的实现还有很多细节部分,这里等待后续研究。 303 | -------------------------------------------------------------------------------- /4-grpc hello world client 解析.md: -------------------------------------------------------------------------------- 1 | > 本文原发表于:https://diu.life/lessons/grpc-read/grpc-hello-world-client-analysis/ 2 | > 最新版本请访问原文链接 3 | 4 | ### grpc hello world client 解析 5 | 6 | 上一节我们介绍了 grpc 输出 hello world 过程中 server 监听和处理请求的过程。这一节中我们将介绍 client 发出请求的过程。 7 | 8 | 来先看代码: 9 | 10 | func main() { 11 | // Set up a connection to the server. 12 | conn, err := grpc.Dial(address, grpc.WithInsecure()) 13 | if err != nil { 14 | log.Fatalf("did not connect: %v", err) 15 | } 16 | defer conn.Close() 17 | c := pb.NewGreeterClient(conn) 18 | 19 | // Contact the server and print out its response. 20 | name := defaultName 21 | if len(os.Args) > 1 { 22 | name = os.Args[1] 23 | } 24 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 25 | defer cancel() 26 | r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) 27 | if err != nil { 28 | log.Fatalf("could not greet: %v", err) 29 | } 30 | log.Printf("Greeting: %s", r.Message) 31 | } 32 | 33 | 可以看到 client 的建立也可以大致分为 3 步: 34 | 35 | 1)创建一个客户端连接 conn 36 | 37 | 2)通过一个 conn 创建一个客户端 38 | 39 | 3)发起 rpc 调用 40 | 41 | ok,那我们开始 step by step ,具体看看每一步做了啥 42 | 43 | #### 1)创建一个客户端连接 conn 44 | 45 | conn, err := grpc.Dial(address, grpc.WithInsecure()) 46 | 47 | 通过 Dial 方法创建 conn,Dial 调用了 DialContext 方法 48 | 49 | func Dial(target string, opts ...DialOption) (*ClientConn, error) { 50 | return DialContext(context.Background(), target, opts...) 51 | } 52 | 53 | 跟进 DialContext,发现 DialContext 这个方法非常长,这里就不贴代码了,具体就是先实例化了一个 ClientConn 的结构体,然后主要为 ClientConn 的 dopts 的各个属性进行初始化赋值。 54 | 55 | cc := &ClientConn{ 56 | target: target, 57 | csMgr: &connectivityStateManager{}, 58 | conns: make(map[*addrConn]struct{}), 59 | dopts: defaultDialOptions(), 60 | blockingpicker: newPickerWrapper(), 61 | czData: new(channelzData), 62 | firstResolveEvent: grpcsync.NewEvent(), 63 | } 64 | 65 | 我们先来看看 ClientConn 的结构 66 | 67 | 68 | type ClientConn struct { 69 | ctx context.Context 70 | cancel context.CancelFunc 71 | 72 | target string 73 | parsedTarget resolver.Target 74 | authority string 75 | dopts dialOptions 76 | csMgr *connectivityStateManager 77 | 78 | balancerBuildOpts balancer.BuildOptions 79 | blockingpicker *pickerWrapper 80 | 81 | mu sync.RWMutex 82 | resolverWrapper *ccResolverWrapper 83 | sc *ServiceConfig 84 | conns map[*addrConn]struct{} 85 | // Keepalive parameter can be updated if a GoAway is received. 86 | mkp keepalive.ClientParameters 87 | curBalancerName string 88 | balancerWrapper *ccBalancerWrapper 89 | retryThrottler atomic.Value 90 | 91 | firstResolveEvent *grpcsync.Event 92 | 93 | channelzID int64 // channelz unique identification number 94 | czData *channelzData 95 | } 96 | 97 | dialOptions 其实就是对客户端属性的一些设置,包括压缩解压缩、是否需要认证、超时时间、是否重试等信息。 98 | 99 | 这里我们来看一下初始化了哪些属性: 100 | 101 | ####connectivityStateManager 102 | 103 | type connectivityStateManager struct { 104 | mu sync.Mutex 105 | state connectivity.State 106 | notifyChan chan struct{} 107 | channelzID int64 108 | } 109 | 110 | 连接的状态管理器,每个连接具有 “IDLE”、“CONNECTING”、“READY”、“TRANSIENT_FAILURE”、“SHUTDOW 111 | N”、“Invalid-State” 这几种状态。 112 | 113 | 114 | #### pickerWrapper 115 | 116 | type pickerWrapper struct { 117 | mu sync.Mutex 118 | done bool 119 | blockingCh chan struct{} 120 | picker balancer.Picker 121 | 122 | // The latest connection happened. 123 | connErrMu sync.Mutex 124 | connErr error 125 | } 126 | 127 | pickerWrapper 是对 balancer.Picker 的一层封装,balancer.Picker 其实是一个负载均衡器,它里面只有一个 Pick 方法,它返回一个 SubConn 连接。 128 | 129 | type Picker interface { 130 | Pick(ctx context.Context, opts PickOptions) (conn SubConn, done func(DoneInfo), err error) 131 | } 132 | 133 | 什么是 SubConn 呢?看一下这个类的介绍 134 | 135 | // Each sub connection contains a list of addresses. gRPC will 136 | // try to connect to them (in sequence), and stop trying the 137 | // remainder once one connection is successful. 138 | 139 | 这里我们就明白了,在分布式环境下,可能会存在多个 client 和 多个 server,client 发起一个 rpc 调用之前,需要通过 balancer 去找到一个 server 的 address,balancer 的 Picker 类返回一个 SubConn,SubConn 里面包含了多个 server 的 address,假如返回的 SubConn 是 “READY” 状态,grpc 会发送 RPC 请求,否则则会阻塞,等待 UpdateBalancerState 这个方法更新连接的状态并且通过 picker 获取一个新的 SubConn 连接。 140 | 141 | #### channelz 142 | 143 | channelz 主要用来监测 server 和 channel 的状态,这里的概念和实现比较复杂,暂时不进行深入研究,感兴趣的同学可以参考:https://github.com/grpc/proposal/blob/master/A14-channelz.md 这个 proposal ,初始化的代码如下: 144 | 145 | if channelz.IsOn() { 146 | if cc.dopts.channelzParentID != 0 { 147 | cc.channelzID = channelz.RegisterChannel(&channelzChannel{cc}, cc.dopts.channelzParentID, target) 148 | channelz.AddTraceEvent(cc.channelzID, &channelz.TraceEventDesc{ 149 | Desc: "Channel Created", 150 | Severity: channelz.CtINFO, 151 | Parent: &channelz.TraceEventDesc{ 152 | Desc: fmt.Sprintf("Nested Channel(id:%d) created", cc.channelzID), 153 | Severity: channelz.CtINFO, 154 | }, 155 | }) 156 | } else { 157 | cc.channelzID = channelz.RegisterChannel(&channelzChannel{cc}, 0, target) 158 | channelz.AddTraceEvent(cc.channelzID, &channelz.TraceEventDesc{ 159 | Desc: "Channel Created", 160 | Severity: channelz.CtINFO, 161 | }) 162 | } 163 | cc.csMgr.channelzID = cc.channelzID 164 | } 165 | 166 | 167 | #### Authentication 168 | 169 | 这一段是对认证信息的初始化校验,这里暂不研究,感兴趣的同学可以了解下 :https://grpc.io/docs/guides/auth/ 170 | 171 | if !cc.dopts.insecure { 172 | if cc.dopts.copts.TransportCredentials == nil && cc.dopts.copts.CredsBundle == nil { 173 | return nil, errNoTransportSecurity 174 | } 175 | if cc.dopts.copts.TransportCredentials != nil && cc.dopts.copts.CredsBundle != nil { 176 | return nil, errTransportCredsAndBundle 177 | } 178 | } else { 179 | if cc.dopts.copts.TransportCredentials != nil || cc.dopts.copts.CredsBundle != nil { 180 | return nil, errCredentialsConflict 181 | } 182 | for _, cd := range cc.dopts.copts.PerRPCCredentials { 183 | if cd.RequireTransportSecurity() { 184 | return nil, errTransportCredentialsMissing 185 | } 186 | } 187 | } 188 | 189 | #### Dialer 190 | 191 | Dialer 是发起 rpc 请求的调用器,dialer 中实现了对 rpc 请求调用的具体细节,所以可以算是我们的重点研究对象之一。dialer 中包括了连接建立、地址解析、服务发现、长连接等等具体策略的实现。 192 | 193 | if cc.dopts.copts.Dialer == nil { 194 | cc.dopts.copts.Dialer = newProxyDialer( 195 | func(ctx context.Context, addr string) (net.Conn, error) { 196 | network, addr := parseDialTarget(addr) 197 | return (&net.Dialer{}).DialContext(ctx, network, addr) 198 | }, 199 | ) 200 | } 201 | 202 | 这一段方法只是简单进行了地址的规则解析,我们具体看 DialContext 方法,其中有一行: 203 | 204 | addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr) 205 | 206 | 可以看到通过 dialer 的 resolver 来进行服务发现,这里以后我们再单独详细讲解。 207 | 208 | 这里值得一提的是,通过 dialContext 可以看出,这里的 dial 有两种请求方式,一种是 dialParallel , 另一种是 dialSerial。dialParallel 发出两个完全相同的请求,采用第一个返回的结果,抛弃掉第二个请求。dialSerial 则是发出一串(多个)请求。然后采取第一个返回的请求结果( 成功或者失败)。 209 | 210 | #### scChan 211 | 212 | scChan 是 dialOptions 中的一个属性,定义如下: 213 | 214 | scChan <-chan ServiceConfig 215 | 216 | 可以看到其实他是一个 ServiceConfig类型的一个 channel,那么 ServiceConfig 是什么呢?源码中对这个类的介绍如下: 217 | 218 | // ServiceConfig is provided by the service provider and contains parameters for how clients that connect to the service should behave. 219 | 220 | 通过介绍得知 ServiceConfig 是服务提供方约定的一些参数。这里说明 client 提供给 server 一个可以通过 channel 来修改这些参数的入口。这里到时候我们介绍服务发现时可以细讲,我们在这里只需要知道 client 的某些属性是可以被 server 修改的就行了 221 | 222 | if cc.dopts.scChan != nil { 223 | // Try to get an initial service config. 224 | select { 225 | case sc, ok := <-cc.dopts.scChan: 226 | if ok { 227 | cc.sc = &sc 228 | scSet = true 229 | } 230 | default: 231 | } 232 | } 233 | 234 | #### 2)通过一个 conn 创建一个客户端 235 | 236 | 通过一个 conn 创建客户端的代码如下: 237 | 238 | c := pb.NewGreeterClient(conn) 239 | 240 | 这一步非常简单,其实是 pb 文件中生成的代码,就是创建一个 greeterClient 的客户端。 241 | 242 | type greeterClient struct { 243 | cc *grpc.ClientConn 244 | } 245 | 246 | func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { 247 | return &greeterClient{cc} 248 | } 249 | 250 | #### 3)发起 rpc 调用 251 | 252 | 前面在创建 Dialer 的时候,我们已经将请求的 target 解析成了 address。我们猜这一步应该是向指定 address 发起 rpc 请求了。来具体看看 253 | 254 | func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { 255 | out := new(HelloReply) 256 | err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) 257 | if err != nil { 258 | return nil, err 259 | } 260 | return out, nil 261 | } 262 | 263 | SayHello 方法是通过调用 Invoke 的方法去发起 rpc 调用, Invoke 方法如下: 264 | 265 | func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error { 266 | // allow interceptor to see all applicable call options, which means those 267 | // configured as defaults from dial option as well as per-call options 268 | opts = combine(cc.dopts.callOptions, opts) 269 | 270 | if cc.dopts.unaryInt != nil { 271 | return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...) 272 | } 273 | return invoke(ctx, method, args, reply, cc, opts...) 274 | } 275 | 276 | func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error { 277 | cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...) 278 | if err != nil { 279 | return err 280 | } 281 | if err := cs.SendMsg(req); err != nil { 282 | return err 283 | } 284 | return cs.RecvMsg(reply) 285 | } 286 | 287 | Invoke 方法调用了 invoke, 在 invoke 这个方法里面,果然不出所料,我们看到了 sendMsg 和 recvMsg 接口,这两个接口在 clientStream 中被实现了。 288 | 289 | #### SendMsg 290 | 291 | 我们先来看看 clientStream 中定义的 sendMsg,关键代码如下: 292 | 293 | func (cs *clientStream) SendMsg(m interface{}) (err error) { 294 | 295 | ... 296 | 297 | // load hdr, payload, data 298 | hdr, payload, data, err := prepareMsg(m, cs.codec, cs.cp, cs.comp) 299 | if err != nil { 300 | return err 301 | } 302 | 303 | ... 304 | 305 | op := func(a *csAttempt) error { 306 | err := a.sendMsg(m, hdr, payload, data) 307 | // nil out the message and uncomp when replaying; they are only needed for 308 | // stats which is disabled for subsequent attempts. 309 | m, data = nil, nil 310 | return err 311 | } 312 | 313 | } 314 | 315 | 先准备数据,然后再调用 csAttempt 这个结构体中的 sendMsg 方法, 316 | 317 | func (a *csAttempt) sendMsg(m interface{}, hdr, payld, data []byte) error { 318 | cs := a.cs 319 | if a.trInfo != nil { 320 | a.mu.Lock() 321 | if a.trInfo.tr != nil { 322 | a.trInfo.tr.LazyLog(&payload{sent: true, msg: m}, true) 323 | } 324 | a.mu.Unlock() 325 | } 326 | if err := a.t.Write(a.s, hdr, payld, &transport.Options{Last: !cs.desc.ClientStreams}); err != nil { 327 | if !cs.desc.ClientStreams { 328 | // For non-client-streaming RPCs, we return nil instead of EOF on error 329 | // because the generated code requires it. finish is not called; RecvMsg() 330 | // will call it with the stream's status independently. 331 | return nil 332 | } 333 | return io.EOF 334 | } 335 | if a.statsHandler != nil { 336 | a.statsHandler.HandleRPC(cs.ctx, outPayload(true, m, data, payld, time.Now())) 337 | } 338 | if channelz.IsOn() { 339 | a.t.IncrMsgSent() 340 | } 341 | return nil 342 | } 343 | 344 | 最终是通过 a.t.Write 发出的数据写操作,a.t 是一个 ClientTransport 类型,所以最终是通过 ClientTransport 这个结构体的 Write 方法发送数据 345 | 346 | #### RecvMsg 347 | 348 | 发送数据是通过 ClientTransport 的 Write 方法,我们猜测接收数据肯定是某个结构体的 Read 方法。这里我们来详细看一下 349 | 350 | func (a *csAttempt) recvMsg(m interface{}, payInfo *payloadInfo) (err error) { 351 | 352 | ... 353 | 354 | err = recv(a.p, cs.codec, a.s, a.dc, m, *cs.callInfo.maxReceiveMessageSize, payInfo, a.decomp) 355 | 356 | ... 357 | 358 | if a.statsHandler != nil { 359 | a.statsHandler.HandleRPC(cs.ctx, &stats.InPayload{ 360 | Client: true, 361 | RecvTime: time.Now(), 362 | Payload: m, 363 | // TODO truncate large payload. 364 | Data: payInfo.uncompressedBytes, 365 | WireLength: payInfo.wireLength, 366 | Length: len(payInfo.uncompressedBytes), 367 | }) 368 | } 369 | 370 | ... 371 | } 372 | 373 | 可以看到调用了 recv 方法: 374 | 375 | func recv(p *parser, c baseCodec, s *transport.Stream, dc Decompressor, m interface{}, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor) error { 376 | d, err := recvAndDecompress(p, s, dc, maxReceiveMessageSize, payInfo, compressor) 377 | ... 378 | } 379 | 380 | 再看 recvAndDecompress 方法,调用了 recvMsg 381 | 382 | func recvAndDecompress(p *parser, s *transport.Stream, dc Decompressor, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor) ([]byte, error) { 383 | pf, d, err := p.recvMsg(maxReceiveMessageSize) 384 | ... 385 | } 386 | 387 | func (p *parser) recvMsg(maxReceiveMessageSize int) (pf payloadFormat, msg []byte, err error) { 388 | if _, err := p.r.Read(p.header[:]); err != nil { 389 | return 0, nil, err 390 | } 391 | 392 | ... 393 | } 394 | 395 | 这里比较清楚了,最终还是调用了 p.r.Read 方法,p.r 是一个 io.Reader 类型。果然万变不离其中,最终都是要落到 IO 上。 396 | 397 | 到这里,整个 client 结构已经基本解析清楚了,but wait,总感觉哪里不太对,接收数据是调用 io.Reader ,按道理发送数据应该也是调用 io.Writer 才对。可是追溯到 ClientTransport 这里,发现它是一个 interface ,并没有实现 Write 方法,所以,Write 也是一个接口,这里是不是可以继续追溯呢? 398 | 399 | Write(s *Stream, hdr []byte, data []byte, opts *Options) error 400 | 401 | 402 | 返回去从头看,我们找到了 transport 的来源,在 Serve() 方法 的 handleRawConn 方法中,newHttp2Transport,创建了一个 Http2Transport ,然后通过 serveStreams 方法将这个 Http2Transport 层层透传下去。 403 | 404 | 405 | // Finish handshaking (HTTP2) 406 | st := s.newHTTP2Transport(conn, authInfo) 407 | if st == nil { 408 | return 409 | } 410 | 411 | rawConn.SetDeadline(time.Time{}) 412 | if !s.addConn(st) { 413 | return 414 | } 415 | go func() { 416 | s.serveStreams(st) 417 | s.removeConn(st) 418 | }() 419 | 420 | 421 | 继续看一下 http2Client 的 Write 方法,如下: 422 | 423 | 424 | func (t *http2Server) Write(s *Stream, hdr []byte, data []byte, opts *Options) error { 425 | ... 426 | 427 | hdr = append(hdr, data[:emptyLen]...) 428 | data = data[emptyLen:] 429 | df := &dataFrame{ 430 | streamID: s.id, 431 | h: hdr, 432 | d: data, 433 | onEachWrite: t.setResetPingStrikes, 434 | } 435 | if err := s.wq.get(int32(len(hdr) + len(data))); err != nil { 436 | select { 437 | case <-t.ctx.Done(): 438 | return ErrConnClosing 439 | default: 440 | } 441 | return ContextErr(s.ctx.Err()) 442 | } 443 | return t.controlBuf.put(df) 444 | } 445 | 446 | 447 | 可以看到,最终是把 data 放到了一个 controlBuf 的结构体里面 448 | 449 | 450 | // controlBuf delivers all the control related tasks (e.g., window 451 | // updates, reset streams, and various settings) to the controller. 452 | controlBuf *controlBuffer 453 | 454 | controlBuf 是 http2 客户端发送数据的实现,这里留待后续研究 455 | 456 | --------------------------------------------------------------------------------