[TOC]

Podstawy http w Go

W bibliotece standardowej Go znajduje się pakiet http, który daje możliwość łatwego pisania serwerów i klientów HTTP.

Prosty klient

Aby napisać program pobierający stronę czy plik udostępniony po HTTP, wystarczy kilka linii kodu, z czego większość to interpretacja parametrów wywołania i obsługa ewentualnych błędów.

package main
import (
    "net/http"
    "flag"
    "fmt"
    "io/ioutil"
    "os"
)
//program właściwy
func main() {
    url, fname := setup()
    //wykonujemy zapytanie
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println(err.String())
        os.Exit(1)
    }
    //czytanie odpowiedzi
    rbody := resp.Body
    buf, rerr := ioutil.ReadAll(rbody)
    rbody.Close()

    if rerr != nil {
        fmt.Println("Cannot read response body")
        fmt.Println(rerr.String())
        os.Exit(1)
    }
    //zapis odpowiedzi do pliku
    ioutil.WriteFile(fname, buf, 0666)
}

//przetwarzanie argumentów i przełączników z linii poleceń
func setup() (url, fname string) {
    //oczekujemy przełącznika -url
    var urlopt = flag.String("url", "", "Resource URL")
    //oczekujemy nazwy pliku do którego zapiszemy pobrany zasób w opcji -o
    var fnameopt = flag.String("o", "out", "Output filename")
    flag.Parse() //przetwarzanie

    fname = *fnameopt
    var urlarg = flag.Arg(0) //url może być argumentem a nie opcją


    //url z argumentu jest ważniejszy niż z przełącznika
    if len(*urlarg) > 0 {
        url = *urlarg
    } else if len(urlopt) > 0 {
        url = urlopt
    } else {
        fmt.Println("No URL provided")
        os.Exit(1)
    }
    return //zwracamy wartości w domyślnych zmiennych
}

Po skompilowaniu:

6g main.go
6l main.6

możemy ściągnąć dowolny zasób z internetu np. tak:

6.out -o index.html http://golang.org.pl

W powyższym kodzie skorzystaliśmy z:

  • http.Get(url string) *http.Response, err os.Error, która wykonuje całą brudną robotę: nawiązuje połączenie, wysyła zapytanie i pobiera zasób.
  • http.Response implementuje interface io.ReadCloser, co umożliwiło skorzystanie z kolejnej metody, bo io.ReadCloser złożenie interfacesów io.Reader i io.Closer
  • io/ioutil.ReadAll(r io.Reader) []byte, err *os.Error, która ułatwia pobieranie danych z bufora
  • io/ioutil.WriteFile(fname string, buf []byte, perm uint32), który zapisuje sekwencje bajtów do pliku o podanej nazwie/ścieżce i prawach.

Prosty server HTTP

@tag{http server} Napisanie bardzo prostego serwera HTTP, też nie wymaga większego wysiłku. Poniżej znajduje się kod serwera, który obsługuje dwa zasoby "/" i "/hello":

package main

import (
    "net/http"
)

func rootHandler(w http.ResponseWriter, req *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`<html>
    <head><title>Prosty serwer</title></head>
    <body>
        <h1>Witaj!</h1>
        <p> To prymitywna strona startowa bardzo prostego serwera HTTP<p>
        <p> Obsługuje jeszcze jeden <a href="/hello">zasób</a> </p>
        <body>

</html>`))

}

func helloHandler(w http.ResponseWriter, req *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`<html>
    <head><title>Hello</title></head>
    <body>
        <h1>Witaj!</h1>
        <p> Strona prezentuje zasób "hello"</p>
        <body>
</html>`))

}

func main() {
    http.HandleFunc("/", rootHandler)
    http.HandleFunc("/hello", helloHandler)
    http.ListenAndServe(":8081", nil)
}

