提出

今天遇到这么个事情,需要从某台主机获得当前公网IP。

由于仅依靠 gethostbyname(3) 不准确,所以会使用到第三方。结果当这台主机更换地址后,发现从第三方获取到的客户端IP仍然不变,于是猜测更换IP的实现方法很可能(实际上也是)公网IP不是在这台主机上,考虑到其特别提到的“更换IP不影响访问”,十有八九是通过网关与路由策略实现的。

于是猜测请求的时候发起的连接带有 Keep-Alive 特性并在内部维护这样一个连接池。

印象中脚本语言标准库里一般是没有 Keep-Alive ,所以默认以为 golang 的 net/httphttp.Client 也是非常原始的。实际上不是,参考文档1

… If the Body is not closed, the Client's underlying RoundTripper (typically Transport) may not be able to re-use a persistent TCP connection to the server for a subsequent "keep-alive" request. …

很多地方都提到了这点,例如2,意思是说希望重用现有连接,需要在使用完毕后将响应内容都读取完(即使需求上不需要),这样相当于可以将连接归还回池子里。

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.

下面是默认的 DefaultClient 使用的 DefaultTransport 的配置参数

var DefaultTransport RoundTripper = &Transport{
        Proxy: ProxyFromEnvironment,
        DialContext: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,  // <== 这里即打开了连接保持
                DualStack: true,
        }).DialContext,
        MaxIdleConns:          100,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
}

禁用保持连接

那么如果现在 不希望 使用连接保持,重用特性的时候呢?golang并没有提供请求级别的控制,而是与Java的httpclient类似,需要在构建 http.Client 时控制传输配置。

按照这个意思,需要构建一个自定义的 http.Transport 实例,对配置内容进行修改

client = &http.Client{
  Timeout: timeout,
  Transport: &http.Transport{
    DialContext: (&net.Dialer{
      Timeout:   timeout,
      KeepAlive: 0,    // 修改为 0 可以生效
    }).DialContext,
  },
}

参考:

\_\_END\_\_