github.com/aacfactory/fns@v1.2.86-0.20240310083819-80d667fc0a17/transports/fast/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 fast 19 20 import ( 21 "bytes" 22 "crypto/tls" 23 "github.com/aacfactory/errors" 24 "github.com/aacfactory/fns/commons/bytex" 25 "github.com/aacfactory/fns/context" 26 "github.com/aacfactory/fns/transports" 27 "github.com/aacfactory/fns/transports/ssl" 28 "github.com/dgrr/http2" 29 "github.com/valyala/fasthttp" 30 "net" 31 "strings" 32 "time" 33 ) 34 35 type ClientHttp2Config struct { 36 Enabled bool `json:"enabled"` 37 PingSeconds int `json:"pingSeconds"` 38 MaxResponseSeconds int `json:"maxResponseSeconds"` 39 } 40 41 type DialerConfig struct { 42 CacheSize int `json:"cacheSize"` 43 ExpireSeconds int `json:"expireSeconds"` 44 } 45 46 type ClientConfig struct { 47 DialDualStack bool `json:"dialDualStack"` 48 MaxConnsPerHost int `json:"maxConnsPerHost"` 49 MaxIdleConnDuration string `json:"maxIdleConnDuration"` 50 MaxConnDuration string `json:"maxConnDuration"` 51 MaxIdemponentCallAttempts int `json:"maxIdemponentCallAttempts"` 52 ReadBufferSize string `json:"readBufferSize"` 53 ReadTimeout string `json:"readTimeout"` 54 WriteBufferSize string `json:"writeBufferSize"` 55 WriteTimeout string `json:"writeTimeout"` 56 MaxResponseBodySize string `json:"maxResponseBodySize"` 57 MaxConnWaitTimeout string `json:"maxConnWaitTimeout"` 58 Dialer DialerConfig `json:"dialer"` 59 IsTLS bool `json:"isTLS"` 60 http2 ClientHttp2Config 61 TLSConfig *tls.Config `json:"-"` 62 TLSDialer ssl.Dialer `json:"-"` 63 } 64 65 func NewClient(address string, config ClientConfig) (client *Client, err error) { 66 maxIdleConnDuration := time.Duration(0) 67 if config.MaxIdleConnDuration != "" { 68 maxIdleConnDuration, err = time.ParseDuration(strings.TrimSpace(config.MaxIdleConnDuration)) 69 if err != nil { 70 err = errors.Warning("fns: build client failed").WithCause(errors.Warning("maxIdleWorkerDuration must be time.Duration format")).WithCause(err).WithMeta("transport", transportName) 71 return 72 } 73 } 74 maxConnDuration := time.Duration(0) 75 if config.MaxConnDuration != "" { 76 maxConnDuration, err = time.ParseDuration(strings.TrimSpace(config.MaxConnDuration)) 77 if err != nil { 78 err = errors.Warning("fns: build client failed").WithCause(errors.Warning("maxConnDuration must be time.Duration format")).WithCause(err).WithMeta("transport", transportName) 79 return 80 } 81 } 82 readBufferSize := uint64(0) 83 if config.ReadBufferSize != "" { 84 readBufferSize, err = bytex.ParseBytes(strings.TrimSpace(config.ReadBufferSize)) 85 if err != nil { 86 err = errors.Warning("fns: build client failed").WithCause(errors.Warning("readBufferSize must be bytes format")).WithCause(err).WithMeta("transport", transportName) 87 return 88 } 89 } 90 readTimeout := 10 * time.Second 91 if config.ReadTimeout != "" { 92 readTimeout, err = time.ParseDuration(strings.TrimSpace(config.ReadTimeout)) 93 if err != nil { 94 err = errors.Warning("fns: build client failed").WithCause(errors.Warning("readTimeout must be time.Duration format")).WithCause(err).WithMeta("transport", transportName) 95 return 96 } 97 } 98 writeBufferSize := uint64(0) 99 if config.WriteBufferSize != "" { 100 writeBufferSize, err = bytex.ParseBytes(strings.TrimSpace(config.WriteBufferSize)) 101 if err != nil { 102 err = errors.Warning("fns: build client failed").WithCause(errors.Warning("writeBufferSize must be bytes format")).WithCause(err).WithMeta("transport", transportName) 103 return 104 } 105 } 106 writeTimeout := 10 * time.Second 107 if config.WriteTimeout != "" { 108 writeTimeout, err = time.ParseDuration(strings.TrimSpace(config.WriteTimeout)) 109 if err != nil { 110 err = errors.Warning("fns: build client failed").WithCause(errors.Warning("writeTimeout must be time.Duration format")).WithCause(err).WithMeta("transport", transportName) 111 return 112 } 113 } 114 maxResponseBodySize := uint64(4 * bytex.MEGABYTE) 115 if config.MaxResponseBodySize != "" { 116 maxResponseBodySize, err = bytex.ParseBytes(strings.TrimSpace(config.MaxResponseBodySize)) 117 if err != nil { 118 err = errors.Warning("fns: build client failed").WithCause(errors.Warning("maxResponseBodySize must be bytes format")).WithCause(err).WithMeta("transport", transportName) 119 return 120 } 121 } 122 maxConnWaitTimeout := time.Duration(0) 123 if config.MaxConnWaitTimeout != "" { 124 maxConnWaitTimeout, err = time.ParseDuration(strings.TrimSpace(config.MaxConnWaitTimeout)) 125 if err != nil { 126 err = errors.Warning("fns: build client failed").WithCause(errors.Warning("maxConnWaitTimeout must be time.Duration format")).WithCause(err).WithMeta("transport", transportName) 127 return 128 } 129 } 130 131 isTLS := config.IsTLS 132 if !isTLS { 133 isTLS = config.TLSConfig != nil 134 } 135 var dialFunc fasthttp.DialFunc 136 if config.TLSDialer != nil { 137 dialFunc = func(addr string) (net.Conn, error) { 138 return config.TLSDialer.DialContext(context.TODO(), "tcp", addr) 139 } 140 } 141 142 hc := &fasthttp.HostClient{ 143 Addr: address, 144 Name: "", 145 NoDefaultUserAgentHeader: true, 146 IsTLS: isTLS, 147 TLSConfig: config.TLSConfig, 148 Dial: dialFunc, 149 MaxConns: config.MaxConnsPerHost, 150 MaxConnDuration: maxConnDuration, 151 MaxIdleConnDuration: maxIdleConnDuration, 152 MaxIdemponentCallAttempts: config.MaxIdemponentCallAttempts, 153 ReadBufferSize: int(readBufferSize), 154 WriteBufferSize: int(writeBufferSize), 155 ReadTimeout: readTimeout, 156 WriteTimeout: writeTimeout, 157 MaxResponseBodySize: int(maxResponseBodySize), 158 DisableHeaderNamesNormalizing: false, 159 DisablePathNormalizing: false, 160 SecureErrorLogMessage: false, 161 MaxConnWaitTimeout: maxConnWaitTimeout, 162 RetryIf: nil, 163 Transport: nil, 164 ConnPoolStrategy: fasthttp.FIFO, 165 } 166 if config.http2.Enabled && isTLS { 167 configErr := http2.ConfigureClient(hc, http2.ClientOpts{ 168 PingInterval: time.Duration(config.http2.PingSeconds) * time.Second, 169 MaxResponseTime: time.Duration(config.http2.MaxResponseSeconds) * time.Second, 170 OnRTT: nil, 171 }) 172 if configErr != nil { 173 err = errors.Warning("fns: build client failed").WithCause(configErr) 174 return 175 } 176 } 177 client = &Client{ 178 address: address, 179 secured: isTLS, 180 host: hc, 181 } 182 return 183 } 184 185 type Client struct { 186 address string 187 secured bool 188 host *fasthttp.HostClient 189 } 190 191 func (client *Client) Do(ctx context.Context, method []byte, path []byte, header transports.Header, body []byte) (status int, responseHeader transports.Header, responseBody []byte, err error) { 192 req := fasthttp.AcquireRequest() 193 194 // method 195 req.Header.SetMethodBytes(method) 196 // header 197 if header != nil { 198 header.Foreach(func(key []byte, values [][]byte) { 199 for _, value := range values { 200 req.Header.AddBytesKV(key, value) 201 } 202 }) 203 } 204 // uri 205 uri := req.URI() 206 if client.secured { 207 uri.SetSchemeBytes(bytex.FromString("https")) 208 } else { 209 uri.SetSchemeBytes(bytex.FromString("http")) 210 } 211 uri.SetHostBytes(bytex.FromString(client.address)) 212 queryIdx := bytes.IndexByte(path, '?') 213 if queryIdx > -1 { 214 if len(path) > queryIdx { 215 uri.SetQueryStringBytes(path[queryIdx+1:]) 216 } 217 path = path[0:queryIdx] 218 } 219 uri.SetPathBytes(path) 220 // body 221 if body != nil && len(body) > 0 { 222 req.SetBodyRaw(body) 223 } 224 // resp 225 resp := fasthttp.AcquireResponse() 226 227 // do 228 deadline, hasDeadline := ctx.Deadline() 229 if hasDeadline { 230 err = client.host.DoDeadline(req, resp, deadline) 231 } else { 232 err = client.host.Do(req, resp) 233 } 234 235 if err != nil { 236 err = errors.Warning("fns: transport client do failed"). 237 WithCause(err). 238 WithMeta("transport", transportName).WithMeta("method", bytex.ToString(method)).WithMeta("path", bytex.ToString(path)) 239 fasthttp.ReleaseRequest(req) 240 fasthttp.ReleaseResponse(resp) 241 return 242 } 243 244 status = resp.StatusCode() 245 246 responseHeader = transports.NewHeader() 247 resp.Header.VisitAll(func(key, value []byte) { 248 responseHeader.Add(key, value) 249 }) 250 251 responseBody = resp.Body() 252 253 fasthttp.ReleaseRequest(req) 254 fasthttp.ReleaseResponse(resp) 255 return 256 } 257 258 func (client *Client) Close() { 259 client.host.CloseIdleConnections() 260 }