Go - це просто. Створюємо HelloWorld веб-сервер

На численні прохання читачів DOU публікую першу статтю про Go.

Нижче будуть розкриті наступні теми:

Створюємо HelloWorld веб-сервер на Go

Опустимо розділи про установку і настройку Go. При бажанні можете почитати самі . Перейдемо відразу до справи :)

Створюємо файл server.go і зберігаємо в нього наступний код:

// Оголошуємо назва пакету.
// Всі *.go - файли повинні починатися з назви пакета.
// Пакет з назвою "main" має спеціальне призначення - він вказує
// компілятору go, що з цього пакету потрібно зібрати самодостатній
// виконуваний файл. Це бінарники, якому для запуску не потрібні додаткові
// залежності - його достатньо скопіювати на комп'ютер і запустити.
package main

// Імпортуємо пакети, використовувані в даному файлі.
//
// Документацію по стандартним і стороннім пакетів легко знайти за адресою
// https://godoc.org/<package_path>.
// Наприклад,
//
// * https://godoc.org/flag
// * https://godoc.org/github.com/valyala/fasthttp
import (
"flag"
"github.com/valyala/fasthttp"
"log"
)

// Оголошуємо глобальну змінну addr, куди буде записано значення параметра
// -addr при запуску програми.
//
// Наприклад, параметр addr стане рівним ":80" для наступної рядка
// запуску:
// ./server -addr=:80
//
// Пропущений IP TCP адресу говорить про те, щоб сервер "слухав"
// на всіх доступних IP-адреси.
//
// flag.String вказує на те, що значення -addr - рядок.
// flag.String приймає три аргументи:
//
// * Назва аргументу, який потрібно розпарсити. "addr" в даному випадку.
// * Значення за замовчуванням. "127.0.0.1:8080" в даному випадку.
// * Опис аргументу, яке виводиться при виклику програми
// з параметром -help.
//
// flag.String повертає покажчик на рядок, де зберігається значення -addr.
var addr = flag.String("addr", "127.0.0.1:8080",
 "TCP address to listen to for incoming connections")

// main - функція, з якої починається виконання програми.
// Ця функцію повинна знаходитися в package main.
func main() {
 // Парсим параметри, вказані в рядку запуску програми.
flag.Parse()

 // Конфігуруємо http сервер.
//
 // См. можливі параметри конфігурації
 // https://godoc.org/github.com/valyala/fasthttp#Server
 s := fasthttp.Server{
 // Hanlder - функція-обробник входять http запитів.
 // См. код функції handler нижче.
 Handler: handler,
}

 // Запускаємо сервер.
//
 // ListenAndServe приймає TCP адресу, де буде запущений сервер.
 // ListenAndServe повертає результат тільки в двох випадках:
//
 // * Якщо у під час запуску сервера сталася помилка.
 // Наприклад, вказану адресу уже зайнятий іншим сервером.
 // Тоді відповідна помилка потрапить в err.
 // * Якщо сервер був зупинений. Тоді err буде дорівнює nil.
 err := s.ListenAndServe(*addr)
 if err != nil {
 log.Fatalf("error in ListenAndServe: %s", err)
}
}

