HTTP Keep-Alive is a sliding renewal reuse of TCP connections at the application layer. If the client/server consistently renews, it becomes a true persistent connection.
Currently, all HTTP network libraries have HTTP Keep-Alive enabled by default. Today, we will dissect HTTP persistent connections from the perspective of underlying TCP connections and troubleshooting.
“I’m just a web programmer, so why do I need to know so much?”
Using Go language to fiddle with an httpServer/httpClient, we’ll briefly discuss the usage style of Go.
Set up an httpserver with Go language net/http
package and inject a Handler to log request details.
package mainimport ( "fmt" "log" "net/http")// IndexHandler logs basic request information: Pay attention to r.RemoteAddrfunc Index(w http.ResponseWriter, r *http.Request) { fmt.Println("receive a request from:", r.RemoteAddr, r.Header) w.Write([]byte("ok"))}// net/http enables persistent connections by defaultfunc main() { fmt.Printf("Starting server at port 8081\n") if err := http.ListenAndServe(":8081", http.HandlerFunc(Index)); err != nil { log.Fatal(err) }}
ListenAndServe
creates the default httpServer. In Go, access permissions are controlled by capitalization of the first letter. If it is capitalized, external packages can access it, analogous to C# global functions or static functions.
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe()}
- The net/http server enables
Keep-Alive
by default, as indicated by the private variable disableKeepAlives in Server.
type Server struct { ... disableKeepAlives int32 // accessed atomically. ...}
Users can also manually disable Keep-Alive; SetKeepAlivesEnabled()
will change the value of the private variable disableKeepAlives
.
s := &http.Server{ Addr: ":8081", Handler: http.HandlerFunc(Index), ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } s.SetKeepAlivesEnabled(true) if err := s.ListenAndServe(); err != nil { log.Fatal(err) }
This is also the basic style of packaging/using Go language.
- Note that I inserted IndexHandler in the httpserver to log basic information of the httpclient. Here’s a point: If the httpclient establishes a new TCP connection, the system will allocate a random port to you based on certain rules.
Launch the server program and access localhost:8081 via a browser,
The server will receive a log as shown below. The red circle indicates the browser used a system-allocated random fixed port to establish a TCP connection.
/>Use net/http to write a client: Send an HTTP request to the server every 1 second.代码语言:javascript
package mainimport ( "fmt" "io/ioutil" "log" "net/http" "time")func main() { client := &http.Client{ Timeout: 10 * time.Second, } for { requestWithClose(client) time.Sleep(time.Second * 1) }}func requestWithClose(client *http.Client) { resp, err := client.Get("http://127.0.0.1:8081") if err != nil { fmt.Printf("error occurred while fetching page, error: %s", err.Error()) return } defer resp.Body.Close() c, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalf("Couldn't parse response body. %+v", err) } fmt.Println(string(c))}
The server receives the following request log:
/>
The red box shows that the httpclient initiated the HTTP request using the fixed port 61799, maintaining HTTP Keep-alive with the client/server.
You can use netstat -an | grep 127.0.0.1:8081
to inspect system TCP connections for a specific IP:
In the client system, only one TCP connection is established to the server, with the port being 61799, resonating with the text above.
Use Wireshark to view TCP connections happening on the localhost network card.
- Each HTTP request/response is not preceded by a TCP three-way handshake.
- After each TCP packet is sent, the other side needs to return an ACK confirmation packet.
What not to do – High Alert
Go’s net/http explicitly states:
If the Body is not both read to EOF and closed, the Client's underlying RoundTripper (typically Transport) may not be able to re-use a persistent TCP connection to the server for a subsequent "keep-alive" request.
This means, after each request, if the httpclient doesn’t finish reading the body or doesn’t close the body, it may cause Keep-alive
to fail, which can lead to goroutine leaks.
代码语言:javascript
// Below code does not finish reading the body, causing Keep-alive to fail.func requestWithClose(client *http.Client) { resp, err := client.Get("http://127.0.0.1:8081") if err != nil { fmt.Printf("error occurred while fetching page, error: %s", err.Error()) return } defer resp.Body.Close() //_, err = ioutil.ReadAll(resp.Body) fmt.Println("ok")}
The server log this time is as follows:
The red box indicates the client persistently used new random ports to establish TCP connections.
View TCP connections established in the client system:
Wireshark packet capture results:
The red box shows a three-way handshake and four-way handshake occurred every time before and after the HTTP request/response.
Summary
- The known httpclient and httpServer currently have keep-alive enabled by default.
- Disabling keep-alive or keep-alive failure can cause the client to frequently establish TCP connections under certain conditions, which can be viewed using netstat -an | grep {ip} to see established TCP connections on the client.
- Wireshark packet capture can clearly show the effect of keep-alive versus non-Keep-alive.