一、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就是路由表,生成代码与运行时在此闭环。