185 lines
5.3 KiB
Markdown
185 lines
5.3 KiB
Markdown
|
|
---
|
|||
|
|
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 长连接也是另一种"多协议共存"场景
|