一、goroutine 模型总览(先看图再读码)
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)
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+)
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 帧挂到对应流),每发现新流就回调:
// 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 世界的路由器":
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合并统一了:
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)之后:
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)
|
| |
新 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就是路由表,生成代码与运行时在此闭环。