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  }