Go net http超时应用场景全面详解

 更新时间:2024年01月04日 11:27:57   作者:Go语言教程  
HTTP是一个复杂的多阶段协议,因此没有一个一刀切的超时解决方案,在这篇文章中,我将分解您可能需要应用超时的各个阶段,并研究在服务器端和客户端上执行超时的不同方法

一、前言

在Go中编写HTTP服务器或客户端时,超时是最容易出错、最微妙的事情之一:有很多选择,错误在很长一段时间内都不会产生任何后果,直到网络出现故障,进程挂起。

二、超时时间

2.1 SetDeadline

首先,您需要了解Go为实现超时而公开的网络原语:Deadlines。

由net.Conn使用Set[Read|Write]Deadline(time.time)方法公开,Deadlines是一个绝对时间,当达到该时间时,所有I/O操作都会失败并出现超时错误。

Deadlines不是超时。一旦设置,它们将永远有效(或直到下一次调用SetDeadline),无论在此期间是否以及如何使用连接。因此,要使用SetDeadline构建超时,您必须在每次读/写操作之前调用它。

您可能不想自己调用SetDeadline,而是让net/http使用其更高级别的超时为您调用它。但是,请记住所有超时都是根据Deadlines实现的,因此它们不会在每次发送或接收数据时重置。

2.2 Server Timeouts

因此你想在互联网上公开Go的帖子发现更多关于服务器超时的信息,特别是关于HTTP/2和Go 1.7错误的信息:

对于暴露在Internet上的HTTP服务器来说,强制客户端连接超时是至关重要的。否则,速度非常慢或正在消失的客户端可能会泄漏文件描述符,并最终导致以下情况:

http: Accept error: accept tcp [::]:80: accept4: 
too many open files; retrying in 5ms

在http.Server中公开了两个超时:ReadTimeout和WriteTimeout。您可以通过显式使用服务器来设置它们:

srv := &http.Server{
    ReadTimeout: 5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
log.Println(srv.ListenAndServe())

ReadTimeout涵盖了从接受连接到完全读取请求正文的时间(如果您确实读取了正文,否则到标头末尾)。它是通过在Accept之后立即调用SetReadDeadline在net/http中实现的。

WriteTimeout通常通过在readRequest结束时调用SetWriteDeadline来覆盖从请求标头读取结束到响应写入结束的时间(也称为ServeHTTP的生存期)。

但是,当连接是HTTPS时,在Accept之后会立即调用SetWriteDeadline,以便它也覆盖作为TLS握手一部分写入的数据包。令人烦恼的是,这意味着(仅在这种情况下)WriteTimeout最终包括头读取和第一个字节等待。

当你处理不受信任的客户端和/或网络时,你应该设置这两个超时,这样客户端就不会因为写或读速度慢而中断连接。

最后是http.TimeoutHandler。它不是Server参数,而是一个限制ServeHTTP调用最长持续时间的Handler包装器。它的工作方式是缓冲响应,如果超过最后期限,则发送504网关超时。请注意,它在1.6中被提出,在1.6.2中。

三、http.ListenAndServe 做的是错误的

顺便说一句,这意味着绕过http.Server的包级便利功能,如http.ListenAndServe、http.Listen AndServeTLS和http.Serve,不适合公共Internet服务器。

这些函数将Tmeouts保留为默认的off值,无法启用它们,因此如果使用它们,很快就会出现连接泄漏和文件描述符用完的情况。我至少犯过六次这个错误。

相反,使用ReadTimeout和WriteTimeout创建一个http.Server实例,并使用其相应的方法,就像上面几段中的示例一样。

3.1 streaming

非常令人恼火的是,没有办法从ServeHTTP访问底层的net.Conn,因此打算流式传输响应的服务器被迫取消设置WriteTimeout(这也可能是它们默认为0的原因)。这是因为如果没有net.Conne访问,就无法在每次写入之前调用SetWriteDeadline来实现适当的空闲(而不是绝对)超时。

此外,无法取消被阻止的ResponseWriter.Write,因为没有记录ResponseWriter.Close(您可以通过接口升级访问)来取消阻止并发写入。因此,也没有办法用Timer手动构建超时。

可悲的是,这意味着流媒体服务器无法真正保护自己免受慢速阅读客户端的攻击。

3.2 Client Timeouts

客户端超时可能更简单,也可能更复杂,这取决于您使用的超时,但对于防止资源泄漏或陷入困境同样重要。

最容易使用的是http.Client的Timeout字段。它涵盖了整个交换,从Dial(如果不重用连接)到读取主体。

c := &http.Client{
    Timeout: 15 * time.Second,
}
resp, err := c.Get("https://blog.filippo.io/")

与上面的服务器端案例一样,包级别的函数(如http.Get请求客户端在没有超时的情况下使用,因此在开放式互联网上使用是危险的。

为了进行更精细的控制,您可以设置许多其他更具体的超时

  • net.Dialer.Timeout限制建立TCP连接所花费的时间(如果需要新的连接)。
  • http.Transport.TLS握手超时限制执行TLS握手所花费的时间
  • http.Transport.ResponseHeaderTimeout限制读取响应标头所花费的时间。
  • http.Transport.ExpectContinueTimeout限制客户端在发送包含Expect:100 continue的请求标头和接收发送正文的批准之间等待的时间。
c := &http.Client{
    Transport: &http.Transport{
        Dial: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).Dial,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
}

据我所知,没有办法具体限制发送请求所花费的时间。读取请求正文所花费的时间可以通过time.Timer手动控制,因为它发生在Client方法返回之后(有关如何取消请求,请参阅下文)。

最后,1.7中新增了http.Transport.IdleConnTimeout。它不控制客户端请求的阻塞阶段,而是控制空闲连接在连接池中保持的时间。

请注意,客户端默认情况下会遵循重定向。http.Client.Timeout包括重定向后花费的所有时间,而细粒度超时是针对每个请求的,因为http.Transport是一个没有重定向概念的较低级别系统。

3.3 Cancel and Context

nethttp提供了两种取消客户端请求的方法:request.cancel和1.7中新增的Context。

Request.Cancel是一个可选通道,当设置并关闭时,会导致请求中止,就像达到Request.Timeout一样。(它们实际上是通过相同的机制实现的,在写这篇文章时,我在1.7中发现了一个错误,所有取消都会作为超时错误返回。)

我们可以使用Request.Cancel和time.Timer来构建一个更精细的超时,允许流式传输,每次我们成功从Body读取一些数据时都会将截止日期向后推:

package main
import (
  "io"
  "io/ioutil"
  "log"
  "net/http"
  "time"
)
func main() {
  c := make(chan struct{})
  timer := time.AfterFunc(5*time.Second, func() {
    close(c)
  })
        // Serve 256 bytes every second.
  req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil)
  if err != nil {
    log.Fatal(err)
  }
  req.Cancel = c
  log.Println("Sending request...")
  resp, err := http.DefaultClient.Do(req)
  if err != nil {
    log.Fatal(err)
  }
  defer resp.Body.Close()
  log.Println("Reading body...")
  for {
    timer.Reset(2 * time.Second)
                // Try instead: timer.Reset(50 * time.Millisecond)
    _, err = io.CopyN(ioutil.Discard, resp.Body, 256)
    if err == io.EOF {
      break
    } else if err != nil {
      log.Fatal(err)
    }
  }
}

在上面的例子中,我们在请求的Do阶段设置了5秒的超时,但随后读取正文,每次超时2秒。可以永远这样流媒体,而不会有陷入困境的风险。如果在超过2秒的时间内没有收到正文数据,那么io.CopyN将返回net/http:requestcancelled。

在1.7中,上下文包升级为标准库。关于上下文有很多需要学习的地方,但就目的而言,您应该知道它们会取代和弃用Request.Cancel。

要使用Contexts取消请求,我们只需获取一个新的Context及其带有Context.WithCancel的cancel()函数,并使用request.WithContext创建一个绑定到它的request。当我们想取消请求时,我们通过调用cancel来取消Context:

ctx, cancel := context.WithCancel(context.TODO())
timer := time.AfterFunc(5*time.Second, func() {
  cancel()
})
req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil)
if err != nil {
  log.Fatal(err)
}
req = req.WithContext(ctx)

