目录
使用
HTTP使用的包是GO提供的包:net/http
简单实用
Get请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Get请求 func httpGet() { resp, err := http.Get("http://www.01happy.com/demo/accept.php?id=1") if err != nil { // handle error } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { // handle error } fmt.Println(string(body)) } |
Post请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func httpPost() { // post请求 resp, err := http.Post("http://www.01happy.com/demo/accept.php", "application/x-www-form-urlencoded", strings.NewReader("name=cjb")) if err != nil { fmt.Println(err) } // 关闭连接 defer resp.Body.Close() // 读取数据 body, err := ioutil.ReadAll(resp.Body) if err != nil { // handle error } fmt.Println(string(body)) } |
Tips:使用这个方法的话,第二个参数要设置成application/x-www-form-urlencoded
,否则post参数无法传递。
表单提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func httpPostForm() { // 表单请求 resp, err := http.PostForm("http://www.01happy.com/demo/accept.php", url.Values{"key": {"Value"}, "id": {"123"}}) if err != nil { // handle error } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { // handle error } fmt.Println(string(body)) } |
复杂的请求
有时需要在请求的时候设置头参数、cookie之类的数据,就可以使用http.Do方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
func HttpPost(c *gin.Context) { httpClient := http.DefaultClient // 处理url requestUrl := "http://localhost:81/api/requestPost?limit=2&locale=zh&page=1" // 处理请求参数 requestParam := url.Values{} requestParam.Set("page","2") requestParam.Add("limit","3") requestParam.Add("locale","en") requestParam.Add("user","admin") // 创建请求 req,err := http.NewRequest(http.MethodPost,requestUrl,strings.NewReader(requestParam.Encode())) if err != nil { fmt.Printf("创建请求出错:%v \n",err.Error()) return } // 设置Header req.Header.Set("Content-Type","application/x-www-form-urlencoded") req.Header.Set("Authorization","Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzI3MTQyNjIsIm9yaWdfaWF0IjoxNjMyNzEyNDYyLCJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6ImdhZ2EifQ.lZ9EbYNqXjHkcQIwmXZAqv2DsOoy14JjhYLdSLeU1dY") // 请求数据 resp,err := httpClient.Do(req) defer resp.Body.Close() if err != nil { fmt.Printf("请求数据出错:%v \n",err.Error()) return } // 处理数据 body,err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Printf("读取body出错:%v \n",err.Error()) return } // httpCode 200 请求成功 处理数据 if resp.StatusCode == http.StatusOK { fmt.Printf("Body数据:%v \n",string(body)) } } |
原理
http请求流程
- 创建http.Client对象
client
- 创建http.Request对象
req
- 发送请求
client.do(req)
- 关闭
resp.Body.Close()
即使直接调用client.Get()
或client.Post()
, 内部同样创建了request
, 且最终总是通过client.Do()
方法调用私有的client.do()
方法, 执行请求;
http.Client
http.Request
http.Transport
http.Client
该类主要功能:
- Cookie
- Timeout
- Redirect
- Transport
我们还可以基于 http.Client
自定义 HTTP 客户端实现,在此之前,我们先来看看 Client
类型的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
type Client struct { // Transport 用于指定单次 HTTP 请求响应的完整流程 // 默认值是 DefaultTransport Transport RoundTripper // CheckRedirect 用于定义重定向处理策略 // 它是一个函数类型,接收 req 和 via 两个参数,分别表示即将发起的请求和已经发起的所有请求,最早的已发起请求在最前面 // 如果不为空,客户端将在跟踪 HTTP 重定向前调用该函数 // 如果返回错误,客户端将直接返回错误,不会再发起该请求 // 如果为空,Client 将采用一种确认策略,会在 10 个连续请求后终止 CheckRedirect func(req *Request, via []*Request) error // Jar 用于指定请求和响应头中的 Cookie // 如果该字段为空,则只有在请求中显式设置的 Cookie 才会被发送 Jar CookieJar // 指定单次 HTTP 请求响应事务的超时时间 // 未设置的话使用 Transport 的默认设置,为零的话表示不设置超时时间 Timeout time.Duration } |
其中 Transport
字段必须实现 http.RoundTripper
接口,Transport
指定了一次 HTTP 事务(请求响应)的完整流程,如果不指定 Transport
,默认会使用 http.DefaultTransport
这个默认实现,比如 http.DefaultClient
就是这么做的,后面我们会深入探讨 http.DefaultTransport
的底层实现。
CheckRedirect
函数用于定义处理重定向的策略。当使用 HTTP 默认客户端提供的 Get()
或者 Head()
方法发送 HTTP 请求时,如果响应状态码为 30x
(比如 301
、302
等),HTTP 客户端会在遵循跳转规则之前先调用这个 CheckRedirect
函数。
Jar
可用于在 HTTP 客户端中设置 Cookie,Jar
类型必须实现 http.CookieJar
接口,该接口预定义了 SetCookies()
和 Cookies()
两个方法。如果 HTTP 客户端中没有设置 Jar
,Cookie 将被忽略而不会发送到客户端。实际上,我们一般都用 http.SetCookie()
方法来设置 Cookie。
Timeout
字段用于指定 Transport
的超时时间,没有指定的话则使用 Transport
自定义的设置。
http.Transport (重点)
- Transport用来缓存连接, 以供将来重用, 而不是根据需要创建
- Transport是并发安全的
- Transport仅是用来发送HTTP或HTTPS的低级功能, 像cookie和redirect等高级功能是http.Client实现的
transport实现了RoundTripper接口,该接口只有一个方法RoundTrip(),故transport的入口函数就是RoundTrip()。transport的主要功能其实就是缓存了长连接,用于大量http请求场景下的连接复用,减少发送请求时TCP(TLS)连接建立的时间损耗,同时transport还能对连接做一些限制,如连接超时时间,每个host的最大连接数等。transport对长连接的缓存和控制仅限于TCP+(TLS)+HTTP1,不对HTTP2做缓存和限制。
tranport包含如下几个主要概念:
- 连接池:在idleConn中保存了不同类型(connectMethodKey)的请求连接(persistConn)。当发生请求时,首先会尝试从连接池中取一条符合其请求类型的连接使用
- readLoop/writeLoop:连接之上的功能,循环处理该类型的请求(发送request,返回response)
- roundTrip:请求的真正入口,接收到一个请求后会交给writeLoop和readLoop处理。
一对readLoop/writeLoop只能处理一条连接,如果这条连接上没有更多的请求,则关闭连接,退出循环,释放系统资源。
Transport结构体中的主要成员如下(没有列出所有成员):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
type Transport struct { // 操作空闲连接池(idleConn)的锁 idleMu sync.Mutex // true: 关闭所有空闲连接; false: 不关闭 wantIdle bool // 空闲连接池(最近使用完的连接) idleConn map[connectMethodKey][]*persistConn // 等待空闲连接的队列, 基于chan实现 idleConnCh map[connectMethodKey]chan *persistConn // 双向队列 idleLRU connLRU // 请求锁 reqMu sync.Mutex // 请求取消器(如: 超时取消) reqCanceler map[*Request]func(error) // altProto的锁 altMu sync.Mutex // 存储的map[string]RoundTripper, key为URI的scheme(如http, https) altProto atomic.Value // 连接数量锁 connCountMu sync.Mutex // 每台主机连接的数量 connPerHostCount map[connectMethodKey]int // 每台主机可用的连接 connPerHostAvailable map[connectMethodKey]chan struct{} // Proxy指定一个函数来返回给定Request的代理 // 代理类型由URL scheme确定。支持http, https等。 默认为http // 如果Proxy为空或返回空的url,则不使用任何代理。 Proxy func(*Request) (*url.URL, error) // DialContext指定用于创建未加密的TCP连接的拨号功能。 // 如果DialContext为nil(并且下面不建议使用的Dial也为nil),则传输使用程序包net进行拨号。 // DialContext与RoundTrip的调用同时运行。 // 当较早的连接在以后的DialContext完成之前处于空闲状态时, // 发起拨号的RoundTrip调用可能会使用先前拨打的连接结束。 DialContext func(ctx context.Context, network, addr string) (net.Conn, error) // Dial指定用于创建未加密的TCP连接的拨号功能。 // 拨号与RoundTrip的呼叫同时运行。 // 当较早的连接在之后的拨号完成之前变为空闲时,发起拨号的RoundTrip呼叫可能会使用先前拨打的连接结束。 // 不推荐使用:改用DialContext,它使传输器在不再需要拨号时立即取消它们。 // 如果两者都设置,则DialContext优先。 Dial func(network, addr string) (net.Conn, error) // DialTLS指定用于为非代理HTTPS请求创建TLS连接的可选拨号功能。 // 如果DialTLS为nil,则使用Dial和TLSClientConfig。 // 如果设置了DialTLS,则Dial Hook不用于HTTPS请求, // 并且TLSClientConfig和TLSHandshakeTimeout将被忽略。 // 假定返回的net.Conn已通过TLS握手。 DialTLS func(network, addr string) (net.Conn, error) // TLSClientConfig指定要与tls.Client一起使用的TLS配置。 // 如果为nil,则使用默认配置。 // 如果为非nil,则默认情况下可能不会启用HTTP / 2支持。 TLSClientConfig *tls.Config // TLSHandshakeTimeout指定等待TLS握手的最大时间。 零表示没有超时。 TLSHandshakeTimeout time.Duration // true: 将禁用HTTP保持活动状态,并且仅将与服务器的连接用于单个HTTP请求。 // 这与类似命名的TCP保持活动无关。 DisableKeepAlives bool // true: 当请求不包含现有的Accept-Encoding值时, // 阻止传输使用“ Accept-Encoding:gzip”请求标头请求压缩。 // 如果传输本身请求gzip并获得gzip压缩的响应,则会在Response.Body中对其进行透明解码。 // 但是,如果用户明确请求gzip,则不会自动将其解压缩。 DisableCompression bool // MaxIdleConns控制所有主机之间的最大空闲(保持活动)连接数。 零表示无限制。 MaxIdleConns int // MaxIdleConnsPerHost控制最大空闲(保持活动)连接以保留每个主机。 // 如果为零,则使用DefaultMaxIdleConnsPerHost=2。 MaxIdleConnsPerHost int // MaxConnsPerHost可以选择限制每个主机的连接总数,包括处于拨号,活动和空闲状态的连接。 // 超出限制时,拨号将阻塞。 // 零表示无限制。 // 对于HTTP / 2,当前仅控制一次创建的新连接数,而不是总数。 // 实际上,使用HTTP / 2的主机只有大约一个空闲连接。 MaxConnsPerHost int // IdleConnTimeout是空闲(保持活动状态)连接在关闭自身之前将保持空闲状态的最长时间。 // 零表示无限制。 IdleConnTimeout time.Duration //(如果非零)指定在完全写入请求(包括其body(如果有))之后等待服务器的响应头的时间。 // 该时间不包括读取响应正文的时间。 ResponseHeaderTimeout time.Duration //(如果非零)指定如果请求具有“期望:100-连续”标头, // 则在完全写入请求标头之后等待服务器的第一个响应标头的时间。 // 零表示没有超时,并导致正文立即发送,而无需等待服务器批准。 // 此时间不包括发送请求标头的时间。 ExpectContinueTimeout time.Duration // TLSNextProto指定在TLS NPN / ALPN协议协商之后,传输方式如何切换到备用协议(例如HTTP / 2)。 // 如果传输使用非空协议名称拨打TLS连接,并且TLSNextProto包含该键的映射条目(例如“ h2”), // 则将以请求的权限(例如“ example.com”或“ example .com:1234“)和TLS连接。 // 该函数必须返回RoundTripper,然后再处理请求。 // 如果TLSNextProto不为nil,则不会自动启用HTTP / 2支持。 TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper // 可以选择指定在CONNECT请求期间发送到代理的header。 ProxyConnectHeader Header // 指定对服务器的响应标头中允许的响应字节数的限制。 // 零表示使用默认限制。 MaxResponseHeaderBytes int64 // nextProtoOnce防止TLSNextProto和h2transport的初始化(通过OnceSetNextProtoDefaults) nextProtoOnce sync.Once // 如果http2已连接,则为非null h2transport h2Transport } |
结合 Transport
数据结构来看下 DefaultTransport
的设置:
- 通过
net.Dialer
初始化 Dial 上下文配置,默认超时时间设置为 30 秒; - 通过
MaxIdleConns
指定最大空闲连接数为 100,未显式设置MaxIdleConnsPerHost
和MaxConnsPerHost
,MaxIdleConnsPerHost
有默认值,通过http.DefaultMaxIdleConnsPerHost
设置,对应缺省值是 2; - 通过
IdleConnTimeout
指定最大空闲连接时间为 90 秒,即当某个空闲连接超过 90 秒没有被复用,则销毁,空闲连接需要DisableKeepAlives
为false
的情况下才可用,即 HTTP 长连接状态下有效(HTTP/1.1以上版本支持长连接,对应请求头Connection:keep-alive
); - 通过
TLSHandshakeTimeout
指定基于 TLS 协议的安全 TCP 连接在被建立时握手阶段的超时时间为 10 秒; - 通过
ExpectContinueTimeout
指定客户端想要使用 POST 请求把一个很大的报文体发送给服务端的时候,先通过发送一个包含了Expect: 100-continue
的请求报文头,来询问服务端是否愿意接收这个大报文体对应的超时时间,这里默认设置为 1 秒。
另外,Transport
包含了 RoundTrip
方法实现,所以实现了 RoundTripper
接口。下面我们来看看 Transport
中 RoundTrip
方法的实现。
Transport.roundTrip
是主入口,它通过传入一个request参数,由此选择一个合适的长连接来发送该request并返回response。整个流程主要分为两步:
使用getConn
函数来获得底层TCP(TLS)连接;调用roundTrip
函数进行上层协议(HTTP)处理。
getConn
用于返回一条长连接。长连接的来源有2种路径:连接池中获取;当连接池中无法获取到时会新建一条连接tryPutIdleConn
函数用来将一条新创建或回收的连接放回连接池中,以便后续使用。与getIdleConnCh
配合使用,后者用于获取一类连接对应的chan
。在如下场景会将一个连接放回idleConn
中
- 在
readLoop
成功之后(当然还有其他判断,如底层链路没有返回EOF错误); - 创建一个新连接且新连接没有被使用时;
roundTrip
一开始发现request被取消时
dialConn
用于新创建一条连接,并为该连接启动readLoop
和writeLoop
addTLS
用于进行非注册协议下的TLS协商在获取到底层TCP(TLS)连接后在
roundTrip
中处理上层协议:即发送HTTP request,返回HTTP response。roundTrip
给writeLoop
提供request,从readLoop
获取response。
一个roundTrip
用于处理一类request。
writeLoop
用于发送request请求
readLoop
循环接收response响应,成功获得response后会将连接返回连接池,便于后续复用。当readLoop
正常处理完一个response之后,会将连接重新放入到连接池中;
当readloop
退出后,该连接会被关闭移除。
Client.do
该方法主要实现了:
- 参数检查
- 默认值设置
- 多跳请求
- 计算超时时间点deadline
- 调用c.send(req, deadline)
Client.send
该方法主要实现了:
- Cookie的装载
- Transport对象的获取
- 调用send(req, c.transport(), deadline)
Transport的默认值
1 2 3 4 5 6 7 8 9 10 11 12 |
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, } |
http.send
该方法主要实现了:
- 参数校验: URL, header, RoundTripper
- 超时取消: setRequestCancel(req, rt, deadline)
- 请求事务: rt.RoundTrip(req)
client.setRequestCancel
该方法主要实现了:
创建一个协程利用select chan机制阻塞等待取消请求
Transport.RoundTrip
该方法主要实现了
- 参数校验: scheme, host, method, protocol…
- 获取缓存的或新建的连接
Transport.getConn
首先从连接池中获取连接t.getIdleConn(cm)
, 获取成功即返回
拨号创建新连接
- 如果达到了最大数量则阻塞, 等待空闲
参考
go http请求流程分析
详解golang net之transport
Go 语言网络编程系列(四)—— HTTP 编程篇:http.Client 底层实现剖析