feat: implement WebServer, Reloader interface and atomic hot-reload methods ReplaceProxies, ReplaceRewrites, ReplaceStatics (by AI)
This commit is contained in:
parent
c207bdd400
commit
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
|
||||||
|
|||||||
3
go.mod
3
go.mod
@ -11,6 +11,7 @@ require (
|
|||||||
apigo.cc/go/log v1.3.0
|
apigo.cc/go/log v1.3.0
|
||||||
apigo.cc/go/redis v1.3.0
|
apigo.cc/go/redis v1.3.0
|
||||||
apigo.cc/go/safe v1.3.0
|
apigo.cc/go/safe v1.3.0
|
||||||
|
apigo.cc/go/starter v1.0.1
|
||||||
apigo.cc/go/timer v1.3.0
|
apigo.cc/go/timer v1.3.0
|
||||||
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
|
||||||
@ -22,7 +23,7 @@ require (
|
|||||||
apigo.cc/go/encoding v1.3.0 // indirect
|
apigo.cc/go/encoding v1.3.0 // indirect
|
||||||
apigo.cc/go/rand v1.3.0 // indirect
|
apigo.cc/go/rand v1.3.0 // indirect
|
||||||
apigo.cc/go/shell v1.3.0 // indirect
|
apigo.cc/go/shell v1.3.0 // indirect
|
||||||
github.com/gomodule/redigo v1.9.3 // 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
|
||||||
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.0 h1:ZTcLYijkqZjSWSCSpJUWMfzJYeJKbwKxquKkPrFsROQ=
|
||||||
apigo.cc/go/cast v1.2.10/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
apigo.cc/go/cast v1.3.0/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||||
apigo.cc/go/config v1.0.8 h1:ZvontnJngNJrm6EJAPYmPhmBnLC9V7g5kZLiuN1MT60=
|
apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg=
|
||||||
apigo.cc/go/config v1.0.8/go.mod h1:FCZj70MCejeWwv81O7sdpg0zmjOzglAMmNEfT3dQYzw=
|
apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI=
|
||||||
apigo.cc/go/crypto v1.1.1 h1:AE0jNtKzcq4euz6fL9MAYEHQpbIEfDTHv2mriP/juig=
|
apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8=
|
||||||
apigo.cc/go/crypto v1.1.1/go.mod h1:Q26As+TQrNs6olGkiVdD6649DJirxA4CUBT4oukKPuw=
|
apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc=
|
||||||
apigo.cc/go/discover v1.0.11 h1:aeAC+xAwGlOeXsRptXJkEn8MvRZ7lom5N5jfBAg9/CE=
|
apigo.cc/go/discover v1.3.0 h1:CXuKtAZygU+4TMHtebVkjWyyWmPgoLbsJFdKFGiCOd8=
|
||||||
apigo.cc/go/discover v1.0.11/go.mod h1:TcIpl1Ocu51koRxugV81Jnz4NH0+Q5f5PF105VczS/0=
|
apigo.cc/go/discover v1.3.0/go.mod h1:VMu1qC6AngVFQMdaCwGoq3/PPX0xDnjkG+1AcSA+Zvs=
|
||||||
apigo.cc/go/encoding v1.1.2 h1:reSrLkyYrtZsf4S91XPdyBY2AQpvA43n9q0Q9wz5uJA=
|
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
|
||||||
apigo.cc/go/encoding v1.1.2/go.mod h1:iLuvrYHEK8mLnk8jijx5Sv1tInFreny0yGNBouA1d20=
|
apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo=
|
||||||
apigo.cc/go/file v1.0.8 h1:GPkixU080cvrmz7cbdXkC2DqMvsWWyY3UzoyUVQYFvs=
|
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
|
||||||
apigo.cc/go/file v1.0.8/go.mod h1:T/wYji/va0S+JM2fAHonhKpnXKIELk/bmgnFEgMMY2s=
|
apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY=
|
||||||
apigo.cc/go/http v1.0.11 h1:EOlMXlTGrWY0RI3MynkV7noT49WiUdGVPdOtDJjIkU4=
|
apigo.cc/go/http v1.3.0 h1:1ZweotOuAxTI8wfib9knWYXM2t0POOJ3ezgOKObH3sg=
|
||||||
apigo.cc/go/http v1.0.11/go.mod h1:K2JgyI7DblfbzAnK1OHx4PS/1Pvcoqcp3g2uwsCPe68=
|
apigo.cc/go/http v1.3.0/go.mod h1:DC3phxBNbt/dOWdhxtffAEYeUs3j6P3BD8e6J8gxU9U=
|
||||||
apigo.cc/go/id v1.0.7 h1:vXCK8mUW3s4cJYmli0o2BxgyI9XbJrG8gSGJOP2Fe4g=
|
apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE=
|
||||||
apigo.cc/go/id v1.0.7/go.mod h1:wXBrPpcEpyUDM7bp7M5uPM9zFw4VcnvXMQLw4Yd+uZE=
|
apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA=
|
||||||
apigo.cc/go/log v1.1.16 h1:uqPqeHvs+FdNupLBzzamJmY4oHAqtPEkGuW/pW5i2nQ=
|
apigo.cc/go/log v1.3.0 h1:61Z80WGN6SnhgxgoR8xuVYIieMdjlJKmf8JX1HXzp0Y=
|
||||||
apigo.cc/go/log v1.1.16/go.mod h1:bOfPXjrX2bY+FNG9eEtBnvaVXoxZDGvz0jQfF3s/mYk=
|
apigo.cc/go/log v1.3.0/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE=
|
||||||
apigo.cc/go/rand v1.0.6 h1:p51rkaDrYUdZPIRbQAujZmQelWg2ipAMts33A/tG7QE=
|
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
|
||||||
apigo.cc/go/rand v1.0.6/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||||
apigo.cc/go/redis v1.0.9 h1:5MnCchcDgoVnGHQn+KJF9PJXXRayK9nlZg+Q0lTnMoU=
|
apigo.cc/go/redis v1.3.0 h1:3NJE3xPXzhCwL+Mh1iyphFrsKWEuPlY26LHJfMVFSeU=
|
||||||
apigo.cc/go/redis v1.0.9/go.mod h1:SVOD7iuUL/jxYEa28qObDQf4GY9UVSKjhM9vVj9TXLI=
|
apigo.cc/go/redis v1.3.0/go.mod h1:KPDPwMOER7WJX3Qev24LTeAOSmCl8OApe8iagPDxOUQ=
|
||||||
apigo.cc/go/safe v1.0.7 h1:f0d+v9K2dHPyG5DNqhyddCmAmSiIqIfkPi/AMED/iQI=
|
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
|
||||||
apigo.cc/go/safe v1.0.7/go.mod h1:Hu7TVDWPe/I+nBZfYJH4mt+ROzG+rwk2D1zHTXj/2eE=
|
apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs=
|
||||||
apigo.cc/go/shell v1.0.6 h1:RngaSMr2AkAFDl545A1Ln+D8ckqV2jknUp4PohDaLIA=
|
apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw=
|
||||||
apigo.cc/go/shell v1.0.6/go.mod h1:X7Nozjd7oau4nvAJCI21vxrxfd4ZL5nE4C6eUsmi2Hc=
|
apigo.cc/go/shell v1.3.0/go.mod h1:aNJiRWibxlA485yX3t+07IVAbrALKmxzv4oGEUC+hK4=
|
||||||
apigo.cc/go/timer v1.0.7 h1:QUH0t7l9kBiGU/QdDNSthnXLfJXOEp+mpdY2+QPlrEI=
|
apigo.cc/go/starter v1.0.1 h1:7Qv/rRlEVlTX7wjr1LpV1XX1wUD4UAssDi6J+YCh73s=
|
||||||
apigo.cc/go/timer v1.0.7/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
|
apigo.cc/go/starter v1.0.1/go.mod h1:xHfo+36hXGdVhhnRqd1l+Vk6Fp1ecN2LDAcsDOVodXk=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
apigo.cc/go/timer v1.3.0 h1:dorVGKw0xR6Gj8Pwfl86K46szMBfD31XyO+uUqxU+EI=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
apigo.cc/go/timer v1.3.0/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=
|
||||||
|
|||||||
28
proxy.go
28
proxy.go
@ -162,3 +162,31 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyRule 定义了外部传递或 Redis 中获取的代理规则配置
|
||||||
|
type ProxyRule struct {
|
||||||
|
Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$
|
||||||
|
AuthLevel int // 所需鉴权级别
|
||||||
|
ToApp string // 目标 AppName 或完整 URL (可含 $1 变量替换)
|
||||||
|
ToPath string // 目标路径 (可含 $1 变量替换)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的所有代理规则。
|
||||||
|
// 该方法非常轻量,仅在赋值瞬间短暂持有写锁,不会阻塞任何并发请求,并且自动淘汰旧规则。
|
||||||
|
func ReplaceProxies(host string, rules []ProxyRule) {
|
||||||
|
newProxies := make([]*proxyType, 0, len(rules))
|
||||||
|
for _, r := range rules {
|
||||||
|
p := &proxyType{authLevel: r.AuthLevel, fromPath: r.Path, toApp: r.ToApp, toPath: r.ToPath}
|
||||||
|
if strings.ContainsRune(r.Path, '(') {
|
||||||
|
matcher, err := regexp.Compile("^" + r.Path + "$")
|
||||||
|
if err == nil {
|
||||||
|
p.matcher = matcher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newProxies = append(newProxies, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPoliciesLock.Lock()
|
||||||
|
defer hostPoliciesLock.Unlock()
|
||||||
|
hostProxies[host] = newProxies
|
||||||
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
25
rewrite.go
25
rewrite.go
@ -101,3 +101,28 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RewriteRule 定义了外部传递的 URL 重写规则
|
||||||
|
type RewriteRule struct {
|
||||||
|
Path string // 原始路径或匹配正则,例如 ^/old/(.*)$
|
||||||
|
ToPath string // 重写后的路径,例如 /new/$1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的所有重写规则。
|
||||||
|
func ReplaceRewrites(host string, rules []RewriteRule) {
|
||||||
|
newRewrites := make([]*rewriteType, 0, len(rules))
|
||||||
|
for _, r := range rules {
|
||||||
|
s := &rewriteType{fromPath: r.Path, toPath: r.ToPath}
|
||||||
|
if strings.ContainsRune(r.Path, '(') {
|
||||||
|
matcher, err := regexp.Compile("^" + r.Path + "$")
|
||||||
|
if err == nil {
|
||||||
|
s.matcher = matcher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newRewrites = append(newRewrites, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPoliciesLock.Lock()
|
||||||
|
defer hostPoliciesLock.Unlock()
|
||||||
|
hostRewrites[host] = newRewrites
|
||||||
|
}
|
||||||
|
|||||||
144
server.go
144
server.go
@ -5,6 +5,7 @@ import (
|
|||||||
"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,46 +13,35 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncStart 异步启动服务
|
// NewWebServer 创建并返回一个新的 WebServer 实例
|
||||||
func AsyncStart() *AsyncServer {
|
func NewWebServer() *WebServer {
|
||||||
as := &AsyncServer{
|
return &WebServer{}
|
||||||
startChan: make(chan bool, 1),
|
|
||||||
stopChan: make(chan os.Signal, 1),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go as.start()
|
// Start 启动服务,实现 starter.Service 接口
|
||||||
|
func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
|
||||||
<-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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析第一个监听配置
|
// 解析第一个监听配置
|
||||||
@ -76,7 +66,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 +82,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 +100,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 +110,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 +151,98 @@ 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)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 短暂等待验证是否闪退
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop 停止服务
|
return nil
|
||||||
func (as *AsyncServer) Stop() {
|
}
|
||||||
|
|
||||||
|
// Stop 停止服务,实现 starter.Service 接口
|
||||||
|
func (ws *WebServer) Stop(ctx context.Context) error {
|
||||||
log.DefaultLogger.Info("service stopping")
|
log.DefaultLogger.Info("service stopping")
|
||||||
if as.discoverer != nil {
|
if ws.discoverer != nil {
|
||||||
as.discoverer.Stop()
|
ws.discoverer.Stop()
|
||||||
|
}
|
||||||
|
if ws.server != nil {
|
||||||
|
if err := ws.server.Shutdown(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.DefaultLogger.Info("service stopped")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Health 检查服务健康状态,实现 starter.Service 接口
|
||||||
|
func (ws *WebServer) Health() error {
|
||||||
|
if ws.server == nil {
|
||||||
|
return fmt.Errorf("server is not running")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload 实现配置重新加载,实现 starter.Reloader 接口
|
||||||
|
func (ws *WebServer) Reload() error {
|
||||||
|
log.DefaultLogger.Info("reloading configurations...")
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
23
static.go
23
static.go
@ -43,6 +43,29 @@ func StaticByHost(path, rootPath, host string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newStatics[path] = &rp
|
||||||
|
}
|
||||||
|
|
||||||
|
staticsByHostLock.Lock()
|
||||||
|
defer staticsByHostLock.Unlock()
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
statics = newStatics
|
||||||
|
} else {
|
||||||
|
staticsByHost[host] = newStatics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getStaticFilePath(requestPath, host string) string {
|
func getStaticFilePath(requestPath, host string) string {
|
||||||
staticsByHostLock.RLock()
|
staticsByHostLock.RLock()
|
||||||
defer staticsByHostLock.RUnlock()
|
defer staticsByHostLock.RUnlock()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user