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