5.3 KiB
5.3 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-04-28 00:00 |
高级运行场景
概述
除了一台服务器上跑一个单一服务之外,还有一些进阶场景:单进程监听多个端口、同时运行 gRPC 和 HTTP 服务、HTTP/2 Server Push 等。这些场景虽然不一定常用,但在特定架构需求下非常关键。
思考题:为什么在同一进程中同时监听 HTTP 和 gRPC 比拆成两个服务更好?又有哪些代价?
正文
1. 单进程多端口监听
一个 Engine 实例可以绑定多个地址:
func main() {
// 主服务 — 对外 API
go func() {
srv := &http.Server{Addr: ":8080", Handler: gin.Default()}
log.Fatal(srv.ListenAndServe())
}()
// 内部服务 — 管理后台或健康检查
internal := gin.New()
internal.GET("/admin/health", adminHealth)
internal.GET("/admin/metrics", adminMetrics)
go func() {
srv := &http.Server{Addr: ":9090", Handler: internal}
log.Fatal(srv.ListenAndServe())
}()
// 阻塞
select {}
}
2. HTTP + gRPC 共存
在一个进程内同时提供 RESTful HTTP 和 gRPC 服务——共享同一个 Engine 实例和配置:
func main() {
r := gin.Default()
// 注册 HTTP 路由
r.GET("/api/users", listUsers)
r.POST("/api/users", createUser)
// gRPC server
grpcServer := grpc.NewServer()
pb.RegisterUserServiceServer(grpcServer, &userGRPCServer{})
// HTTP + gRPC 共享同一个端口 —— 通过 HTTP Upgrade 协议协商
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("listen failed: %v", err)
}
// HTTP/2 multiplexing:gRPC 要求 HTTP/2
mux := http.NewServeMux()
mux.Handle("/", r) // Gin 处理普通请求
mux.Handle("/_grpc_", grpcHandler) // gRPC 处理 gRPC 请求
// 注意:真正的 HTTP/2 mux 需要更复杂的配置
// 推荐使用 github.com/bazelbuild/rules_go/go/runfiles 或其他 multiplexer
// 最简单的方式是分别监听不同端口
log.Println("HTTP on :8080, gRPC on :50051")
go func() {
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatalf("http failed: %v", err)
}
}()
go func() {
lis, _ := net.Listen("tcp", ":50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("grpc failed: %v", err)
}
}()
select {}
}
两种部署策略对比:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 同进程不同端口 | 共享配置、部署简单 | 端口占用 |
| Nginx 反代 | 协议隔离清晰 | 网络跳数增加 |
| gRPC-Gateway | 统一入口、单一端口 | 增加复杂度 |
3. HTTP/2 Server Push
Go 的 net/http 原生支持 HTTP/2 Push(Gin 作为 http.Handler 也继承了这个能力),但现代浏览器已不再推荐:
func pushHandler(c *gin.Context) {
pusher, ok := c.Writer.(http.Pusher)
if !ok {
c.String(http.StatusOK, "push not supported")
return
}
// 推送 CSS 文件给浏览器(不等 JS 请求后再传)
if err := pusher.Push("/static/style.css", nil); err != nil {
log.Printf("push failed: %v", err)
}
c.HTML(http.StatusOK, "index", gin.H{"title": "Home"})
}
现状提示: Chrome/Safari 已于 2017-2018 年废弃 HTTP/2 Push 的支持。MDN 明确标注该 API 为 deprecated。不建议在新项目中使用。
4. HTTP/3 (QUIC) 支持
Go 1.21+ 开始实验性支持 QUIC(UDP-based HTTP/3),但不需要额外配置——只要你的服务端启用了 TLS 并且客户端支持 HTTP/3,Go 会自动协商:
srv := &http.Server{
Addr: ":443",
Handler: r,
}
// Go 自动尝试使用 QUIC listener
// 前提:启用 TLS(HTTP/3 仅适用于 HTTPS)
srv.ListenAndServeTLS("", "")
5. 运行时动态切换端口
有时需要根据配置或环境变量动态调整监听端口:
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
addr := ":" + port
// 优雅切换端口:先 shutdown 旧 server,再启动新 server
// (见 [[13-graceful-shutdown]])
switchPort(addr)
}
6. 条件启动(Feature Flag)
根据配置决定是否开启某些路由或服务:
func setupRoutes(r *gin.Engine) {
r.GET("/ping", pingHandler)
// 只有开启了 feature flag 才注册实验性功能
if os.Getenv("ENABLE_EXPERIMENTAL") == "true" {
r.GET("/experimental/features", experimentalFeatures)
}
// 只在调试模式下注册调试路由
if gin.Mode() == gin.DebugMode {
r.GET("/debug/vars", pprof.Index)
r.GET("/debug/pprof/", pprof.Index)
}
}
思考题:如果你需要在不停机的情况下动态增删路由,Gin 能做到吗?它的路由树一旦注册后能否删除某条路由?
答案:Gin 的路由注册是单向的——addRoute 只加不减。如果需要在运行时动态管理路由,可以考虑使用独立的 RouterGroup 隔离功能模块,通过标志位控制是否注册该 Group。
关联笔记
- GIN/12-server-config — http.Server 的高级配置
- GIN/13-graceful-shutdown — 停机时如何正确关闭多个 goroutine
- GIN/21-websocket — WebSocket 长连接也是另一种"多协议共存"场景