Пишем HTTP/1.1 & HTTP/2 клиент и сервер на Golang

Автор оригинала: Chanaka Lakmal
  • Перевод


Golang — отличный язык программирования с широким спектром возможностей. В этой статье показано, как на Go можно написать клиент и сервер для протоколов HTTP/1.1 и HTTP/2.

Skillbox рекомендует: Практический курс «Python-разработчик с нуля».

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

HTTP/2 — вторая крупная версия сетевого протокола HTTP, используемая для доступа к World Wide Web. Протокол основан на SPDY. Подробнее об этой версии можно узнать на GitHub.

HTTP/1.1 Server


Код, размещенный ниже, демонстрирует принцип написания HTTP/1.1-сервера на Go. Функция main запускает службу HTTP(S) с портом 9191 и путем /hello/sayHello. echoPayload отвечает за логику echo, занимаясь анализом входящего трафика и реагируя соответствующим образом. При необходимости echoPayload можно модифицировать.

package main
 
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
 
func main() {
http.HandleFunc("/hello/sayHello", echoPayload)
log.Printf("Go Backend: { HTTPVersion = 1 }; serving on https://localhost:9191/hello/sayHello")
log.Fatal(http.ListenAndServeTLS(":9191", "./cert/server.crt", "./cert/server.key", nil))
}
 
func echoPayload(w http.ResponseWriter, req *http.Request) {
log.Printf("Request connection: %s, path: %s", req.Proto, req.URL.Path[1:])
defer req.Body.Close()
contents, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Fatalf("Oops! Failed reading body of the request.\n %s", err)
http.Error(w, err.Error(), 500)
}
fmt.Fprintf(w, "%s\n", string(contents))

Поскольку служба HTTP(S) уже запущена, нужно предоставить сертификат и ключ сервера. Оба объекта сохранены в директории cert с названиями server.crt и server.key.

Пример сертификата и ключа — ниже.

./cert/server.crt

-----BEGIN CERTIFICATE-----
MIID+zCCAuOgAwIBAgIJAPsvGCCAC2i+MA0GCSqGSIb3DQEBCwUAMIGTMQswCQYD
VQQGEwJMSzEQMA4GA1UECAwHV2VzdGVybjEQMA4GA1UEBwwHQ29sb21ibzESMBAG
A1UECgwJTERDTEFLTUFMMRQwEgYDVQQLDAtFbmdpbmVlcmluZzESMBAGA1UEAwwJ
bG9jYWxob3N0MSIwIAYJKoZIhvcNAQkBFhNsZGNsYWttYWxAZ21haWwuY29tMB4X
DTE5MDQyMDA1MjczM1oXDTIwMDQxOTA1MjczM1owgZMxCzAJBgNVBAYTAkxLMRAw
DgYDVQQIDAdXZXN0ZXJuMRAwDgYDVQQHDAdDb2xvbWJvMRIwEAYDVQQKDAlMRENM
QUtNQUwxFDASBgNVBAsMC0VuZ2luZWVyaW5nMRIwEAYDVQQDDAlsb2NhbGhvc3Qx
IjAgBgkqhkiG9w0BCQEWE2xkY2xha21hbEBnbWFpbC5jb20wggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC9PKAOlJcOBUI9CGnVjMjQHNRqYv01CaUdC4/e
YFyegxLpoMpYvEC+nYlHT2j7BOhQBV+TkH1D4YOK2WP3V0FLv5hM7Nxsgf25WNHa
zi2DTBvcBgB9sDJA/avIvF+63+Btnyggp3xq6MaHy5DNH0kPnSiPiy7PRKToEUn6
oqPnB10rRBFZqs3ePmEDxVL3T/TUZSXR3P95fV1vDCqrJbr3YwWOzFCq8kEJFslK
B7GSEKpPgmK0g5krmAQqUOuCJ3/xFlCP4trKg/lvSJZ5S/LZD5teDDg6Ax3Mvthj
kMh9/OM5GGTTjRwhct9dHjFI8POj+TMbLZvoPVXjsmATEgtLAgMBAAGjUDBOMB0G
A1UdDgQWBBQ1CmWXmrHOv6b8f763/bk80EpbajAfBgNVHSMEGDAWgBQ1CmWXmrHO
v6b8f763/bk80EpbajAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAH
D51Uoe2K4N9/GxRgww5mMW2dUJ7Hc/tGsr/J1fNqHY8SXNAn5i+GwI+xBvwxFHL3
KZHbfq7eYDE5EItt3cZp5ySSscdTEay9ReH2+8k32gpH46CMwPV3XvtQuBVVAC4u
szrq1eWKhYI2zf4iUVpwvq89OynVGIp0atng+q3A2cBhi3NGo6Ho1s2rywQyqiq8
up4PUSVQ6WBoJFx5PEEDxD84VMS7Pan6dT34b9n56tq5R06retZTUZ8jMM88CGX4
88pSPU+XImp6DdNVBmW6Lz76jiSNHLkZGm4jumjeyUGzBjBEBOgSegeWlinMtWE9
gaVxeUHrqHk8xzwJ4oIu
-----END CERTIFICATE-----

