<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          網(wǎng)友很強(qiáng)大,發(fā)現(xiàn)了Go并發(fā)下載的Bug

          共 15548字,需瀏覽 32分鐘

           ·

          2021-07-12 12:13

          閱讀本文大概需要 5 分鐘。

          大家好,我是 polarisxu。

          前幾天我寫(xiě)了一篇文章:Go項(xiàng)目實(shí)戰(zhàn):一步步構(gòu)建一個(gè)并發(fā)文件下載器,有小伙伴評(píng)論問(wèn),請(qǐng)求 https://studygolang.com/dl/golang/go1.16.5.src.tar.gz 為什么沒(méi)有返回 Accept-Ranges。在寫(xiě)那篇文章時(shí),我也試了,確實(shí)沒(méi)有返回,因此我以為它不支持。

          但有一個(gè)小伙伴很認(rèn)真,他改用 GET 方法請(qǐng)求這個(gè)地址,結(jié)果卻有 Accept-Ranges,于是就很困惑,問(wèn)我什么原因。經(jīng)過(guò)一頓操作猛如虎,終于知道原因了。記錄下排查過(guò)程,供大家參考?。ㄐ』锇榈牧粞钥梢圆榭茨瞧恼拢?/p>

          01 排查過(guò)程

          通過(guò) curl 命令,分別用 GET 和 HEAD 方法請(qǐng)求這個(gè)地址,結(jié)果如下:

          $ curl -X GET --head https://studygolang.com/dl/golang/go1.16.5.src.tar.gz
          HTTP/1.1 303 See Other
          Server: nginx
          Date: Wed, 07 Jul 2021 09:09:35 GMT
          Content-Length: 0
          Connection: keep-alive
          Location: https://golang.google.cn/dl/go1.16.5.src.tar.gz
          X-Request-Id: 83ee595c-6270-4fb0-a2f1-98fdc4d315be

          $ curl --head https://studygolang.com/dl/golang/go1.16.5.src.tar.gz
          HTTP/1.1 200 OK
          Server: nginx
          Date: Wed, 07 Jul 2021 09:09:44 GMT
          Connection: keep-alive
          X-Request-Id: f2ba473d-5bee-44c3-a591-02c358551235

          雖然都沒(méi)有 Accept-Ranges,但有一個(gè)奇怪現(xiàn)象:一個(gè)狀態(tài)碼是 303,一個(gè)是 200。很顯然,303 是正確的,HEAD 為什么會(huì)是 200?

          我以為是 Nginx 對(duì) HEAD 請(qǐng)求做了特殊處理,于是直接訪問(wèn) Go 服務(wù)的方式(不經(jīng)過(guò) Nginx 代理),結(jié)果一樣。

          于是,我用 Go 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Web 服務(wù),Handler 里面也重定向。

          func main() {
           http.HandleFunc("/dl"func(w http.ResponseWriter, r *http.Request) {
            http.Redirect(w, r, "/", http.StatusSeeOther)
           })
           http.HandleFunc("/"func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintf(w, "Hello World")
           })
           http.ListenAndServe(":2022"nil)
          }

          用 curl 請(qǐng)求 http://localhost:2022/dl,GET 和 HEAD 都返回 303。于是我懷疑是不是 Echo 框架哪里的問(wèn)題(studygolang 使用 Echo 框架構(gòu)建的)。

          所以,我用 Echo 框架寫(xiě)個(gè) Web 服務(wù)測(cè)試:

          func main() {
           e := echo.New()
            
           e.GET("/dl"func(ctx echo.Context) error {
              return ctx.Redirect(http.StatusSeeOther, "/")
            })
            e.GET("/"func(ctx echo.Context) error {
              return ctx.String(http.StatusOK, "Hello World!")
            })
           
           e.Logger.Fatal(e.Start(":2022"))
          }

          同樣用 curl 請(qǐng)求  http://localhost:2022/dl,GET 返回 303,而 HEAD 報(bào) 405 Method Not Allowed,這符合預(yù)期。我們的路由設(shè)置只允許 GET 請(qǐng)求。但為什么 studygolang 沒(méi)有返回 405,因?yàn)樗蚕拗浦荒?GET 請(qǐng)求。

          于是我對(duì)隨便一個(gè)地址發(fā)起 HEAD 請(qǐng)求,發(fā)現(xiàn)都返回 200,可見(jiàn) HTTP 錯(cuò)誤被“吞掉”了。查找 studygolang 的中間件,發(fā)現(xiàn)了這個(gè):

          func HTTPError() echo.MiddlewareFunc {
           return func(next echo.HandlerFunc) echo.HandlerFunc {
            return func(ctx echo.Context) error {
             if err := next(ctx); err != nil {

              if !ctx.Response().Committed {
               if he, ok := err.(*echo.HTTPError); ok {
                switch he.Code {
                case http.StatusNotFound:
                 if util.IsAjax(ctx) {
                  return ctx.String(http.StatusOK, `{"ok":0,"error":"接口不存在"}`)
                 }
                 return Render(ctx, "404.html"nil)
                case http.StatusForbidden:
                 if util.IsAjax(ctx) {
                  return ctx.String(http.StatusOK, `{"ok":0,"error":"沒(méi)有權(quán)限訪問(wèn)"}`)
                 }
                 return Render(ctx, "403.html"map[string]interface{}{"msg": he.Message})
                case http.StatusInternalServerError:
                 if util.IsAjax(ctx) {
                  return ctx.String(http.StatusOK, `{"ok":0,"error":"接口服務(wù)器錯(cuò)誤"}`)
                 }
                 return Render(ctx, "500.html"nil)
               }
              }
             }
             return nil
            }
           }
          }

          這里對(duì) 404、403、500 錯(cuò)誤都做了處理,但其他 HTTP 錯(cuò)誤直接忽略了,導(dǎo)致最后返回了 200 OK。只需要在上面 switch 語(yǔ)句加一個(gè) default 分支,同時(shí)把 err 原樣 return,采用系統(tǒng)默認(rèn)處理方式:

          default:
           return err

          這樣 405 Method Not Allowed 會(huì)正常返回。

          同時(shí),為了解決 HEAD 能用來(lái)判斷下載行為,針對(duì)下載路由,我加上了允許 HEAD 請(qǐng)求,這樣就解決了小伙伴們的困惑。

          02 curl 和 Go 代碼行為異同

          不知道大家發(fā)現(xiàn)沒(méi)有,通過(guò) curl 請(qǐng)求 https://studygolang.com/dl/golang/go1.16.5.src.tar.gz 和 Go 代碼請(qǐng)求,結(jié)果是不一樣的:

          $ curl -X GET --head https://studygolang.com/dl/golang/go1.16.5.src.tar.gz
          HTTP/1.1 303 See Other
          Server: nginx
          Date: Thu, 08 Jul 2021 02:05:10 GMT
          Content-Length: 0
          Connection: keep-alive
          Location: https://golang.google.cn/dl/go1.16.5.src.tar.gz
          X-Request-Id: 14d741ca-65c1-4b05-90b8-bef5c8b5a0a3

          返回的是 303 重定向,自然沒(méi)有 Accept-Ranges 頭。

          但改用如下 Go 代碼:

          resp, err := http.Get("https://studygolang.com/dl/golang/go1.16.5.src.tar.gz")
          if err != nil {
            fmt.Println("get err", err)
            return
          }

          fmt.Println(resp)
          fmt.Println("ranges", resp.Header.Get("Accept-Ranges"))

          返回的是 200,且有 Accept-Ranges 頭。可以猜測(cè),應(yīng)該是 Go 根據(jù)重定向遞歸請(qǐng)求重定向后的地址??梢圆榭丛创a確認(rèn)下。

          通過(guò)這個(gè)可以看到:https://docs.studygolang.com/src/net/http/client.go?s=20406:20458#L574,核心代碼如下(比較容易看懂):

          // 循環(huán)處理所有需要處理的 url(包括重定向后的)
          for {
            // For all but the first request, create the next
            // request hop and replace req.
            if len(reqs) > 0 {
                // 如果是重定向,請(qǐng)求重定向地址
             loc := resp.Header.Get("Location")
             if loc == "" {
              resp.closeBody()
              return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))
             }
             u, err := req.URL.Parse(loc)
             if err != nil {
              resp.closeBody()
              return nil, uerr(fmt.Errorf("failed to parse Location header %q: %v", loc, err))
             }
             host := ""
             if req.Host != "" && req.Host != req.URL.Host {
              // If the caller specified a custom Host header and the
              // redirect location is relative, preserve the Host header
              // through the redirect. See issue #22233.
              if u, _ := url.Parse(loc); u != nil && !u.IsAbs() {
               host = req.Host
              }
             }
             ireq := reqs[0]
             req = &Request{
              Method:   redirectMethod,
              Response: resp,
              URL:      u,
              Header:   make(Header),
              Host:     host,
              Cancel:   ireq.Cancel,
              ctx:      ireq.ctx,
             }
             if includeBody && ireq.GetBody != nil {
              req.Body, err = ireq.GetBody()
              if err != nil {
               resp.closeBody()
               return nil, uerr(err)
              }
              req.ContentLength = ireq.ContentLength
             }

             // Copy original headers before setting the Referer,
             // in case the user set Referer on their first request.
             // If they really want to override, they can do it in
             // their CheckRedirect func.
             copyHeaders(req)

             // Add the Referer header from the most recent
             // request URL to the new one, if it's not https->http:
             if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
              req.Header.Set("Referer", ref)
             }
             err = c.checkRedirect(req, reqs)

             // Sentinel error to let users select the
             // previous response, without closing its
             // body. See Issue 10069.
             if err == ErrUseLastResponse {
              return resp, nil
             }

             // Close the previous response's body. But
             // read at least some of the body so if it's
             // small the underlying TCP connection will be
             // re-used. No need to check for errors: if it
             // fails, the Transport won't reuse it anyway.
             const maxBodySlurpSize = 2 << 10
             if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
              io.CopyN(io.Discard, resp.Body, maxBodySlurpSize)
             }
             resp.Body.Close()

             if err != nil {
              // Special case for Go 1 compatibility: return both the response
              // and an error if the CheckRedirect function failed.
              // See https://golang.org/issue/3795
              // The resp.Body has already been closed.
              ue := uerr(err)
              ue.(*url.Error).URL = loc
              return resp, ue
             }
            }

            reqs = append(reqs, req)
            var err error
            var didTimeout func() bool
            if resp, didTimeout, err = c.send(req, deadline); err != nil {
             // c.send() always closes req.Body
             reqBodyClosed = true
             if !deadline.IsZero() && didTimeout() {
              err = &httpError{
               // TODO: early in cycle: s/Client.Timeout exceeded/timeout or context cancellation/
               err:     err.Error() + " (Client.Timeout exceeded while awaiting headers)",
               timeout: true,
              }
             }
             return nil, uerr(err)
            }

             // 確認(rèn)重定向行為
            var shouldRedirect bool
            redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
            if !shouldRedirect {
             return resp, nil
            }

            req.closeBody()
           }

          可以進(jìn)一步看 redirectBehavior 函數(shù) https://docs.studygolang.com/src/net/http/client.go?s=20406:20458#L497:

          func redirectBehavior(reqMethod string, resp *Response, ireq *Request) (redirectMethod string, shouldRedirect, includeBody bool) {
           switch resp.StatusCode {
           case 301302303:
            redirectMethod = reqMethod
            shouldRedirect = true
            includeBody = false

            // RFC 2616 allowed automatic redirection only with GET and
            // HEAD requests. RFC 7231 lifts this restriction, but we still
            // restrict other methods to GET to maintain compatibility.
            // See Issue 18570.
            if reqMethod != "GET" && reqMethod != "HEAD" {
             redirectMethod = "GET"
            }
           case 307308:
            redirectMethod = reqMethod
            shouldRedirect = true
            includeBody = true

            // Treat 307 and 308 specially, since they're new in
            // Go 1.8, and they also require re-sending the request body.
            if resp.Header.Get("Location") == "" {
             // 308s have been observed in the wild being served
             // without Location headers. Since Go 1.7 and earlier
             // didn't follow these codes, just stop here instead
             // of returning an error.
             // See Issue 17773.
             shouldRedirect = false
             break
            }
            if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
             // We had a request body, and 307/308 require
             // re-sending it, but GetBody is not defined. So just
             // return this response to the user instead of an
             // error, like we did in Go 1.7 and earlier.
             shouldRedirect = false
            }
           }
           return redirectMethod, shouldRedirect, includeBody
          }

          很清晰了吧。

          03 總結(jié)

          很開(kāi)心,還是有讀者很認(rèn)真的在看我的文章,在跟著動(dòng)手實(shí)踐,還對(duì)其中的點(diǎn)提出質(zhì)疑。希望通過(guò)這篇文章,大家能夠?qū)?HTTP 協(xié)議有更深的認(rèn)識(shí),同時(shí)體會(huì)問(wèn)題排查的思路。

          有其他問(wèn)題,也歡迎留言交流!




          往期推薦


          我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語(yǔ)言并創(chuàng)建了 Go 語(yǔ)言中文網(wǎng)!著有《Go語(yǔ)言編程之旅》、開(kāi)源圖書(shū)《Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)》等。


          堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場(chǎng)心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長(zhǎng)!也歡迎加我微信好友交流:gopherstudio

          瀏覽 38
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日日碰狠狠添天天爽 | 久久精品影片 | 国产又爽又黄在线 | 18禁无码永久免费网站大全 | 黄色在线观看有限公司jb啊啊相当到位 |