Rozwój serwerów internetowych na Golang — od prostego do skomplikowanego

Rozwój serwerów internetowych na Golang — od prostego do skomplikowanego

Pięć lat temu zacząłem opracowywać Gophish, co dało mi możliwość nauczenia się Golanga. Zrozumiałem, że Go to potężny język, którego możliwości są wzbogacane przez liczne biblioteki. Go jest wszechstronny: w szczególności umożliwia bezproblemowe opracowywanie aplikacji serwerowych.

Ten artykuł poświęcony jest tworzeniu serwera w Go. Zacznijmy od prostych rzeczy, jak „Hello world!”, a skończymy na aplikacji z następującymi funkcjami:

— Użycie Let’s Encrypt do HTTPS.
— Działa jako router API.
— Praca z middleware.
— Obsługa plików statycznych.
— Poprawne zakończenie pracy.

Skillbox poleca: Praktyczny kurs „Python-developer od podstaw”.

Przypominamy: dla wszystkich czytelników „Habra” — zniżka 10 000 rubli przy zapisie na dowolny kurs Skillbox z kodem promocyjnym „Habra”.

Witaj, świecie!

Stworzenie serwera WWW w Go jest bardzo szybkie. Oto przykład użycia handlera, który zwraca obiecane wyżej „Witaj, świecie!”.

package main
 
import (
"fmt"
"net/http"
)
 
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
})
http.ListenAndServe(":80", nil)
}

Po tym, jeśli uruchomisz aplikację i otworzysz stronę localhost, od razu zobaczysz tekst „Witaj, świecie!” (oczywiście, jeśli wszystko działa poprawnie).

Następnie wielokrotnie będziemy korzystać z handlera, ale najpierw zrozummy, jak to wszystko działa.

net/http

W przykładzie użyto pakietu net/http, co jest podstawowym narzędziem w Go do tworzenia zarówno serwerów, jak i klientów HTTP. Aby zrozumieć kod, rozważmy znaczenie trzech ważnych elementów: http.Handler, http.ServeMux i http.Server.

HTTP-handlery

Gdy otrzymujemy zapytanie, handler je analizuje i formułuje odpowiedź. Handlery w Go są zaimplementowane w następujący sposób:

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

W pierwszym przykładzie użyto funkcji pomocniczej http.HandleFunc. Owrapowuje ona inną funkcję, która z kolei przyjmuje http.ResponseWriter i http.Request w ServeHTTP.

Innymi słowy, handlery w Golangu są reprezentowane przez jeden interfejs, co daje programiście liczne możliwości. Na przykład middleware jest wdrażane za pomocą handlera, gdzie ServeHTTP najpierw coś robi, a potem wywołuje metodę ServeHTTP innego handlera.

Jak wspomniano powyżej, handlery po prostu formułują odpowiedzi na zapytania. Ale który konkretnie handler należy użyć w danym momencie?

Trasowanie zapytań

Aby dokonać właściwego wyboru, skorzystaj z HTTP-multiplexer. W wielu bibliotekach nazywa się go muxer lub router, ale wszystko to jest to samo. Funkcja multiplexer polega na analizie ścieżki zapytania i wyborze odpowiedniego obsługującego.

Jeśli jednak potrzebujesz wsparcia dla zaawansowanego routingu, lepiej skorzystać z zewnętrznych bibliotek. Niektóre z najbardziej zaawansowanych to gorilla/mux i go-chi/chi, te biblioteki umożliwiają realizację przetwarzania pośredniego bez większych problemów. Dzięki nim można skonfigurować routing z wildcardami i wykonać szereg innych zadań. Ich zaletą jest kompatybilność ze standardowymi HTTP-handlerami. W rezultacie możesz napisać prosty kod z możliwością jego modyfikacji w przyszłości.

Praca z bardziej złożonymi frameworkami w zwykłej sytuacji wymagać będzie niezbyt standardowych rozwiązań, co znacznie komplikuje korzystanie z domyślnych obsługujących. Do stworzenia zdecydowanej większości aplikacji wystarczy kombinacja domyślnej biblioteki oraz prostego routera.

Obsługa zapytań

Ponadto potrzebujemy komponentu, który będzie 'wsłuchiwał się' w przychodzące połączenia i przekierowywał wszystkie zapytania do odpowiedniego obsługującego. Z tym zadaniem bez trudu poradzi sobie http.Server.

Poniżej widać, że serwer odpowiada za wszystkie zadania związane z obsługą połączeń. Należy do nich na przykład praca z protokołem TLS. Standardowy serwer HTTP jest wykorzystywany do realizacji wywołania http.ListenAndServer.

Teraz przyjrzyjmy się bardziej złożonym przykładom.

Dodawanie Let’s Encrypt

Domyślnie nasze aplikacje działają w protokole HTTP, jednak zaleca się korzystanie z protokołu HTTPS. W Go można to zrobić bez problemów. Jeśli uzyskałeś certyfikat i klucz prywatny, wystarczy wskazać ListenAndServeTLS z odpowiednimi plikami certyfikatu i klucza.

http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)

Zawsze można zrobić lepiej.

Let’s Encrypt oferuje bezpłatne certyfikaty z możliwością automatycznego odnawiania. Aby skorzystać z usługi, potrzebny jest pakiet autocert.