./cert/server.key

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9PKAOlJcOBUI9
CGnVjMjQHNRqYv01CaUdC4/eYFyegxLpoMpYvEC+nYlHT2j7BOhQBV+TkH1D4YOK
2WP3V0FLv5hM7Nxsgf25WNHazi2DTBvcBgB9sDJA/avIvF+63+Btnyggp3xq6MaH
y5DNH0kPnSiPiy7PRKToEUn6oqPnB10rRBFZqs3ePmEDxVL3T/TUZSXR3P95fV1v
DCqrJbr3YwWOzFCq8kEJFslKB7GSEKpPgmK0g5krmAQqUOuCJ3/xFlCP4trKg/lv
SJZ5S/LZD5teDDg6Ax3MvthjkMh9/OM5GGTTjRwhct9dHjFI8POj+TMbLZvoPVXj
smATEgtLAgMBAAECggEAbaS2yDvn2cPKQTqit4y+vXY2zP1V4GkaNd4BGcOTZnRj
fOIg25EXoln8tEiadva888BpREKvkakUYlraxPDVcGIuiEOk42nd7Io97R0Q2cY7
ThxcJHb2ZxmTctdSUCBvFJTm1ySzve3pOb0ExRSfbGCOo7zs/kKzmZKK3qFlffGS
Ga9O7hyLOuXPU22CM+5Lq0JPTER73z0DpAweZc0L14j6dzhcG3qUwk0K6K47VZgE
NhEORul7xDj91bh2iEoSbaQe8HxLaMQoMXOC/9oey2UKKTe9WZE3+XCvg+vkw/sS
biQ+b4EZ9LuhAhCZ0UE6+y7PZY+8G/YsbGg0Zo8cAQKBgQDyTuG47rWBgbdHsEB/
MSKGU6w+a1SdLk6jG+Enji5Q624/h0xt5nF9ah2eRf3Rlhn9WEKM/uE9ouEODBKE
8rnIDsjufEMI8moPEloRBSsxPNw+fNMSSCZjL+qPtTJUbRio7WA23sfdnE57ygBa
wlPQ9UBBWSm2se4veEZtHjtngQKBgQDH7gnH5Att6ZYazRTgD72g0C5v1l4LYVEQ
jxdBcs6TJA8wHfifZ45F67W95QunmM813UxfS+ybdjytlb8/lmi2BnK6lDx5HWIL
31jnbg2CxCrNv9oZLjKVDmkp4WUcEp5W33R1/MGDTRfyARP+6QYQO/ATMdqtm5Uu
cD6clrL4ywKBgQCQ0niy0WmGaAMlQ8CoxLM/2c6+1+OQtlalwkoGHEKudqhELBeQ
MAVw0fW13Vtg4vfRpejQ4J26+xjMDocbEv/bBIsvjvF57XlaXLucJJy2Jwv0BSMa
cCkRa1gkYEYek74DaSzyXqDSYVO/RPKFTFRQNeUbqbD20s3rbVWablFPAQKBgB5y
zUCJJYh2w6qPQzegjhO4wOm9bxMyngL0l+ka0AUuv7VnSx8TyWIytLoX8P90UVJ1
wpTc3ksK5dDV9ot7n7ThJIXv34nehLkkKckNRLd+oro1FsUw+PkkebWsIxb0avL2
EymI9fvGOPhdW6s91/OO/VAfDpvUDxNEevSkKtujAoGAcMOsXtn/UyT3Lssxgla3
K+DCaFhAQPSUXOmpZwEbQ0yQlksDe4flsam8bEDI5D5iHx1ziSfh583qJl3BEZ5u
VZTEO2YLvT9QRz7pv2qspqj7nzSyBU2BFAajq43/G1b8FHfVgN+YdVtzVrigfql5
2a+JxOxFfpjnGQ7RfSxSb+Q=
-----END PRIVATE KEY-----

Пора провести тестовый запуск службы, использовав следующую команду:

