Files
gf/net/ghttp/ghttp_server.go
Lance Add d8a173d9f0 feat(instance): migrate instance containers to type-safe generics (#4617)
### 变更说明

本次重构将项目中用于**实例管理的容器**从 `StrAnyMap`/`IntAnyMap` 迁移到类型安全的泛型实现
`KVMapWithChecker`,同时将相关的 `glist.List` 和 `gqueue.Queue`
替换为对应的泛型版本,以提高实例管理的类型安全性。并且减少原先代码中的大量类型断言,提高性能。

### 前因

目前`goframe`中大量使用了包含`any`的容器,然后通过断言去转换类型,麻烦且影响性能,尤其是对`gdb/gredis/glog`等需要高频获取`instance`实例的组件影响较大。最近几个版本中gf完成了数据结构容器的泛型化改造,以及我最近解决了其中几个泛型容器对于`typed
nil`过滤的问题,所以可以逐步迁移这些实例容器到泛型容器,减少断言优化性能

### 主要改进

#### 1. 实例容器泛型化

以下模块的实例管理容器已迁移到泛型实现:

**核心实例管理**:
- `database/gdb`: 数据库实例容器 → `KVMap[string, DB]`
- `database/gredis`: Redis 实例容器 → `KVMap[string, *Redis]`
- `database/gredis`: Redis 配置容器 → `KVMap[string, *Config]`
- `os/gcfg`: 配置实例容器 → `KVMap[string, *Config]`
- `os/glog`: 日志实例容器 → `KVMap[string, *Logger]`
- `os/gview`: 视图实例容器 → `KVMap[string, *View]`
- `i18n/gi18n`: 国际化实例容器 → `KVMap[string, *Manager]`

**网络服务实例**:
- `net/ghttp`: HTTP 服务器容器 → `KVMap[string, *Server]`
- `net/gtcp`: TCP 服务器容器 → `KVMap[any, *Server]`
- `net/gudp`: UDP 服务器容器 → `KVMap[string, *Server]`

**其他实例容器**:
- `os/gres`: 资源实例容器 → `KVMap[string, *Resource]`
- `os/gfpool`: 文件池容器 → `KVMap[string, *Pool]`
- `os/gspath`: 路径搜索容器 → `KVMap[string, *SPath]`
- `net/gtcp`: 连接池容器 → `KVMap[string, *gpool.Pool]`

#### 2. 相关数据结构泛型化

- `os/gfsnotify`: 回调列表 → `TList[*Callback]`,事件队列 → `TQueue[*Event]`
- `os/grpool`: 任务队列 → `TList[*localPoolItem]`
- `os/gcache`: 事件队列 → `TList[*adapterMemoryEvent]`
- `net/ghttp`: 解析项列表 → `TList[*HandlerItemParsed]`
- `os/gproc`: 消息队列 → `TQueue[*MsgRequest]`
- `os/gmlock`: 锁映射 → `KVMap[string, *sync.RWMutex]`

### 技术实现

1. **引入检查器函数**: 为每个实例容器添加 `checker` 函数用于空值检测
2. **消除类型断言**: 实例获取时无需 `v.(*Type)` 转换
3. **明确函数签名**: `GetOrSetFuncLock` 的回调从 `func() any` 改为 `func() T`

### 使用示例

#### 实例容器的变更

**变更前**:
```go
// 旧的实例管理方式
var instances = gmap.NewStrAnyMap(true)

func Instance(name string) *Logger {
    v := instances.GetOrSetFuncLock(name, func() any {
        return New()
    })
    return v.(*Logger)  // 需要类型断言
}
```


**变更后**:
```go
// 新的泛型实例容器
var (
    checker   = func(v *Logger) bool { return v == nil }
    instances = gmap.NewKVMapWithChecker[string, *Logger](checker, true)
)

func Instance(name string) *Logger {
    return instances.GetOrSetFuncLock(name, New)  // 直接返回,无需断言
}
```


#### 队列容器的变更

**变更前**:
```go
// 旧的队列方式
events := gqueue.New()
events.Push(&Event{Path: "/tmp/file"})

if v := events.Pop(); v != nil {
    event := v.(*Event)  // 需要类型断言
    handleEvent(event)
}
```


**变更后**:
```go
// 新的泛型队列
events := gqueue.NewTQueue[*Event]()
events.Push(&Event{Path: "/tmp/file"})

if event := events.Pop(); event != nil {
    handleEvent(event)  // event 已是 *Event 类型
}
```


### 收益

-  **编译时类型安全**: 实例容器的类型错误在编译期捕获
-  **消除运行时断言**: 避免类型断言带来的 panic 风险
-  **提升代码可读性**: 实例管理逻辑更清晰
-  **改善开发体验**: IDE 类型提示和代码补全更准确

### 性能权衡

**编译时**:
- 泛型实例化会增加编译时间和二进制体积
- 预估编译时间增加 5-15%,二进制体积增加约 1-2MB

**运行时**:
- 减少类型断言的反射开销
- 提升实例获取等热点路径的性能
2026-01-16 15:23:13 +08:00

702 lines
19 KiB
Go

// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package ghttp
import (
"bytes"
"context"
"fmt"
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/container/gset"
"github.com/gogf/gf/v2/container/gtype"
"github.com/gogf/gf/v2/debug/gdebug"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/net/ghttp/internal/graceful"
"github.com/gogf/gf/v2/net/ghttp/internal/swaggerui"
"github.com/gogf/gf/v2/net/goai"
"github.com/gogf/gf/v2/net/gsvc"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/genv"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gproc"
"github.com/gogf/gf/v2/os/gsession"
"github.com/gogf/gf/v2/os/gtimer"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
func init() {
// Initialize the method map.
for _, v := range strings.Split(supportedHttpMethods, ",") {
methodsMap[v] = struct{}{}
}
}
// serverProcessInit initializes some process configurations, which can only be done once.
func serverProcessInit() {
var ctx = context.TODO()
if !serverProcessInitialized.Cas(false, true) {
return
}
// This means it is a restart server. It should kill its parent before starting its listening,
// to avoid duplicated port listening in two processes.
if !genv.Get(adminActionRestartEnvKey).IsEmpty() {
if p, err := os.FindProcess(gproc.PPid()); err == nil {
if err = p.Kill(); err != nil {
intlog.Errorf(ctx, `%+v`, err)
}
if _, err = p.Wait(); err != nil {
intlog.Errorf(ctx, `%+v`, err)
}
} else {
glog.Error(ctx, err)
}
}
// Process message handler.
// It enabled only a graceful feature is enabled.
if gracefulEnabled {
intlog.Printf(ctx, "pid[%d]: graceful reload feature is enabled", gproc.Pid())
go handleProcessMessage()
} else {
intlog.Printf(ctx, "pid[%d]: graceful reload feature is disabled", gproc.Pid())
}
// It's an ugly calling for better initializing the main package path
// in source development environment. It is useful only be used in main goroutine.
// It fails to retrieve the main package path in asynchronous goroutines.
gfile.MainPkgPath()
}
// GetServer creates and returns a server instance using given name and default configurations.
// Note that the parameter `name` should be unique for different servers. It returns an existing
// server instance if given `name` is already existing in the server mapping.
func GetServer(name ...any) *Server {
serverName := DefaultServerName
if len(name) > 0 && name[0] != "" {
serverName = gconv.String(name[0])
}
return serverMapping.GetOrSetFuncLock(serverName, func() *Server {
s := &Server{
instance: serverName,
plugins: make([]Plugin, 0),
servers: make([]*graceful.Server, 0),
closeChan: make(chan struct{}, 10000),
serverCount: gtype.NewInt(),
statusHandlerMap: make(map[string][]HandlerFunc),
serveTree: make(map[string]any),
serveCache: gcache.New(),
routesMap: make(map[string][]*HandlerItem),
openapi: goai.New(),
registrar: gsvc.GetRegistry(),
}
// Initialize the server using default configurations.
if err := s.SetConfig(NewConfig()); err != nil {
panic(gerror.WrapCode(gcode.CodeInvalidConfiguration, err, ""))
}
// It enables OpenTelemetry for server in default.
s.Use(internalMiddlewareServerTracing)
return s
})
}
// Start starts listening on configured port.
// This function does not block the process, you can use function Wait blocking the process.
func (s *Server) Start() error {
var ctx = gctx.GetInitCtx()
// Swagger UI.
if s.config.SwaggerPath != "" {
swaggerui.Init()
s.AddStaticPath(s.config.SwaggerPath, swaggerUIPackedPath)
s.BindHookHandler(s.config.SwaggerPath+"/*", HookBeforeServe, s.swaggerUI)
}
// OpenApi specification json producing handler.
if s.config.OpenApiPath != "" {
s.BindHandler(s.config.OpenApiPath, s.openapiSpec)
}
// Register group routes.
s.handlePreBindItems(ctx)
// Server process initialization, which can only be initialized once.
serverProcessInit()
// Server can only be run once.
if s.Status() == ServerStatusRunning {
return gerror.NewCode(gcode.CodeInvalidOperation, "server is already running")
}
// Logging path setting check.
if s.config.LogPath != "" && s.config.LogPath != s.config.Logger.GetPath() {
if err := s.config.Logger.SetPath(s.config.LogPath); err != nil {
return err
}
}
// Default session storage.
if s.config.SessionStorage == nil {
sessionStoragePath := ""
if s.config.SessionPath != "" {
sessionStoragePath = gfile.Join(s.config.SessionPath, s.config.Name)
if !gfile.Exists(sessionStoragePath) {
if err := gfile.Mkdir(sessionStoragePath); err != nil {
return gerror.Wrapf(err, `mkdir failed for "%s"`, sessionStoragePath)
}
}
}
s.config.SessionStorage = gsession.NewStorageFile(sessionStoragePath, s.config.SessionMaxAge)
}
// Initialize session manager when start running.
s.sessionManager = gsession.New(
s.config.SessionMaxAge,
s.config.SessionStorage,
)
// PProf feature.
if s.config.PProfEnabled {
s.EnablePProf(s.config.PProfPattern)
}
// Default HTTP handler.
if s.config.Handler == nil {
s.config.Handler = s.ServeHTTP
}
// Install external plugins.
for _, p := range s.plugins {
if err := p.Install(s); err != nil {
s.Logger().Fatalf(ctx, `%+v`, err)
}
}
// Check the group routes again for internally registered routes.
s.handlePreBindItems(ctx)
// If there's no route registered and no static service enabled,
// it then returns an error of invalid usage of server.
if len(s.routesMap) == 0 && !s.config.FileServerEnabled {
return gerror.NewCode(
gcode.CodeInvalidOperation,
`there's no route set or static feature enabled, did you forget import the router?`,
)
}
// ================================================================================================
// Start the HTTP server.
// ================================================================================================
reloaded := false
fdMapStr := genv.Get(adminActionReloadEnvKey).String()
if len(fdMapStr) > 0 {
sfm := bufferToServerFdMap([]byte(fdMapStr))
if v, ok := sfm[s.config.Name]; ok {
s.startServer(v)
reloaded = true
}
}
if !reloaded {
s.startServer(nil)
}
// Swagger UI info.
if s.config.SwaggerPath != "" {
s.Logger().Infof(
ctx,
`swagger ui is serving at address: %s%s/`,
s.getLocalListenedAddress(),
s.config.SwaggerPath,
)
}
// OpenApi specification info.
if s.config.OpenApiPath != "" {
s.Logger().Infof(
ctx,
`openapi specification is serving at address: %s%s`,
s.getLocalListenedAddress(),
s.config.OpenApiPath,
)
} else {
if s.config.SwaggerPath != "" {
s.Logger().Warning(
ctx,
`openapi specification is disabled but swagger ui is serving, which might make no sense`,
)
} else {
s.Logger().Info(
ctx,
`openapi specification is disabled`,
)
}
}
// If this is a child process, it then notifies its parent exit.
if gproc.IsChild() {
var gracefulTimeout = time.Duration(s.config.GracefulTimeout) * time.Second
gtimer.SetTimeout(ctx, gracefulTimeout, func(ctx context.Context) {
intlog.Printf(
ctx,
`pid[%d]: notice parent server graceful shuttingdown, ppid: %d`,
gproc.Pid(), gproc.PPid(),
)
if err := gproc.Send(gproc.PPid(), []byte("exit"), adminGProcCommGroup); err != nil {
intlog.Errorf(ctx, `server error in process communication: %+v`, err)
}
})
}
s.initOpenApi()
s.doServiceRegister()
s.doRouterMapDump()
return nil
}
func (s *Server) getLocalListenedAddress() string {
return fmt.Sprintf(`http://127.0.0.1:%d`, s.GetListenedPort())
}
// doRouterMapDump checks and dumps the router map to the log.
func (s *Server) doRouterMapDump() {
if !s.config.DumpRouterMap {
return
}
var (
ctx = context.TODO()
routes = s.GetRoutes()
isJustDefaultServerAndDomain = true
headers = []string{
"SERVER", "DOMAIN", "ADDRESS", "METHOD", "ROUTE", "HANDLER", "MIDDLEWARE",
}
)
for _, item := range routes {
if item.Server != DefaultServerName || item.Domain != DefaultDomainName {
isJustDefaultServerAndDomain = false
break
}
}
if isJustDefaultServerAndDomain {
headers = []string{"ADDRESS", "METHOD", "ROUTE", "HANDLER", "MIDDLEWARE"}
}
if len(routes) > 0 {
buffer := bytes.NewBuffer(nil)
table := tablewriter.NewTable(buffer,
tablewriter.WithRenderer(renderer.NewBlueprint(
tw.Rendition{
Settings: tw.Settings{
Separators: tw.Separators{BetweenRows: tw.On},
},
Symbols: tw.NewSymbolCustom("HTTP").WithCenter("|"),
})),
)
table.Header(headers)
for _, item := range routes {
var (
data = make([]string, 0)
handlerName = gstr.TrimRightStr(item.Handler.Name, "-fm")
middlewares = gstr.SplitAndTrim(item.Middleware, ",")
)
// No printing special internal middleware that may lead confused.
if gstr.SubStrFromREx(handlerName, ".") == noPrintInternalRoute {
continue
}
for k, v := range middlewares {
middlewares[k] = gstr.TrimRightStr(v, "-fm")
}
item.Middleware = gstr.Join(middlewares, "\n")
if isJustDefaultServerAndDomain {
data = append(
data,
item.Address,
item.Method,
item.Route,
handlerName,
item.Middleware,
)
} else {
data = append(
data,
item.Server,
item.Domain,
item.Address,
item.Method,
item.Route,
handlerName,
item.Middleware,
)
}
_ = table.Append(data)
}
_ = table.Render()
s.config.Logger.Header(false).Printf(ctx, "\n%s", buffer.String())
}
}
// GetOpenApi returns the OpenApi specification management object of the current server.
func (s *Server) GetOpenApi() *goai.OpenApiV3 {
return s.openapi
}
// GetRoutes retrieves and returns the router array.
func (s *Server) GetRoutes() []RouterItem {
var (
m = make(map[string]*garray.SortedArray)
routeFilterSet = gset.NewStrSet()
address = s.GetListenedAddress()
)
if s.config.HTTPSAddr != "" {
if len(address) > 0 {
address += ","
}
address += "tls" + s.config.HTTPSAddr
}
for k, handlerItems := range s.routesMap {
array, _ := gregex.MatchString(`(.*?)%([A-Z]+):(.+)@(.+)`, k)
for index := len(handlerItems) - 1; index >= 0; index-- {
var (
handlerItem = handlerItems[index]
item = RouterItem{
Server: s.config.Name,
Address: address,
Domain: array[4],
Type: handlerItem.Type,
Middleware: array[1],
Method: array[2],
Route: array[3],
Priority: index,
Handler: handlerItem,
}
)
switch item.Handler.Type {
case HandlerTypeObject, HandlerTypeHandler:
item.IsServiceHandler = true
case HandlerTypeMiddleware:
item.Middleware = "GLOBAL MIDDLEWARE"
}
// Repeated route filtering for dump.
var setKey = fmt.Sprintf(
`%s|%s|%s|%s`,
item.Method, item.Route, item.Domain, item.Type,
)
if !routeFilterSet.AddIfNotExist(setKey) {
continue
}
if len(item.Handler.Middleware) > 0 {
for _, v := range item.Handler.Middleware {
if item.Middleware != "" {
item.Middleware += ","
}
item.Middleware += gdebug.FuncName(v)
}
}
// If the domain does not exist in the dump map, it creates the map.
// The value of the map is a custom sorted array.
if _, ok := m[item.Domain]; !ok {
// Sort in ASC order.
m[item.Domain] = garray.NewSortedArray(func(v1, v2 any) int {
item1 := v1.(RouterItem)
item2 := v2.(RouterItem)
r := 0
if r = strings.Compare(item1.Domain, item2.Domain); r == 0 {
if r = strings.Compare(item1.Route, item2.Route); r == 0 {
if r = strings.Compare(item1.Method, item2.Method); r == 0 {
if item1.Handler.Type == HandlerTypeMiddleware && item2.Handler.Type != HandlerTypeMiddleware {
return -1
} else if item1.Handler.Type == HandlerTypeMiddleware && item2.Handler.Type == HandlerTypeMiddleware {
return 1
} else if r = strings.Compare(item1.Middleware, item2.Middleware); r == 0 {
r = item2.Priority - item1.Priority
}
}
}
}
return r
})
}
m[item.Domain].Add(item)
}
}
routerArray := make([]RouterItem, 0, 128)
for _, array := range m {
for _, v := range array.Slice() {
routerArray = append(routerArray, v.(RouterItem))
}
}
return routerArray
}
// Run starts server listening in blocking way.
// It's commonly used for single server situation.
func (s *Server) Run() {
var ctx = context.TODO()
if err := s.Start(); err != nil {
s.Logger().Fatalf(ctx, `%+v`, err)
}
// Signal handler in asynchronous way.
go handleProcessSignal()
// Blocking using the channel for graceful restart.
<-s.closeChan
// Shutdown the server
_ = s.Shutdown()
}
// Wait blocks to wait for all servers done.
// It's commonly used in multiple server situation.
func Wait() {
var ctx = context.TODO()
// Signal handler in asynchronous way.
go handleProcessSignal()
<-allShutdownChan
// Remove plugins.
serverMapping.Iterator(func(k string, v *Server) bool {
if len(v.plugins) > 0 {
for _, p := range v.plugins {
intlog.Printf(ctx, `remove plugin: %s`, p.Name())
if err := p.Remove(); err != nil {
intlog.Errorf(ctx, `%+v`, err)
}
}
}
return true
})
glog.Infof(ctx, "pid[%d]: all servers shutdown", gproc.Pid())
}
// startServer starts the underlying server listening.
func (s *Server) startServer(fdMap listenerFdMap) {
var (
ctx = context.TODO()
httpsEnabled bool
)
// HTTPS
if s.config.TLSConfig != nil || (s.config.HTTPSCertPath != "" && s.config.HTTPSKeyPath != "") {
if len(s.config.HTTPSAddr) == 0 {
if len(s.config.Address) > 0 {
s.config.HTTPSAddr = s.config.Address
s.config.Address = ""
} else {
s.config.HTTPSAddr = defaultHttpsAddr
}
}
httpsEnabled = len(s.config.HTTPSAddr) > 0
var array []string
if v, ok := fdMap["https"]; ok && len(v) > 0 {
array = strings.Split(v, ",")
} else {
array = strings.Split(s.config.HTTPSAddr, ",")
}
for _, v := range array {
if len(v) == 0 {
continue
}
var (
fd = 0
itemFunc = v
addrAndFd = strings.Split(v, "#")
)
if len(addrAndFd) > 1 {
itemFunc = addrAndFd[0]
// The Windows OS does not support socket file descriptor passing
// from parent process.
if runtime.GOOS != "windows" {
fd = gconv.Int(addrAndFd[1])
}
}
if fd > 0 {
s.servers = append(s.servers, s.newGracefulServer(itemFunc, fd))
} else {
s.servers = append(s.servers, s.newGracefulServer(itemFunc, 0))
}
s.servers[len(s.servers)-1].SetIsHttps(true)
}
}
// HTTP
if !httpsEnabled && len(s.config.Address) == 0 {
s.config.Address = defaultHttpAddr
}
var array []string
if v, ok := fdMap["http"]; ok && len(v) > 0 {
array = gstr.SplitAndTrim(v, ",")
} else {
array = gstr.SplitAndTrim(s.config.Address, ",")
}
for _, v := range array {
if len(v) == 0 {
continue
}
var (
fd = 0
itemFunc = v
addrAndFd = strings.Split(v, "#")
)
if len(addrAndFd) > 1 {
itemFunc = addrAndFd[0]
// The Window OS does not support socket file descriptor passing
// from the parent process.
if runtime.GOOS != "windows" {
fd = gconv.Int(addrAndFd[1])
}
}
if fd > 0 {
s.servers = append(s.servers, s.newGracefulServer(itemFunc, fd))
} else {
s.servers = append(s.servers, s.newGracefulServer(itemFunc, 0))
}
}
// Start listening asynchronously.
serverRunning.Add(1)
var wg = &sync.WaitGroup{}
for _, gs := range s.servers {
wg.Add(1)
go s.startGracefulServer(ctx, wg, gs)
}
wg.Wait()
}
func (s *Server) startGracefulServer(ctx context.Context, wg *sync.WaitGroup, server *graceful.Server) {
s.serverCount.Add(1)
var err error
// Create listener.
if server.IsHttps() {
err = server.CreateListenerTLS(
s.config.HTTPSCertPath, s.config.HTTPSKeyPath, s.config.TLSConfig,
)
} else {
err = server.CreateListener()
}
if err != nil {
s.Logger().Fatalf(ctx, `%+v`, err)
}
wg.Done()
// Start listening and serving in blocking way.
err = server.Serve(ctx)
// The process exits if the server is closed with none closing error.
if err != nil && !strings.EqualFold(http.ErrServerClosed.Error(), err.Error()) {
s.Logger().Fatalf(ctx, `%+v`, err)
}
// If all the underlying servers' shutdown, the process exits.
if s.serverCount.Add(-1) < 1 {
s.closeChan <- struct{}{}
if serverRunning.Add(-1) < 1 {
serverMapping.Remove(s.instance)
allShutdownChan <- struct{}{}
}
}
}
// Status retrieves and returns the server status.
func (s *Server) Status() ServerStatus {
if serverRunning.Val() == 0 {
return ServerStatusStopped
}
// If any underlying server is running, the server status is running.
for _, v := range s.servers {
if v.Status() == ServerStatusRunning {
return ServerStatusRunning
}
}
return ServerStatusStopped
}
// getListenerFdMap retrieves and returns the socket file descriptors.
// The key of the returned map is "http" and "https".
func (s *Server) getListenerFdMap() map[string]string {
m := map[string]string{
"https": "",
"http": "",
}
for _, v := range s.servers {
str := v.GetAddress() + "#" + gconv.String(v.Fd()) + ","
if v.IsHttps() {
if len(m["https"]) > 0 {
m["https"] += ","
}
m["https"] += str
} else {
if len(m["http"]) > 0 {
m["http"] += ","
}
m["http"] += str
}
}
return m
}
// GetListenedPort returns a port currently listened to by the server.
// It prioritizes the HTTP port if both HTTP and HTTPS are enabled.
func (s *Server) GetListenedPort() int {
for _, server := range s.servers {
if !server.IsHttps() {
return server.GetListenedPort()
}
}
for _, server := range s.servers {
if server.IsHttps() {
return server.GetListenedPort()
}
}
return -1
}
// GetListenedHTTPSPort retrieves and returns one port which is listened using TLS by current server.
func (s *Server) GetListenedHTTPSPort() int {
for _, server := range s.servers {
if server.IsHttps() {
return server.GetListenedPort()
}
}
return -1
}
// GetListenedPorts retrieves and returns the ports which are listened by current server.
func (s *Server) GetListenedPorts() []int {
ports := make([]int, 0)
for _, server := range s.servers {
ports = append(ports, server.GetListenedPort())
}
return ports
}
// GetListenedAddress retrieves and returns the address string which are listened by current server.
func (s *Server) GetListenedAddress() string {
if !gstr.Contains(s.config.Address, FreePortAddress) {
return s.config.Address
}
var (
address = s.config.Address
listenedPorts = s.GetListenedPorts()
)
for _, listenedPort := range listenedPorts {
address = gstr.Replace(address, FreePortAddress, fmt.Sprintf(`:%d`, listenedPort), 1)
}
return address
}