go Http客户端

理解 timeout

timeout 又可以细分为 connect timeout、read timeout、write timeout。而 read timeout 和 write timeout 必须是在 connect 之后才能发生。

  • ReadTimeout的时间计算是从连接被接受(accept)到request body完全被读取(如果你不读取body,那么时间截止到读完header为止)
  • WriteTimeout的时间计算正常是从request header的读取结束开始,到response write结束为止 (也就是ServeHTTP方法的生命周期)

http vs fasthttp

  • fasthttp 使用 tcp 做长连接,使用连接池复用连接
  • net/http 是短连接,用完即断开连接

net/http

// net/http client 示例
const DefaultTimeout = 30 * time.Second // 请求超时时间

var HttpClient = CreateHttpClient() // http client

func CreateHttpClient() *http.Client {
    transport := &http.Transport{
        TLSHandshakeTimeout: 10 * time.Second,
        MaxIdleConnsPerHost: 300,
        MaxConnsPerHost:     500,
        IdleConnTimeout:     time.Minute, // 空闲链接时间:控制连接池中一个连接可以idle多长时间
        WriteBufferSize:     1024 * 1024,
        ReadBufferSize:      1024 * 1024 * 10,
    }
    client := &http.Client{
        Timeout:   DefaultTimeout,
        Transport: transport,
    }
    return client
}

func (c *client) GetHttp(dto *RequestForm) error {
    paramsStr, err := helper.HttpBuildQuery(dto.Params)
    if err != nil {
        return err
    }

    req, err := http.NewRequest(http.MethodGet, "https://xxx.com" + dto.Uri + "?" + paramsStr, nil)
    if err != nil {
        return err
    }

    if dto.Header != nil {
        for k, v := range dto.Header {
            req.Header.Set(k, strings.Join(v, ","))
        }
    }

    resp ,err := HttpClient.Do(req)
    if err != nil {
        return err
    }

    defer func() {
        _ = resp.Body.Close()
    }()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }

    dto.RespBody = string(body)

    if resp.StatusCode != http.StatusOK {
        return errors.New(gjson.Get(dto.RespBody, "message").String())
    }

    if dto.Resp != nil {
        helper.JsonUnmarshal(dto.RespBody, dto.Resp)
    }

    return nil
}

fasthttp

github.com/valyala/fasthttp

