一、net/http的httpclient发起http请求

方法

  1. 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))
}
  1. post请求
    • 方法一:http.Post方法
func httpPost() {
    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))
}
"application/x-www-form-urlencoded"
  • 方法二:http.PostForm方法
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))
}
  1. 复杂的请求:若需要设置请求头参数,cookie之类的数据,就使用http.Do方法
func httpDo() {
    client := &http.Client{}
    req, err := http.NewRequest("POST", "http://www.01happy.com/demo/accept.php", strings.NewReader("name=cjb"))
    if err != nil {
        // handle error
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Set("Cookie", "name=anny")
    resp, err := client.Do(req)
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        // handle error
    }
    fmt.Println(string(body))
}
  1. Head请求:Head方法,只返回页面的首部
  2. 注意:
    • 要调用resp.Body.Close()关闭response.body。如果resp.body没有关闭,则Client底层RoundTripper将无法重用存在的TCP连接去服务接下来的请求

第二步:Do/Get/Post方法的实现(以Do为例)

  1. 处理请求,添加referer、method字段
  2. 调用send方法,向request添加cookie
  3. 检查http头是否合法,若合法调用transport的RoundTrip方法

第三步:精髓:调用transport的RoundTrip方法

++transport.go:++

struct:

type Transport struct {
    idleMu     sync.Mutex
    wantIdle   bool // user has requested to close all idle conns
    idleConn   map[connectMethodKey][]*persistConn
    idleConnCh map[connectMethodKey]chan *persistConn

    reqMu       sync.Mutex
    reqCanceler map[*Request]func()

    altMu    sync.RWMutex
    altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper
    //Dial获取一个tcp 连接,也就是net.Conn结构,你就记住可以往里面写request
    //然后从里面搞到response就行了
    Dial func(network, addr string) (net.Conn, error)
}
  1. 两个map:
    • idleConn:保存从 connectMethodKey (代表着不同的协议 不同的host,也就是不同的请求)到 persistConn 的映射
    • idleConnCh:用来在并发http请求的时候在多个 goroutine 里面相互发送持久连接,也就是说, 这些持久连接是可以重复利用的, 你的http请求用某个persistConn用完了,通过这个channel发送给其他http请求使用这个persistConn

==连接池:==

连接池

RoundTrip方法:

func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
    ...
    pconn, err := t.getConn(req, cm)
    if err != nil {
        t.setReqCanceler(req, nil)
        req.closeBody()
        return nil, err
    }

    return pconn.roundTrip(treq)
}

省略前面对参数的检查部分,主要有两步:

pconn, err := t.getConn(req, cm)
func (t *Transport) getConn(req *Request, cm connectMethod) (*persistConn, error) {
    ...

    type dialRes struct {
        pc  *persistConn
        err error
    }
    dialc := make(chan dialRes)
    //定义了一个发送 persistConn的channel

    ...

    // 启动了一个goroutine, 这个goroutine 获取里面调用dialConn搞到
    // persistConn, 然后发送到上面建立的channel  dialc里面,    
    go func() {
        pc, err := t.dialConn(cm)
        dialc <- dialRes{pc, err}
    }()

    idleConnCh := t.getIdleConnCh(cm)
    select {
    case v := <-dialc:
        // dialc 我们的 dial 方法先搞到通过 dialc通道发过来了
        return v.pc, v.err
    case pc := <-idleConnCh:
        // 这里代表其他的http请求用完了归还的persistConn通过idleConnCh这个    
        // channel发送来的
        handlePendingDial()
        return pc, nil
    case <-req.Cancel:
        handlePendingDial()
        return nil, errors.New("net/http: request canceled while waiting for connection")
    case <-cancelc:
        handlePendingDial()
        return nil, errors.New("net/http: request canceled while waiting for connection")
    }
}
  1. 定义一个发送 persistConn的channel dialc
  2. 启动了一个goroutine, 这个goroutine 获取里面调用dialConn搞到persistConn, 然后发送到dialc里面
  3. 主协程goroutine在 select里面监听多个channel,看看哪个通道里面先发过来 persistConn,就用哪个,然后return

第二步:调用这个持久连接persistConn 这个struct的roundTrip方法

image
三个goroutine通过channel互相协作的过程,
1. 主goroutine ->requestAndChan -> 读循环goroutine:读循环goroutine 通过channel requestAndChan 接受主goroutine发送的request(rc := <-pc.reqch), 并从tcp输出流中读取response, 然后反序列化到结构体中, 最后通过channel 返给主goroutine (rc.ch <- responseAndError{resp, err} )
2. 主goroutine ->writeRequest-> 写循环goroutine:select channel中主gouroutine的request,然后写入tcp输入流,如果出错了,channel 通知调用者
3. 主goroutine 通过select 监听各个channel上的数据, 比如请求取消, timeout,长连接挂了,写流出错,读流出错, 都是其他goroutine 发送过来的, 跟中断一样,然后相应处理

image

二、使用net/http的参数设置:
  1. 粗粒度:
    使用http.Client的 Timeout字段。
    它的时间计算包括从连接(Dial)到读完response body。
  2. 细粒度:细粒度只对于单次连接起作用

    • net.Dialer.Timeout 限制建立TCP连接的时间
    • http.Transport.TLSHandshakeTimeout 限制 TLS握手的时间
    • http.Transport.ResponseHeaderTimeout 限制读取response header的时间
    • http.Transport.ExpectContinueTimeout 限制client在发送包含 Expect: 100-continue的header到收到继续发送body的response之间的时间等待。
    • http.Transport.IdleConnTimeout,控制连接池中一个连接可以idle多长时间。
  3. http.Client的默认超时时限是0,不超时,可以设置。

  4. 实际上是一个连接池,全局复用。初始化Transport,然后复用

    • 参数:
    • DisableKeepAlives默认为false(长连接)
    • MaxIdleConns连接池对所有host的最大连接数量,默认无穷大
    • MaxIdleConnsPerHHost连接池对每个host的最大连接数量。(一个host的情况下最好与上面保持一致)
    • IdleConnTimeout空闲时间设置
    • 函数:
    • DialContext用于创建(http)连接。