Go — это просто. Создаем HelloWorld веб-сервер
По многочисленным просьбам читателей DOU публикую первую статью про Go.
Ниже будут раскрыты следующие темы:
- как создать на Go простой веб-сервер, которому не нужны Apache с nginx’ом;
- как добавить поддержку https без использования OpenSSL;
- как перестать платить за TLS-сертификаты и беспокоиться об их своевременном обновлении.
Создаем 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!\n") }
Этот файл использует сторонний пакет — 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 1 connections Thread Stats Avg Stdev Max +/- Stdev Latency 13.25us 24.26us 1.70ms 99.74% Req/Sec 76.71k 4.43k 81.43k 91.09% 771609 requests in 10.10s, 109.64MB read Requests/sec: 76399.78 Transfer/sec: 10.86MB
Через 1000 одновременных подключений:
$ wrk -t 2 -c 1000 http://127.0.0.1:8080/ Running 10s test @ http://127.0.0.1:8080/ 2 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 5.24ms 3.44ms 150.36ms 89.22% Req/Sec 82.85k 11.32k 110.93k 68.18% 1643377 requests in 10.01s, 233.52MB read Requests/sec: 164183.83 Transfer/sec: 23.33MB
Через 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 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.40ms 1.46ms 39.22ms 90.68% Req/Sec 824.20k 75.28k 0.99M 78.50% 16469856 requests in 10.05s, 2.29GB read Requests/sec: 1638496.88 Transfer/sec: 232.83MB
Как видите, простой веб-сервер из пары десятков строчек на Go не требует ни nginx, ни Apache, и может обрабатывать более 1,6 млн запросов в секунду на обычном ноуте трехлетней давности.
Добавляем поддержку https
В стандартную поставку Go входит пакет crypto/tls, с помощью которого можно настраивать https на любой вкус и цвет. За разработку данного пакета отвечает Adam Langley, автор BoringSSL. Несколько фактов про crypto/tls:
- он написан на Go с чистого листа, без использования OpenSSL либо других сторонних TLS-библиотек;
- в crypto/tls находится на порядок меньше дыр по сравнению с количеством уязвимостей в OpenSSL;
- этот пакет активно используется в CloudFlare, которая регулярно вносит в него различные оптимизации и улучшения.
Если у вас уже есть 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!\n") }
Теперь при указании параметров -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!\n") }
Перед компиляцией данного кода понадобится скачать еще один сторонний пакет — 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. Код получается лаконичным и простым, без лишних абстракций и
В статье рассмотрен простейший http-сервер, выдающий «Hello, world!». На Go можно создавать сервера и прокси с намного более сложной логикой. В качестве примера рекомендую оценить простой в использовании http-прокси, балансировщик нагрузки и TLS termination прокси, который также умеет экономить трафик — httptp.
Предлагайте в комментариях темы по Go для следующих статей.