本文介绍在Golang网络编程中,为HTTP请求或者TCP请求设置超时。这篇文章的内容来自PPT,所以文字不多,都是图片或者代码。
Socker编程:如下图所示
go socket编程HTTP编程:
Client示例如下:
transport := &http.Transport{
Dial: (&net.Dialer{
Timeout: 10 * time.Second,
}).Dial,
MaxIdleConns: 2,
}
client := &http.Client{
Timeout: time.Second * 30,
Transport: transport,
}
resp, _:= client.Get("http://localhost:8888/hello")
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
Server示例如下
http.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request){
fmt.Printf("receive msg form: %v", req.RemoteAddr)
_, _ := io.WriteString(w, "hello world!\n")
})
server := &http.Server{Addr: ":8888"}
_ := server.ListenAndServe()
2. 超时设置
首先介绍HTTP超时设置,Server端,主要关注四个参数:ReadTimeout、ReadHeaderTimeout、WriteTimeout、TimeoutHandler,代码示例如下
type EchoHandler struct {}
func (handler *EchoHandler)ServeHTTP(w http.ResponseWriter, req *http.Request) {
n, _ := io.WriteString(w, "echo !\n")
}
func main() {
timeoutHandler := http.TimeoutHandler(&EchoHandler{}, 5 * time.Second, "echo timed out!")
http.Handle("/echo", timeoutHandler)
listener, _ := net.Listen("tcp", ":8888")
server := &http.Server{
ReadTimeout: time.Second * 5,
WriteTimeout: time.Second * 10,
ReadHeaderTimeout: time.Second * 3,
}
_ := server.Serve(listener)
}
下面一张图形象地显示了HTTP在服务端的参数设置,来源:the complete guide to golang net http timeouts。
HTTP服务端超时设置我们举一个TimeoutHandler参数的例子,这个参数是指从读取request body到返回Response的时间(即处理请求的时间),我们设置超时时间为5秒,并在函数处理时Sleep 1分钟,抓包观察连接变化。代码如下:
func (handler *EchoHandler)ServeHTTP(w http.ResponseWriter, req *http.Request) {
time.Sleep(time.Second * 60)
n, _ := io.WriteString(w, "echo ! \n")
}
timeoutHandler := http.TimeoutHandler(&EchoHandler{}, 5 * time.Second, "echo timed out!")
使用tcpdump结合wireshark抓包分析如下:
TimeoutHandler超时可见,在读取Request body 5秒之后,(第18秒到23秒),服务端向客户端发送了statuscode为503(服务不可用)的报文,客户端收到的信息如下:
err: nil
statuscode: 503
body: "echo timed out!"
HTTP client端设置超时代码片段如下:
transport := &http.Transport{
Dial: (&net.Dialer{
Timeout: 10 * time.Second,
}).Dial,
IdleConnTimeout: 10 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
}
client := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
下面图片显示了各个参数起作用的时间。
HTTP client设置我们看一下Dialer.Timeout,这个参数限制TCP建立连接的时间,在TCP建立连接时,主动发起请求的一方发送syn报文,如果syn报文没有得到回应,就会对这个syn报文进行重传。在linux中,会对syn报文进行6次重传,第i次重传之后等待2^(i-1)秒,共计等待127秒作用(这个只是一个估计值,不是绝对的)。linux与此相关的两个参数为:
net.ipv4.tcp_syn_retries 默认为6
net.ipv4.tcp_synack_retries 默认为5,即对synack报文进行5次重传
下面几个是对连接进行抓包的示例,分别对不同情况下的报文进行抓包分析。
示例一:一次正常的HTTP请求流程
一次正常的HTTP请求过程从报文中可以看到,首先是TCP三次握手的过程 ,然后主动发起连接的一方发送了一个create volume的请求,server对此请求进行了回应(发送了一个ack),此后的时间,链路进入idle状态(双方都没有等待ACK),在这段时间内,服务器在处理请求,请求处理完成之后,向客户端发送数据
示例二:Server在处理请求过程中宕机。
如果未为客户端设置超时或者KeepAlive, 如果服务端在处理请求的时候宕机(此时链路处于idle状态),那么这个连接在客户端将一直保持,如果不进行处理这个无效的连接将一直占用文件描述符(可能会导致无法建立新连接或者打开文件),用netstat命令查看连接状态如下,
连接一直保持示例三:Server未宕机,Client处理请求超时
这种情况下,为客户端设置了超时时间(http.Client.Timeout=600s),那么客户端在超时时间到达时发送fin报文给服务端,服务端对这个fin报文进行回应,但是因为服务端还没处理完,服务端并不会发送fin报文,此时客户端直接从fin_wait2状态到close状态。linux中控制fin_wait2时间的变量为net.ipv4.tcp_fin_time,默认为60秒,抓包分析如下:
Client超时示例四,Server宕机、Client超时
在这种情况下,client会发送fin报文给server,跟示例三一样,但是server并不会对这个fin进行回应,因为它宕机了,这个时候client会对这个fin报文进行重传,重传到一定次数就断开连接,linux控制重传的参数为: tcp_retries1, tcp_retries2,默认情况下,普通报文重传16左右(整个过程持续14分钟)fin报文的重传次数我不确定,持续时间差不多。重传等待时间是上次等待时间的两倍。
fin报文重传TCP超时设置
TCP超时设置比较简单,主要是三个参数,这三个方法每次读写前都需要重置一下,因为是设置绝对时间。
Conn.SetDeadline(t time.Time)
Conn.SetReadDeadline(t time.Time)
Conn.SetWriteDeadline(t time.Time)
示例代码如下,参考 Tcp timeout example
bufReader := bufio.NewReader(conn)
timeoutDuration := 5 * time.Second
for {
// Set a deadline for reading. Read operation will fail if no data is received after deadline.
conn.SetReadDeadline(time.Now().Add(timeoutDuration))
bytes, err := bufReader.ReadBytes('\n')
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%s", bytes)
}
3. KeepAlive介绍
首先介绍TCP keepAlive,TCP的keepAlive作用如下
KeepAlive在linux中有三个参数,分别表示开始发送探测报文之前的等待时间,发送探测报文的间隔,以及发送探测报文的此时。
net.ipv4.tcp_keepalive_time
net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_probes
查看修改配置方法的方法为:
查看 sysctl -a | grep tcp_keepalive
修改配置文件/etc/sysctl.conf,执行sysctl -p生效
关于TCP keepAlive可以参考TCP Keepalive HOWTO,下面是TCP keepalive的示例
Golang TCP KeepAlive的设置很简单,在Socket情况下,仅需要设置两个参数:
TCPConn.SetKeepAlive(true)
TCPConn.SetKeepAlivePeriod(3 * time.Minute)
在HTTP情况下设置分为Client和Server,Client设置如下:
netTransport := &http.Transport{
Dial: (&net.Dialer{
KeepAlive: 3 * time.Minute,
}).Dial,
}
Server设置如下:
Server.ListenAndServe() // enable
Server.ListenAndServeTLS() // enable
Server.Serve() // disable
Golang的实现为,probes使用linux系统的默认值,intvl的值与time值相等,为参数设置的值,关于golang tcp keepalive可以参考Using TCP keepalive with Go
再介绍HTTP KeepAlive,HTTP keepAlive是一种连接复用技术,意在减少连接建立和关闭所消耗的时间。
Golang HTTP keepalive设置如下,client:
http.Transport{
DisableKeepAlives: false,
MaxIdleConnsPerHost: 2, // Per proxy, scheme, addr
MaxIdleConns: 2,
IdleConnTimeout: 10 * time.Second,
}
Server:
Server.SetKeepAlivesEnabled(true) // Server disable + Client enable = disable
4. 问题调试方法
netstat 命令
netstat no | grep 8808
-o: 查看状态与计时器
tcpdump 命令
tcpdump -i enp0s31f6 host 172.16.3.113 -w tcpdump.cap
wireshark 软件
使用过滤条件
tcp.port == 8808
ip.addr == 172.16.3.11