模拟 http 客户端请求[参考文章 https://juejin.im/post/6844903761832476686]

fasthttp 不会将请求 header 值存储为导出的 map[string]string,而是存储为未存储的 []byte (将索引存储到其中)

var req fasthttp.Request

// 获取请求头选项
v := string(req.Request.Header.Peek("User-Agent"))

// 打印请求头
fmt.Println(string(req.Header.Header()))

服务器端超时设置

参考文章

http.Server有两个设置超时的方法: ReadTimeout 和 andWriteTimeout`。你可以显示地设置它们:

  • ReadTimeout的时间计算是从连接被接受(accept)到request body完全被读取(如果你不读取body,那么时间截止到读完header为止)。
  • WriteTimeout的时间计算正常是从request header的读取结束开始,到 response write结束为止 (也就是 ServeHTTP 方法的声明周期)。

可能报错

  • timeout
  • dialing to the given TCP address timed out【解决方案:请求服务器负载过高导致,提升请求服务器的配置或者降低并发数】
  • the server closed connection before returning the first response byte. Make sure the server returns ‘Connection: close’ response header before closing the connection【解决方案:调小 MaxIdleConnDuration 参数,避免请求服务的 keep-alive 过短主动关闭】
  • no free connections available to host【解决方案:调高参数 MaxConnsPerHost(每个 host 最大的连接数,默认512)】
    // fasthttp client 示例
    type RequestForm struct {
        Uri      string                 // 请求uri
        Params   map[string]interface{} // 请求参数
        Header   url.Values             // 请求头
        RespBody string                 // 响应结果
        Resp     interface{}            // 响应结果结构化
    }
    
    type client struct{}
    
    type IClient interface {
        Get(dto *RequestForm) error
        Post(dto *RequestForm) error
    }
    
    const ttl = 5 * time.Second
    
    var (
        defaultDialer = &fasthttp.TCPDialer{Concurrency: 200} // tcp 并发200
    
        FastClient = CreateFastHttpClient()
    
        Client IClient = new(client)
    )
    
    func CreateFastHttpClient() fasthttp.Client {
        return fasthttp.Client{
            MaxConnsPerHost:     300,
            MaxIdleConnDuration: 10 * time.Second, // 空闲链接时间应短,避免请求服务的 keep-alive 过短主动关闭,默认10秒
            MaxConnDuration:     10 * time.Minute,
            ReadTimeout:         30 * time.Second,
            WriteTimeout:        30 * time.Second,
            MaxResponseBodySize: 1024 * 1024 * 10,
            MaxConnWaitTimeout:  time.Minute,
            Dial: func(addr string) (net.Conn, error) {
                idx := 3 // 重试三次
                for {
                    idx--
                    conn, err := defaultDialer.DialTimeout(addr, 10*time.Second) // tcp连接超时时间10s
                    if err != fasthttp.ErrDialTimeout || idx == 0 {
                        return conn, err
                    }
                }
            },
        }
    }
    
    func (*client) createSign(dto *RequestForm, ts string) (string, error) {
        paramsStr, err := helper.HttpBuildQuery(dto.Params)
        if err != nil {
            return "", err
        }
        paramsStr = helper.Md5(paramsStr)
        sign := helper.Md5(config.OAPrefix + config.OAKey + config.OASecret + dto.Uri + ts + paramsStr)
        return sign, nil
    }
    
    func (c *client) Get(dto *RequestForm) error {
        req := fasthttp.AcquireRequest()
        resp := fasthttp.AcquireResponse()
        defer func() {
            fasthttp.ReleaseResponse(resp)
            fasthttp.ReleaseRequest(req)
        }()
    
        ts := strconv.FormatInt(time.Now().Unix(), 10)
        sign, err := c.createSign(dto, ts)
        if err != nil {
            return err
        }
    
        dto.Params["sign"] = sign
        dto.Params["api"] = dto.Uri
        dto.Params["key"] = config.OAKey
        dto.Params["ts"] = ts
        paramsStr, err := helper.HttpBuildQuery(dto.Params)
        if err != nil {
            return err
        }
    
        req.SetRequestURI("https://xxx.com" + "?" + paramsStr)
        req.Header.SetMethod(http.MethodGet)
    
        if dto.Header != nil {
            for k, v := range dto.Header {
                req.Header.Set(k, strings.Join(v, ","))
            }
        }
    
        if err := FastClient.DoTimeout(req, resp, ttl); err != nil {
            return err
        }
    
        dto.RespBody = string(resp.Body())
    
        if resp.StatusCode() != fasthttp.StatusOK || !gjson.Get(dto.RespBody, "success").Bool() {
            return errors.New(gjson.Get(dto.RespBody, "msg").String())
        }
    
        if dto.Resp != nil {
            helper.JsonUnmarshal(dto.RespBody, dto.Resp)
        }
    
        return nil
    }
    
    func (c *client) Post(dto *RequestForm) error {
        req := fasthttp.AcquireRequest()
        resp := fasthttp.AcquireResponse()
        defer func() {
            fasthttp.ReleaseResponse(resp)
            fasthttp.ReleaseRequest(req)
        }()
    
        ts := strconv.FormatInt(time.Now().Unix(), 10)
        sign, err := c.createSign(dto, ts)
        if err != nil {
            return err
        }
    
        dto.Params["sign"] = sign
        dto.Params["api"] = dto.Uri
        dto.Params["key"] = config.OAKey
        dto.Params["ts"] = ts
    
        // application/json 编码方式
        req.SetBody(helper.JsonMarshalByte(dto.Params))
        req.Header.SetContentType("application/json")
    
        // application/x-www-form-urlencoded 编码方式
        // paramsStr, err := helper.HttpBuildQuery(dto.Params)
        // if err != nil {
        //     return err
        // }
        // req.SetBody([]byte(paramsStr))
        // req.Header.SetContentType("application/x-www-form-urlencoded")
    
        req.SetRequestURI("http://xxx.com" + dto.Uri)
        req.Header.SetMethod(http.MethodPost)
    
        if dto.Header != nil {
            for k, v := range dto.Header {
                req.Header.Set(k, strings.Join(v, ","))
            }
        }
    
        if err := FastClient.DoTimeout(req, resp, ttl); err != nil {
            return err
        }
    
        dto.RespBody = string(resp.Body())
    
        if resp.StatusCode() != fasthttp.StatusOK || !gjson.Get(dto.RespBody, "success").Bool() {
            return errors.New(gjson.Get(dto.RespBody, "msg").String())
        }
    
        if dto.Resp != nil {
            helper.JsonUnmarshal(dto.RespBody, dto.Resp)
        }
    
        return nil
    }
    
    // 上传文件到服务端,link: 文件链接,objResp:结果结构体
    func UploadFile(link string, objResp interface{}) error {
        // 下载文件
        _, stream, err := FastClient.Get(nil, link)
        if err != nil {
            return err
        }
    
        urlInfo, err := url.Parse(link)
        if err != nil {
            return err
        }
    
        filename := fmt.Sprintf("%s%s", uuid.New().String(), path.Ext(urlInfo.Path))
    
        httpReq := fasthttp.AcquireRequest()
        defer fasthttp.ReleaseRequest(httpReq)
    
        httpResp := fasthttp.AcquireResponse()
        defer fasthttp.ReleaseResponse(httpResp)
    
        //新建一个缓冲,用于存放文件内容
        bodyBufer := &bytes.Buffer{}
        //创建一个multipart文件写入器,方便按照http规定格式写入内容
        bodyWriter := multipart.NewWriter(bodyBufer)
        //从bodyWriter生成fileWriter,并将文件内容写入fileWriter,多个文件可进行多次
        fileWriter,err := bodyWriter.CreateFormFile("media", filename)
        if err != nil{
            fmt.Println(err.Error())
            return err
        }
    
        _, err = io.Copy(fileWriter, bytes.NewReader(stream))
        if err != nil{
            return err
        }
    
        // 停止写入
        _ = bodyWriter.Close()
    
        httpReq.SetRequestURI("https://qyapi.weixin.qq.com/cgi-bin/media/upload")
        httpReq.Header.SetContentType(bodyWriter.FormDataContentType())
        httpReq.SetBody(bodyBufer.Bytes())
        httpReq.Header.SetMethod(http.MethodPost)
    
        if err := FastClient.DoTimeout(httpReq, httpResp, HttpTTL); err != nil {
            return err
        }
    
        return json.Unmarshal(httpResp.Body(), &objResp)
    }

什么是Keep-Alive模式

我们知道HTTP协议采用“请求-应答”模式,当使用普通模式,即非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成之后立即断开连接(HTTP协议为无连接的协议);当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。

http 1.0中默认是关闭的,需要在http头加入"Connection: keep-alive",才能启用Keep-Alive;http 1.1中默认启用Keep-Alive,如果加入"Connection: close “,才关闭。目前大部分浏览器都是用http1.1协议,也就是说默认都会发起Keep-Alive的连接请求了,所以是否能完成一个完整的Keep-Alive连接就看服务器设置情况。

决定着我们是不是要开启 KeepAlive 的因素是一个页面请求中是否会向会向服务器发出多个HTTP的请求。会则开启能够提高访问性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值