Files
apinto/node/fasthttp-client/client.go

257 lines
5.7 KiB
Go

package fasthttp_client
import (
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
"github.com/eolinker/eosc/eocontext"
"github.com/valyala/fasthttp"
)
type Addr struct {
IP net.IP
Port int
}
func resolveAddr(scheme string, addr string) (*Addr, error) {
as := strings.Split(addr, ":")
if len(as) < 2 {
if scheme == "http" {
addr = fmt.Sprintf("%s:80", addr)
} else if scheme == "https" {
addr = fmt.Sprintf("%s:443", addr)
}
}
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
}
return &Addr{
IP: tcpAddr.IP,
Port: tcpAddr.Port,
}, nil
}
func ProxyTimeout(scheme string, node eocontext.INode, req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) (*Addr, error) {
tcpAddr, err := resolveAddr(scheme, node.Addr())
if err != nil {
return nil, err
}
addr := fmt.Sprintf("%s://%s:%d", scheme, tcpAddr.IP.String(), tcpAddr.Port)
err = defaultClient.ProxyTimeout(addr, req, resp, timeout)
if err != nil {
node.Down()
}
return tcpAddr, err
}
var defaultClient Client
const (
DefaultMaxConns = 10240
DefaultMaxConnWaitTimeout = time.Second * 60
DefaultMaxRedirectCount = 2
)
// Client implements http client.
//
// Copying Client by value is prohibited. Create new instance instead.
//
// It is safe calling Client methods from concurrently running goroutines.
//
// The fields of a Client should not be changed while it is in use.
type Client struct {
mLock sync.Mutex
m map[string]*fasthttp.HostClient
ms map[string]*fasthttp.HostClient
}
func readAddress(addr string) (scheme, host string) {
if i := strings.Index(addr, "://"); i > 0 {
return strings.ToLower(addr[:i]), addr[i+3:]
}
return "http", addr
}
func (c *Client) getHostClient(addr string) (*fasthttp.HostClient, string, error) {
scheme, host := readAddress(addr)
isTLS := false
if strings.EqualFold(scheme, "https") {
isTLS = true
} else if !strings.EqualFold(scheme, "http") {
return nil, "", fmt.Errorf("unsupported protocol %q. http and https are supported", scheme)
}
startCleaner := false
c.mLock.Lock()
m := c.m
if isTLS {
m = c.ms
}
if m == nil {
m = make(map[string]*fasthttp.HostClient)
if isTLS {
c.ms = m
} else {
c.m = m
}
}
hc := m[host]
if hc == nil {
hc = &fasthttp.HostClient{
Addr: addMissingPort(host, isTLS),
IsTLS: isTLS,
Dial: Dial,
MaxConns: DefaultMaxConns,
MaxConnWaitTimeout: DefaultMaxConnWaitTimeout,
RetryIf: func(request *fasthttp.Request) bool {
return false
},
}
m[host] = hc
if len(m) == 1 {
startCleaner = true
}
}
c.mLock.Unlock()
if startCleaner {
go c.mCleaner(m)
}
return hc, scheme, nil
}
//func (c *Client) getDialFunc() fasthttp.DialFunc {
// return func(addr string) (net.Conn, error) {
// atomic.AddInt64(&dialCount, 1)
// conn, err := tcpDial.Dial(addr)
// if err != nil {
// return nil, err
// }
// c.conn = conn
// return &debugConn{Conn: conn}, nil
// }
//}
//
//func (c *Client) RemoteAddr() string {
// if c.conn != nil {
// return c.conn.RemoteAddr().String()
// }
// return "unknown"
//}
//
//func (c *Client) LocalAddr() string {
// if c.conn != nil {
// return c.conn.RemoteAddr().String()
// }
// return "unknown"
//}
// ProxyTimeout performs the given request and waits for response during
// the given timeout duration.
//
// Request must contain at least non-zero RequestURI with full url (including
// scheme and host) or non-zero Host header + RequestURI.
//
// Client determines the server to be requested in the following order:
//
// - from RequestURI if it contains full url with scheme and host;
// - from Host header otherwise.
//
// The function doesn't follow redirects. Use Get* for following redirects.
//
// Response is ignored if resp is nil.
//
// ErrTimeout is returned if the response wasn't returned during
// the given timeout.
//
// ErrNoFreeConns is returned if all Client.MaxConnsPerHost connections
// to the requested host are busy.
//
// It is recommended obtaining req and resp via AcquireRequest
// and AcquireResponse in performance-critical code.
//
// Warning: ProxyTimeout does not terminate the request itself. The request will
// continue in the background and the response will be discarded.
// If requests take too long and the connection pool gets filled up please
// try setting a ReadTimeout.
func (c *Client) ProxyTimeout(addr string, req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {
client, scheme, err := c.getHostClient(addr)
if err != nil {
return err
}
request := req
request.URI().SetScheme(scheme)
request.Header.ResetConnectionClose()
request.Header.Set("Connection", "keep-alive")
connectionClose := resp.ConnectionClose()
err = client.DoTimeout(request, resp, timeout)
if err != nil {
return err
}
if fasthttp.StatusCodeIsRedirect(resp.StatusCode()) {
err = client.DoRedirects(request, resp, DefaultMaxRedirectCount)
if err != nil {
return err
}
}
if connectionClose {
resp.SetConnectionClose()
}
return nil
}
func (c *Client) mCleaner(m map[string]*fasthttp.HostClient) {
mustStop := false
//sleep := c.MaxIdleConnDuration
//if sleep < time.Second {
// sleep = time.Second
//} else if sleep > 10*time.Second {
// sleep = 10 * time.Second
//}
sleep := time.Second * 10
for {
c.mLock.Lock()
for k, v := range m {
shouldRemove := v.ConnsCount() == 0
if shouldRemove {
delete(m, k)
}
}
if len(m) == 0 {
mustStop = true
}
c.mLock.Unlock()
if mustStop {
break
}
time.Sleep(sleep)
}
}
func addMissingPort(addr string, isTLS bool) string {
n := strings.Index(addr, ":")
if n >= 0 {
return addr
}
port := 80
if isTLS {
port = 443
}
return net.JoinHostPort(addr, strconv.Itoa(port))
}