HTTP is the basic protocol of the Internet today, carrying the majority of the application layer traffic on the Internet, and from the current trend, in the next 10 years, http will remain the main protocol for Internet applications. Based on the Go standard library we can easily set up an http server to handle client-side http requests, or create an http client to send http requests to the server.
Initially, the early http 1.0 protocol only supported short connections, i.e. for each request sent by the client, a new TCP connection was established with the server, and the connection would be removed after the request was processed. Obviously each tcp connection handshake and dismantling will bring a large loss, in order to make full use of the established connection, later http 1.0 updates and http 1.1 support the addition of Connection: keep-alive in the http request header to tell the other party not to close the link after the request response is complete, and to reuse the connection next time to continue to transmit subsequent requests and responses. The post-HTTP protocol specification specifies that HTTP/1.0 versions need to add Connection: keep-alive to the request header if they want to maintain a long connection, while HTTP/1.1 versions will support keep-alive long connections as the default option, with or without this request header.
In this article we’ll take a look at the http.Server
and http.Client
of the net/http
package in the Go standard library’s handling of keep-alive long connections and how to turn off the keep-alive mechanism on the Server and Client side.
1. http packages enable keep-alive by default
As per the HTTP/1.1 specification, the http server and client implementations of the Go http package treat all connections as long connections by default, regardless of whether the initial requests on those connections have Connection: keep-alive.
The following is a http client and a http server implementation, respectively, using the default mechanism of the go http package.
The http client implementation with keep-alive enabled by default.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
//github.com/bigwhite/experiments/http-keep-alive/client-keepalive-on/client.go
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
c := &http.Client{}
req, err := http.NewRequest("Get", "http://localhost:8080", nil)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", *req)
for i := 0; i < 5; i++ {
resp, err := c.Do(req)
if err != nil {
fmt.Println("http get error:", err)
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("read body error:", err)
return
}
fmt.Println("response body:", string(b))
}
}
|
The http server implementation with keep-alive enabled by default.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
//github.com/bigwhite/experiments/http-keep-alive/server-keepalive-on/server.go
package main
import (
"fmt"
"net/http"
)
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Println("receive a request from:", r.RemoteAddr, r.Header)
w.Write([]byte("ok"))
}
func main() {
var s = http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(Index),
}
s.ListenAndServe()
}
|
Now we start the http server above:
1
2
|
// server-keepalive-on目录下
$go run server.go
|
We use the above client to make 5 http requests to the server.
1
2
3
4
5
6
7
8
|
// client-keepalive-on目录下
$go run client.go
http.Request{Method:"Get", URL:(*url.URL)(0xc00016a000), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{}, Body:io.ReadCloser(nil), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:"localhost:8080", Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:"", RequestURI:"", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.emptyCtx)(0xc00012c008)}
response body: ok
response body: ok
response body: ok
response body: ok
response body: ok
|
The log output from the server side during this period is as follows.
1
2
3
4
5
|
receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
|
Let’s briefly analyze the output from both ends.
- From the header fields of the requests printed on the server side, the request header from the client side does not explicitly contain Connection: keep-alive , but only two header fields, Accept-Encoding and User-Agent.
- The five requests processed on the server side all came from the same connection “[::1]:55238”, and the server side kept the connection by default instead of closing it after processing a request, indicating that both sides reused the http connection created by the first request.
Even if our client side sends a request every 5 seconds, the server side does not close the connection by default (we buffer log packets with fmt packets and output a timestamped log).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// client-keepalive-on目录下
$go run client-with-delay.go
http.Request{Method:"Get", URL:(*url.URL)(0xc00016a000), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{}, Body:io.ReadCloser(nil), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:"localhost:8080", Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:"", RequestURI:"", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.emptyCtx)(0xc00012c008)}
2021/01/03 12:25:21 response body: ok
2021/01/03 12:25:26 response body: ok
2021/01/03 12:25:31 response body: ok
2021/01/03 12:25:36 response body: ok
2021/01/03 12:25:41 response body: ok
// server-keepalive-on目录下
$go run server.go
2021/01/03 12:25:21 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 12:25:26 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 12:25:31 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 12:25:36 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 12:25:41 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
|
2. http client side sends requests based on non-keep-alive connections
Sometimes the http client does not have a high density of data requests on a connection, so the client does not want to keep the connection for a long time (taking up port resources), so how can the client coordinate with the Server to close the connection after the request returns an answer? Let’s see how to implement this scenario in Go.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
//github.com/bigwhite/experiments/http-keep-alive/client-keepalive-off/client.go
... ...
func main() {
tr := &http.Transport{
DisableKeepAlives: true,
}
c := &http.Client{
Transport: tr,
}
req, err := http.NewRequest("Get", "http://localhost:8080", nil)
if err != nil {
panic(err)
}
for i := 0; i < 5; i++ {
resp, err := c.Do(req)
if err != nil {
fmt.Println("http get error:", err)
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("read body error:", err)
return
}
log.Println("response body:", string(b))
time.Sleep(5 * time.Second)
}
}
|
The underlying data connection establishment and maintenance of http.Client is implemented by http.Transport. http.Transport structure has a DisableKeepAlives field with a default value of false, which starts keep-alive. alive, and then assign this Transport instance to the Transport field of the http Client instance as the initial value.
Next, we use this client to send five requests to the http server above, with an interval of 5 seconds between requests (to simulate an idle connection), and we get the following results (observed by printing information from the server side).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 在client-keepalive-off下面
$go run client.go
2021/01/03 12:42:38 response body: ok
2021/01/03 12:42:43 response body: ok
2021/01/03 12:42:48 response body: ok
2021/01/03 12:42:53 response body: ok
2021/01/03 12:42:58 response body: ok
// 在server-keepalive-on下面
$go run server.go
2021/01/03 12:42:38 receive a request from: [::1]:62287 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
2021/01/03 12:42:43 receive a request from: [::1]:62301 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
2021/01/03 12:42:48 receive a request from: [::1]:62314 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
2021/01/03 12:42:53 receive a request from: [::1]:62328 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
2021/01/03 12:42:58 receive a request from: [::1]:62342 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
|
From the output of the Server, the request from the client adds the Connection:[close] header field, and when such a request is received, the Server side no longer maintains this connection. We also see in the logs above that each request is sent out on a different client port, so clearly these are five different connections.
3. Create an http server that does not support keep-alive connections
Suppose we have a requirement that the server does not support keep-alive connections at all, and the server will close the connection after returning an answer, regardless of whether the client sends a request with Connection: keep-alive explicitly in the header. So how can we achieve this in Go? Let’s look at the following code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//github.com/bigwhite/experiments/http-keep-alive/server-keepalive-off/server.go
package main
import (
"log"
"net/http"
)
func Index(w http.ResponseWriter, r *http.Request) {
log.Println("receive a request from:", r.RemoteAddr, r.Header)
w.Write([]byte("ok"))
}
func main() {
var s = http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(Index),
}
s.SetKeepAlivesEnabled(false)
s.ListenAndServe()
}
|
We see that before ListenAndServe, we call the SetKeepAlivesEnabled method of http.Server and pass in the false argument, so that we turn off support for keep-alive connections to that Server at the global level, and we use the previous client-keepalive -on the following client to send five requests to the Server.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 在client-keepalive-on下面
$go run client.go
http.Request{Method:"Get", URL:(*url.URL)(0xc000174000), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{}, Body:io.ReadCloser(nil), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:"localhost:8080", Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:"", RequestURI:"", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.emptyCtx)(0xc00013a008)}
2021/01/03 13:30:08 response body: ok
2021/01/03 13:30:08 response body: ok
2021/01/03 13:30:08 response body: ok
2021/01/03 13:30:08 response body: ok
2021/01/03 13:30:08 response body: ok
// 在server-keepalive-off下面
$go run server.go
2021/01/03 13:30:08 receive a request from: [::1]:53005 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 13:30:08 receive a request from: [::1]:53006 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 13:30:08 receive a request from: [::1]:53007 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 13:30:08 receive a request from: [::1]:53008 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 13:30:08 receive a request from: [::1]:53009 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
|
We see that the Server closes the connection transmitting the request after processing each request, which causes the client to measure having to create a new connection for each request (as seen by the client address and port output from the server).
4. http server that supports long connection idle timeout closure
Obviously the above server is “too overbearing” and the above “one-size-fits-all” mechanism does not make sense for a client who wants to reuse connections and improve the efficiency of request and reply transmission. So is there a mechanism that allows the http server to keep-alive on connections with high data density, and to clean up those idle connections that have not been transmitted for a long time, releasing the occupied system resources? Let’s look at the following go implementation of the server.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//github.com/bigwhite/experiments/http-keep-alive/server-keepalive-with-idletimeout/server.go
package main
import (
"log"
"net/http"
"time"
)
func Index(w http.ResponseWriter, r *http.Request) {
log.Println("receive a request from:", r.RemoteAddr, r.Header)
w.Write([]byte("ok"))
}
func main() {
var s = http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(Index),
IdleTimeout: 5 * time.Second,
}
s.ListenAndServe()
}
|
From the code we see that we only explicitly assign an explicit value to the field IdleTimeout when we create the http.Server
instance, setting the idle connection timeout to 5s. Here is a comment from the Go standard library about the field IdleTimeout for http.Server
.
1
2
3
4
5
6
|
// $GOROOT/src/net/server.go
// IdleTimeout是当启用keep-alive时等待下一个请求的最大时间。
// 如果IdleTimeout为零,则使用ReadTimeout的值。如果两者都是
// 零,则没有超时。
IdleTimeout time.Duration
|
Let’s see how it works and if it is what we expect. To test the effect, we modified the client side and put it under client-keepalive-on-with-idle.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
//github.com/bigwhite/experiments/http-keep-alive/client-keepalive-on-with-idle/client.go
... ...
func main() {
c := &http.Client{}
req, err := http.NewRequest("Get", "http://localhost:8080", nil)
if err != nil {
panic(err)
}
for i := 0; i < 5; i++ {
log.Printf("round %d begin:\n", i+1)
for j := 0; j < i+1; j++ {
resp, err := c.Do(req)
if err != nil {
fmt.Println("http get error:", err)
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("read body error:", err)
return
}
log.Println("response body:", string(b))
}
log.Printf("round %d end\n", i+1)
time.Sleep(7 * time.Second)
}
}
|
The client request is divided into 5 rounds, with an interval of 7 seconds between rounds, and the following is the communication process and results.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
// 在client-keepalive-on-with-idle下
$go run client.go
2021/01/03 14:17:05 round 1 begin:
2021/01/03 14:17:05 response body: ok
2021/01/03 14:17:05 round 1 end
2021/01/03 14:17:12 round 2 begin:
2021/01/03 14:17:12 response body: ok
2021/01/03 14:17:12 response body: ok
2021/01/03 14:17:12 round 2 end
2021/01/03 14:17:19 round 3 begin:
2021/01/03 14:17:19 response body: ok
2021/01/03 14:17:19 response body: ok
2021/01/03 14:17:19 response body: ok
2021/01/03 14:17:19 round 3 end
2021/01/03 14:17:26 round 4 begin:
2021/01/03 14:17:26 response body: ok
2021/01/03 14:17:26 response body: ok
2021/01/03 14:17:26 response body: ok
2021/01/03 14:17:26 response body: ok
2021/01/03 14:17:26 round 4 end
2021/01/03 14:17:33 round 5 begin:
2021/01/03 14:17:33 response body: ok
2021/01/03 14:17:33 response body: ok
2021/01/03 14:17:33 response body: ok
2021/01/03 14:17:33 response body: ok
2021/01/03 14:17:33 response body: ok
2021/01/03 14:17:33 round 5 end
// 在server-keepalive-with-idletimeout下
$go run server.go
2021/01/03 14:17:05 receive a request from: [::1]:64071 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:12 receive a request from: [::1]:64145 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:12 receive a request from: [::1]:64145 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:19 receive a request from: [::1]:64189 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:19 receive a request from: [::1]:64189 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:19 receive a request from: [::1]:64189 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:26 receive a request from: [::1]:64250 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:26 receive a request from: [::1]:64250 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:26 receive a request from: [::1]:64250 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:26 receive a request from: [::1]:64250 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
|
We see that.
- (a) Within each round, all requests on the client side are multiplexed with established connections.
- However, between each round, as Sleep is 7 seconds beyond the idletimeout on the server side, the connection from the previous round is dismantled and the new round has to rebuild the connection.
The effect we expect is achieved!
5. One http client can manage connections to multiple servers
Client is not a one-to-one relationship with a server, it enables one-to-many http communication, meaning that a single http client can manage connections to multiple servers and prioritize multiplexing connections to the same server (keep-alive) instead of creating new connections, as we saw above. as we saw above. Let’s create a client that sends requests to multiple servers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
//github.com/bigwhite/experiments/http-keep-alive/client-keepalive-on-to-multiple-servers/client.go
... ...
func main() {
c := &http.Client{}
req1, err := http.NewRequest("Get", "http://localhost:8080", nil)
if err != nil {
panic(err)
}
req2, err := http.NewRequest("Get", "http://localhost:8081", nil)
if err != nil {
panic(err)
}
for i := 0; i < 5; i++ {
resp1, err := c.Do(req1)
if err != nil {
fmt.Println("http get error:", err)
return
}
defer resp1.Body.Close()
b1, err := ioutil.ReadAll(resp1.Body)
if err != nil {
fmt.Println("read body error:", err)
return
}
log.Println("response1 body:", string(b1))
resp2, err := c.Do(req2)
if err != nil {
fmt.Println("http get error:", err)
return
}
defer resp2.Body.Close()
b2, err := ioutil.ReadAll(resp2.Body)
if err != nil {
fmt.Println("read body error:", err)
return
}
log.Println("response2 body:", string(b2))
time.Sleep(5 * time.Second)
}
}
|
We create two default http servers, listening to 8080 and 8081 respectively, and run the above client.
1
2
3
4
5
6
7
8
9
10
11
|
$go run client.go
2021/01/03 14:52:20 response1 body: ok
2021/01/03 14:52:20 response2 body: ok
2021/01/03 14:52:25 response1 body: ok
2021/01/03 14:52:25 response2 body: ok
2021/01/03 14:52:30 response1 body: ok
2021/01/03 14:52:30 response2 body: ok
2021/01/03 14:52:35 response1 body: ok
2021/01/03 14:52:35 response2 body: ok
2021/01/03 14:52:40 response1 body: ok
2021/01/03 14:52:40 response2 body: ok
|
The output of the server side is as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// server1(8080):
2021/01/03 14:52:20 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:52:25 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:52:30 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:52:35 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:52:40 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
// server2(8081):
2021/01/03 14:52:20 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:52:25 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:52:30 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:52:35 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
2021/01/03 14:52:40 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
|
We see that the client supports communication with multiple servers at the same time and for each server can use keep-alive connections for efficient communication.
The source code for this article can be downloaded from here.