以上就是Go net http超时应用场景全面详解的详细内容,更多关于Go net http超时的资料请关注脚本之家其它相关文章!

相关文章

  • 使用go操作redis的有序集合(zset)

    使用go操作redis的有序集合(zset)

    这篇文章主要介绍了使用go操作redis的有序集合(zset),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • go语言中HTTPRouter的算法演进

    go语言中HTTPRouter的算法演进

    这篇文章主要从开发中常见的应用场景 “路由管理” 为例,为大家详细介绍一下三种常用的实现方案背后的数据结构和算法,感兴趣的小伙伴可以了解一下
    2023-08-08
  • 详解Go语言中数组,切片和映射的使用

    详解Go语言中数组,切片和映射的使用

    Arrays (数组), Slices (切片) 和 Maps (映射) 是常见的一类数据结构。这篇文章将为大家详细介绍一下Go语言中数组,切片和映射的使用,感兴趣的可以学习一下
    2022-07-07
  • golang开启mod后import报红的简单解决方案

    golang开启mod后import报红的简单解决方案

    这篇文章主要给大家介绍了关于golang开启mod后import报红的简单解决方案,文中通过图文将解决的办法介绍的非常详细,对大家的学习或者工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2024-01-01
  • go语言反射的基础教程示例

    go语言反射的基础教程示例

    这篇文章主要为大家介绍了go语言反射的基础教程,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • Golang中的占位符详解

    Golang中的占位符详解

    这篇文章主要给大家详细总结了Golang中的占位符用法,文章通过代码示例介绍的非常详细,对我们学习Golang占位符有一定的帮助,需要的朋友可以参考下
    2023-07-07
  • golang实现并发数控制的方法

    golang实现并发数控制的方法

    下面小编就为大家分享一篇golang实现并发数控制的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12
  • gin正确多次读取http request body内容实现详解

    gin正确多次读取http request body内容实现详解

    这篇文章主要为大家介绍了gin正确多次读取http request body内容实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • golang chan传递数据的性能开销详解

    golang chan传递数据的性能开销详解

    这篇文章主要为大家详细介绍了Golang中chan在接收和发送数据时因为“复制”而产生的开销,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下
    2024-01-01
  • go下载指定版本的依赖包图文详解

    go下载指定版本的依赖包图文详解

    由于依赖包的每个版本都有一个唯一的目录,所以在多项目场景中需要使用同一个依赖包的多版本时才不会产生冲突,下面这篇文章主要给大家介绍了关于go下载指定版本的依赖包的相关资料,需要的朋友可以参考下
    2023-04-04

最新评论