Najłatwiejszym sposobem jego skonfigurowania jest skorzystanie z metody autocert.NewListener w kombinacji z http.Serve. Metoda ta pozwala uzyskiwać i odnawiać certyfikaty TLS, podczas gdy serwer HTTP obsługuje zapytania:

http.Serve(autocert.NewListener("example.com"), nil)

Jeśli otworzymy w przeglądarce example.com, otrzymamy odpowiedź HTTPS „Hello, world!”.

Jeśli potrzebna jest dokładniejsza konfiguracja, warto skorzystać z menedżera autocert.Manager. Następnie tworzymy własną instancję http.Server (dotychczas używaliśmy domyślnej) i dodajemy menedżera do TLSConfig serwera:

m := &autocert.Manager{
Cache:      autocert.DirCache("golang-autocert"),
Prompt:     autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
}
server := &http.Server{
    Addr:      ":443",
    TLSConfig: m.TLSConfig(),
}
server.ListenAndServeTLS("", "")

To prosty sposób na wdrożenie pełnej obsługi HTTPS z automatycznym odnawianiem certyfikatu.

Dodawanie niestandardowych tras

Domyślny router zawarty w standardowej bibliotece jest dobry, ale bardzo prosty. W większości aplikacji potrzebne są bardziej skomplikowane trasy, w tym zagnieżdżone i wildcard, oraz procedura ustawiania szablonów i parametrów ścieżek.

W takim przypadku warto użyć pakietów gorilla/mux i go-chi/chi. Z ostatnim nauczymy się pracować — poniżej znajduje się przykład.

Dane — plik api/v1/api.go, zawierający trasy dla naszego API:

/ HelloResponse is the JSON representation for a customized message
type HelloResponse struct {
Message string `json:"message"`
}
 
// HelloName returns a personalized JSON message
func HelloName(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
response := HelloResponse{
Message: fmt.Sprintf("Hello %s!", name),
}
jsonResponse(w, response, http.StatusOK)
}
 
// NewRouter returns an HTTP handler that implements the routes for the API
func NewRouter() http.Handler {
r := chi.NewRouter()
r.Get("/{name}", HelloName)
return r
}

Ustawiamy dla tras prefiks api/v1 w głównym pliku.

Możemy następnie zamontować go do naszego głównego routera pod prefiksem api/v1 w naszej głównej aplikacji:

// NewRouter returns a new HTTP handler that implements the main server routes
func NewRouter() http.Handler {
router := chi.NewRouter()
    router.Mount("/api/v1/", v1.NewRouter())
    return router
}
http.Serve(autocert.NewListener("example.com"), NewRouter())

Łatwość pracy ze złożonymi trasami w Go umożliwia uproszczenie struktury w obsłudze dużych, złożonych aplikacji.

Praca z middleware

W przypadku przetwarzania pośredniego polega to na owijaniu jednego HTTP-handlera innym, co pozwala szybko przeprowadzić autoryzację, kompresję, logowanie oraz inne funkcje.

Jako przykład rozważmy interfejs http.Handler, którego użyjemy do napisania handlera z autoryzacją użytkowników serwisu.

func RequireAuthentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !isAuthenticated(r) {
            http.Redirect(w, r, "\/login", http.StatusTemporaryRedirect)
            return
        }
        \/\/ Zakładając, że autoryzacja przeszła, uruchom oryginalny handler
        next.ServeHTTP(w, r)
    })
}

Są też zewnętrzne routery, na przykład chi, które pozwalają rozszerzyć funkcjonalność przetwarzania pośredniego.

Praca ze statycznymi plikami

Standardowa biblioteka Go zawiera możliwości pracy ze statycznymi treściami, w tym obrazami oraz plikami JavaScript i CSS. Można uzyskać do nich dostęp za pomocą funkcji http.FileServer. Zwraca ona handler, który udostępnia pliki z określonego katalogu.

func NewRouter() http.Handler {
    router := chi.NewRouter()
    r.Get("/{name}", HelloName)
 
// Konfiguracja serwera plików statycznych
staticPath, _ := filepath.Abs("../../../static/")
fs := http.FileServer(http.Dir(staticPath))
    router.Handle("/*", fs)
    
    return r

Należy pamiętać, że http.Dir wyświetla zawartość katalogu tylko wtedy, gdy nie ma w nim głównego pliku index.html. W takim przypadku, aby uniknąć kompromitacji katalogu, warto użyć pakietu nieindeksowany.

Poprawne zakończenie pracy

W Go istnieje również funkcja, która pozwala na poprawne zakończenie pracy serwera HTTP. Można to zrobić za pomocą metody Shutdown(). Serwer uruchamia się w gorutinie, po czym kanał nasłuchuje sygnału przerwania. Gdy tylko sygnał zostanie odebrany, serwer się wyłącza, jednak nie od razu, a po kilku sekundach.

handler := server.NewRouter()
srv := &http.Server{
    Handler: handler,
}
 
go func() {
srv.Serve(autocert.NewListener(domains...))
}()
 
// Czekaj na przerwanie
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
 
// Próba poprawnego zamknięcia
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)

Na zakończenie

Go to potężny język z niemal uniwersalną standardową biblioteką. Jego domyślne możliwości są szerokie, a można je wzmocnić za pomocą interfejsów — co pozwala na tworzenie naprawdę niezawodnych serwerów HTTP.

Skillbox poleca:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster