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)

scss 复制代码
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+)

scss 复制代码
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合并统一了:

scss 复制代码
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) 之后:

lua 复制代码
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 就是路由表,生成代码与运行时在此闭环。
相关推荐
SamDeepThinking1 小时前
CyclicBarrier的底层实现
java·后端·面试
god_cvz1 小时前
第二章:6、对话记忆与引用标注
后端·ai编程
拖孩1 小时前
全程 AI 写的小程序,刚接上会员虚拟支付,上线第二天就有人真金白银买单了|双签名、定价、iOS 的坑我全踩了一遍
前端·后端·微信小程序
我是人✓1 小时前
SpringMVC异常处理与拦截器实战揭秘
java·开发语言·后端
仟里码1 小时前
九:goroutine(1.什么是协程)
后端
不是万能的豆学长1 小时前
排查Go服务40天后的内存泄漏问题
开发语言·后端·golang·go
阿狗童鞋1 小时前
SpringSecurity权限认证实战指南
java·后端·spring
韩振方1 小时前
Kubernetes Service 让会更换的 Pod 有稳定访问入口
后端
用户125758524361 小时前
Golang后台管理框架推荐:XYGo Admin与devinggo怎么选?
后端·go·ai编程
仟里码1 小时前
十一:goroutine(3. select基本使用)
后端