go-grpc服务端调用链

一、goroutine 模型总览(先看图再读码)

Go 复制代码
s.Serve(lis)                       ← 主 goroutine:Accept 循环(server.go:906)
   │
   ├─ go handleRawConn(conn)       ← 每条 TCP 连接 1 个(server.go:991)
   │     │
   │     └─ newHTTP2Server → st.HandleStreams(...)   ← 连接读循环
   │           │
   │           └─ go handleStream(st, stream)        ← 每条 RPC 1 个(server.go:1096)
   │                 │
   │                 └─ processRPC → 拦截链 → 你的 SayHello   ← 业务跑在这里
   │
   └─ (可选) serverWorkerChannel   ← numServerWorkers>0 时,固定 worker 池
                                      替代 per-stream goroutine,防 goroutine 爆炸

三层职责:Accept(不阻塞)→ 连接读循环(解析帧)→ 流处理(业务逻辑) 。这就是经典的 goroutine-per-connection + goroutine-per-request 模型。

二、逐站精读

站① Serve:带退避的 Accept 循环(server.go:906-996)

Go 复制代码
func (s *Server) Serve(lis net.Listener) error {
    ...
    for {
        rawConn, err := lis.Accept()
        if err != nil {
            if ne, ok := err.(interface{ Temporary() bool }); ok && ne.Temporary() {
                // 临时错误:5ms 起步的指数退避,封顶 1s(server.go:956-961)
                // ------ Accept 也怕打满 CPU,和客户端重连退避同一思路
                ...continue
            }
            return err
        }
        // 每条连接立刻丢给新 goroutine,绝不阻塞 Accept 循环(server.go:985-994)
        s.serveWG.Add(1)
        go func() {
            s.handleRawConn(lis.Addr().String(), rawConn)
            s.serveWG.Done()
        }()
    }
}

细节品味:serveWG(WaitGroup)让 GracefulStop 能等所有连接处理完------优雅退出在第一行就埋了伏笔。

站② handleRawConn → 建传输层(server.go:1000+)

Go 复制代码
func (s *Server) handleRawConn(lisAddr string, rawConn net.Conn) {
    if s.quit.HasFired() { rawConn.Close(); return }   // 已停机:直接掐新连接
    rawConn.SetDeadline(time.Now().Add(s.opts.connectionTimeout)) // 握手超时
    // 完成 TLS 握手(若配了)→ newHTTP2Server 建立 HTTP/2 传输
    // 失败 → 关连接走人(错误连接不值得占资源)
    ...
}

newHTTP2Server(internal/transport/http2_server.go)做三件事:HTTP/2 握手交换 SETTINGS、分配连接级流控窗口、启动 reader 循环和 loopyWriter(与客户端对称,11 篇)。

站③ HandleStreams:帧 → 流 的分发

http2_server.go 的读循环逐帧解析(HEADERS 帧开流、DATA 帧挂到对应流),每发现新流就回调:

Go 复制代码
// server.go:1090-1100(节选)
st.HandleStreams(ctx, func(stream *transport.ServerStream) {
    s.handlersWG.Add(1)
    streamQuota.acquire()          // 限流:并发流数受 serverOption 约束
    f := func() {
        defer streamQuota.release()
        defer s.handlersWG.Done()
        s.handleStream(st, stream) // ← 每条流最终进这里
    }
    if s.opts.numServerWorkers > 0 {
        select {
        case s.serverWorkerChannel <- f:   // worker 池模式
            return
        default:                            // 池满 → 回退 goroutine 模式
        }
    }
    go f()
})

两个生产级细节:

  • streamQuota:单连接并发流上限,防止一条恶意连接拖死服务端;
  • worker 池可选 :grpc.ServerOption 设 numServerWorkers,把 per-stream goroutine 换成固定池------高 QPS 场景减少调度开销。

站④ handleStream:方法路由(server.go:1537-1597)

这段是"HTTP/2 世界的路由器":

Go 复制代码
func (s *Server) handleStream(t transport.ServerTransport, stream *transport.ServerStream) {
    ...
    sm := stream.Method()                       // "/helloworld.Greeter/SayHello"
    service, method := sm拆包                   // "helloworld.Greeter" / "SayHello"
    //                                        (server.go:1562-1573,CutPrefix+LastIndex)

    if srv, known := s.services[service]; known {
        if sd, ok := srv.streams[method]; ok {  // ② 查该服务的方法表
            s.processRPC(ctx, stream, srv, sd, ti)   // ③ 命中 → 处理
            return
        }
    }
    // 未命中 → WriteStatus(codes.Unimplemented)   ← 你调错方法名时见到的错误
}

s.services 这张 map 就是 RegisterGreeterServer 注册时填的(key 来自 02 篇讲的 ServiceDesc)------02 篇的生成物与 09 篇的路由此刻接上了。

站⑤ processRPC:拦截链 + 业务(server.go:1292+)

v1.85 把老版本的 processUnaryRPC/processStreamingRPC合并统一了:

Go 复制代码
func (s *Server) processRPC(ctx context.Context, stream *transport.ServerStream,
    info *serviceInfo, sd *StreamDesc, trInfo *traceInfo) (err error) {

    // ① stats/channelz 埋点(Begin 事件)
    // ② 组装 serverStream:解析器 parser、codec(按 Content-Subtype 选)、
    //    消息大小上限、trace ------ 这是传给你 handler 的"流句柄"本体
    ss := &serverStream{ctx: ctx, s: stream, codec: s.getCodec(...), ...}

    // ③ 统一的 defer:stats.End/channelz 计数/trace 收尾
    //    (server.go:1319-1361 注释还解释了为何合并成一个 defer:省栈帧)

    // ④ 穿过服务端拦截链(04 篇注册的 ChainUnaryInterceptor 在此生效)
    //    一元: chainedInterceptors → 你的 SayHello
    //    流式: chainedStreamInterceptors → 你的 RouteChat 等
    // ⑤ handler 返回 → 把结果/status 装进响应帧与 trailer → 流关闭
}

注意 s.getCodec(stream.ContentSubtype())------服务端按请求头里的编码子类型选编解码器(encoding 注册表的运行时查询),所以客户端换编码不用改服务端。

站⑥ 响应回程

你的 SayHello 返回 (*HelloReply, error) 之后:

Go 复制代码
reply ──► codec.Marshal ──► 5字节前缀+体 ──► transport 写队列
error ──► status.Status ──► trailer(grpc-status/grpc-message)
     ──► loopyWriter 打帧(DATA + HEADERS-trailer) ──► 客户端 08 篇站⑦

正常路径 grpc-status=0;handler panic 没有 recover 拦截器的话,这条流挂掉但 server 进程不挂(grpc-go 自己在 processRPC 外层兜了 panic→Internal 错误的边界------但你的日志会丢,所以 04 篇 recovery 拦截器仍是标配)。

三、Stop vs GracefulStop(server.go:1676/1683)

|--------|---------------------------|--------------------------|
| | Stop() | GracefulStop() |
| 新 RPC | 立即拒绝 | 拒绝(发 GOAWAY 通知客户端"我要退役") |
| 存量 RPC | 立刻断,客户端收到 Unavailable | 等全部完成(或 ctx 兜底) |
| 连接 | 关 | 先 GOAWAY 让客户端平滑迁到别的实例 |
| 用途 | 致命错误自保 | 发版/缩容(06 篇清单第 4 条) |

源码线索:s.quit(Event)、serveWG/handlersWG 两个 WaitGroup 分层等待------连接层和流层的生命周期是分开管的。分层等待------连接层和流层的生命周期是分开管的。

五、本篇小结

|------------------|----------------|----------------------------------|
| 站点 | 文件:行 | 干什么 |
| Serve | server.go:906 | Accept 循环 + 临时错误指数退避 |
| handleRawConn | server.go:1000 | TLS/HTTP2 握手,建传输 |
| HandleStreams 回调 | server.go:1090 | per-stream goroutine 或 worker 池 |
| handleStream | server.go:1537 | "/服务/方法" 路由到 ServiceDesc |
| processRPC | server.go:1292 | 组 serverStream → 拦截链 → 业务 |
| 回程 | --- | codec + status → loopyWriter → 帧 |

  • 服务端 = Accept 循环 + 每连接读循环 + 每流 goroutine 三层;
  • 老教程的 processUnaryRPC 已统一为 processRPC(版本差注意);
  • register 的 ServiceDesc 就是路由表,生成代码与运行时在此闭环。
相关推荐
1024奇点8 小时前
【仓颉语言入门 · 第二篇 第 22 课】
开发语言·ide·开源
实名上网宋凯宣8 小时前
Django
后端·python·django
Warren2Lynch9 小时前
C4 模型详解:利用 Visual Paradigm 可视化软件架构的综合指南
开发语言·uml
Knight_AL10 小时前
Java 文件下载时文件名中的 `#` 被转换成 `%23`?一次 URL 编码与解码问题排查
java·开发语言
利威尔xu10 小时前
Go IM 源码实战(14):本地缓存快 3000 倍,失效却难 10 倍
后端·缓存·微服务·面试·golang
FRing0310 小时前
vue2干货总结2(v-bind、v-for、v-model、指令修饰符、v-bind操作class和style)
开发语言·前端·vue.js·vue2
逃逸线LOF11 小时前
Ribbon负载均衡原理(Irule组件、源码分析)
后端·spring cloud·ribbon
by2099911 小时前
C++ 智能指针的所有权与使用
开发语言·c++·经验分享
砚底藏山河11 小时前
python量化自定义周期 K 线合成
java·开发语言·数据库·python·金融
慢云智慧空间11 小时前
从设备智能到空间理解,空间智能终端解决方案商如何重新定义行业标准
后端