Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7f0693809 | ||
|
|
2e902c4ec8 | ||
|
|
8a2f76ffc9 | ||
|
|
1bf819281a | ||
|
|
9fd0ed8d5b | ||
|
|
3cde76a6b0 | ||
|
|
2b7e11e7d2 | ||
|
|
3925767d2e |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,9 +1,8 @@
|
|||||||
.log.meta.json
|
.log.meta.json
|
||||||
|
|
||||||
.ai/
|
.ai/
|
||||||
|
|
||||||
.geminiignore
|
.geminiignore
|
||||||
.gemini
|
.gemini
|
||||||
env.json
|
env.json
|
||||||
env.yml
|
env.yml
|
||||||
env.yaml
|
env.yaml
|
||||||
|
/CODE-FULL.md
|
||||||
|
|||||||
53
README.md
53
README.md
@ -59,6 +59,59 @@ func main() {
|
|||||||
- **文档生成**: `service.MakeDocument()` 返回全量接口描述
|
- **文档生成**: `service.MakeDocument()` 返回全量接口描述
|
||||||
- **依赖注入**: `service.GetInjectT[T]()` 快速获取已注入的对象或组件
|
- **依赖注入**: `service.GetInjectT[T]()` 快速获取已注入的对象或组件
|
||||||
|
|
||||||
|
## 配置指南 (ServiceConfig)
|
||||||
|
|
||||||
|
详细配置项可查阅 `config.go` 中的 `ServiceConfig` 结构。通过 `config.Load` 支持从 `env.yml` 或环境变量加载。
|
||||||
|
|
||||||
|
### 1. 基础服务配置
|
||||||
|
```yaml
|
||||||
|
service:
|
||||||
|
App: "user-service" # 应用名称 (缺省自动探测)
|
||||||
|
Listen: ":8080,http|:443" # 监听端口, 多监听用 | 隔开, 选项用 , 隔开 (http/h2/h2c)
|
||||||
|
Register: "127.0.0.1:6379" # 发现中心地址 (Redis URL 或配置名)
|
||||||
|
Weight: 100 # 服务发现权重
|
||||||
|
ReadTimeout: 30000 # 读取请求超时 (ms)
|
||||||
|
WriteTimeout: 30000 # 写入响应超时 (ms)
|
||||||
|
MaxHeaderBytes: 1048576 # 最大头部字节数 (1MB)
|
||||||
|
StopTimeout: 5000 # 优雅停机等待时间 (ms)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 下游调用配置 (Calls)
|
||||||
|
```yaml
|
||||||
|
service:
|
||||||
|
Calls:
|
||||||
|
order-service: # 目标应用名
|
||||||
|
Timeout: 5000 # 调用超时时间 (ms)
|
||||||
|
Http2: true # 强制启用 H2C
|
||||||
|
Token: "secure-token" # 访问凭证
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 声明式路由与代理 (Nginx 模式)
|
||||||
|
除了代码注册,也支持在配置文件中直接声明:
|
||||||
|
```yaml
|
||||||
|
service:
|
||||||
|
# 代理规则 (Proxy)
|
||||||
|
Proxies:
|
||||||
|
"aa.com": # 指定 Host (支持 aa.com, :8080, *, "")
|
||||||
|
- Path: "/api/*" # 必须以 /* 结尾启用前缀匹配
|
||||||
|
ToApp: "backend-app" # 转发至服务发现的应用名
|
||||||
|
ToPath: "/v1/*" # 后缀映射
|
||||||
|
- Path: "/direct" # 精确匹配
|
||||||
|
ToApp: "http://1.1.1.1:80" # 直接转发至 URL
|
||||||
|
ToPath: "/hello"
|
||||||
|
|
||||||
|
# 重写规则 (Rewrite)
|
||||||
|
Rewrites:
|
||||||
|
"*": # 全局规则
|
||||||
|
- Path: "^/old/(.*)$" # 带捕获组的正则匹配
|
||||||
|
ToPath: "/new/$1" # 变量替换
|
||||||
|
|
||||||
|
# 静态目录 (Static)
|
||||||
|
Statics:
|
||||||
|
"www.example.com":
|
||||||
|
"/ui": "./dist" # 将 /ui 路径映射至本地 ./dist 目录
|
||||||
|
```
|
||||||
|
|
||||||
## 基础设施对齐
|
## 基础设施对齐
|
||||||
- **类型转换**: `apigo.cc/go/cast`
|
- **类型转换**: `apigo.cc/go/cast`
|
||||||
- **日志系统**: `apigo.cc/go/log`
|
- **日志系统**: `apigo.cc/go/log`
|
||||||
|
|||||||
88
config.go
88
config.go
@ -1,5 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
// CertSet SSL 证书配置
|
// CertSet SSL 证书配置
|
||||||
type CertSet struct {
|
type CertSet struct {
|
||||||
CertFile string
|
CertFile string
|
||||||
@ -60,6 +65,89 @@ type ServiceConfig struct {
|
|||||||
MaxUploadBufferPerConnection int32 // 每个连接的最大上传缓冲区大小
|
MaxUploadBufferPerConnection int32 // 每个连接的最大上传缓冲区大小
|
||||||
MaxUploadBufferPerStream int32 // 每个流的最大上传缓冲区大小
|
MaxUploadBufferPerStream int32 // 每个流的最大上传缓冲区大小
|
||||||
StopTimeout int // 停止服务的超时时间 (ms)
|
StopTimeout int // 停止服务的超时时间 (ms)
|
||||||
|
|
||||||
|
// 从配置文件中加载的静态路由策略 (按 Host 分组,全局配置用 "" 或 "*")
|
||||||
|
Proxies map[string]map[string]any
|
||||||
|
Rewrites map[string]map[string]any
|
||||||
|
Statics map[string]map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
var Config = ServiceConfig{}
|
var Config = ServiceConfig{}
|
||||||
|
|
||||||
|
// ApplyConfig 将 ServiceConfig 中的路由策略应用到内部的文件级策略中
|
||||||
|
func ApplyConfig() {
|
||||||
|
hostPoliciesLock.Lock()
|
||||||
|
defer hostPoliciesLock.Unlock()
|
||||||
|
|
||||||
|
// 1. Proxies KV 解析
|
||||||
|
fileProxies = make(map[string][]*proxyType)
|
||||||
|
for host, kv := range Config.Proxies {
|
||||||
|
if host == "*" {
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
rules := make([]*proxyType, 0, len(kv))
|
||||||
|
for path, val := range kv {
|
||||||
|
if to, ok := val.(string); ok {
|
||||||
|
// 极简 KV 模式: "/api/*": "user-svc/v1/*"
|
||||||
|
rules = append(rules, parseProxyRule(0, path, "", "", to))
|
||||||
|
} else {
|
||||||
|
// 对象模式: "/api/*": {"To": "...", "Auth": 1}
|
||||||
|
m, _ := cast.ToMap[string, any](val)
|
||||||
|
rules = append(rules, parseProxyRule(
|
||||||
|
cast.Int(m["Auth"]),
|
||||||
|
path,
|
||||||
|
cast.String(m["ToApp"]),
|
||||||
|
cast.String(m["ToPath"]),
|
||||||
|
cast.String(m["To"]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileProxies[host] = rules
|
||||||
|
rebuildProxiesUnderLock(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Rewrites KV 解析
|
||||||
|
fileRewrites = make(map[string][]*rewriteType)
|
||||||
|
for host, kv := range Config.Rewrites {
|
||||||
|
if host == "*" {
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
rules := make([]*rewriteType, 0, len(kv))
|
||||||
|
for path, val := range kv {
|
||||||
|
if to, ok := val.(string); ok {
|
||||||
|
rules = append(rules, parseRewriteRule(path, "", to))
|
||||||
|
} else {
|
||||||
|
m, _ := cast.ToMap[string, any](val)
|
||||||
|
rules = append(rules, parseRewriteRule(
|
||||||
|
path,
|
||||||
|
cast.String(m["ToPath"]),
|
||||||
|
cast.String(m["To"]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileRewrites[host] = rules
|
||||||
|
rebuildRewritesUnderLock(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
staticsByHostLock.Lock()
|
||||||
|
defer staticsByHostLock.Unlock()
|
||||||
|
fileStatics = make(map[string]map[string]*string)
|
||||||
|
|
||||||
|
for host, config := range Config.Statics {
|
||||||
|
if host == "*" {
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
newStatics := make(map[string]*string, len(config))
|
||||||
|
for path, rootPath := range config {
|
||||||
|
rp := rootPath
|
||||||
|
if !filepath.IsAbs(rp) {
|
||||||
|
if absPath, err := filepath.Abs(rp); err == nil {
|
||||||
|
rp = absPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newStatics[path] = &rp
|
||||||
|
}
|
||||||
|
fileStatics[host] = newStatics
|
||||||
|
rebuildStaticsUnderLock(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
31
go.mod
31
go.mod
@ -3,26 +3,27 @@ module apigo.cc/go/service
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.3.0
|
apigo.cc/go/cast v1.3.3
|
||||||
apigo.cc/go/discover v1.3.0
|
apigo.cc/go/config v1.3.1
|
||||||
apigo.cc/go/file v1.3.0
|
apigo.cc/go/discover v1.3.2
|
||||||
apigo.cc/go/http v1.3.0
|
apigo.cc/go/file v1.3.2
|
||||||
apigo.cc/go/id v1.3.0
|
apigo.cc/go/http v1.3.2
|
||||||
apigo.cc/go/log v1.3.0
|
apigo.cc/go/id v1.3.1
|
||||||
apigo.cc/go/redis v1.3.0
|
apigo.cc/go/log v1.3.4
|
||||||
apigo.cc/go/safe v1.3.0
|
apigo.cc/go/redis v1.3.2
|
||||||
apigo.cc/go/timer v1.3.0
|
apigo.cc/go/safe v1.3.1
|
||||||
|
apigo.cc/go/starter v1.0.5
|
||||||
|
apigo.cc/go/timer v1.3.1
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
golang.org/x/net v0.54.0
|
golang.org/x/net v0.54.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/config v1.3.0 // indirect
|
apigo.cc/go/crypto v1.3.1 // indirect
|
||||||
apigo.cc/go/crypto v1.3.0 // indirect
|
apigo.cc/go/encoding v1.3.1 // indirect
|
||||||
apigo.cc/go/encoding v1.3.0 // indirect
|
apigo.cc/go/rand v1.3.1 // indirect
|
||||||
apigo.cc/go/rand v1.3.0 // indirect
|
apigo.cc/go/shell v1.3.1 // indirect
|
||||||
apigo.cc/go/shell v1.3.0 // indirect
|
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||||
github.com/gomodule/redigo v1.9.3 // indirect
|
|
||||||
golang.org/x/crypto v0.51.0 // indirect
|
golang.org/x/crypto v0.51.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
|||||||
68
go.sum
68
go.sum
@ -1,47 +1,43 @@
|
|||||||
apigo.cc/go/cast v1.2.10 h1:wa9/hz6GW6Z+5co6l7LftMn2Eo06WpVHHDCCQphnmH8=
|
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
|
||||||
apigo.cc/go/cast v1.2.10/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||||
apigo.cc/go/config v1.0.8 h1:ZvontnJngNJrm6EJAPYmPhmBnLC9V7g5kZLiuN1MT60=
|
apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE=
|
||||||
apigo.cc/go/config v1.0.8/go.mod h1:FCZj70MCejeWwv81O7sdpg0zmjOzglAMmNEfT3dQYzw=
|
apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U=
|
||||||
apigo.cc/go/crypto v1.1.1 h1:AE0jNtKzcq4euz6fL9MAYEHQpbIEfDTHv2mriP/juig=
|
apigo.cc/go/crypto v1.3.1 h1:ulQ2zX9bUWirk0sEacx1Srsjs2Jow7HlZq7ED7msNcg=
|
||||||
apigo.cc/go/crypto v1.1.1/go.mod h1:Q26As+TQrNs6olGkiVdD6649DJirxA4CUBT4oukKPuw=
|
apigo.cc/go/crypto v1.3.1/go.mod h1:SwHlBFDPddttWgFFtzsEMla8CM/rcFy9nvdsJjW4CIs=
|
||||||
apigo.cc/go/discover v1.0.11 h1:aeAC+xAwGlOeXsRptXJkEn8MvRZ7lom5N5jfBAg9/CE=
|
apigo.cc/go/discover v1.3.2 h1:hzo5PQSAbJrF9Qk7yCMoXWatHGe7lR2MNUqLLQ5oUSk=
|
||||||
apigo.cc/go/discover v1.0.11/go.mod h1:TcIpl1Ocu51koRxugV81Jnz4NH0+Q5f5PF105VczS/0=
|
apigo.cc/go/discover v1.3.2/go.mod h1:77kZLGovdCYAMspkmL6iif65Yzhyg143ffQseZx+j40=
|
||||||
apigo.cc/go/encoding v1.1.2 h1:reSrLkyYrtZsf4S91XPdyBY2AQpvA43n9q0Q9wz5uJA=
|
apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
|
||||||
apigo.cc/go/encoding v1.1.2/go.mod h1:iLuvrYHEK8mLnk8jijx5Sv1tInFreny0yGNBouA1d20=
|
apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
|
||||||
apigo.cc/go/file v1.0.8 h1:GPkixU080cvrmz7cbdXkC2DqMvsWWyY3UzoyUVQYFvs=
|
apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4=
|
||||||
apigo.cc/go/file v1.0.8/go.mod h1:T/wYji/va0S+JM2fAHonhKpnXKIELk/bmgnFEgMMY2s=
|
apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs=
|
||||||
apigo.cc/go/http v1.0.11 h1:EOlMXlTGrWY0RI3MynkV7noT49WiUdGVPdOtDJjIkU4=
|
apigo.cc/go/http v1.3.2 h1:0Or5KfoIq4+yeWKYusYPV8XLPw8XuzJMeaFv7dZViLI=
|
||||||
apigo.cc/go/http v1.0.11/go.mod h1:K2JgyI7DblfbzAnK1OHx4PS/1Pvcoqcp3g2uwsCPe68=
|
apigo.cc/go/http v1.3.2/go.mod h1:Q9R7Ors0Fz2A6Mxg0dykO2PjCzdAHRRXreOUMjMOLwA=
|
||||||
apigo.cc/go/id v1.0.7 h1:vXCK8mUW3s4cJYmli0o2BxgyI9XbJrG8gSGJOP2Fe4g=
|
apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk=
|
||||||
apigo.cc/go/id v1.0.7/go.mod h1:wXBrPpcEpyUDM7bp7M5uPM9zFw4VcnvXMQLw4Yd+uZE=
|
apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4=
|
||||||
apigo.cc/go/log v1.1.16 h1:uqPqeHvs+FdNupLBzzamJmY4oHAqtPEkGuW/pW5i2nQ=
|
apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g=
|
||||||
apigo.cc/go/log v1.1.16/go.mod h1:bOfPXjrX2bY+FNG9eEtBnvaVXoxZDGvz0jQfF3s/mYk=
|
apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20=
|
||||||
apigo.cc/go/rand v1.0.6 h1:p51rkaDrYUdZPIRbQAujZmQelWg2ipAMts33A/tG7QE=
|
apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
|
||||||
apigo.cc/go/rand v1.0.6/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||||
apigo.cc/go/redis v1.0.9 h1:5MnCchcDgoVnGHQn+KJF9PJXXRayK9nlZg+Q0lTnMoU=
|
apigo.cc/go/redis v1.3.2 h1:iUWL/CHHnfonz0dJq6/V4IG3QuXBoHA2L1xnoGEbNEQ=
|
||||||
apigo.cc/go/redis v1.0.9/go.mod h1:SVOD7iuUL/jxYEa28qObDQf4GY9UVSKjhM9vVj9TXLI=
|
apigo.cc/go/redis v1.3.2/go.mod h1:/k5wcfAzB9jrfd9otabio9CPUxEsLPgEs4oggBG5sbs=
|
||||||
apigo.cc/go/safe v1.0.7 h1:f0d+v9K2dHPyG5DNqhyddCmAmSiIqIfkPi/AMED/iQI=
|
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
|
||||||
apigo.cc/go/safe v1.0.7/go.mod h1:Hu7TVDWPe/I+nBZfYJH4mt+ROzG+rwk2D1zHTXj/2eE=
|
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
|
||||||
apigo.cc/go/shell v1.0.6 h1:RngaSMr2AkAFDl545A1Ln+D8ckqV2jknUp4PohDaLIA=
|
apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8=
|
||||||
apigo.cc/go/shell v1.0.6/go.mod h1:X7Nozjd7oau4nvAJCI21vxrxfd4ZL5nE4C6eUsmi2Hc=
|
apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw=
|
||||||
apigo.cc/go/timer v1.0.7 h1:QUH0t7l9kBiGU/QdDNSthnXLfJXOEp+mpdY2+QPlrEI=
|
apigo.cc/go/starter v1.0.5 h1:pgjBun7zc3J+3hcWnP22bGtgB3+TiNXOrOGVyGM7u1Q=
|
||||||
apigo.cc/go/timer v1.0.7/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
|
apigo.cc/go/starter v1.0.5/go.mod h1:auAvnBknZuMMps6HRtlwf8Z5gCHRQYsVw7WhhiGwWpg=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
apigo.cc/go/timer v1.3.1 h1:YMSusF1LfJYOf6tAW94Yipj3pHrX6QhfP7Rk3nGFT8k=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
apigo.cc/go/timer v1.3.1/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
|
||||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
|
|||||||
115
proxy.go
115
proxy.go
@ -12,28 +12,86 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type proxyType struct {
|
type proxyType struct {
|
||||||
matcher *regexp.Regexp
|
matcher *regexp.Regexp
|
||||||
authLevel int
|
authLevel int
|
||||||
fromPath string
|
fromPath string
|
||||||
toApp string
|
toApp string
|
||||||
toPath string
|
toPath string
|
||||||
|
hasWildcard bool
|
||||||
|
prefix string
|
||||||
|
toPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HostContext) Proxy(authLevel int, path string, toApp, toPath string) *HostContext {
|
func parseTo(to string) (app, path string) {
|
||||||
|
if to == "" {
|
||||||
|
return "", "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找协议
|
||||||
|
if strings.Contains(to, "://") {
|
||||||
|
protocolPart, rest, _ := strings.Cut(to, "://")
|
||||||
|
// 协议后的第一个 /
|
||||||
|
if firstSlash := strings.Index(rest, "/"); firstSlash != -1 {
|
||||||
|
app = protocolPart + "://" + rest[:firstSlash]
|
||||||
|
path = rest[firstSlash:]
|
||||||
|
} else {
|
||||||
|
app = to
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 无协议,第一个 / 之前是 app
|
||||||
|
if firstSlash := strings.Index(to, "/"); firstSlash != -1 {
|
||||||
|
app = to[:firstSlash]
|
||||||
|
path = to[firstSlash:]
|
||||||
|
} else {
|
||||||
|
app = to
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProxyRule(authLevel int, path, toApp, toPath string, to string) *proxyType {
|
||||||
|
if to != "" {
|
||||||
|
toApp, toPath = parseTo(to)
|
||||||
|
}
|
||||||
|
|
||||||
p := &proxyType{authLevel: authLevel, fromPath: path, toApp: toApp, toPath: toPath}
|
p := &proxyType{authLevel: authLevel, fromPath: path, toApp: toApp, toPath: toPath}
|
||||||
if strings.Contains(path, "(") {
|
if strings.ContainsRune(path, '(') {
|
||||||
matcher, err := regexp.Compile("^" + path + "$")
|
matcher, err := regexp.Compile("^" + path + "$")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
p.matcher = matcher
|
p.matcher = matcher
|
||||||
}
|
}
|
||||||
|
} else if strings.HasSuffix(path, "/*") {
|
||||||
|
p.hasWildcard = true
|
||||||
|
p.prefix = path[:len(path)-1]
|
||||||
|
if strings.HasSuffix(toPath, "/*") {
|
||||||
|
p.toPrefix = toPath[:len(toPath)-1]
|
||||||
|
} else {
|
||||||
|
p.toPrefix = toPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HostContext) Proxy(authLevel int, path string, to string) *HostContext {
|
||||||
|
p := parseProxyRule(authLevel, path, "", "", to)
|
||||||
|
|
||||||
hostPoliciesLock.Lock()
|
hostPoliciesLock.Lock()
|
||||||
defer hostPoliciesLock.Unlock()
|
defer hostPoliciesLock.Unlock()
|
||||||
hostProxies[hc.host] = append(hostProxies[hc.host], p)
|
codeProxies[hc.host] = append(codeProxies[hc.host], p)
|
||||||
|
rebuildProxiesUnderLock(hc.host)
|
||||||
return hc
|
return hc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rebuildProxiesUnderLock(host string) {
|
||||||
|
var combined []*proxyType
|
||||||
|
combined = append(combined, codeProxies[host]...)
|
||||||
|
combined = append(combined, fileProxies[host]...)
|
||||||
|
combined = append(combined, dynamicProxies[host]...)
|
||||||
|
hostProxies[host] = combined
|
||||||
|
}
|
||||||
|
|
||||||
var httpClientPool *gohttp.Client
|
var httpClientPool *gohttp.Client
|
||||||
|
|
||||||
func findProxy(request *Request) (int, *string, *string, string) {
|
func findProxy(request *Request) (int, *string, *string, string) {
|
||||||
@ -62,12 +120,7 @@ func findProxy(request *Request) (int, *string, *string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, pi := range proxies {
|
for _, pi := range proxies {
|
||||||
if pi.matcher == nil {
|
if pi.matcher != nil {
|
||||||
if pi.fromPath == requestPath {
|
|
||||||
toPath := pi.toPath + queryString
|
|
||||||
return pi.authLevel, &pi.toApp, &toPath, h
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
finds := pi.matcher.FindAllStringSubmatch(requestPath, 1)
|
finds := pi.matcher.FindAllStringSubmatch(requestPath, 1)
|
||||||
if len(finds) > 0 {
|
if len(finds) > 0 {
|
||||||
toApp := pi.toApp
|
toApp := pi.toApp
|
||||||
@ -79,6 +132,18 @@ func findProxy(request *Request) (int, *string, *string, string) {
|
|||||||
toPath += queryString
|
toPath += queryString
|
||||||
return pi.authLevel, &toApp, &toPath, h
|
return pi.authLevel, &toApp, &toPath, h
|
||||||
}
|
}
|
||||||
|
} else if pi.hasWildcard {
|
||||||
|
if strings.HasPrefix(requestPath, pi.prefix) {
|
||||||
|
suffix := requestPath[len(pi.prefix):]
|
||||||
|
toPath := pi.toPrefix + suffix + queryString
|
||||||
|
toApp := pi.toApp
|
||||||
|
return pi.authLevel, &toApp, &toPath, h
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pi.fromPath == requestPath {
|
||||||
|
toPath := pi.toPath + queryString
|
||||||
|
return pi.authLevel, &pi.toApp, &toPath, h
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,3 +227,25 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyRule 定义了外部传递或 Redis 中获取的代理规则配置
|
||||||
|
type ProxyRule struct {
|
||||||
|
Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$
|
||||||
|
AuthLevel int // 所需鉴权级别
|
||||||
|
To string // 目标地址,格式为 "app/path" 或 "http://url/path" (支持后缀 /* 映射)
|
||||||
|
ToApp string // [Deprecated] 目标 AppName 或完整 URL (可含 $1 变量替换)
|
||||||
|
ToPath string // [Deprecated] 目标路径 (可含 $1 变量替换)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的动态代理规则。
|
||||||
|
func ReplaceProxies(host string, rules []ProxyRule) {
|
||||||
|
newProxies := make([]*proxyType, 0, len(rules))
|
||||||
|
for _, r := range rules {
|
||||||
|
newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath, r.To))
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPoliciesLock.Lock()
|
||||||
|
defer hostPoliciesLock.Unlock()
|
||||||
|
dynamicProxies[host] = newProxies
|
||||||
|
rebuildProxiesUnderLock(host)
|
||||||
|
}
|
||||||
|
|||||||
@ -43,7 +43,7 @@ func TestProxyDirect(t *testing.T) {
|
|||||||
defer backend.Close()
|
defer backend.Close()
|
||||||
|
|
||||||
// 注册代理规则
|
// 注册代理规则
|
||||||
Host("*").Proxy(0, "/proxy", backend.URL, "/hello")
|
Host("*").Proxy(0, "/proxy", backend.URL+"/hello")
|
||||||
|
|
||||||
rh := &RouteHandler{}
|
rh := &RouteHandler{}
|
||||||
req := httptest.NewRequest("GET", "/proxy", nil)
|
req := httptest.NewRequest("GET", "/proxy", nil)
|
||||||
|
|||||||
33
reload.go
Normal file
33
reload.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apigo.cc/go/log"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reloadHooks []func() error
|
||||||
|
reloadLock sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnReload 注册一个在接收到 SIGHUP 信号时触发的重新加载钩子
|
||||||
|
func OnReload(handler func() error) {
|
||||||
|
reloadLock.Lock()
|
||||||
|
defer reloadLock.Unlock()
|
||||||
|
reloadHooks = append(reloadHooks, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerReload 触发所有注册的重新加载钩子
|
||||||
|
func triggerReload() error {
|
||||||
|
reloadLock.RLock()
|
||||||
|
hooks := make([]func() error, len(reloadHooks))
|
||||||
|
copy(hooks, reloadHooks)
|
||||||
|
reloadLock.RUnlock()
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
if err := hook(); err != nil {
|
||||||
|
log.DefaultLogger.Error("reload hook failed", "error", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
83
rewrite.go
83
rewrite.go
@ -9,27 +9,54 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type rewriteType struct {
|
type rewriteType struct {
|
||||||
matcher *regexp.Regexp
|
matcher *regexp.Regexp
|
||||||
fromPath string
|
fromPath string
|
||||||
toPath string
|
toPath string
|
||||||
|
hasWildcard bool
|
||||||
|
prefix string
|
||||||
|
toPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HostContext) Rewrite(path string, toPath string) *HostContext {
|
func parseRewriteRule(fromPath, toPath, to string) *rewriteType {
|
||||||
s := &rewriteType{fromPath: path, toPath: toPath}
|
if to != "" {
|
||||||
|
toPath = to
|
||||||
if strings.ContainsRune(path, '(') {
|
}
|
||||||
matcher, err := regexp.Compile("^" + path + "$")
|
s := &rewriteType{fromPath: fromPath, toPath: toPath}
|
||||||
|
if strings.ContainsRune(fromPath, '(') {
|
||||||
|
matcher, err := regexp.Compile("^" + fromPath + "$")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.matcher = matcher
|
s.matcher = matcher
|
||||||
}
|
}
|
||||||
|
} else if strings.HasSuffix(fromPath, "/*") {
|
||||||
|
s.hasWildcard = true
|
||||||
|
s.prefix = fromPath[:len(fromPath)-1]
|
||||||
|
if strings.HasSuffix(toPath, "/*") {
|
||||||
|
s.toPrefix = toPath[:len(toPath)-1]
|
||||||
|
} else {
|
||||||
|
s.toPrefix = toPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HostContext) Rewrite(path string, to string) *HostContext {
|
||||||
|
s := parseRewriteRule(path, "", to)
|
||||||
|
|
||||||
hostPoliciesLock.Lock()
|
hostPoliciesLock.Lock()
|
||||||
defer hostPoliciesLock.Unlock()
|
defer hostPoliciesLock.Unlock()
|
||||||
hostRewrites[hc.host] = append(hostRewrites[hc.host], s)
|
codeRewrites[hc.host] = append(codeRewrites[hc.host], s)
|
||||||
|
rebuildRewritesUnderLock(hc.host)
|
||||||
return hc
|
return hc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rebuildRewritesUnderLock(host string) {
|
||||||
|
var combined []*rewriteType
|
||||||
|
combined = append(combined, codeRewrites[host]...)
|
||||||
|
combined = append(combined, fileRewrites[host]...)
|
||||||
|
combined = append(combined, dynamicRewrites[host]...)
|
||||||
|
hostRewrites[host] = combined
|
||||||
|
}
|
||||||
|
|
||||||
func processRewrite(request *Request, response *Response, logger *log.Logger) bool {
|
func processRewrite(request *Request, response *Response, logger *log.Logger) bool {
|
||||||
host := request.Host
|
host := request.Host
|
||||||
hostOnly, port, _ := strings.Cut(host, ":")
|
hostOnly, port, _ := strings.Cut(host, ":")
|
||||||
@ -59,12 +86,7 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
|
|||||||
found := false
|
found := false
|
||||||
rewriteToPath := ""
|
rewriteToPath := ""
|
||||||
|
|
||||||
if ri.matcher == nil {
|
if ri.matcher != nil {
|
||||||
if ri.fromPath == requestPath {
|
|
||||||
rewriteToPath = ri.toPath
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
finds := ri.matcher.FindAllStringSubmatch(request.RequestURI, 1)
|
finds := ri.matcher.FindAllStringSubmatch(request.RequestURI, 1)
|
||||||
if len(finds) > 0 {
|
if len(finds) > 0 {
|
||||||
toPath := ri.toPath
|
toPath := ri.toPath
|
||||||
@ -74,6 +96,17 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
|
|||||||
rewriteToPath = toPath
|
rewriteToPath = toPath
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
|
} else if ri.hasWildcard {
|
||||||
|
if strings.HasPrefix(requestPath, ri.prefix) {
|
||||||
|
suffix := requestPath[len(ri.prefix):]
|
||||||
|
rewriteToPath = ri.toPrefix + suffix
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ri.fromPath == requestPath {
|
||||||
|
rewriteToPath = ri.toPath
|
||||||
|
found = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
@ -101,3 +134,23 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RewriteRule 定义了外部传递的 URL 重写规则
|
||||||
|
type RewriteRule struct {
|
||||||
|
Path string // 原始路径或匹配正则,例如 ^/old/(.*)$
|
||||||
|
To string // 目标路径或完整 URL,例如 /new/$1
|
||||||
|
ToPath string // [Deprecated] 重写后的路径
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的动态重写规则。
|
||||||
|
func ReplaceRewrites(host string, rules []RewriteRule) {
|
||||||
|
newRewrites := make([]*rewriteType, 0, len(rules))
|
||||||
|
for _, r := range rules {
|
||||||
|
newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath, r.To))
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPoliciesLock.Lock()
|
||||||
|
defer hostPoliciesLock.Unlock()
|
||||||
|
dynamicRewrites[host] = newRewrites
|
||||||
|
rebuildRewritesUnderLock(host)
|
||||||
|
}
|
||||||
|
|||||||
183
server.go
183
server.go
@ -1,10 +1,12 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apigo.cc/go/config"
|
||||||
"apigo.cc/go/discover"
|
"apigo.cc/go/discover"
|
||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
"apigo.cc/go/redis"
|
"apigo.cc/go/redis"
|
||||||
"apigo.cc/go/safe"
|
"apigo.cc/go/safe"
|
||||||
|
"apigo.cc/go/starter"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
@ -12,60 +14,59 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GlobalDiscoverer 供服务框架内部使用的发现实例
|
// GlobalDiscoverer 供服务框架内部使用的发现实例
|
||||||
var GlobalDiscoverer *discover.Discoverer
|
var GlobalDiscoverer *discover.Discoverer
|
||||||
|
|
||||||
// AsyncServer 异步服务实例
|
// WebServer 实现了 starter.Service 和 starter.Reloader 接口
|
||||||
type AsyncServer struct {
|
type WebServer struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
Addr string
|
Addr string
|
||||||
stopChan chan os.Signal
|
|
||||||
startChan chan bool
|
|
||||||
useDiscover bool
|
useDiscover bool
|
||||||
discoverer *discover.Discoverer
|
discoverer *discover.Discoverer
|
||||||
|
logger *log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncStart 异步启动服务
|
// NewWebServer 创建并返回一个新的 WebServer 实例
|
||||||
func AsyncStart() *AsyncServer {
|
func NewWebServer() *WebServer {
|
||||||
as := &AsyncServer{
|
return &WebServer{}
|
||||||
startChan: make(chan bool, 1),
|
}
|
||||||
stopChan: make(chan os.Signal, 1),
|
|
||||||
|
// Start 启动服务,实现 starter.Service 接口
|
||||||
|
func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.DefaultLogger
|
||||||
}
|
}
|
||||||
|
ws.logger = logger
|
||||||
|
|
||||||
go as.start()
|
|
||||||
|
|
||||||
<-as.startChan
|
|
||||||
return as
|
|
||||||
}
|
|
||||||
|
|
||||||
func (as *AsyncServer) start() {
|
|
||||||
listenStr := Config.Listen
|
listenStr := Config.Listen
|
||||||
as.useDiscover = false
|
ws.useDiscover = false
|
||||||
|
|
||||||
if listenStr == "" {
|
if listenStr == "" {
|
||||||
listenStr = ":0,h2c"
|
listenStr = ":0,h2c"
|
||||||
as.useDiscover = true
|
ws.useDiscover = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析第一个监听配置
|
// 解析第一个监听配置
|
||||||
part := strings.Split(listenStr, "|")[0]
|
part := strings.Split(listenStr, "|")[0]
|
||||||
addr, opts, _ := strings.Cut(part, ",")
|
addr, opts, _ := strings.Cut(part, ",")
|
||||||
|
|
||||||
protocol := "http"
|
protocol := ""
|
||||||
for _, opt := range strings.Split(opts, ",") {
|
for _, opt := range strings.Split(opts, ",") {
|
||||||
opt = strings.ToLower(strings.TrimSpace(opt))
|
opt = strings.ToLower(strings.TrimSpace(opt))
|
||||||
if opt == "h2c" || opt == "h2" {
|
if opt == "h2c" || opt == "h2" || opt == "http" || opt == "https" {
|
||||||
protocol = opt
|
protocol = opt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if protocol == "" {
|
||||||
|
protocol = "http" // Default to http
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.Contains(addr, ":") {
|
if !strings.Contains(addr, ":") {
|
||||||
addr = ":" + addr
|
addr = ":" + addr
|
||||||
}
|
}
|
||||||
@ -76,7 +77,7 @@ func (as *AsyncServer) start() {
|
|||||||
}
|
}
|
||||||
appName := Config.App
|
appName := Config.App
|
||||||
if appName != "" || Config.Register != "" {
|
if appName != "" || Config.Register != "" {
|
||||||
as.useDiscover = true
|
ws.useDiscover = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化服务器唯一标识 (8位,物理上限 3,844/s)
|
// 初始化服务器唯一标识 (8位,物理上限 3,844/s)
|
||||||
@ -92,18 +93,16 @@ func (as *AsyncServer) start() {
|
|||||||
|
|
||||||
listener, err := net.Listen("tcp", addr)
|
listener, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.DefaultLogger.Error("failed to listen", "addr", addr, "error", err.Error())
|
return fmt.Errorf("failed to listen on %s: %w", addr, err)
|
||||||
as.startChan <- false
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
as.listener = listener
|
ws.listener = listener
|
||||||
as.Addr = listener.Addr().String()
|
ws.Addr = listener.Addr().String()
|
||||||
serverAddr = as.Addr
|
serverAddr = ws.Addr
|
||||||
|
|
||||||
// 如果使用了随机端口且没有明确指定不需要服务发现,则开启
|
// 如果使用了随机端口且没有明确指定不需要服务发现,则开启
|
||||||
if addr == ":0" || strings.HasSuffix(addr, ":0") {
|
if addr == ":0" || strings.HasSuffix(addr, ":0") {
|
||||||
as.useDiscover = true
|
ws.useDiscover = true
|
||||||
}
|
}
|
||||||
|
|
||||||
h2s := &http2.Server{}
|
h2s := &http2.Server{}
|
||||||
@ -112,7 +111,7 @@ func (as *AsyncServer) start() {
|
|||||||
handler = h2c.NewHandler(handler, h2s)
|
handler = h2c.NewHandler(handler, h2s)
|
||||||
}
|
}
|
||||||
|
|
||||||
as.server = &http.Server{
|
ws.server = &http.Server{
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
ReadTimeout: time.Duration(Config.ReadTimeout) * time.Millisecond,
|
ReadTimeout: time.Duration(Config.ReadTimeout) * time.Millisecond,
|
||||||
ReadHeaderTimeout: time.Duration(Config.ReadHeaderTimeout) * time.Millisecond,
|
ReadHeaderTimeout: time.Duration(Config.ReadHeaderTimeout) * time.Millisecond,
|
||||||
@ -122,8 +121,8 @@ func (as *AsyncServer) start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动服务发现
|
// 启动服务发现
|
||||||
if as.useDiscover {
|
if ws.useDiscover {
|
||||||
_, port, _ := net.SplitHostPort(as.Addr)
|
_, port, _ := net.SplitHostPort(ws.Addr)
|
||||||
ip := GetServerIp()
|
ip := GetServerIp()
|
||||||
discoverAddr := fmt.Sprintf("%s:%s", ip, port)
|
discoverAddr := fmt.Sprintf("%s:%s", ip, port)
|
||||||
|
|
||||||
@ -163,52 +162,118 @@ func (as *AsyncServer) start() {
|
|||||||
registry = "127.0.0.1:6379::15" // Default fallback
|
registry = "127.0.0.1:6379::15" // Default fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
as.discoverer = discover.Start(registry, appName, discoverAddr, log.DefaultLogger, discConf)
|
ws.discoverer = discover.Start(registry, appName, discoverAddr, logger, discConf)
|
||||||
GlobalDiscoverer = as.discoverer
|
GlobalDiscoverer = ws.discoverer
|
||||||
if as.discoverer != nil {
|
if ws.discoverer != nil {
|
||||||
log.DefaultLogger.Info("discover registered", "app", appName, "addr", discoverAddr)
|
logger.Info("discover registered", "app", appName, "addr", discoverAddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signal.Notify(as.stopChan, os.Interrupt, syscall.SIGTERM)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.DefaultLogger.Info("service starting", "addr", as.Addr, "proto", protocol)
|
logger.Info("service starting", "addr", ws.Addr, "proto", protocol)
|
||||||
as.startChan <- true
|
if err := ws.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
if err := as.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
errChan <- err
|
||||||
log.DefaultLogger.Error("server error", "error", err.Error())
|
|
||||||
}
|
}
|
||||||
|
close(errChan)
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
// Stop 停止服务
|
// 短暂等待验证是否闪退
|
||||||
func (as *AsyncServer) Stop() {
|
select {
|
||||||
log.DefaultLogger.Info("service stopping")
|
case err := <-errChan:
|
||||||
if as.discoverer != nil {
|
if err != nil {
|
||||||
as.discoverer.Stop()
|
return err
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止服务,实现 starter.Service 接口
|
||||||
|
func (ws *WebServer) Stop(ctx context.Context) error {
|
||||||
|
logger := ws.logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.DefaultLogger
|
||||||
|
}
|
||||||
|
logger.Info("service stopping")
|
||||||
|
if ws.discoverer != nil {
|
||||||
|
ws.discoverer.Stop()
|
||||||
|
}
|
||||||
|
if ws.server != nil {
|
||||||
|
if err := ws.server.Shutdown(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Info("service stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status 检查服务健康状态,实现 starter.Service 接口
|
||||||
|
func (ws *WebServer) Status() (string, error) {
|
||||||
|
if ws.server == nil {
|
||||||
|
return "", fmt.Errorf("server is not running")
|
||||||
|
}
|
||||||
|
return ws.Addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload 实现配置重新加载,实现 starter.Reloader 接口
|
||||||
|
func (ws *WebServer) Reload() error {
|
||||||
|
logger := ws.logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.DefaultLogger
|
||||||
|
}
|
||||||
|
logger.Info("reloading configurations...")
|
||||||
|
|
||||||
|
// 重新加载配置文件中的策略
|
||||||
|
appName := Config.App
|
||||||
|
if appName == "" {
|
||||||
|
appName = GetDefaultName()
|
||||||
|
}
|
||||||
|
if err := config.Load(&Config, appName); err != nil {
|
||||||
|
logger.Error("failed to load config during reload", "error", err.Error())
|
||||||
|
}
|
||||||
|
ApplyConfig()
|
||||||
|
|
||||||
|
// 触发业务挂载的 Hook
|
||||||
|
return triggerReload()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsyncServer 兼容旧版异步服务实例
|
||||||
|
type AsyncServer struct {
|
||||||
|
*WebServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 兼容旧版的无参数停止方法
|
||||||
|
func (as *AsyncServer) Stop() {
|
||||||
stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond
|
stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond
|
||||||
if stopTimeout <= 0 {
|
if stopTimeout <= 0 {
|
||||||
stopTimeout = 5 * time.Second
|
stopTimeout = 5 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
_ = as.WebServer.Stop(ctx)
|
||||||
if err := as.server.Shutdown(ctx); err != nil {
|
|
||||||
log.DefaultLogger.Error("server shutdown error", "error", err.Error())
|
|
||||||
}
|
|
||||||
log.DefaultLogger.Info("service stopped")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait 等待服务结束 (信号监听)
|
// AsyncStart 兼容旧版的异步启动方法
|
||||||
|
func AsyncStart() *AsyncServer {
|
||||||
|
ws := NewWebServer()
|
||||||
|
_ = ws.Start(context.Background(), log.DefaultLogger)
|
||||||
|
return &AsyncServer{WebServer: ws}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 等待服务结束 (兼容旧版,直接阻塞)
|
||||||
func (as *AsyncServer) Wait() {
|
func (as *AsyncServer) Wait() {
|
||||||
<-as.stopChan
|
select {}
|
||||||
as.Stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start 同步启动服务
|
// Start 兼容旧版的同步启动方法 (通过内部注册 starter 实现)
|
||||||
func Start() {
|
func Start() {
|
||||||
AsyncStart().Wait()
|
stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond
|
||||||
|
if stopTimeout <= 0 {
|
||||||
|
stopTimeout = 5 * time.Second
|
||||||
|
}
|
||||||
|
starter.Register("web-server", NewWebServer(), 100, 5*time.Second, stopTimeout)
|
||||||
|
starter.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
81
service.go
81
service.go
@ -72,9 +72,19 @@ var (
|
|||||||
websocketServicesLock = sync.RWMutex{}
|
websocketServicesLock = sync.RWMutex{}
|
||||||
websocketServicesList = make([]*websocketServiceType, 0)
|
websocketServicesList = make([]*websocketServiceType, 0)
|
||||||
|
|
||||||
// Rewrite 与 Proxy 按 Host 隔离
|
// Rewrite 与 Proxy 按 Host 隔离 (编译后的最终路由)
|
||||||
hostRewrites = make(map[string][]*rewriteType)
|
hostRewrites = make(map[string][]*rewriteType)
|
||||||
hostProxies = make(map[string][]*proxyType)
|
hostProxies = make(map[string][]*proxyType)
|
||||||
|
|
||||||
|
// 按来源隔离的策略,避免互相覆盖
|
||||||
|
codeProxies = make(map[string][]*proxyType)
|
||||||
|
fileProxies = make(map[string][]*proxyType)
|
||||||
|
dynamicProxies = make(map[string][]*proxyType)
|
||||||
|
|
||||||
|
codeRewrites = make(map[string][]*rewriteType)
|
||||||
|
fileRewrites = make(map[string][]*rewriteType)
|
||||||
|
dynamicRewrites = make(map[string][]*rewriteType)
|
||||||
|
|
||||||
hostPoliciesLock = sync.RWMutex{}
|
hostPoliciesLock = sync.RWMutex{}
|
||||||
|
|
||||||
// 过滤器与拦截器
|
// 过滤器与拦截器
|
||||||
@ -141,6 +151,26 @@ func Host(host string) *HostContext {
|
|||||||
return &HostContext{host: host}
|
return &HostContext{host: host}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register 注册一个 Web 服务 (使用默认 Host "*")
|
||||||
|
func Register(method, path string, serviceFunc any) *webServiceType {
|
||||||
|
return Host("*").Register(method, path, serviceFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterWebsocket 注册一个 WebSocket 服务 (使用默认 Host "*")
|
||||||
|
func RegisterWebsocket(path string, serviceFunc any) *websocketServiceType {
|
||||||
|
return Host("*").WebSocket(path, serviceFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy 注册一个代理转发 (使用默认 Host "*")
|
||||||
|
func Proxy(authLevel int, path string, to string) {
|
||||||
|
Host("*").Proxy(authLevel, path, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restful 注册一个符合 RESTful 规范的服务结构体 (使用默认 Host "*")
|
||||||
|
func Restful(authLevel int, path string, serviceStruct any) {
|
||||||
|
Host("*").Restful(authLevel, path, serviceStruct)
|
||||||
|
}
|
||||||
|
|
||||||
func (hc *HostContext) Register(method, path string, serviceFunc any) *webServiceType {
|
func (hc *HostContext) Register(method, path string, serviceFunc any) *webServiceType {
|
||||||
s, err := makeCachedService(serviceFunc)
|
s, err := makeCachedService(serviceFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -250,16 +280,14 @@ func (gc *GroupContext) WebSocket(path string, serviceFunc any) *websocketServic
|
|||||||
return gc.hc.WebSocket(gc.prefix+path, serviceFunc)
|
return gc.hc.WebSocket(gc.prefix+path, serviceFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GroupContext) Rewrite(path string, toPath string) *GroupContext {
|
func (gc *GroupContext) Rewrite(path string, to string) *GroupContext {
|
||||||
gc.hc.Rewrite(gc.prefix+path, toPath)
|
gc.hc.Rewrite(gc.prefix+path, to)
|
||||||
return gc
|
return gc
|
||||||
}
|
}
|
||||||
|
func (gc *GroupContext) Proxy(authLevel int, path string, to string) *GroupContext {
|
||||||
func (gc *GroupContext) Proxy(authLevel int, path string, toApp, toPath string) *GroupContext {
|
gc.hc.Proxy(authLevel, gc.prefix+path, to)
|
||||||
gc.hc.Proxy(authLevel, gc.prefix+path, toApp, toPath)
|
|
||||||
return gc
|
return gc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketServiceType {
|
func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketServiceType {
|
||||||
funcType := reflect.TypeOf(serviceFunc)
|
funcType := reflect.TypeOf(serviceFunc)
|
||||||
if funcType.Kind() != reflect.Func {
|
if funcType.Kind() != reflect.Func {
|
||||||
@ -283,6 +311,45 @@ func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketService
|
|||||||
return ws
|
return ws
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restful 自动根据方法名注册 RESTful 服务
|
||||||
|
func (hc *HostContext) Restful(authLevel int, path string, serviceStruct any) {
|
||||||
|
v := reflect.ValueOf(serviceStruct)
|
||||||
|
t := v.Type()
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < v.NumMethod(); i++ {
|
||||||
|
methodName := v.Type().Method(i).Name
|
||||||
|
var httpMethod string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(methodName, "Get"):
|
||||||
|
httpMethod = "GET"
|
||||||
|
case strings.HasPrefix(methodName, "Post"):
|
||||||
|
httpMethod = "POST"
|
||||||
|
case strings.HasPrefix(methodName, "Put"):
|
||||||
|
httpMethod = "PUT"
|
||||||
|
case strings.HasPrefix(methodName, "Delete"):
|
||||||
|
httpMethod = "DELETE"
|
||||||
|
case strings.HasPrefix(methodName, "Patch"):
|
||||||
|
httpMethod = "PATCH"
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
subPath := strings.ToLower(methodName[len(httpMethod):])
|
||||||
|
if subPath == "" {
|
||||||
|
hc.Register(httpMethod, path, v.Method(i).Interface()).Auth(authLevel)
|
||||||
|
} else {
|
||||||
|
fullPath := path
|
||||||
|
if !strings.HasSuffix(fullPath, "/") {
|
||||||
|
fullPath += "/"
|
||||||
|
}
|
||||||
|
hc.Register(httpMethod, fullPath+subPath, v.Method(i).Interface()).Auth(authLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// webServiceType 链式配置方法
|
// webServiceType 链式配置方法
|
||||||
func (s *webServiceType) Auth(level int) *webServiceType {
|
func (s *webServiceType) Auth(level int) *webServiceType {
|
||||||
s.authLevel = level
|
s.authLevel = level
|
||||||
|
|||||||
55
static.go
55
static.go
@ -14,6 +14,11 @@ import (
|
|||||||
var (
|
var (
|
||||||
statics = make(map[string]*string)
|
statics = make(map[string]*string)
|
||||||
staticsByHost = make(map[string]map[string]*string)
|
staticsByHost = make(map[string]map[string]*string)
|
||||||
|
|
||||||
|
codeStatics = make(map[string]map[string]*string)
|
||||||
|
fileStatics = make(map[string]map[string]*string)
|
||||||
|
dynamicStatics = make(map[string]map[string]*string)
|
||||||
|
|
||||||
staticsByHostLock = sync.RWMutex{}
|
staticsByHostLock = sync.RWMutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,13 +38,51 @@ func StaticByHost(path, rootPath, host string) {
|
|||||||
staticsByHostLock.Lock()
|
staticsByHostLock.Lock()
|
||||||
defer staticsByHostLock.Unlock()
|
defer staticsByHostLock.Unlock()
|
||||||
|
|
||||||
if host == "" {
|
if codeStatics[host] == nil {
|
||||||
statics[path] = &rootPath
|
codeStatics[host] = make(map[string]*string)
|
||||||
} else {
|
}
|
||||||
if staticsByHost[host] == nil {
|
codeStatics[host][path] = &rootPath
|
||||||
staticsByHost[host] = make(map[string]*string)
|
rebuildStaticsUnderLock(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceStatics 使用 Copy-on-Write 机制原子地替换指定 host 下的动态静态目录规则
|
||||||
|
func ReplaceStatics(host string, config map[string]string) {
|
||||||
|
newStatics := make(map[string]*string, len(config))
|
||||||
|
for path, rootPath := range config {
|
||||||
|
rp := rootPath
|
||||||
|
if !filepath.IsAbs(rp) {
|
||||||
|
if absPath, err := filepath.Abs(rp); err == nil {
|
||||||
|
rp = absPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
staticsByHost[host][path] = &rootPath
|
newStatics[path] = &rp
|
||||||
|
}
|
||||||
|
|
||||||
|
staticsByHostLock.Lock()
|
||||||
|
defer staticsByHostLock.Unlock()
|
||||||
|
|
||||||
|
dynamicStatics[host] = newStatics
|
||||||
|
rebuildStaticsUnderLock(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebuildStaticsUnderLock(host string) {
|
||||||
|
combined := make(map[string]*string)
|
||||||
|
|
||||||
|
// 合并三种来源的静态路由
|
||||||
|
for k, v := range codeStatics[host] {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range fileStatics[host] {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range dynamicStatics[host] {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
statics = combined
|
||||||
|
} else {
|
||||||
|
staticsByHost[host] = combined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user