You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
6.2 KiB
Go
178 lines
6.2 KiB
Go
package ahttp
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"github.com/go-resty/resty/v2"
|
|
"go.uber.org/zap"
|
|
"net"
|
|
"net/http"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Config 包含 HTTP 客户端的配置选项
|
|
// Config contains the configuration options for the HTTP client
|
|
type Config struct {
|
|
MaxIdleConnections int // 连接池的最大空闲连接数 / Maximum idle connections in the connection pool
|
|
IdleConnectionTimeout time.Duration // 空闲连接超时时间 / Timeout for idle connections
|
|
DisableCompression bool // 禁用压缩 / Disable compression
|
|
DisableKeepAlives bool // 禁用 keep-alive / Disable keep-alive
|
|
InsecureSkipVerify bool // 跳过 TLS 证书验证 / Skip TLS certificate verification
|
|
Timeout time.Duration // 总超时时间 / Total timeout duration
|
|
TLSHandshakeTimeout time.Duration // TLS 握手超时时间 / Timeout for TLS handshake
|
|
ExpectContinueTimeout time.Duration // 100-continue 超时时间 / Timeout for 100-continue
|
|
MaxConnectionsPerHost int // 每主机的最大连接数 / Maximum connections per host
|
|
RetryAttempts int // 请求重试次数 / Number of retry attempts for failed requests
|
|
DialerTimeout time.Duration // Dialer 的连接超时时间 / Dialer connection timeout
|
|
DialerKeepAlive time.Duration // Dialer 的 Keep-Alive 时间 / Dialer keep-alive time
|
|
}
|
|
|
|
// HttpClient 是对 Resty 客户端的封装,支持自定义配置和日志 / HttpClient is a wrapper for the Resty client with custom configuration and logging support
|
|
type HttpClient struct {
|
|
httpClient *resty.Client // Resty 客户端实例 / Resty client instance
|
|
httpTransport *http.Transport // HTTP 传输层配置 / HTTP transport layer configuration
|
|
logger *zap.Logger // 日志记录器 / Logger instance
|
|
config *Config
|
|
}
|
|
|
|
var (
|
|
singletonClient *HttpClient
|
|
once sync.Once
|
|
)
|
|
|
|
// New 创建一个新的 HTTP 客户端实例 / NewHttpClient creates a new HTTP client instance
|
|
func New(config *Config) *HttpClient {
|
|
once.Do(func() {
|
|
if config == nil {
|
|
config = defaultConfig()
|
|
}
|
|
|
|
singletonClient = &HttpClient{
|
|
httpClient: newRestyClient(config),
|
|
httpTransport: newTransport(config),
|
|
config: config,
|
|
}
|
|
})
|
|
return singletonClient
|
|
}
|
|
|
|
// newRestyClient 配置 Resty 客户端 / Configures the Resty client
|
|
func newRestyClient(config *Config) *resty.Client {
|
|
client := resty.NewWithClient(&http.Client{
|
|
Transport: newTransport(config),
|
|
Timeout: config.Timeout,
|
|
})
|
|
|
|
client.SetRetryCount(config.RetryAttempts)
|
|
client.SetRetryWaitTime(1 * time.Second)
|
|
client.SetRetryMaxWaitTime(10 * time.Second)
|
|
|
|
return client
|
|
}
|
|
|
|
// defaultConfig 返回默认的配置值 / Returns the default configuration values
|
|
func defaultConfig() *Config {
|
|
return &Config{
|
|
MaxIdleConnections: runtime.GOMAXPROCS(0) * 200,
|
|
IdleConnectionTimeout: 120 * time.Second,
|
|
DisableCompression: false,
|
|
Timeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 30 * time.Second,
|
|
ExpectContinueTimeout: 2 * time.Second,
|
|
MaxConnectionsPerHost: runtime.GOMAXPROCS(0) * 100,
|
|
RetryAttempts: 3,
|
|
DialerTimeout: 15 * time.Second, // 默认 Dialer 超时时间
|
|
DialerKeepAlive: 60 * time.Second, // 默认 Dialer Keep-Alive 时间
|
|
}
|
|
}
|
|
|
|
// newTransport 创建并配置 HTTP 传输层 / Creates and configures the HTTP transport
|
|
func newTransport(config *Config) *http.Transport {
|
|
tcpDialer := &net.Dialer{
|
|
Timeout: config.DialerTimeout,
|
|
KeepAlive: config.DialerKeepAlive,
|
|
DualStack: true,
|
|
}
|
|
|
|
return &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: tcpDialer.DialContext,
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: config.MaxIdleConnections,
|
|
MaxIdleConnsPerHost: config.MaxConnectionsPerHost,
|
|
IdleConnTimeout: config.IdleConnectionTimeout,
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: config.InsecureSkipVerify,
|
|
MinVersion: tls.VersionTLS12,
|
|
MaxVersion: tls.VersionSSL30,
|
|
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
|
|
},
|
|
TLSHandshakeTimeout: config.TLSHandshakeTimeout,
|
|
ExpectContinueTimeout: config.ExpectContinueTimeout,
|
|
DisableCompression: config.DisableCompression,
|
|
}
|
|
}
|
|
|
|
// SetLog 设置日志记录器 / SetLog sets the logger
|
|
func (h *HttpClient) SetLog(logger *zap.Logger) *HttpClient {
|
|
if logger == nil {
|
|
return h
|
|
}
|
|
h.logger = logger
|
|
h.httpClient.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
|
|
logger.Info("Request",
|
|
zap.String("URL", r.URL),
|
|
zap.String("Method", r.Method),
|
|
zap.Any("Headers", r.Header),
|
|
zap.Any("Cookies", r.Cookies),
|
|
zap.Any("FormData", r.FormData),
|
|
zap.Any("QueryParam", r.QueryParam),
|
|
zap.Any("Body", r.Body),
|
|
zap.Duration("Timeout", h.config.Timeout))
|
|
return nil
|
|
})
|
|
|
|
h.httpClient.OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
|
|
if r.StatusCode() >= 400 {
|
|
logger.Error("Request failed",
|
|
zap.Int("StatusCode", r.StatusCode()),
|
|
zap.String("URL", r.Request.URL),
|
|
zap.String("Method", r.Request.Method),
|
|
zap.Any("Headers", r.Header),
|
|
zap.Any("Cookies", r.Cookies),
|
|
zap.Any("FormData", r.Request.FormData),
|
|
zap.Any("QueryParam", r.Request.QueryParam),
|
|
zap.ByteString("Body", r.Body()),
|
|
zap.Duration("Duration", time.Since(r.Request.Time)),
|
|
)
|
|
} else {
|
|
logger.Info("Response",
|
|
zap.Int("StatusCode", r.StatusCode()),
|
|
zap.String("URL", r.Request.URL),
|
|
zap.String("Method", r.Request.Method),
|
|
zap.Any("Headers", r.Header),
|
|
zap.Any("Cookies", r.Cookies),
|
|
zap.Any("FormData", r.Request.FormData),
|
|
zap.Any("QueryParam", r.Request.QueryParam),
|
|
zap.ByteString("Body", r.Body()),
|
|
zap.Duration("Duration", time.Since(r.Request.Time)),
|
|
)
|
|
}
|
|
return nil
|
|
})
|
|
return h
|
|
}
|
|
|
|
// Client 返回 Resty 客户端的请求实例 / Returns a request instance of the Resty client
|
|
func (h *HttpClient) Client() *resty.Client {
|
|
h.httpClient.SetHeader("User-Agent", "Mozilla/5.0 GameServer")
|
|
return h.httpClient
|
|
}
|
|
|
|
// Client 返回 Resty 客户端的请求实例 / Returns a request instance of the Resty client
|
|
func (h *HttpClient) Request() *resty.Request {
|
|
h.httpClient.SetHeader("User-Agent", "Mozilla/5.0 GameServer")
|
|
return h.httpClient.R()
|
|
}
|