Files
cs-note/hhs/GIN/15-advanced-running.md
T
2026-05-24 11:42:38 +08:00

185 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [后端, Go, Gin, 服务器, 多服务]
create time: 2026-04-28 00:00
---
# 高级运行场景
## 概述
除了一台服务器上跑一个单一服务之外,还有一些进阶场景:单进程监听多个端口、同时运行 gRPC 和 HTTP 服务、HTTP/2 Server Push 等。这些场景虽然不一定常用,但在特定架构需求下非常关键。
思考题:为什么在同一进程中同时监听 HTTP 和 gRPC 比拆成两个服务更好?又有哪些代价?
## 正文
### 1. 单进程多端口监听
一个 Engine 实例可以绑定多个地址:
```go
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 实例和配置:
```go
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` 也继承了这个能力),但现代浏览器已不再推荐:
```go
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 会自动协商:
```go
srv := &http.Server{
Addr: ":443",
Handler: r,
}
// Go 自动尝试使用 QUIC listener
// 前提:启用 TLS(HTTP/3 仅适用于 HTTPS)
srv.ListenAndServeTLS("", "")
```
### 5. 运行时动态切换端口
有时需要根据配置或环境变量动态调整监听端口:
```go
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
addr := ":" + port
// 优雅切换端口:先 shutdown 旧 server,再启动新 server
// (见 [[13-graceful-shutdown]])
switchPort(addr)
}
```
### 6. 条件启动(Feature Flag)
根据配置决定是否开启某些路由或服务:
```go
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 长连接也是另一种"多协议共存"场景