gRPC 源码分析(二): gRPC Server 的 RPC 连接阶段

1,583 阅读4分钟

gRPC 源码分析(二): gRPC Server 的 RPC 连接阶段

当 gRPC client 向 gRPC server 发送 request 时, client 和 server 之间的交互其实可以大致分为两个阶段:

  • RPC 连接阶段: client 和 server 建立 TCP 连接, 并设置好 HTTP2 初始化参数.
  • RPC 交互阶段: client 发送 request 数据给 server, server 执行并返回执行结果给 client.

本篇中, 我们就一起来看看在 RPC 连接阶段, gRPC server 都做了什么事情.

gRPC server 如何等待 client 并建立 TCP 连接

lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
	log.Fatalf("failed to listen: %v", err)
}

s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})

if err := s.Serve(lis); err != nil {
	log.Fatalf("failed to serve: %v", err)
}

gRPC server 依赖于 golang 原生的 net.Listener 来创建和 client 之间建立 TCP 连接. 在调用 s.Serve(list) 之后, 程序会阻塞并等待来自 client 的访问.

func (s *Server) Serve(lis net.Listener) error {
	......
	s.serve = true
	......
	for {
		rawConn, err := lis.Accept()
		if err != nil {
			......
		}
		......
		s.serveWG.Add(1)
		go func() {
			s.handleRawConn(lis.Addr().String(), rawConn)
			s.serveWG.Done()
		}()

}

和绝大部分 server 一样, gRPC server 在一个 for 循环中等待来自 client 的访问, 创建一个 golang 原生的 net.Conn , 并创建一个新的 goroutine 来处理这个 net.Conn . 所有来自这个 client 的 request, 不论这个 client 调用哪一个远程方法, 或者调用几次, 都会由着一个 goroutine 处理. 接下来我们看看 gRPC server 是如何处理这个 net.Conn 的.

gRPC server 在哪里进行 RPC 连接和 RPC 交互

func (s *Server) handleRawConn(lisAddr string, rawConn net.Conn) {
	// 如果 gRPC server 已经关闭, 那么同样关闭这个 TCP 连接
	if s.quit.HasFired() {
		rawConn.Close()
		return
	}
	// 为这个 TCP 连接设置 deadline
	rawConn.SetDeadline(time.Now().Add(s.opts.connectionTimeout))

	// Finish handshaking (HTTP2)
	// RPC 连接阶段, server 和 client 之间进行 HTTP2 的握手
	st := s.newHTTP2Transport(rawConn)
	rawConn.SetDeadline(time.Time{})
	if st == nil {
		return
	}

	if !s.addConn(lisAddr, st) {
		return
	}
	// RPC 交互阶段, 在新的 goroutine 中处理来自 client 的数据
	go func() {
		s.serveStreams(st)
		s.removeConn(lisAddr, st)
	}()
}

可以看到对于每一个来自 client 的新的连接, gRPC server 都会经历 RPC 连接阶段和 RPC 交互阶段. 在本篇中我们主要来看看在 RPC 连接阶段, 也就是 s.netHTTP2Transport 中发生了什么.

func (s *Server) newHTTP2Transport(c net.Conn) transport.ServerTransport {
	// 组装 ServerConfig
	config := &transport.ServerConfig{
		MaxStreams:            s.opts.maxConcurrentStreams,
		ConnectionTimeout:     s.opts.connectionTimeout,
		......
	}
	
	// 根据 config 的配置信息, 和 client 进行 HTTP2 的握手
	st, err := transport.NewServerTransport(c, config)
	if err != nil {
		......
	}

	return st
}

根据使用者在启动 gRPC server 时的配置项, 或者默认的配置项, 调用 transport.NewServerTransport 来和 client 完成 HTTP2 的握手, transport.NewServerTransport 是一个很长的函数, 我们下面一点点来看.

gRPC server 和 client 的 HTTP2 握手

func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport, err error) {
	......
	rawConn := conn
	......
	writeBufSize := config.WriteBufferSize
	readBufSize := config.ReadBufferSize
	maxHeaderListSize := defaultServerMaxHeaderListSize
	if config.MaxHeaderListSize != nil {
		maxHeaderListSize = *config.MaxHeaderListSize
	}
	framer := newFramer(conn, writeBufSize, readBufSize, maxHeaderListSize)
	......
}

首先创建了一个 framer, 用来负责接收和发送 HTTP2 frame, 是 server 和 client 交流的实际接口.

func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport, err error) {
	......
	// Send initial settings as connection preface to client.
	isettings := []http2.Setting{{
		ID:  http2.SettingMaxFrameSize,
		Val: http2MaxFrameLen,
	}}

	maxStreams := config.MaxStreams
	if maxStreams == 0 {
		maxStreams = math.MaxUint32
	} else {
		isettings = append(isettings, http2.Setting{
			ID:  http2.SettingMaxConcurrentStreams,
			Val: maxStreams,
		})
	}
	dynamicWindow := true
	iwz := int32(initialWindowSize)
	if config.InitialWindowSize >= defaultWindowSize {
		iwz = config.InitialWindowSize
		dynamicWindow = false
	}
	......
	// 通过 framer, 将 server 端的初始 HTTP2 配置信息发送给 client
	if err := framer.fr.WriteSettings(isettings...); err != nil {
		return nil, connectionErrorf(false, err, "transport: %v", err)
	}
	......
}

gRPC server 端首先明确自己的 HTTP2 的初始配置, 比如 InitialWindowSize, MaxHeaderListSize 等等, 并将这些配置信息通过 framer.fr 发送给 client. framer.fr 实际上就是 golang 原生的 http2.Framer . 在底层, 这些配置信息会被包裹在一个 Setting Frame 中以二进制的格式发送给 client.

client 在收到 Setting Frame 后, 根据自身的情况调整一些参数, 并同样发送一个 Setting Frame 返还给 client.

func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport, err error) {
	......
	t := &http2Server{
		......
		conn:              conn,
		remoteAddr:        conn.RemoteAddr(),
		localAddr:         conn.LocalAddr(),
		......
		framer:            framer,
		......
	}
	// controlBuf 是用来缓存 Setting Frame 等和设置相关的 frame 的缓存. 在 flow control 的相关章节会详细分析它的作用.
	t.controlBuf = newControlBuffer(t.done)
	......
	// flush framer, 确保向 client 发送了 Setting Frame.
	t.framer.writer.Flush()
	......
}

gRPC server 在发送了 setting frame 之后, 创建好 http2Server 对象. 并等待 client 的后续消息.

func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport, err error) {
	......
	// Check the validity of client preface.
	preface := make([]byte, len(clientPreface))
	if _, err := io.ReadFull(t.conn, preface); err != nil {
		......
	}
	......
	if !bytes.Equal(preface, clientPreface) {
		return nil, connectionErrorf(false, nil, "transport: http2Server.HandleStreams received bogus greeting from client: %q", preface)
	}

	frame, err := t.framer.fr.ReadFrame()
	......
	sf, ok := frame.(*http2.SettingsFrame)
	if !ok {
		return nil, connectionErrorf(false, nil, "transport: http2Server.HandleStreams saw invalid preface type %T from client", frame)
	}
	// server 根据 client 发送的 setting frame, 调整自身的 HTTP2 配置
	t.handleSettings(sf)
	......
}

在 HTTP2 中, client 和 server 都要求在建立连接之前发送一个 connection preface, 作为对所使用协议的最终确认, 并确定 HTTP2 连接的初始设置. client 发送的 preface 以一个 24 字节的序列开始, 用 16 进制表示为 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a .之后紧跟着一个 setting frame, 用来表示 client 端最终决定的 HTTP2 配置参数. NewServerTransport 中包含了读取并验证 preface, 以及读取并应用 setting frame 的代码.

func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport, err error) {
	......
	go func() {
		t.loopy = newLoopyWriter(serverSide, t.framer, t.controlBuf, t.bdpEst)
		t.loopy.ssGoAwayHandler = t.outgoingGoAwayHandler
		if err := t.loopy.run(); err != nil {
			if logger.V(logLevel) {
				logger.Errorf("transport: loopyWriter.run returning. Err: %v", err)
			}
		}
		t.conn.Close()
		t.controlBuf.finish()
		close(t.writerDone)
	}()
	go t.keepalive()
	return t, nil
}

此时 server 和 client 之间的 HTTP2 连接已经建立完成, RPC 连接阶段完毕. 在 NetServerTransport 的最后启动了 loopyWriter , 开始了 RPC 交互阶段. loopyWriter 不断地从 controlBuf 中读取 control frames (包括 setting frame), 并将缓存中的 frame 发送给 client. 可以说 loopyWriter 就是 gRPC server 控制流量以及发送数据的地方. 详细内容会在后续的篇章陆续介绍.

总结

gRPC server 通过在独立的 goroutine 中处理来自 client 的 TCP connection, 并通过 HTTP2 的标准握手流程和 client 建立 HTTP2 连接.