github.com/aacfactory/fns@v1.2.86-0.20240310083819-80d667fc0a17/transports/standard/client.go (about) 1 /* 2 * Copyright 2023 Wang Min Xiang 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package standard 19 20 import ( 21 sc "context" 22 "crypto/tls" 23 "fmt" 24 "github.com/aacfactory/errors" 25 "github.com/aacfactory/fns/commons/bytex" 26 "github.com/aacfactory/fns/context" 27 "github.com/aacfactory/fns/transports" 28 "github.com/aacfactory/fns/transports/ssl" 29 "github.com/valyala/bytebufferpool" 30 "io" 31 "net" 32 "net/http" 33 "strings" 34 "time" 35 ) 36 37 type DialerConfig struct { 38 CacheSize int `json:"cacheSize"` 39 ExpireSeconds int `json:"expireSeconds"` 40 } 41 42 type ClientConfig struct { 43 MaxConnsPerHost int `json:"maxConnsPerHost"` 44 MaxResponseHeaderSize string `json:"maxResponseHeaderSize"` 45 Timeout string `json:"timeout"` 46 DisableKeepAlive bool `json:"disableKeepAlive"` 47 MaxIdleConnsPerHost int `json:"maxIdleConnsPerHost"` 48 IdleConnTimeout string `json:"idleConnTimeout"` 49 TLSHandshakeTimeout string `json:"tlsHandshakeTimeout"` 50 ExpectContinueTimeout string `json:"expectContinueTimeout"` 51 Dialer DialerConfig `json:"dialer"` 52 IsTLS bool `json:"isTLS"` 53 TLSConfig *tls.Config `json:"-"` 54 TLSDialer ssl.Dialer `json:"-"` 55 } 56 57 func (config *ClientConfig) MaxConnectionsPerHost() (n int) { 58 if config.MaxConnsPerHost < 1 { 59 config.MaxConnsPerHost = 64 60 } 61 n = config.MaxConnsPerHost 62 return 63 } 64 65 func (config *ClientConfig) MaxIdleConnectionsPerHost() (n int) { 66 if config.MaxIdleConnsPerHost < 1 { 67 config.MaxIdleConnsPerHost = 100 68 } 69 n = config.MaxIdleConnsPerHost 70 return 71 } 72 73 func (config *ClientConfig) MaxResponseHeaderByteSize() (n uint64, err error) { 74 maxResponseHeaderSize := strings.TrimSpace(config.MaxResponseHeaderSize) 75 if maxResponseHeaderSize == "" { 76 maxResponseHeaderSize = "4KB" 77 } 78 n, err = bytex.ParseBytes(maxResponseHeaderSize) 79 if err != nil { 80 err = errors.Warning("maxResponseHeaderBytes is invalid").WithCause(err).WithMeta("hit", "format must be bytes") 81 return 82 } 83 return 84 } 85 86 func (config *ClientConfig) TimeoutDuration() (n time.Duration, err error) { 87 timeout := strings.TrimSpace(config.Timeout) 88 if timeout == "" { 89 timeout = "2s" 90 } 91 n, err = time.ParseDuration(timeout) 92 if err != nil { 93 err = errors.Warning("timeout is invalid").WithCause(err).WithMeta("hit", "format must be time.Duration") 94 return 95 } 96 return 97 } 98 99 func (config *ClientConfig) IdleConnTimeoutDuration() (n time.Duration, err error) { 100 timeout := strings.TrimSpace(config.IdleConnTimeout) 101 if timeout == "" { 102 timeout = "90s" 103 } 104 n, err = time.ParseDuration(timeout) 105 if err != nil { 106 err = errors.Warning("idle conn timeout is invalid").WithCause(err).WithMeta("hit", "format must be time.Duration") 107 return 108 } 109 return 110 } 111 112 func (config *ClientConfig) TLSHandshakeTimeoutDuration() (n time.Duration, err error) { 113 timeout := strings.TrimSpace(config.TLSHandshakeTimeout) 114 if timeout == "" { 115 timeout = "10s" 116 } 117 n, err = time.ParseDuration(timeout) 118 if err != nil { 119 err = errors.Warning("tls handshake timeout is invalid").WithCause(err).WithMeta("hit", "format must be time.Duration") 120 return 121 } 122 return 123 } 124 125 func (config *ClientConfig) ExpectContinueTimeoutDuration() (n time.Duration, err error) { 126 timeout := strings.TrimSpace(config.ExpectContinueTimeout) 127 if timeout == "" { 128 timeout = "1s" 129 } 130 n, err = time.ParseDuration(timeout) 131 if err != nil { 132 err = errors.Warning("expect continue timeout is invalid").WithCause(err).WithMeta("hit", "format must be time.Duration") 133 return 134 } 135 return 136 } 137 138 func NewClient(address string, config *ClientConfig) (client *Client, err error) { 139 maxResponseHeaderBytes, maxResponseHeaderBytesErr := config.MaxResponseHeaderByteSize() 140 if maxResponseHeaderBytesErr != nil { 141 err = maxResponseHeaderBytesErr 142 return 143 } 144 timeout, timeoutErr := config.TimeoutDuration() 145 if timeoutErr != nil { 146 err = timeoutErr 147 return 148 } 149 idleConnTimeout, idleConnTimeoutErr := config.IdleConnTimeoutDuration() 150 if idleConnTimeoutErr != nil { 151 err = idleConnTimeoutErr 152 return 153 } 154 tlsHandshakeTimeout, tlsHandshakeTimeoutErr := config.TLSHandshakeTimeoutDuration() 155 if tlsHandshakeTimeoutErr != nil { 156 err = tlsHandshakeTimeoutErr 157 return 158 } 159 expectContinueTimeout, expectContinueTimeoutErr := config.ExpectContinueTimeoutDuration() 160 if expectContinueTimeoutErr != nil { 161 err = expectContinueTimeoutErr 162 return 163 } 164 isTLS := config.IsTLS 165 if !isTLS { 166 isTLS = config.TLSConfig != nil 167 } 168 var dialFunc func(ctx sc.Context, network, addr string) (net.Conn, error) 169 if config.TLSDialer != nil { 170 dialFunc = config.TLSDialer.DialContext 171 } 172 roundTripper := &http.Transport{ 173 Proxy: http.ProxyFromEnvironment, 174 DialContext: dialFunc, 175 DialTLSContext: nil, 176 TLSClientConfig: config.TLSConfig, 177 TLSHandshakeTimeout: tlsHandshakeTimeout, 178 DisableKeepAlives: config.DisableKeepAlive, 179 DisableCompression: false, 180 MaxIdleConns: config.MaxIdleConnectionsPerHost(), 181 MaxIdleConnsPerHost: config.MaxIdleConnectionsPerHost(), 182 MaxConnsPerHost: config.MaxConnectionsPerHost(), 183 IdleConnTimeout: idleConnTimeout, 184 ResponseHeaderTimeout: 0, 185 ExpectContinueTimeout: expectContinueTimeout, 186 TLSNextProto: nil, 187 MaxResponseHeaderBytes: int64(maxResponseHeaderBytes), 188 WriteBufferSize: 4096, 189 ReadBufferSize: 4096, 190 ForceAttemptHTTP2: true, 191 } 192 client = &Client{ 193 address: address, 194 secured: isTLS, 195 host: &http.Client{ 196 Transport: roundTripper, 197 CheckRedirect: nil, 198 Jar: nil, 199 Timeout: timeout, 200 }, 201 } 202 return 203 } 204 205 type Client struct { 206 address string 207 secured bool 208 host *http.Client 209 } 210 211 func (c *Client) Key() (key string) { 212 key = c.address 213 return 214 } 215 216 func (c *Client) Do(ctx context.Context, method []byte, path []byte, header transports.Header, body []byte) (status int, responseHeader transports.Header, responseBody []byte, err error) { 217 url := "" 218 if c.secured { 219 url = fmt.Sprintf("https://%s%s", c.address, bytex.ToString(path)) 220 } else { 221 url = fmt.Sprintf("http://%s%s", c.address, bytex.ToString(path)) 222 } 223 rb := bytex.AcquireBuffer() 224 defer bytex.ReleaseBuffer(rb) 225 _, _ = rb.Write(body) 226 227 r, rErr := http.NewRequestWithContext(ctx, bytex.ToString(method), url, rb) 228 if rErr != nil { 229 err = errors.Warning("http: create request failed").WithCause(rErr) 230 return 231 } 232 if header != nil { 233 header.Foreach(func(key []byte, values [][]byte) { 234 for _, value := range values { 235 r.Header.Add(bytex.ToString(key), bytex.ToString(value)) 236 } 237 }) 238 } 239 240 resp, doErr := c.host.Do(r) 241 if doErr != nil { 242 if errors.Wrap(doErr).Contains(context.Canceled) || errors.Wrap(doErr).Contains(context.DeadlineExceeded) { 243 err = errors.Timeout("http: do failed").WithCause(doErr) 244 return 245 } 246 err = errors.Warning("http: do failed").WithCause(doErr) 247 return 248 } 249 buf := bytex.Acquire4KBuffer() 250 defer bytex.Release4KBuffer(buf) 251 b := bytebufferpool.Get() 252 defer bytebufferpool.Put(b) 253 for { 254 n, readErr := resp.Body.Read(buf) 255 _, _ = b.Write(buf[0:n]) 256 if readErr != nil { 257 if readErr == io.EOF { 258 break 259 } 260 _ = resp.Body.Close() 261 err = errors.Warning("http: do failed").WithCause(errors.Warning("read response body failed").WithCause(readErr)) 262 return 263 } 264 } 265 status = resp.StatusCode 266 responseHeader = WrapHttpHeader(resp.Header) 267 responseBody = bytex.FromString(b.String()) 268 return 269 } 270 271 func (c *Client) Close() { 272 c.host.CloseIdleConnections() 273 return 274 }