Compare commits

...

7 Commits
v1.3.2 ... main

10 changed files with 471 additions and 125 deletions

View File

@ -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`

View File

@ -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)
}
}

30
go.mod
View File

@ -3,26 +3,26 @@ 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/starter v1.0.1 apigo.cc/go/safe v1.3.1
apigo.cc/go/timer v1.3.0 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 v2.0.0+incompatible // 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

60
go.sum
View File

@ -1,33 +1,33 @@
apigo.cc/go/cast v1.3.0 h1:ZTcLYijkqZjSWSCSpJUWMfzJYeJKbwKxquKkPrFsROQ= apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
apigo.cc/go/cast v1.3.0/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg= apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE=
apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI= apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U=
apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8= apigo.cc/go/crypto v1.3.1 h1:ulQ2zX9bUWirk0sEacx1Srsjs2Jow7HlZq7ED7msNcg=
apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc= apigo.cc/go/crypto v1.3.1/go.mod h1:SwHlBFDPddttWgFFtzsEMla8CM/rcFy9nvdsJjW4CIs=
apigo.cc/go/discover v1.3.0 h1:CXuKtAZygU+4TMHtebVkjWyyWmPgoLbsJFdKFGiCOd8= apigo.cc/go/discover v1.3.2 h1:hzo5PQSAbJrF9Qk7yCMoXWatHGe7lR2MNUqLLQ5oUSk=
apigo.cc/go/discover v1.3.0/go.mod h1:VMu1qC6AngVFQMdaCwGoq3/PPX0xDnjkG+1AcSA+Zvs= apigo.cc/go/discover v1.3.2/go.mod h1:77kZLGovdCYAMspkmL6iif65Yzhyg143ffQseZx+j40=
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs= apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo= apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg= apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4=
apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY= apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs=
apigo.cc/go/http v1.3.0 h1:1ZweotOuAxTI8wfib9knWYXM2t0POOJ3ezgOKObH3sg= apigo.cc/go/http v1.3.2 h1:0Or5KfoIq4+yeWKYusYPV8XLPw8XuzJMeaFv7dZViLI=
apigo.cc/go/http v1.3.0/go.mod h1:DC3phxBNbt/dOWdhxtffAEYeUs3j6P3BD8e6J8gxU9U= apigo.cc/go/http v1.3.2/go.mod h1:Q9R7Ors0Fz2A6Mxg0dykO2PjCzdAHRRXreOUMjMOLwA=
apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE= apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk=
apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA= apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4=
apigo.cc/go/log v1.3.0 h1:61Z80WGN6SnhgxgoR8xuVYIieMdjlJKmf8JX1HXzp0Y= apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g=
apigo.cc/go/log v1.3.0/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE= apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20=
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU= apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/redis v1.3.0 h1:3NJE3xPXzhCwL+Mh1iyphFrsKWEuPlY26LHJfMVFSeU= apigo.cc/go/redis v1.3.2 h1:iUWL/CHHnfonz0dJq6/V4IG3QuXBoHA2L1xnoGEbNEQ=
apigo.cc/go/redis v1.3.0/go.mod h1:KPDPwMOER7WJX3Qev24LTeAOSmCl8OApe8iagPDxOUQ= apigo.cc/go/redis v1.3.2/go.mod h1:/k5wcfAzB9jrfd9otabio9CPUxEsLPgEs4oggBG5sbs=
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok= apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs= apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw= apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8=
apigo.cc/go/shell v1.3.0/go.mod h1:aNJiRWibxlA485yX3t+07IVAbrALKmxzv4oGEUC+hK4= apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw=
apigo.cc/go/starter v1.0.1 h1:7Qv/rRlEVlTX7wjr1LpV1XX1wUD4UAssDi6J+YCh73s= apigo.cc/go/starter v1.0.5 h1:pgjBun7zc3J+3hcWnP22bGtgB3+TiNXOrOGVyGM7u1Q=
apigo.cc/go/starter v1.0.1/go.mod h1:xHfo+36hXGdVhhnRqd1l+Vk6Fp1ecN2LDAcsDOVodXk= apigo.cc/go/starter v1.0.5/go.mod h1:auAvnBknZuMMps6HRtlwf8Z5gCHRQYsVw7WhhiGwWpg=
apigo.cc/go/timer v1.3.0 h1:dorVGKw0xR6Gj8Pwfl86K46szMBfD31XyO+uUqxU+EI= apigo.cc/go/timer v1.3.1 h1:YMSusF1LfJYOf6tAW94Yipj3pHrX6QhfP7Rk3nGFT8k=
apigo.cc/go/timer v1.3.0/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs= apigo.cc/go/timer v1.3.1/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 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=

113
proxy.go
View File

@ -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
}
} }
} }
} }
@ -167,26 +232,20 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) {
type ProxyRule struct { type ProxyRule struct {
Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$ Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$
AuthLevel int // 所需鉴权级别 AuthLevel int // 所需鉴权级别
ToApp string // 目标 AppName 或完整 URL (可含 $1 变量替换) To string // 目标地址,格式为 "app/path" 或 "http://url/path" (支持后缀 /* 映射)
ToPath string // 目标路径 (可含 $1 变量替换) ToApp string // [Deprecated] 目标 AppName 或完整 URL (可含 $1 变量替换)
ToPath string // [Deprecated] 目标路径 (可含 $1 变量替换)
} }
// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的所有代理规则。 // ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的动态代理规则。
// 该方法非常轻量,仅在赋值瞬间短暂持有写锁,不会阻塞任何并发请求,并且自动淘汰旧规则。
func ReplaceProxies(host string, rules []ProxyRule) { func ReplaceProxies(host string, rules []ProxyRule) {
newProxies := make([]*proxyType, 0, len(rules)) newProxies := make([]*proxyType, 0, len(rules))
for _, r := range rules { for _, r := range rules {
p := &proxyType{authLevel: r.AuthLevel, fromPath: r.Path, toApp: r.ToApp, toPath: r.ToPath} newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath, r.To))
if strings.ContainsRune(r.Path, '(') {
matcher, err := regexp.Compile("^" + r.Path + "$")
if err == nil {
p.matcher = matcher
}
}
newProxies = append(newProxies, p)
} }
hostPoliciesLock.Lock() hostPoliciesLock.Lock()
defer hostPoliciesLock.Unlock() defer hostPoliciesLock.Unlock()
hostProxies[host] = newProxies dynamicProxies[host] = newProxies
rebuildProxiesUnderLock(host)
} }

View File

@ -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)

View File

@ -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 {
@ -105,24 +138,19 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
// RewriteRule 定义了外部传递的 URL 重写规则 // RewriteRule 定义了外部传递的 URL 重写规则
type RewriteRule struct { type RewriteRule struct {
Path string // 原始路径或匹配正则,例如 ^/old/(.*)$ Path string // 原始路径或匹配正则,例如 ^/old/(.*)$
ToPath string // 重写后的路径,例如 /new/$1 To string // 目标路径或完整 URL例如 /new/$1
ToPath string // [Deprecated] 重写后的路径
} }
// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的所有重写规则。 // ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的动态重写规则。
func ReplaceRewrites(host string, rules []RewriteRule) { func ReplaceRewrites(host string, rules []RewriteRule) {
newRewrites := make([]*rewriteType, 0, len(rules)) newRewrites := make([]*rewriteType, 0, len(rules))
for _, r := range rules { for _, r := range rules {
s := &rewriteType{fromPath: r.Path, toPath: r.ToPath} newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath, r.To))
if strings.ContainsRune(r.Path, '(') {
matcher, err := regexp.Compile("^" + r.Path + "$")
if err == nil {
s.matcher = matcher
}
}
newRewrites = append(newRewrites, s)
} }
hostPoliciesLock.Lock() hostPoliciesLock.Lock()
defer hostPoliciesLock.Unlock() defer hostPoliciesLock.Unlock()
hostRewrites[host] = newRewrites dynamicRewrites[host] = newRewrites
rebuildRewritesUnderLock(host)
} }

View File

@ -1,6 +1,7 @@
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"
@ -27,6 +28,7 @@ type WebServer struct {
Addr string Addr string
useDiscover bool useDiscover bool
discoverer *discover.Discoverer discoverer *discover.Discoverer
logger *log.Logger
} }
// NewWebServer 创建并返回一个新的 WebServer 实例 // NewWebServer 创建并返回一个新的 WebServer 实例
@ -36,6 +38,11 @@ func NewWebServer() *WebServer {
// Start 启动服务,实现 starter.Service 接口 // Start 启动服务,实现 starter.Service 接口
func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error { func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
if logger == nil {
logger = log.DefaultLogger
}
ws.logger = logger
listenStr := Config.Listen listenStr := Config.Listen
ws.useDiscover = false ws.useDiscover = false
@ -48,14 +55,18 @@ func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
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
} }
@ -182,7 +193,11 @@ func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
// Stop 停止服务,实现 starter.Service 接口 // Stop 停止服务,实现 starter.Service 接口
func (ws *WebServer) Stop(ctx context.Context) error { func (ws *WebServer) Stop(ctx context.Context) error {
log.DefaultLogger.Info("service stopping") logger := ws.logger
if logger == nil {
logger = log.DefaultLogger
}
logger.Info("service stopping")
if ws.discoverer != nil { if ws.discoverer != nil {
ws.discoverer.Stop() ws.discoverer.Stop()
} }
@ -191,21 +206,37 @@ func (ws *WebServer) Stop(ctx context.Context) error {
return err return err
} }
} }
log.DefaultLogger.Info("service stopped") logger.Info("service stopped")
return nil return nil
} }
// Health 检查服务健康状态,实现 starter.Service 接口 // Status 检查服务健康状态,实现 starter.Service 接口
func (ws *WebServer) Health() error { func (ws *WebServer) Status() (string, error) {
if ws.server == nil { if ws.server == nil {
return fmt.Errorf("server is not running") return "", fmt.Errorf("server is not running")
} }
return nil return ws.Addr, nil
} }
// Reload 实现配置重新加载,实现 starter.Reloader 接口 // Reload 实现配置重新加载,实现 starter.Reloader 接口
func (ws *WebServer) Reload() error { func (ws *WebServer) Reload() error {
log.DefaultLogger.Info("reloading configurations...") 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() return triggerReload()
} }

View File

@ -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

View File

@ -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,17 +38,14 @@ 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 {
staticsByHost[host] = make(map[string]*string)
}
staticsByHost[host][path] = &rootPath
} }
codeStatics[host][path] = &rootPath
rebuildStaticsUnderLock(host)
} }
// ReplaceStatics 使用 Copy-on-Write 机制原子地替换指定 host 下的所有静态目录规则 // ReplaceStatics 使用 Copy-on-Write 机制原子地替换指定 host 下的动态静态目录规则
func ReplaceStatics(host string, config map[string]string) { func ReplaceStatics(host string, config map[string]string) {
newStatics := make(map[string]*string, len(config)) newStatics := make(map[string]*string, len(config))
for path, rootPath := range config { for path, rootPath := range config {
@ -59,10 +61,28 @@ func ReplaceStatics(host string, config map[string]string) {
staticsByHostLock.Lock() staticsByHostLock.Lock()
defer staticsByHostLock.Unlock() 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 == "" { if host == "" {
statics = newStatics statics = combined
} else { } else {
staticsByHost[host] = newStatics staticsByHost[host] = combined
} }
} }