Bu örnekte, TLS tabanlı bir HTTP/2 sunucusu oluşturacağız ve istemcinin geleneksel HTTP/1 yerine HTTP/2 protokolü üzerinden iletişim kurmasına izin vereceğiz.


Geleneksel HTTP/1 iletişimleri kendi bağlantıları üzerinden yapılır, böylece her bağlantı için bir istek yapılır. Bağlantı görüşmesi, TLS anlaşması, TCP yavaş başlatma stratejisi vb. işlemler her istek için gerçekleşeceğinden, istek ve cevap süreci yavaş olacaktır. Bu sorunun çözümü HTTP/2 protokolünü kullanmaktır. HTTP/2 iletişimi durumunda, tek bir bağlantı üzerinden birden fazla istek gerçekleştirilebilir. Bu bağlantı görüşmesi, TLS anlaşması, TCP yavaş başlatma stratejisi vb. işlemlerin sadece bir kez gerçekleşmesi anlamına gelir, ki bu da performansı artırır. Bu varsayılan olarak etkin olan HTTP keep-alive (canlı tutma) özelliğine de bağlıdır, ancak özel bir aktarım (transport) oluştururken bu özelliği yanlışlıkla devre dışı bırakabilirsiniz. Devre dışı bırakmak için bir nedeniniz yoksa (olabilirliğini hayal edemiyorum), kullanmanızı öneririm.


Bağlantının yeniden kullanımını yanlışlıkla devre dışı bırakmanın bir başka nedeni ise, isteğe gelen cevabı okuduktan hemen sonra kapatmamaktır. Pipeline özelliği, çoklu istek ve yanıtlarının seri yerine paralel olarak iletilmesine izin verir.


Hatırlatıcı


HTTP/2'nin performans avantajları eşzamanlı/paralel (pipeline) ve toplu istekler kullanıldığında ortaya çıkar. Örneğin testlerime baktığımda, HTTP/2 500.000 civarı isteği (30 saniye ~) işleyebilirken, HTTP/1 güçlükle 100 isteği birçok "broken pipe", "connection reset by peer" hataları vererek işleyebildi. Testler, goroutinler kullanılarak gerçekleştirildi.


Zaman aşımları


Server



Client



SSL


HTTP/2 protokolü TLS'yi mecbur kılar, bu yüzden önce bir SSL anahtarı ve bir SSL sertifikası (anahtar) oluşturmamız gerekir. Kendiliğinden imzalı özel ve halka açık anahtar çifti oluşturmak için devam edelim.


Özel anahtar


Bu server için kullanılacak.


$ openssl genrsa -out cert/private.key 4096

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

Halka açık sertifika (anahtar)


Bu hem server hem de client için kullanılacak.


$ 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

Yukarıdaki Common Name alanı çok önemlidir. Girdiğiniz bilgiler, istemcinizin istek URL'sine veya tls.Config.ServerName uymazsa, aşağıdaki hatalardan birini alırsınız.


// 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


Yapı


.
├── 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",
}
}

Çalıştırmak


Bu çalışır, istemci isteklerini bekler ve yanıtlar.


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

Client


Yapı


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


main.go


HTTP/2 paketini kurmak için go get -u golang.org/x/net/http2 komutunu çalıştırın. Aşağıda gördüğümüz gibi varsayılan http paketini kullanan transport1 fonksiyonunu iptal ettim. Bu bize HTTP/2 transport'un nasıl kullanılabileceğini gösteriyor. Teknik olarak transport1 ve transport2 fonksiyonlarının istek ve cevap sonuçları aynı olacaktır.


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",
}
}

Çalıştırmak


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

Referanslar