$ go run http_server.go

Ответ должен быть таким:

Go Backend: { HTTPVersion = 1 }; serving on https://localhost:9191/hello/sayHello

Теперь инициируем службу при помощи запроса HTTP/1.1 POST (небезопасный и безопасный режимы):

$ curl -k -v https://localhost:9191/hello/sayHello -d "Hello Go!"
 
$ curl -v --cacert ./cert/server.crt https://localhost:9191/hello/sayHello -d "Hello Go!"

Ниже — результат работы программы. Первая часть — детали рукопожатия TLS сервера и клиента. Вторая — детали запроса и ответа HTTP/1.1. В конце — текстовое сообщение «Hello Go!».

* Trying 127.0.0.1…
* Connected to localhost (127.0.0.1) port 9191 (#0)
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: none
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: C=LK; ST=Western; L=Colombo; O=LDCLAKMAL; OU=Engineering; CN=Chanaka Lakmal; emailAddress=ldclakmal@gmail.com
* start date: Apr 20 03:03:58 2019 GMT
* expire date: Apr 19 03:03:58 2020 GMT
* issuer: C=LK; ST=Western; L=Colombo; O=LDCLAKMAL; OU=Engineering; CN=Chanaka Lakmal; emailAddress=ldclakmal@gmail.com
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> POST /hello/sayHello HTTP/1.1
> Host: localhost:9191
> User-Agent: curl/7.46.0
> Accept: */*
> Content-Length: 9
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 9 out of 9 bytes
< HTTP/1.1 200 OK
< Date: Sat, 20 Apr 2019 06:56:19 GMT
< Content-Length: 10
< Content-Type: text/plain; charset=utf-8
<
Hello Go!
* Connection #0 to host localhost left intact

HTTP/1.1 Client


Новый код — пример написания простого клиента HTTP/1.1 на Go. Этот клиент отправляет запрос HTTP(S) POST на localhost:9191/hello/sayHello с сообщением “Hello Go!”.

package main
 
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)
 
func main() {
client := &http.Client{}
 
// Create a pool with the server certificate since it is not signed
// by a known CA
caCert, err := ioutil.ReadFile("./cert/server.crt")
if err != nil {
log.Fatalf("Reading server certificate: %s", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
 
// Create TLS configuration with the certificate of the server
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}
 
// Use the proper transport in the client
client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
 
// Perform the request
resp, err := client.Post("https://localhost:9191/hello/sayHello", "text/plain", bytes.NewBufferString("Hello Go!"))
if err != nil {
log.Fatalf("Failed get: %s", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed reading response body: %s", err)
}
fmt.Printf("Got response %d: %s %s", resp.StatusCode, resp.Proto, string(body))

Поскольку здесь тоже запускается служба HTTP(S), нужно предоставить сертификат и ключ. Нужные данные размещены в директории cert с названиями server.crt и server.key.

Для того чтобы запустить клиент, нужно инициализировать сервер. Для этого выполняем описанные в первом разделе действия либо же запускаем любую HTTP(S)-службу с портом 9191 и путем /hello/sayHello.

$ go run http_client.go

Вывод должен быть таким:

Got response 200: HTTP/1.1 Hello Go!

Клиент показывает работу команды curl:

$ curl -v --cacert ./cert/server.crt https://localhost:9191/hello/sayHello -d "Hello Go!"

После того как закончена работа с HTTP/1.1, стоит попробовать повторить то же самое для HTTP/2.

HTTP/2 Server


Как и в предыдущем разделе, сначала нужно создать эхо-сервер.

package main
 
import (
"fmt"
"golang.org/x/net/http2"
"io/ioutil"
"log"
"net/http"
)
 
func main() {
var httpServer = http.Server{
Addr: ":9191",
}
var http2Server = http2.Server{}
_ = http2.ConfigureServer(&httpServer, &http2Server)
http.HandleFunc("/hello/sayHello", echoPayload)
log.Printf("Go Backend: { HTTPVersion = 2 }; serving on https://localhost:9191/hello/sayHello")
log.Fatal(httpServer.ListenAndServeTLS("./cert/server.crt", "./cert/server.key"))
}
 
func echoPayload(w http.ResponseWriter, req *http.Request) {
log.Printf("Request connection: %s, path: %s", req.Proto, req.URL.Path[1:])
defer req.Body.Close()
contents, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Fatalf("Oops! Failed reading body of the request.\n %s", err)
http.Error(w, err.Error(), 500)
}
fmt.Fprintf(w, "%s\n", string(contents))

Для того чтобы проверить работу сервера, нужно отправить команду:

$ go run http2_server.go

Ответ должен быть таким:

Go Backend: { HTTPVersion = 2 }; serving on https://localhost:9191/hello/sayHello

Теперь сервер запущен, инициализировать его можно при помощи запросов HTTP/1.1 или HTTP/2. Команды HTTP/1.1 POST для тестирования работы сервера приведены ниже:

$ curl -k -v https://localhost:9191/hello/sayHello -d "Hello Go!"
 
$ curl -v --cacert ./cert/server.crt https://localhost:9191/hello/sayHello -d "Hello Go!"

И то же самое, но уже с HTTP/2 POST:

$ curl -k -v --http2 https://localhost:9191/hello/sayHello -d "Hello Go!"
 
$ curl -v --http2 --cacert ./cert/server.crt https://localhost:9191/hello/sayHello -d "Hello Go!"

Ниже — пример вывода работы сервера. Первая часть — TLS-рукопожатие между сервером и клиентом, вторая — детали запроса и ответа HTTP/2.

* Trying 127.0.0.1…
* Connected to localhost (127.0.0.1) port 9191 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: src/hello/cert/server.crt
CApath: none
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
* subject: C=LK; ST=Western; L=Colombo; O=LDCLAKMAL; OU=Engineering; CN=localhost; emailAddress=ldclakmal@gmail.com
* start date: Apr 20 05:27:33 2019 GMT
* expire date: Apr 19 05:27:33 2020 GMT
* common name: localhost (matched)
* issuer: C=LK; ST=Western; L=Colombo; O=LDCLAKMAL; OU=Engineering; CN=localhost; emailAddress=ldclakmal@gmail.com
* SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* TCP_NODELAY set
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x10ddf20)
> POST /hello/sayHello HTTP/1.1
> Host: localhost:9191
> User-Agent: curl/7.46.0
> Accept: */*
> Content-Length: 9
> Content-Type: application/x-www-form-urlencoded
>
* We are completely uploaded and fine
< HTTP/2.0 200
< content-type:text/plain; charset=utf-8
< content-length:10
< date:Sat, 20 Apr 2019 06:54:50 GMT
<
Hello Go!
* Connection #0 to host localhost left intact

HTTP/2 Client


Последняя часть — создание HTTP/2-клиента. Вот реализация отправки HTTP(S) POST запроса для localhost:9191/hello/sayHello:

package main
 
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"golang.org/x/net/http2"
"io/ioutil"
"log"
"net/http"
)
 
