理解 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的请求。会则开启能够提高访问性能。