// handler обробляє вхідні запити.
func handler(ctx *fasthttp.RequestCtx) {
 ctx.WriteString("Hello, world!
")
}

Цей файл використовують сторонній пакет — github.com/valyala/fasthttp , який потрібно встановити перед компіляцією. Зробимо це:

$ go get -u github.com/valyala/fasthttp

Исходники всіх сторонніх пакетів, отримані з допомогою go get , зберігаються в папку $GOPATH/src/ . Про $GOPATH можна почитати в офіційній документації .

Тепер скомпилируем наш веб-сервер:

$ go build ./server.go

У поточному каталозі повинен з'явитися виконуваний файл з ім'ям server. Переконаємося в цьому:

$ ls -l | grep server
-rwxrwxr-x 1 aliaksandr aliaksandr 6140200 May 7 19:39 server
-rw-rw-r-- 1 aliaksandr aliaksandr 4020 May 7 19:31 server.go

Перевіримо параметри, які він приймає:

$ ./server -help
Usage of ./server:
 -addr string
 TCP address to listen to for incoming connections (default "127.0.0.1:8080")

Запустимо його:

$ ./server

В окремому вікні переконаємося з допомогою nc , що сервер працює:

$ nc 127.0.0.1 8080
GET/HTTP/1.0

HTTP/1.1 200 OK
Server: fasthttp
Date: Sun, 07 May 2017 17:43:40 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 14
Connection: close

Hello, world!

Також можна відкрити 127.0.0.1:8080 в браузері і переконатися, що сервер працює.

Перевіримо швидкість його роботи за допомогою wrk .
Через одне підключення:

$ wrk -t 1 -c 1 http://127.0.0.1:8080/
Running 10s test @ http://127.0.0.1:8080/
 1 threads and connections 1
 Thread Stats Avg Stdev Max +/- Stdev
 Latency 13.25 us 24.26 us 1.70 ms 99.74%
 Req/Sec 76.71 k 4.43 k 81.43 k 91.09%
 771609 requests in 10.10 s, 109.64 read MB
Requests/sec: 76399.78
Transfer/sec: 10.86 MB

Через 1000 одночасних підключень:

$ wrk -t 2 -c 1000 http://127.0.0.1:8080/
Running 10s test @ http://127.0.0.1:8080/
 2 threads and connections 1000
 Thread Stats Avg Stdev Max +/- Stdev
 Latency 5.24 ms 3.44 ms 150.36 ms 89.22%
 Req/Sec 82.85 k 11.32 k 110.93 k 68.18%
 1643377 requests in 10.01 s, 233.52 read MB
Requests/sec: 164183.83
Transfer/sec: 23.33 MB

Через 100 одночасних підключень, в кожному з 32 pipelined запиту:

$ wrk -t 2 -c 100 -s pipeline.lua http://127.0.0.1:8080 --/32
Running 10s test @ http://127.0.0.1:8080
 2 threads and connections 100
 Thread Stats Avg Stdev Max +/- Stdev
 Latency 1.40 ms 1.46 ms 39.22 ms 90.68%
 Req/Sec 824.20 k 75.28 k 0.99 M 78.50%
 16469856 requests in 10.05 s, 2.29 GB read
Requests/sec: 1638496.88
Transfer/sec: 232.83 MB

Як бачите, простий веб-сервер з пари десятків рядків на Go не вимагає ні nginx, ні Apache, і може обробляти більше 1,6 млн запитів в секунду на звичайному ноут трирічної давності.

Додаємо підтримку https

У стандартну поставку Go входить пакет crypto/tls , з допомогою якого можна настроювати https на будь-який смак і колір. За розробку даного пакету відповідає Adam Langley , автор BoringSSL . Кілька фактів про crypto/tls :

Якщо у вас вже є TLS-сертифікат і ви хочете швидше включити підтримку https, то просто замініть наступний рядок в server.go :

err := s.ListenAndServe(*addr)

На

err := s.ListenAndServeTLS(*addr, certFile, keyFile)

Де certFile і keyFile — шляхи до файлів сертифіката відповідного ключа.

Якщо потрібна додаткова настройка https, наприклад, як описано в статті Exposing Go on the Internet , то потрібно трохи повозитися:

package main

import (
"crypto/tls"
"flag"
"github.com/valyala/fasthttp"
"log"
"net"
)

var (
 addr = flag.String("addr", "127.0.0.1:8080", "TCP address to listen to for http")
 tlsAddr = flag.String("tlsAddr", "", "TCP address to listen to for https")
 tlsCertFile = flag.String("tlsCertFile", "", "Path to TLS certificate file")
 tlsKeyFile = flag.String("tlsKeyFile", "", "Path to TLS key file")
)

func main() {
flag.Parse()

 // Намагаємося запустити сервер https
startTLS()

 // Запускаємо http сервер
 log.Printf("Serving http on -addr=%q", *addr)
 err := fasthttp.ListenAndServe(*addr, handler)
 if err != nil {
 log.Fatalf("error in ListenAndServe: %s", err)
}
}


func startTLS() {
 if len(*tlsAddr) == 0 {
 log.Printf("-tlsAddr is empty, so skip serving https")
return
}

 // Читаємо TLS сертифікат з файлу
 cert, err := tls.LoadX509KeyPair(*tlsCertFile, *tlsKeyFile)
 if err != nil {
 log.Fatalf("cannot load cert for -tlsCertFile=%q, -tlsKeyFile=%q: %s"
 *tlsCertFile, *tlsKeyFile, err)
}

 // Створюємо net.Listener'а, який приймає підключення по -tlsAddr.
 ln, err := net.Listen("tcp4", *tlsAddr)
 if err != nil {
 log.Fatalf("cannot listen for -tlsAddr=%q: %s", *tlsAddr, err)
}

 // Створюємо потрібну конфігурацію tls.
 // См. https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/ .
 tlsConfig := tls.Config{
 PreferServerCipherSuites: true,
 CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519,
},
 MinVersion: tls.VersionTLS12,
 CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
 Certificates: []tls.Certificate{cert},
}


 // Створюємо net.Listener'а для підключень tls поверх створеного
 // вище net.Listener'а
 tlsLn := tls.NewListener(ln, &tlsConfig)

 // запускаємо сервер https в окремому потоці
 log.Printf("Serving https on -tlsAddr=%q", *tlsAddr)
 go fasthttp.Serve(tlsLn, handler)
}

