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 连接.