W funkcji main zarejestrowaliśmy dwie funkcje typu type HandlerFunc func(ResponseWriter, *Request), do obsługi konkretnych ścieżek. Następnie wystartowaliśmy serwer na porcie 8081. Po skompilowaniu i uruchomieniu programu możesz zobaczyć efekt za pomocą dowolnego klienta http (np. tego, który jest powyżej).

Powyższy mechanizm pozwala na implementację praktycznie dowolnej usługi HTTP.

Warto wiedzieć, że każde wywołanie funkcji typu http.HandleFunc odbywa się w osobnej goroutine, więc przynajmniej w teorii wydajność serwera powinna rosnąć niemal liniowo, wraz z ilością dostępnych rdzeni procesora, ilości wątków możliwych do uruchomienia na każdym z nich i ilością pamięci.

Nie próbowałem potwierdzać tej teorii, nie mniej moje testy wykazały, że taki serwer jest w stanie udźwignąć 16K żądań na sekundę bez większych problemów - na maszynie czterordzeniowej zużycie procesora było na poziomie 183% (czyli tak jakby 1.83 rdzenia).

Bardzo prosty serwer plikowy

Pakiet http udostępnia kilka innych umilaczy dla serwerowców. Jedną z nich jest handler umożliwiający pisanie prostych serwerów plikowych. Najprostszy serwer plikowy ma za zadanie serwować pliki z jakiegoś drzewa katalogów. Możemy taki napisać w kilku linijkach:

package main

import (
    "net/http"
)
func main() {
    http.Handle("/", http.FileServer(http.Dir("sciezka/do/katalogu/")))
    http.ListenAndServe(":8081", nil)
}

Tak przy okazji, zwróć uwagę na to, że do funkcji ListenAndServe przekazaliśmy nil w drugim parametrze. Zostanie to zinterpretowane jako chęć skorzystania z domyślnego dyspozytora zapytań. Jest to wartość typu http.ServeMux tworzona automatycznie w pakiecie http i dostępna za pomocą zmiennej pakietowej http.DefaultServeMux. Czym są "muxy", jak samodzielnie zarządzać połączeniem i wiele innych kwestii poruszę (kiedyś) w osobnym artykule dla zaawansowanych. Niecierpliwym zaś polecam przestudiowanie kodu źródłowego pliku server.go pakietu net/http.

Nie mniej funkcja http.ListenAndServe w drugim parametrze może przyjąć dowolną wartość spełniającą interface http.Handler, czyli m.in. typ wartości zwracanej z funkcji http.FileServer. Stąd wniosek, że powyższy kod można było by skrócić o pierwsze wywołanie i funkcję main zdefiniować następująco:

func main() {
    http.ListenAndServe(":8081",
        http.FileServer(http.Dir("sciezka/do/katalogu/")))
}

Choć nie zawsze to jest to czego tak naprawdę chcemy. Częściej chcemy, by nasz serwer plikowy działał jako jeden z zasobów. Czyli by pliki z katalogu filesRootDir były dostępne np. pod zasobem "/static/". Ale FileServer interpretuje całe URI jako ścieżkę relatywną do filesRootDir co skutkuje tym, że jeśli nie mamy w katalogu filesRootDir katalogu static to FileServer będzie zwracał 404.

Na szczęście dodano funkcję, która opakowuje oryginalny Handler w taki sposób, że przed wywołaniem handlera właściwego, wycina zbędny przedrostek z URI. Jej zastosowanie możesz prześledzić w poniższym przykładzie:

func main() {
    resourcePath := "static/"
    filesRootDir := "/sciezka/do/katalogu/z/plikami/"

    //tworzymy handler
    fileServHanler := http.FileServer(http.Dir(filesRootDir))

    //opakowujemy go w wycinarkę przedrostków
    strippedHandler := http.StripPrefix(resourcePath, fileServHanler)

    //zarejestrujmy handler w muxie
    http.Handle(resourcePath, strippedHandler)
    //odpalamy serwer
    http.ListenAndServe(":8081", nil)
}