使用golang进行业务开发时,通常会遇到需要发起HTTP调用请求,用于获取业务所需的数据进行下一步处理。
golang在标准库中直接提供了net/http包,通过这个包可以很方便的去发起一个HTTP请求。
对于golang的net/http库其使用通常有两种方式:
1. 使用DefaultClient;2. 使用自定义Client。下面来看看两种方式的用法
net/http使用
1. 使用DefalutClient
对于没有高并发的场景下,使用DefaultClient十分简单,能够快速达到目的。下面看一个示例:
resp, err := http.Get("http://www.example.com")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("read body err:", err)
}
fmt.Println("body:", string(body))
这里直接调用net/http封装提供的Get函数,标准库中还封装提供了Post,Head函数。
这些封装函数背后都是使用的DefaultClient。下面看看Get函数的源码:
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
DefaultClient 是一个全局Client结构,其定义如下:
// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}
2. 使用自定义Client
实际应用中,为应对各种不同的场景通常需要自定以http Client来满足要求,实现目的。
下面看看自定义client的常见用法:
- 超时设置
client := http.Client{
Timeout: 10 * time.Second,
}
- 代理设置
proxy, _ := url.Parse(proxyUrl)
tr := &http.Transport{
Proxy: http.ProxyURL(proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Transport: tr,
Timeout: time.Second * 5, //超时时间
}
resp, err := client.Get(webUrl)
if err != nil {
fmt.Println("出错了", err)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
- 连接池设置
proxy := func(_ *http.Request) (*url.URL, error) {
return url.Parse("http://" + netproxy.GetAddr())
}
httpTransport = &http.Transport{
Proxy: proxy,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
httpClient = &http.Client{
Transport: httpTransport,
}
实际应用中client通常是作为全局变量来使用,初始化一次即可。无需每次请求都重新定义一次,
因为client中底层使用的transport是一个连接池,不同请求会取用一条不同的连接。
连接池
上面说到,http client中transport是一个连接池,其无论是DefaultClient还是自定义Client都是用到的。
默认的transport定义如下:
// DefaultTransport is the default implementation of Transport and is
// used by DefaultClient. It establishes network connections as needed
// and caches them for reuse by subsequent calls. It uses HTTP proxies
// as directed by the $HTTP_PROXY and $NO_PROXY (or $http_proxy and
// $no_proxy) environment variables.
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// DefaultMaxIdleConnsPerHost is the default value of Transport's
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2
如果在http client中没有设置transport属性,那么它就会使用默认的transport。在默认中最大的空闲连接数为100,
每个Host最大空闲数为2. 但是默认的配置中只有关于空闲连接的配置,在实际大量并发的情况下会创建很多连接,
进而导致性能急剧下降。
如果需要控制合适的连接数,就需要使用自定义的client和transport,通常根据应用场景需要调整配置参数。
type HTTPTransportParam struct {
MaxIdleConnsPerHost int
MaxIdleConns int
MaxConnsPerHost int
IdleConnTimeout int
DialTimeout int
KeepAlive int
}
var httpTrans *HTTPTransportParam
httpTransport = &http.Transport{
Proxy: proxy,
DialContext: (&net.Dialer{
Timeout: time.Duration(httpTrans.DialTimeout) * time.Millisecond,
KeepAlive: time.Duration(httpTrans.KeepAlive) * time.Millisecond,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: httpTrans.MaxIdleConns,
MaxIdleConnsPerHost: httpTrans.MaxIdleConnsPerHost,
MaxConnsPerHost: httpTrans.MaxConnsPerHost,
IdleConnTimeout: time.Duration(httpTrans.IdleConnTimeout) * time.Millisecond,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
httpClient = &http.Client{
Transport: httpTransport,
}
一般比较关心的参数分两部分:context参数和连接参数
context参数是指建立连接时涉及的参数包括超时,保活参数
连接参数是指与pool相关的参数包括:最大空闲连接数,最大连接数和空闲超时时间。
尤其是最大连接数,默认是没有限制的,如果并发量大的时候会引起大量的连接,进而导致性能下降。
所以需要根据实际情况合理调整最大连接数参数的配置。另外,这里的最大连接数也只是针对单个host的限制,
暂时没找到限制总连接数及主机连接池的控制入口。
net/http的连接池是默认全局共用的,假如后端主机虽然只有一百多台,如果我有100个协程,
有概率会出现同时针对一主机并发访问,那么一个主机就有100个连接,100个后端主机就会产生10000个连接。
这问题的概率在生产环境中经常出现。100台没问题,那么更多呢? 单ip在主动连接可用的端口不到65535的…
所以,大家也要考虑到这问题。
简单的做法可以在进程的连接数做计数,当达到一定的阈值后,进行短连接请求, 但这样带来的问题是time wait过多,
重复的建连效率也在下降。
总结
golang的标准库中提供了很多使用的库,但在实际使用时需要注意应用场景及合适的使用方式。
默认情况的配置通常只是适用与功能实现,对性能有要求的场景通常需要仔细考量和分析具体的使用方式。