golang http

91 阅读2分钟

request 设置超时

gosamples.dev/http-client…

httpClient := http.Client{Timeout: 2 * time.Second}
if _, err := httpClient.Get("http://localhost:8090/timeout"); err != nil {
   log.Fatal(err)
}

重试

github.com/avast/retry…

httpClient.Get 流程

  • Client.do 调用 c.send(req, deadline)

  • send 调用 setRequestCancel, resp, err = rt.RoundTrip(req)

    • setRequestCancel里面 req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
  • Transport.roundTrip 调用 pconn.roundTrip

...
ctx := req.Context()
...
for {
   select {
   case <-ctx.Done():
      req.closeBody()
      return nil, ctx.Err()
   default:
   }
...

// Get the cached or newly-created connection
pconn, err := t.getConn(treq, cm)
...
resp, err = pconn.roundTrip(treq)
  • Transport.getConn 调用 queueForIdleConn (Look for most recently-used idle connection.), 没有获取到再 queueForDial -> go t.dialConnFor(w)

  • Transport.dialConn

pconn = &persistConn{
   t:             t,
   cacheKey:      cm.key(),
   reqch:         make(chan requestAndChan, 1),
   writech:       make(chan writeRequest, 1),
   closech:       make(chan struct{}),
   writeErrCh:    make(chan error, 1),
   writeLoopDone: make(chan struct{}),
}

go pconn.readLoop()
go pconn.writeLoop()
return pconn, nil
  • persistConn.roundTrip
writeErrCh := make(chan error, 1)
pc.writech <- writeRequest{req, writeErrCh, continueCh}

resc := make(chan responseAndError)
pc.reqch <- requestAndChan{
   req:        req.Request,
   cancelKey:  req.cancelKey,
   ch:         resc,
   addedGzip:  requestedGzip,
   continueCh: continueCh,
   callerGone: gone,
}


... // 省略其他代码
for {
   ...// 省略其他代码
   select {

    case re := <-resc:

    return re.res, nil
    }
    
}
  • persistConn.readLoop
rc := <-pc.reqch  // 获取 reqch

resp, err = pc.readResponse(rc, trace)// readResponse


waitForBodyRead := make(chan bool, 2)

body := &bodyEOFSignal{ //封装body
   body: resp.Body,
   earlyCloseFn: func() error {
      waitForBodyRead <- false
      <-eofc // will be closed by deferred call at the end of the function
      return nil

   },
   fn: func(err error) error {
      isEOF := err == io.EOF
      waitForBodyRead <- isEOF
      if isEOF {
         <-eofc // see comment above eofc declaration
      } else if err != nil {
         if cerr := pc.canceled(); cerr != nil {
            return cerr
         }
      }
      return err
   },
}

resp.Body = body // 设置 resp.Body


select {
case rc.ch <- responseAndError{res: resp}://把resp写到rc.ch里面
case <-rc.callerGone:
   return
}

Transport, DefaultTransport

// Transport is an implementation of [RoundTripper] that supports HTTP, // HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT). // // By default, Transport caches connections for future re-use. // This may leave many open connections when accessing many hosts. // This behavior can be managed using [Transport.CloseIdleConnections] method // and the [Transport.MaxIdleConnsPerHost] and [Transport.DisableKeepAlives] fields. //

type Transport struct {
    ...
    idleConn     map[connectMethodKey][]*persistConn // most recently used at end
    ...
}
type connectMethodKey struct {
   proxy, scheme, addr string
   onlyH1              bool
}
var DefaultTransport RoundTripper = &Transport{
   Proxy: ProxyFromEnvironment,
   DialContext: defaultTransportDialContext(&net.Dialer{
      Timeout:   30 * time.Second,
      KeepAlive: 30 * time.Second,
   }),
   ForceAttemptHTTP2:     true,
   MaxIdleConns:          100,
   IdleConnTimeout:       90 * time.Second,
   TLSHandshakeTimeout:   10 * time.Second,
   ExpectContinueTimeout: 1 * time.Second,
}

resp.Body.Close()

It is the caller's responsibility to // close Body. The default HTTP client's Transport may not // reuse HTTP/1.x "keep-alive" TCP connections if the Body is // not read to completion and closed.

在readLoop中,只有bodyEOF为true才会回收conn

waitForBodyRead 只有在 bodyEOFSignal.earlyCloseFn, bodyEOFSignal.fn 的时候才会写入,在bodyEOFSignal.Close 和 在bodyEOFSignal.Read 到eof的时候才会调用

select {
case bodyEOF := <-waitForBodyRead:
   replaced := pc.t.replaceReqCanceler(rc.cancelKey, nil) // before pc might return to idle pool
   alive = alive &&
      bodyEOF &&
      !pc.sawEOF &&
      pc.wroteRequest() &&
      replaced && tryPutIdleConn(trace)
   if bodyEOF {
      eofc <- struct{}{}
   }

不执行resp.Body.Close() 时 goroutine 泄露问题

func main() {
   num := 6
   for index := 0; index < num; index++ {
      _, _ = http.Get("https://www.baidu.com") //只get时有 13 个  goroutine
      //log.Println(reflect.TypeOf(resp.Body))       // *http.gzipReader
      //_, _ = io.ReadAll(resp.Body) // 加上ReadAll 或者 close 就只有3了
      //resp.Body.Close()
   }
   
   fmt.Printf("此时goroutine个数= %d\n", runtime.NumGoroutine())
}

http.Get 默认使用 DefaultTransport 管理连接。

It establishes network connections as needed and caches them for reuse by subsequent calls.

go pconn.readLoop()  // 启动一个读goroutine
go pconn.writeLoop() // 启动一个写goroutine

一次建立连接,就会启动一个读goroutine写goroutine。这就是为什么一次http.Get()会泄漏两个goroutine的来源。