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  }