Важные концепции обработчиков веб-серверов в Golang

После знакомства с множеством разработчиков Golang на проектах или во время собеседований меня всегда интересовала одна вещь. Многие разработчики не знают, как на самом деле работают веб-серверы в Golang.

Когда разработчик Golang начинает с объяснения мне, что такое веб-сервер, рассказывая мне о Gin, Gorilla Mux или Iris, я знаю, что отсутствуют некоторые основы, а точнее некоторые основы пакета net / http.

Означает ли это, что если вы полностью понимаете концепции Handlers, ResponseWriter и т. д., Вам не следует использовать инструменты или фреймворки? Точно нет! Но если вы хотите полностью понять, что вы делать с этими инструментами, вам необходимо понимать основы веб-сервера Golang.

Вот почему я решил сделать простое объяснение, слой за слоем, того, как работает обработка в net / http, с примерами.

Прежде всего, обработка HTTP-запросов – это не концепция Go. Это концепция HTTP: https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview#components_of_http-based_systems .

ListenAndServe, сервер запускается…

func main () { 
   http. ListenAndServe (": 8080", nil) 
}

Функция ListenAndServe – это точка входа на ваш сервер. Вы можете дать этой функции адрес и / или порт для прослушивания и обработки (я объясню это позже).
Использование «http.ListenAndServe» заставляет вас использовать «конфигурацию сервера по умолчанию»: DefaultServeMux . ListenAndServe Doc

Но наиболее эффективный способ запустить ваш веб-сервер – определить собственный объект http.Server, чтобы не использовать DefaultServeMux . Сделать это можно так:

func main() {
   customServer := http.Server{
      Addr:              ":8080",
      ReadTimeout:       30 * time.Second,
      ReadHeaderTimeout: 30 * time.Second,
      Handler:           nil,
   }
   customServer.ListenAndServe()
}

В обоих случаях мы видим, что мы установили значение Handler равным нулю . Это означает, что мы используем обработчик Go по умолчанию.

Обработка запросов…

Давайте теперь посмотрим, как мы обрабатываем HTTP-запросы с помощью обработчиков.

Основной способ обработки запроса – использовать функцию HandleFunc из пакета http. Это повлияет на функцию, принимающую http.ResponseWriter и http.Request на строковый URL-адрес.
Делая это, мы создаем обработчик для определенного маршрута и передаем его серверу go по умолчанию DefaultServeMux.

func defaultMuxHandler(writer http.ResponseWriter, request *http.Request) {
   json.NewEncoder(writer).Encode("default")
}
func main() {
   http.HandleFunc("/default", defaultMuxHandler)
   http.ListenAndServe(":8080", nil)
}

Но давайте посмотрим, что на самом деле происходит за кулисами, давайте разберемся, почему и как эта функция, предоставленная HandleFunc, обрабатывает http-запросы.

Вот код из пакета net / http, который мы объясним:

// The definition of the http.HandleFunc function
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
   DefaultServeMux.HandleFunc(pattern, handler)
}
// The method called by the function above that make the user custom function an HandlerFunc type implementing the ServeHTTP method
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
   if handler == nil {
      panic("http: nil handler")
   }
   mux.Handle(pattern, HandlerFunc(handler))
}
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
   f(w, r)
}

Функция http.HandleFunc вызывает HandleFunc класса DefaultServeMux.
Затем DefaultServeMux.HandleFunc берет обработчик (наша настраиваемая функция) и делает его типом HandlerFunc.
Это возможно, потому что обработчик имеет ту же сигнатуру, что и тип HandlerFunc. Тип HandlerFunc – это адаптер, позволяющий использовать обычные функции в качестве обработчиков HTTP. Этот тип реализует интерфейс Handler путем реализации функции ServeHTTP .

Так функция, которую мы определили, стала обработчиком, способным обрабатывать http-запросы для нашего сервера. (Я рекомендую вам позже посмотреть, как net / http использует функцию ServeHTTP).

Итак, мы только что показали, что для создания собственного обработчика нам нужно создать тип, реализующий интерфейс обработчика с функцией ServeHTTP.

Создадим свой Handler, разберемся, как его использовать и какие преимущества:

// Implementation of our own struct that is our Handler
type CustomHandler struct {
}
// The implementaiton of the ServeHTTP function that handle http requests
func (c CustomHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request){
   if request.URL.Path == "/custom" {
      json.NewEncoder(writer).Encode("custom")
   } else {
      writer.WriteHeader(http.StatusNotFound)
   }
}
func main() {
   customHandler := CustomHandler{}
   http.ListenAndServe(":8080", customHandler)
}

Наш собственный обработчик CustomHandler – это структура, реализующая интерфейс Handler путем определения функции ServeHTTP . Эта функция является отправной точкой для наших запросов.

Затем мы передаем реализацию нашего настраиваемого обработчика http.ListenAndServe . Например, эта функция проверяет, является ли базовый путь URL-адреса запроса «/ custom», в противном случае она возвращает ответ http 404.
На этом уровне вы можете полностью контролировать свои правила маршрутизации, коды ответов или любые проверки . Например, мы могли бы создать другой пользовательский обработчик для обработки всего, что отвечает на проверку пути «/ custom». Полный пример кода приведен в конце этой статьи.

На этом этапе мы используем наш пользовательский обработчик с сервером Golang по умолчанию. Но, как мы обсуждали ранее, наиболее эффективный способ – использовать наш Handler с нашим собственным экземпляром Server :

func main() {
   customHandler := CustomHandler{}
   customServer := http.Server{
      Addr:              ":8080",
      ReadTimeout:       30 * time.Second,
      ReadHeaderTimeout: 30 * time.Second,
      Handler:           customHandler,
   }
   customServer.ListenAndServe()
}

Теперь наш сервер будет обслуживать наш пользовательский обработчик.

Вот как работает основа обработки запросов в Golang с простыми обработчиками, которые можно определить с помощью ServeHTTP.

Мы можем использовать все концепции, описанные выше, для разработки полноценного веб-сервера с полным контролем над обработкой HTTP-запросов.

Вот полный пример базового веб-сервера с настраиваемыми обработчиками:

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"path"
	"strings"
	"time"
)

// Get the BasePath of the url which is the first word
// and the following path value
func getPaths(url string) (basepath string, nextpath string) {
	cleanPath := path.Clean(url)[1:]
	basePath := strings.Split(cleanPath, "/")[0]
	return basePath, cleanPath[len(basePath):]
}

type FoodHandler struct {
	mangoHandler     *MangoHandler
	pineappleHandler *PineappleHandler
}

// Function to implement the http Handler interface
func (handler *FoodHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request)  {
	basePath, nextPath := getPaths(request.URL.Path)

	// Allow a full control over the redirection on the basepath
	// Then every ServeHTTP will have the same full control over the request
	request.URL.Path = nextPath
	switch basePath {
	case "mango":
		handler.mangoHandler.ServeHTTP(writer, request)
	case "pineapple":
		handler.pineappleHandler.ServeHTTP(writer, request)
	case "":
		// Allow a full control over the response per HTTP Method for this basepath
		// Could use a switch case here with a function per HTTP Method
		switch request.Method {
		case http.MethodGet:
			writer.Header().Set("Content-Type", "application/json; charset=utf-8")
			json.NewEncoder(writer).Encode(map[string]interface{}{"message": "home page"})
		case http.MethodPost:
			// [...]
		case http.MethodDelete:
			// [...]
		default:
			writer.WriteHeader(http.StatusMethodNotAllowed)
			writer.Header().Set("Content-Type", "application/json; charset=utf-8")
			json.NewEncoder(writer).Encode(map[string]interface{}{"message": "wrong http method"})
		}
	default:
		writer.WriteHeader(http.StatusNotFound)
		writer.Header().Set("Content-Type", "application/json; charset=utf-8")
		json.NewEncoder(writer).Encode(map[string]interface{}{"message": "route matches no host"})
	}
}

type PineappleHandler struct {}

func (handler *PineappleHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	// Deal with Pineapple Handler
	// [...]
}

type MangoHandler struct {}

func (handler *MangoHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	// Deal with Mango Handler
	// [...]
}

func main() {
	foodHandler := &FoodHandler{}
	server := http.Server{
		Handler: foodHandler,
		Addr: ":8080",
		ReadTimeout: 5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
	log.Fatal(server.ListenAndServe())
}

Теперь, когда вы полностью понимаете, как Go обрабатывает HTTP-запросы, вы можете понять, как работает ваш любимый веб-фреймворк или библиотека, и каковы могут быть преимущества и недостатки использования этих инструментов.

Ответить