func handler(ctx *fasthttp.RequestCtx) {
 ctx.WriteString("Hello, world!
")
}

Тепер при вказуванні параметрів -tlsAddr , -tlsCertFile і -tlsKeyFile сервер буде приймати https-запитів -tlsAddr додатково до http-запитами на -addr . І знову ніяких nginx'ов c apache'ами і openssl'ами не потрібно. Швидкість обробки https-трафіку сервером на Go порівнянна зі швидкістю nginx , тому перед ним не потрібно ставити TLS termination proxy .

Автоматизуємо безкоштовне отримання і оновлення TLS-сертифікатів

Багато вже чули про чудовий сервіс letsencrypt.org , який видає всім бажаючим безкоштовні TLS-сертифікати. І ці сертифікати визнаються всіма сучасними браузерами. Нижче показано, наскільки просто додати підтримку автоматичного отримання і оновлення TLS-сертифікатів letsencrypt.org в наш сервер на Go:

package main

import (
"crypto/tls"
"flag"
"github.com/valyala/fasthttp"
"golang.org/x/crypto/acme/autocert"
"log"
"net"
)

var (
 addr = flag.String("addr", "127.0.0.1:8080", "TCP address to listen to for http")
 tlsAddr = flag.String("tlsAddr", "", "TCP address to listen to for https")
 tlsCertFile = flag.String("tlsCertFile", "", "Path to TLS certificate file. "+
 "The certificate is automatically generated and put "+
 "to -autocertCacheDir if empty")
 tlsKeyFile = flag.String("tlsKeyFile", "", "Path to TLS key file. "+
 "The key is automatically generated and put "+
 "to -autocertCacheDir if empty")
 autocertCacheDir = flag.String("autocertCacheDir", "autocert-cache",
 "Path to the directory where letsencrypt certs are cached")
)

func main() {
flag.Parse()

 // Намагаємося запустити сервер https
startTLS()

 // Запускаємо http сервер
 log.Printf("Serving http on -addr=%q", *addr)
 err := fasthttp.ListenAndServe(*addr, handler)
 if err != nil {
 log.Fatalf("error in ListenAndServe: %s", err)
}
}

func startTLS() {
 if len(*tlsAddr) == 0 {
 log.Printf("-tlsAddr is empty, so skip serving https")
return
}

 // Створюємо net.Listener'а, який приймає підключення по -tlsAddr.
 ln, err := net.Listen("tcp4", *tlsAddr)
 if err != nil {
 log.Fatalf("cannot listen for -tlsAddr=%q: %s", *tlsAddr, err)
}

 // Створюємо потрібну конфігурацію tls.
 // См. https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/ .
 tlsConfig := tls.Config{
 PreferServerCipherSuites: true,
 CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519,
},
}

 if len(*tlsCertFile) > 0 {
 // Читаємо TLS сертифікат з файлу
 cert, err := tls.LoadX509KeyPair(*tlsCertFile, *tlsKeyFile)
 if err != nil {
 log.Fatalf("cannot load cert for -tlsCertFile=%q, -tlsKeyFile=%q: %s", *tlsCertFile, *tlsKeyFile, err)
}
 tlsConfig.Certificates = []tls.Certificate{cert}
 } else {
 // Налаштовуємо автоматичне створення і оновлення сертифікатів.
 m := autocert.Manager{
 Prompt: autocert.AcceptTOS,

 // Сертифікати будуть кешуватися в -autocertCacheDir,
 // щоб при рестарт сервера не доводилося
 // перестворювати їх знову.
 Cache: autocert.DirCache(*autocertCacheDir),
}
 tlsConfig.GetCertificate = m.GetCertificate
}

 // Створюємо net.Listener'а для підключень tls поверх створеного
 // вище net.Listener'а
 tlsLn := tls.NewListener(ln, &tlsConfig)

 // запускаємо сервер https в окремому потоці
 log.Printf("Serving https on -tlsAddr=%q", *tlsAddr)
 go fasthttp.Serve(tlsLn, handler)
}

func handler(ctx *fasthttp.RequestCtx) {
 ctx.WriteString("Hello, world!
")
}

Перед компіляцією даного коду знадобиться завантажити ще один сторонній пакет — golang.org/x/crypto/acme/autocert , який відповідає за автоматичне створення і оновлення TLS-сертифікатів:

$ go get -u golang.org/x/crypto/acme/autocert

Тепер сервер буде автоматично створювати і оновлювати TLS-сертифікати для всіх hostname'ів, запитаних за адресою https -tlsAddr , якщо не зазначено -tlsCertFile . Виписані сертифікати будуть кешуватися в каталозі -autocertCacheDir .

Висновок

Як ви могли переконатися, на Go можна легко і невимушено створювати самодостатні високопродуктивні http і https-сервери, яким не потрібні ніякі залежності, включаючи Apache, nginx і OpenSSL. Код виходить лаконічним і простим, без зайвих абстракцій і xml-конфіги.

У статті розглянуто найпростіший http-сервер, який видає «Hello, world!». На Go можна створювати сервера проксі з набагато більш складною логікою. В якості прикладу рекомендую оцінити простий у використанні http-проксі, балансувальник навантаження і TLS termination проксі, який також вміє економити трафік — httptp .

Пропонуйте в коментарях теми Go для наступних статей.

Опубліковано: 17/05/17 @ 10:00
Розділ Хостинг

Рекомендуємо:

DOU Проектор: Raccoon.world - гаджети для взаємодії з цифровою реальністю
DevOps дайджест #13: Docker поплив, біля Go водяться дракони
DOU Books: 5 книг по програмуванню, які радить Денис Шевченко, Director of Technology в Plarium
Якщо ви не вмієте говорити «ні», то ваше «так» нічого не значить
Якщо ви не вмієте говорити "ні", то ваше "так" нічого не значить