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 для следующих статей.

Похожие статьи:
У многих склонность к точным наукам начинает проявляться еще в детстве. В школе попадаешь в математический класс. С русским языком...
Компания Astell&Kern представила на российском рынке свой новый аксессуар, разработанный специально для флагманского плеера АК 380...
DOU опитав айтівців, які через повномасштабне вторгнення росіян долучилися до Збройних сил України. Коли та як вони ухвалили...
У свіжому дайджесті DOU News обговорюємо європейську Дію, +1 єдиноррога з України, нові правила вступу від МОН, результати...
У перші три місяці війни в Україні зареєстрували майже п’ять тисяч ФОПів з КВЕДом «Комп’ютерне програмування»....
Яндекс.Метрика