Go中的HTTP请求之——HTTP1.1请求流程分析

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"来自公众号:新世界杂货铺"}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http是目前应用最为广泛, 也是程序员接触最多的协议之一。今天笔者站在GoPher的角度对http1.1的请求流程进行全面的分析。希望读者读完此文后, 能够有以下几个收获:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"对http1.1的请求流程有一个大概的了解"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在平时的开发中能够更好地重用底层TCP连接"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"对http1.1的线头阻塞能有一个更清楚的认识"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"HTTP1.1流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天内容较多, 废话不多说, 直接上干货。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f0/f03408c0795f6067928d91c8e3959e74.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下来, 笔者将根据流程图,对除了NewRequest以外的函数进行逐步的展开和分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"(*Client).do"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*Client).do方法的核心代码是一个没有结束条件的for循环。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"for {\n\t// For all but the first request, create the next\n\t// request hop and replace req.\n\tif len(reqs) > 0 {\n\t\tloc := resp.Header.Get(\"Location\")\n\t\t// ...此处省略代码...\n\t\terr = c.checkRedirect(req, reqs)\n\t\t// ...此处省略很多代码...\n\t}\n\n\treqs = append(reqs, req)\n\tvar err error\n\tvar didTimeout func() bool\n\tif resp, didTimeout, err = c.send(req, deadline); err != nil {\n\t\t// c.send() always closes req.Body\n\t\treqBodyClosed = true\n\t\t// ...此处省略代码...\n\t\treturn nil, uerr(err)\n\t}\n\n\tvar shouldRedirect bool\n\tredirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])\n\tif !shouldRedirect {\n\t\treturn resp, nil\n\t}\n\n\treq.closeBody()\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代码中, 请求第一次进入会调用"},{"type":"codeinline","content":[{"type":"text","text":"c.send"}]},{"type":"text","text":", 得到响应后会判断请求是否需要重定向, 如果需要重定向则继续循环, 否则返回响应。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"进入重定向流程后, 这里笔者简单介绍一下"},{"type":"codeinline","content":[{"type":"text","text":"checkRedirect"}]},{"type":"text","text":"函数:"}]},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func defaultCheckRedirect(req *Request, via []*Request) error {\n\tif len(via) >= 10 {\n\t\treturn errors.New(\"stopped after 10 redirects\")\n\t}\n\treturn nil\n}\n// ...\nfunc (c *Client) checkRedirect(req *Request, via []*Request) error {\n\tfn := c.CheckRedirect\n\tif fn == nil {\n\t\tfn = defaultCheckRedirect\n\t}\n\treturn fn(req, via)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知, 用户可以自己定义重定向的检查规则。如果用户没有自定义检查规则, 则"},{"type":"text","marks":[{"type":"strong"}],"text":"重定向次数不能超过10次"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"(*Client).send"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(\\*Client).send方法逻辑较为简单, 主要看用户有没有为"},{"type":"text","marks":[{"type":"strong"}],"text":"http.Client的Jar"},{"type":"text","text":"字段实现"},{"type":"codeinline","content":[{"type":"text","text":"CookieJar"}]},{"type":"text","text":"接口。主要流程如下:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"如果实现了CookieJar接口, 为Request添加保存的cookie信息。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"调用"},{"type":"codeinline","content":[{"type":"text","text":"send"}]},{"type":"text","text":"函数。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果实现了CookieJar接口, 将Response中的cookie信息保存下来。"}]}]}]},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// didTimeout is non-nil only if err != nil.\nfunc (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {\n\tif c.Jar != nil {\n\t\tfor _, cookie := range c.Jar.Cookies(req.URL) {\n\t\t\treq.AddCookie(cookie)\n\t\t}\n\t}\n\tresp, didTimeout, err = send(req, c.transport(), deadline)\n\tif err != nil {\n\t\treturn nil, didTimeout, err\n\t}\n\tif c.Jar != nil {\n\t\tif rc := resp.Cookies(); len(rc) > 0 {\n\t\t\tc.Jar.SetCookies(req.URL, rc)\n\t\t}\n\t}\n\treturn resp, nil, nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外, 我们还需要关注"},{"type":"codeinline","content":[{"type":"text","text":"c.transport()"}]},{"type":"text","text":"的调用。如果用户未对http.Client指定Transport则会使用go默认的DefaultTransport。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"该Transport实现RoundTripper接口。在go中RoundTripper的定义为“"},{"type":"text","marks":[{"type":"strong"}],"text":"执行单个HTTP事务的能力,获取给定请求的响应"},{"type":"text","text":"”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (c *Client) transport() RoundTripper {\n\tif c.Transport != nil {\n\t\treturn c.Transport\n\t}\n\treturn DefaultTransport\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"send"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"send函数会检查request的URL,以及参数的rt, 和header值。如果URL和rt为nil则直接返回错误。同时, 如果请求中设置了用户信息, 还会检查并设置basic的验证头信息,最后调用"},{"type":"codeinline","content":[{"type":"text","text":"rt.RoundTrip"}]},{"type":"text","text":"得到请求的响应。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {\n\treq := ireq // req is either the original request, or a modified fork\n\t// ...此处省略代码...\n\tif u := req.URL.User; u != nil && req.Header.Get(\"Authorization\") == \"\" {\n\t\tusername := u.Username()\n\t\tpassword, _ := u.Password()\n\t\tforkReq()\n\t\treq.Header = cloneOrMakeHeader(ireq.Header)\n\t\treq.Header.Set(\"Authorization\", \"Basic \"+basicAuth(username, password))\n\t}\n\n\tif !deadline.IsZero() {\n\t\tforkReq()\n\t}\n\tstopTimer, didTimeout := setRequestCancel(req, rt, deadline)\n\n\tresp, err = rt.RoundTrip(req)\n\tif err != nil {\n // ...此处省略代码...\n\t\treturn nil, didTimeout, err\n\t}\n\t// ...此处省略代码...\n\treturn resp, nil, nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"(*Transport).RoundTrip"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(\\"},{"type":"text","marks":[{"type":"italic"}],"text":"Transport).RoundTrip的逻辑很简单,它会调用(\\"},{"type":"text","text":"Transport).roundTrip方法,因此本节实际上是对(\\*Transport).roundTrip方法的分析。"}]},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) RoundTrip(req *Request) (*Response, error) {\n\treturn t.roundTrip(req)\n}\nfunc (t *Transport) roundTrip(req *Request) (*Response, error) {\n\t// ...此处省略校验header头和headervalue的代码以及其他代码...\n\n\tfor {\n\t\tselect {\n\t\tcase 0 && !stop {\n\t\t\tpconn := list[len(list)-1]\n\n\t\t\t// See whether this connection has been idle too long, considering\n\t\t\t// only the wall time (the Round(0)), in case this is a laptop or VM\n\t\t\t// coming out of suspend with previously cached idle connections.\n\t\t\ttooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)\n\t\t\t// ...此处省略代码...\n\t\t\tdelivered = w.tryDeliver(pconn, nil)\n\t\t\tif delivered {\n\t\t\t\t// ...此处省略代码...\n\t\t\t}\n\t\t\tstop = true\n\t\t}\n\t\tif len(list) > 0 {\n\t\t\tt.idleConn[w.key] = list\n\t\t} else {\n\t\t\tdelete(t.idleConn, w.key)\n\t\t}\n\t\tif stop {\n\t\t\treturn delivered\n\t\t}\n\t}\n\n\t// Register to receive next connection that becomes idle.\n\tif t.idleConnWait == nil {\n\t\tt.idleConnWait = make(map[connectMethodKey]wantConnQueue)\n\t}\n\tq := t.idleConnWait[w.key]\n\tq.cleanFront()\n\tq.pushBack(w)\n\tt.idleConnWait[w.key] = q\n\treturn false\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中"},{"type":"codeinline","content":[{"type":"text","text":"w.tryDeliver"}]},{"type":"text","text":"方法主要作用是将连接协程安全的赋值给"},{"type":"codeinline","content":[{"type":"text","text":"w.pc"}]},{"type":"text","text":",并关闭"},{"type":"codeinline","content":[{"type":"text","text":"w.ready"}]},{"type":"text","text":"管道。此时我们便可以和(*Transport).getConn中调用queueForIdleConn成功后的返回值对应上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"(*Transport).queueForDial"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*Transport).queueForDial方法包含三个步骤:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"如果t.MaxConnsPerHost小于等于0,执行"},{"type":"codeinline","content":[{"type":"text","text":"go t.dialConnFor(w)"}]},{"type":"text","text":"并返回。其中MaxConnsPerHost代表着每个host的最大连接数,小于等于0表示不限制。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果当前host的连接数不超过t.MaxConnsPerHost,对当前host的连接数+1,然后执行"},{"type":"codeinline","content":[{"type":"text","text":"go t.dialConnFor(w)"}]},{"type":"text","text":"并返回。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果当前host的连接数等于t.MaxConnsPerHost,则将"},{"type":"codeinline","content":[{"type":"text","text":"wantConn"}]},{"type":"text","text":"结构体变量放入"},{"type":"codeinline","content":[{"type":"text","text":"t.connsPerHostWait[w.key]"}]},{"type":"text","text":"等待队列,此处wantConn结构体变量就是前面提到的"},{"type":"codeinline","content":[{"type":"text","text":"w"}]},{"type":"text","text":"。另外在放入等待队列前会先清除队列中已经失效或者不再等待的变量。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) queueForDial(w *wantConn) {\n\tw.beforeDial()\n\tif t.MaxConnsPerHost <= 0 {\n\t\tgo t.dialConnFor(w)\n\t\treturn\n\t}\n\n\tt.connsPerHostMu.Lock()\n\tdefer t.connsPerHostMu.Unlock()\n\n\tif n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {\n\t\tif t.connsPerHost == nil {\n\t\t\tt.connsPerHost = make(map[connectMethodKey]int)\n\t\t}\n\t\tt.connsPerHost[w.key] = n + 1\n\t\tgo t.dialConnFor(w)\n\t\treturn\n\t}\n\n\tif t.connsPerHostWait == nil {\n\t\tt.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)\n\t}\n\tq := t.connsPerHostWait[w.key]\n\tq.cleanFront()\n\tq.pushBack(w)\n\tt.connsPerHostWait[w.key] = q\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"(*Transport).dialConnFor"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(\\*Transport).dialConnFor方法调用"},{"type":"text","marks":[{"type":"strong"}],"text":"t.dialConn"},{"type":"text","text":"获取一个真正的"},{"type":"codeinline","content":[{"type":"text","text":"*persistConn"}]},{"type":"text","text":"。并将这个连接传递给w, 如果w已经获取到了连接,则会传递失败,此时调用"},{"type":"codeinline","content":[{"type":"text","text":"t.putOrCloseIdleConn"}]},{"type":"text","text":"将连接放回空闲连接池。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果连接获取错误则会调用"},{"type":"codeinline","content":[{"type":"text","text":"t.decConnsPerHost"}]},{"type":"text","text":"减少当前host的连接数。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) dialConnFor(w *wantConn) {\n\tdefer w.afterDial()\n\n\tpc, err := t.dialConn(w.ctx, w.cm)\n\tdelivered := w.tryDeliver(pc, err)\n\tif err == nil && (!delivered || pc.alt != nil) {\n\t\t// pconn was not passed to w,\n\t\t// or it is HTTP/2 and can be shared.\n\t\t// Add to the idle connection pool.\n\t\tt.putOrCloseIdleConn(pc)\n\t}\n\tif err != nil {\n\t\tt.decConnsPerHost(w.key)\n\t}\n}"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*Transport).putOrCloseIdleConn方法"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) putOrCloseIdleConn(pconn *persistConn) {\n\tif err := t.tryPutIdleConn(pconn); err != nil {\n\t\tpconn.close(err)\n\t}\n}\nfunc (t *Transport) tryPutIdleConn(pconn *persistConn) error {\n\tif t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 {\n\t\treturn errKeepAlivesDisabled\n\t}\n\t// ...此处省略代码...\n\tt.idleMu.Lock()\n\tdefer t.idleMu.Unlock()\n\t// ...此处省略代码...\n\t\n\t// Deliver pconn to goroutine waiting for idle connection, if any.\n\t// (They may be actively dialing, but this conn is ready first.\n\t// Chrome calls this socket late binding.\n\t// See https://insouciant.org/tech/connection-management-in-chromium/.)\n\tkey := pconn.cacheKey\n\tif q, ok := t.idleConnWait[key]; ok {\n\t\tdone := false\n\t\tif pconn.alt == nil {\n\t\t\t// HTTP/1.\n\t\t\t// Loop over the waiting list until we find a w that isn't done already, and hand it pconn.\n\t\t\tfor q.len() > 0 {\n\t\t\t\tw := q.popFront()\n\t\t\t\tif w.tryDeliver(pconn, nil) {\n\t\t\t\t\tdone = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// HTTP/2.\n\t\t\t// Can hand the same pconn to everyone in the waiting list,\n\t\t\t// and we still won't be done: we want to put it in the idle\n\t\t\t// list unconditionally, for any future clients too.\n\t\t\tfor q.len() > 0 {\n\t\t\t\tw := q.popFront()\n\t\t\t\tw.tryDeliver(pconn, nil)\n\t\t\t}\n\t\t}\n\t\tif q.len() == 0 {\n\t\t\tdelete(t.idleConnWait, key)\n\t\t} else {\n\t\t\tt.idleConnWait[key] = q\n\t\t}\n\t\tif done {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif t.closeIdle {\n\t\treturn errCloseIdle\n\t}\n\tif t.idleConn == nil {\n\t\tt.idleConn = make(map[connectMethodKey][]*persistConn)\n\t}\n\tidles := t.idleConn[key]\n\tif len(idles) >= t.maxIdleConnsPerHost() {\n\t\treturn errTooManyIdleHost\n\t}\n\t// ...此处省略代码...\n\tt.idleConn[key] = append(idles, pconn)\n\tt.idleLRU.add(pconn)\n\t// ...此处省略代码...\n\t// Set idle timer, but only for HTTP/1 (pconn.alt == nil).\n\t// The HTTP/2 implementation manages the idle timer itself\n\t// (see idleConnTimeout in h2_bundle.go).\n\tif t.IdleConnTimeout > 0 && pconn.alt == nil {\n\t\tif pconn.idleTimer != nil {\n\t\t\tpconn.idleTimer.Reset(t.IdleConnTimeout)\n\t\t} else {\n\t\t\tpconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)\n\t\t}\n\t}\n\tpconn.idleAt = time.Now()\n\treturn nil\n}\nfunc (t *Transport) maxIdleConnsPerHost() int {\n\tif v := t.MaxIdleConnsPerHost; v != 0 {\n\t\treturn v\n\t}\n\treturn DefaultMaxIdleConnsPerHost // 2\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知,将连接放入t.idleConn前,先检查t.idleConnWait的数量。如果有请求在等待空闲连接, 则将连接复用,没有空闲连接时,才将连接放入t.idleConn。连接放入t.idleConn后,还会重置连接的可空闲时间。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外在t.putOrCloseIdleConn函数中还需要注意两点:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"如果用户自定义了http.client,且将DisableKeepAlives设置为true,或者将MaxIdleConnsPerHost设置为负数,则连接不会放入t.idleConn即连接不能复用。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在判断已有空闲连接数量时, 如果MaxIdleConnsPerHost 不等于0, 则返回用户设置的数量,否则返回默认值2,详见上面的"},{"type":"codeinline","content":[{"type":"text","text":"(*Transport).maxIdleConnsPerHost"}]},{"type":"text","text":" 函数。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"综上, 我们知道对于部分有连接数限制的业务, 我们可以为http.Client自定义一个Transport, 并设置Transport的"},{"type":"codeinline","content":[{"type":"text","text":"MaxConnsPerHost"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"MaxIdleConnsPerHost"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"IdleConnTimeout"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"DisableKeepAlives"}]},{"type":"text","text":"从而达到即限制连接数量,又能保证一定的并发。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*Transport).decConnsPerHost方法"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) decConnsPerHost(key connectMethodKey) {\n\t// ...此处省略代码...\n\tt.connsPerHostMu.Lock()\n\tdefer t.connsPerHostMu.Unlock()\n\tn := t.connsPerHost[key]\n\t// ...此处省略代码...\n\n\t// Can we hand this count to a goroutine still waiting to dial?\n\t// (Some goroutines on the wait list may have timed out or\n\t// gotten a connection another way. If they're all gone,\n\t// we don't want to kick off any spurious dial operations.)\n\tif q := t.connsPerHostWait[key]; q.len() > 0 {\n\t\tdone := false\n\t\tfor q.len() > 0 {\n\t\t\tw := q.popFront()\n\t\t\tif w.waiting() {\n\t\t\t\tgo t.dialConnFor(w)\n\t\t\t\tdone = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif q.len() == 0 {\n\t\t\tdelete(t.connsPerHostWait, key)\n\t\t} else {\n\t\t\t// q is a value (like a slice), so we have to store\n\t\t\t// the updated q back into the map.\n\t\t\tt.connsPerHostWait[key] = q\n\t\t}\n\t\tif done {\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Otherwise, decrement the recorded count.\n\tif n--; n == 0 {\n\t\tdelete(t.connsPerHost, key)\n\t} else {\n\t\tt.connsPerHost[key] = n\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知, decConnsPerHost方法主要干了两件事:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"判断是否有请求在等待拨号, 如果有则执行"},{"type":"codeinline","content":[{"type":"text","text":"go t.dialConnFor(w)"}]},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果没有请求在等待拨号, 则减少当前host的连接数量。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"###### (*Transport).dialConn"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根据http.Client的默认配置和实际的debug结果,(*Transport).dialConn方法主要逻辑如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"调用"},{"type":"codeinline","content":[{"type":"text","text":"t.dial(ctx, \"tcp\", cm.addr())"}]},{"type":"text","text":"创建TCP连接。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果是https的请求, 则对请求建立安全的tls传输通道。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"为persistConn创建读写buffer, 如果用户没有自定义读写buffer的大小, 根据writeBufferSize和readBufferSize方法可知, 读写bufffer的大小默认为4096。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"执行"},{"type":"codeinline","content":[{"type":"text","text":"go pconn.readLoop()"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"go pconn.writeLoop()"}]},{"type":"text","text":"开启读写循环然后返回连接。 "}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {\n\tpconn = &persistConn{\n\t\tt: t,\n\t\tcacheKey: cm.key(),\n\t\treqch: make(chan requestAndChan, 1),\n\t\twritech: make(chan writeRequest, 1),\n\t\tclosech: make(chan struct{}),\n\t\twriteErrCh: make(chan error, 1),\n\t\twriteLoopDone: make(chan struct{}),\n\t}\n\t// ...此处省略代码...\n\tif cm.scheme() == \"https\" && t.hasCustomTLSDialer() {\n\t\t// ...此处省略代码...\n\t} else {\n\t\tconn, err := t.dial(ctx, \"tcp\", cm.addr())\n\t\tif err != nil {\n\t\t\treturn nil, wrapErr(err)\n\t\t}\n\t\tpconn.conn = conn\n\t\tif cm.scheme() == \"https\" {\n\t\t\tvar firstTLSHost string\n\t\t\tif firstTLSHost, _, err = net.SplitHostPort(cm.addr()); err != nil {\n\t\t\t\treturn nil, wrapErr(err)\n\t\t\t}\n\t\t\tif err = pconn.addTLS(firstTLSHost, trace); err != nil {\n\t\t\t\treturn nil, wrapErr(err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Proxy setup.\n\tswitch { // ...此处省略代码... }\n\n\tif cm.proxyURL != nil && cm.targetScheme == \"https\" {\n\t\t// ...此处省略代码...\n\t}\n\n\tif s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != \"\" {\n\t\t// ...此处省略代码...\n\t}\n\n\tpconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())\n\tpconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())\n\n\tgo pconn.readLoop()\n\tgo pconn.writeLoop()\n\treturn pconn, nil\n}\nfunc (t *Transport) writeBufferSize() int {\n\tif t.WriteBufferSize > 0 {\n\t\treturn t.WriteBufferSize\n\t}\n\treturn 4 << 10\n}\n\nfunc (t *Transport) readBufferSize() int {\n\tif t.ReadBufferSize > 0 {\n\t\treturn t.ReadBufferSize\n\t}\n\treturn 4 << 10\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"(*persistConn).roundTrip"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*persistConn).roundTrip方法是http1.1请求的核心之一,该方法在这里获取真实的Response并返回给上层。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {\n\t// ...此处省略代码...\n\n\tgone := make(chan struct{})\n\tdefer close(gone)\n\t// ...此处省略代码...\n\tconst debugRoundTrip = false\n\n\t// Write the request concurrently with waiting for a response,\n\t// in case the server decides to reply before reading our full\n\t// request body.\n\tstartBytesWritten := pc.nwrite\n\twriteErrCh := make(chan error, 1)\n\tpc.writech "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">上述流程中笔者对很多细节并未详细提及或者仅一笔带过,希望读者酌情参考。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"总结"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在go中发起http1.1的请求时, 如果遇到不关心响应的请求,请务必完整读取响应内容以保证连接的复用性。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果遇到对连接数有限制的业务,可以通过自定义http.Client的Transport, 并设置Transport的"},{"type":"codeinline","content":[{"type":"text","text":"MaxConnsPerHost"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"MaxIdleConnsPerHost"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"IdleConnTimeout"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"DisableKeepAlives"}]},{"type":"text","text":"的值,来控制连接数。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果对于重定向业务逻辑有需求,可以自定义http.Client的"},{"type":"codeinline","content":[{"type":"text","text":"CheckRedirect"}]},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"在http1.1,中一个连接上的请求,只有等前一个请求处理完之后才能继续下一个请求。如果前面的请求处理较慢, 则后面的请求必须等待, 这就是http1.1中的线头阻塞。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注: 写本文时, 笔者所用go版本为: go1.14.2 "}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"生命不息, 探索不止, 后续将持续更新有关于go的技术探索 "}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原创不易, 卑微求关注收藏二连."}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章