In this example we are going to create a TLS based HTTP/2 server and let client communicate with it over HTTP/2 protocol instead of traditional HTTP/1.


Conventional HTTP/1 communications are made over their own connections so one request per one connection. This would be slow because connection negotiation, TLS handshake, TCP slow-start strategy so on. takes place for each request. The solution to this problem is to use HTTP/2 protocol. In the case of HTTP/2 communications, multiple requests can be made over a single connection. That means connection negotiation, TLS handshake, TCP slow-start strategy so on. takes place only once which improves the performance. This also depends on the HTTP keep-alive feature which is enabled by default but you might accidentally disable it while creating a custom transport. Unless you have a reason to disable it (I cannot imagine why you would anyway), I suggest you to use it.


Another reason you might accidentally disable connection reusing is not closing the response body as soon as using it. Pipelining allows multiple request and their responses to be communicated in parallel rather than in serial. For this to work you will have to use res.Body.Close() to close body as soon as possible without deferring it so that the following requests, if any, use existing connections to improve the overall performance.


Remember


The performance benefits of HTTP/2 come from simultaneous/parallel (pipelining) and batch requests. For example based on my tests, HTTP/2 was able to handle 500,000 requests (30sec~) but HTTP/1 barely managed 100 requests with many "broken pipe", "connection reset by peer" errors. The tests issued requests as in goroutines.


Timeouts


Server



Client



SSL


HTTP/2 protocol enforces TLS so we will have to first create a SSL key and a SSL certificate. Let's move on to create self-signed private and public key pair.


Private key


This will be used only for the server.


$ openssl genrsa -out cert/private.key 4096

Generating RSA private key, 4096 bit long modulus
..........................++
..........................++

Public certificate


This will be used for both the server and the client.


$ openssl req -new -x509 -sha256 -days 1825 -key cert/private.key -out cert/public.crt

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.

Country Name (2 letter code) [AU]:UK
State or Province Name (full name) [Some-State]:London
Locality Name (eg, city) []:City of London
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Inanzzz Ltd
Organizational Unit Name (eg, section) []:Engineering
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:myemail@mydomain.com

The Common Name field above is very important. If what you enter doesn't match what your client uses as part of the request host/URL or tls.Config.ServerName, you will get one of the errors below.


// Client side errors.
Get https://0.0.0.0:8443: x509: cannot validate certificate for 0.0.0.0 because it doesn't contain any IP SANs
Get https://127.0.0.1:8443: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs
Get https://localhost:8443: x509: certificate is not valid for any names, but wanted to match localhost
Get https://localhost:8443: x509: certificate is valid for localhost, not helloworld

// Server side errors.
http: TLS handshake error from 127.0.0.1:59981: remote error: tls: bad certificate
http: TLS handshake error from [::1]:59873: remote error: tls: bad certificate

Server


Structure


.
├── cert
│   ├── private.key
│   └── public.crt
└── cmd
   └── server
   └── main.go


main.go


package main

import (
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)

func main() {
server := &http.Server{
Addr: ":8443",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
TLSConfig: tlsConfig(),
}

//// Having this does not change anything but just showing.
//// go get -u golang.org/x/net/http2
//if err := http2.ConfigureServer(server, nil); err != nil {
// log.Fatal(err)
//}

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf("Protocol: %s", r.Proto)))
})

if err := server.ListenAndServeTLS("", ""); err != nil {
log.Fatal(err)
}
}

func tlsConfig() *tls.Config {
crt, err := ioutil.ReadFile("./cert/public.crt")
if err != nil {
log.Fatal(err)
}

key, err := ioutil.ReadFile("./cert/private.key")
if err != nil {
log.Fatal(err)
}

cert, err := tls.X509KeyPair(crt, key)
if err != nil {
log.Fatal(err)
}

return &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "localhost",
}
}

Run


This runs, waits for client requests and responds to them.


$ go run -race cmd/server/main.go 

Client


Structure


.
├── cert
│   └── public.crt
└── cmd
   └── client
   └── main.go


main.go


Run go get -u golang.org/x/net/http2 to install HTTP/2 package. As seen below I commented out transport1 function which depends on the default http package. It shows us how to create a HTTP/2 transport. Technically the transport1 and transport2 requests result in same response as shown below.


package main

import (
"crypto/tls"
"crypto/x509"
"fmt"
"golang.org/x/net/http2"
"io/ioutil"
"log"
"net/http"
)

func main() {
client := &http.Client{Transport: transport2()}

res, err := client.Get("https://localhost:8443")
if err != nil {
log.Fatal(err)
}

body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}

res.Body.Close()

fmt.Printf("Code: %d\n", res.StatusCode)
fmt.Printf("Body: %s\n", body)
}

func transport2() *http2.Transport {
return &http2.Transport{
TLSClientConfig: tlsConfig(),
DisableCompression: true,
AllowHTTP: false,
}
}

//func transport1() *http.Transport {
// return &http.Transport{
// // Original configurations from `http.DefaultTransport` variable.
// DialContext: (&net.Dialer{
// Timeout: 30 * time.Second,
// KeepAlive: 30 * time.Second,
// }).DialContext,
// ForceAttemptHTTP2: true, // Set it to false to enforce HTTP/1
// MaxIdleConns: 100,
// IdleConnTimeout: 90 * time.Second,
// TLSHandshakeTimeout: 10 * time.Second,
// ExpectContinueTimeout: 1 * time.Second,
//
// // Our custom configurations.
// ResponseHeaderTimeout: 10 * time.Second,
// DisableCompression: true,
// // Set DisableKeepAlives to true when using HTTP/1 otherwise it will cause error: dial tcp [::1]:8090: socket: too many open files
// DisableKeepAlives: false,
// TLSClientConfig: tlsConfig(),
// }
//}

func tlsConfig() *tls.Config {
crt, err := ioutil.ReadFile("./cert/public.crt")
if err != nil {
log.Fatal(err)
}

rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(crt)

return &tls.Config{
RootCAs: rootCAs,
InsecureSkipVerify: false,
ServerName: "localhost",
}
}

Run


$ go run -race cmd/client/main.go
Code: 200
Body: Protocol: HTTP/2.0

References