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:

Если у вас уже есть 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. Код получается лаконичным и простым, без лишних абстракций и xml-конфигов.

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

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

Похожие статьи:
Savvy IT School приглашает на курсы для начинающих программистов по специальности Frontend Developer. Для кого эта программа? Для начинающих и тех,...
Пол Гончар — айтишник с 20-летним опытом, большую часть которого получил в США. 9 лет работает в Apple, сейчас — Senior Domain Engineer. Мы решили...
Почитать Фейсбуковская компания Parse выложила в открытый доступ свои SDK. Пост о том, как строить современные библиотеки. Длинная...
Меня зовут Максим, я работаю тестировщиком ПО, с интересом слежу за событиями в мире тестирования и IT. Самое полезное собираю...
Представляем обновленный список 25 крупнейших IT-работодателей Украины. За период с июля 2015 по январь 2016 суммарное количество...
Яндекс.Метрика