func main() {
client := &http.Client{}
 
// Create a pool with the server certificate since it is not signed
// by a known CA
caCert, err := ioutil.ReadFile("./cert/server.crt")
if err != nil {
log.Fatalf("Reading server certificate: %s", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
 
// Create TLS configuration with the certificate of the server
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}
 
// Use the proper transport in the client
client.Transport = &http2.Transport{
TLSClientConfig: tlsConfig,
}
 
// Perform the request
resp, err := client.Post("https://localhost:9191/hello/sayHello", "text/plain", bytes.NewBufferString("Hello Go!"))
if err != nil {
log.Fatalf("Failed get: %s", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed reading response body: %s", err)
}
fmt.Printf("Got response %d: %s %s", resp.StatusCode, resp.Proto, string(body))

Как и прежде, для работы нужны ключ и сертификат. Они сохранены в cert с названиями server.crt и server.key.

Запуск клиента:

$ go run http2_client.go

И ожидаемый ответ:

Got response 200: HTTP/2 Hello Go!

Детализация работы клиента:

$ curl -v --http2 --cacert ./cert/server.crt https://localhost:9191/hello/sayHello -d "Hello Go!"

Вот и все. Работа относительно простая, но она дает понимание того, как реализовать базовые сетевые сервисы на языке Go.

Skillbox рекомендует:

  • +21
  • 4,1k
  • 4
Skillbox
171,93
Онлайн-университет профессий будущего
Поделиться публикацией

Комментарии 4

    +2
    Мне кажется что второй сервер и клиент можно было бы написать дифами — различия то там в 1-2х строчках.
      +4
      Ну надо ж статью растянуть.
      +1
      Начиная с версии 1.6, HTTP/2 работает по умолчанию из стандартной библиотеки (net/http) при использовании HTTPS.

      Go + HTTP/2
        0
        Ничего не имею против Go, но всегда умиляют фразы типа «Golang — отличный язык программирования с широким спектром возможностей.» в статьях об очередном сервере на Go

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое