Obsah
1. Vývoj síťových aplikací v programovacím jazyku Go
2. Jednoduchý klient přijímající libovolnou sekvenci bajtů
3. Server vracející počet realizovaných připojení
4. Server, který dokáže obsloužit větší množství klientů
5. Server posílající textové zprávy
6. Klient pracující v textovém režimu
7. Získání IP adresy pro specifikované hostname
8. Další funkce používané v souvislosti s IP adresami
10. Poslání HTTP dotazu a základní zpracování odpovědi serveru
11. Zobrazení hlaviček vrácených v odpovědi HTTP serveru
12. Vytvoření jednoduchého HTTP serveru
13. Server s měnitelným stavem a problémy, které to způsobí
14. Role mutexů v programovacím jazyce Go
15. Úprava HTTP serveru takovým způsobem, aby používal mutexy
16. HTTP server nabízející statický obsah (soubory)
17. Kombinovaný HTTP server poskytující statický i dynamický obsah
18. Repositář s demonstračními příklady
1. Vývoj síťových aplikací v programovacím jazyku Go
Již v úvodním článku seriálu o programovacím jazyku Go jsme si řekli, že po představení stabilní verze Go nastala poměrně zajímavá a vlastně i neočekávaná situace – tvůrci Go totiž předvídali spíše přechod programátorů používajících jazyky C a C++ ke Go, ovšem programovací jazyk Go a jeho možnosti objevili programátoři, kteří psali své síťové a speciálně pak webové aplikace v Pythonu, Ruby či Java/TypeScriptu (Node.js). Přepis do jazyka Go mnohdy znamenal řádový a někdy i dvouřádový (100×) nárůst výkonu těchto aplikací. Do jisté míry je nárůst výkonu způsoben překladem do nativního kódu, ovšem nesmíme zapomenout na gorutiny, které nejsou (na rozdíl od klasických vláken) příliš náročné na paměť, takže se můžeme setkat s aplikacemi, v nichž bez větších problémů běží stovky či dokonce tisíce gorutin (ostatně benchmark z předchozího článku vytvořil až 4097 gorutin, a to bez jakéhokoli viditelného problému).
Dnes se seznámíme se základními balíčky určenými právě pro tvorbu síťových aplikací – jednoduchých a jednorázových utilit, ale například i HTTP serverů (což je užitečné téma, kterému se budeme věnovat od dvanácté kapitoly). Samozřejmě se nebude jednat pouze o popis balíčků určených pouze pro práci se síťovými protokoly, protože typické síťové aplikace vyžadují i další funkcionalitu – zpracování formátů JSON a XML (čtení i zápis), kódování Base64, kryptografické algoritmy atd. atd.
2. Jednoduchý klient přijímající libovolnou sekvenci bajtů
Síťové aplikace se vytváří na různých úrovních abstrakce – buď se pouze otevřou připojení (například s využitím Unix socketů) a následný komunikační protokol je naprogramován přímo vývojářem, nebo se naopak využije nějaký již existující protokol na vyšší síťové vrstvě (HTTP atd.). Nejprve si ukážeme komunikaci mezi jednoduchým klientem a serverem na nižší úrovni, kdy náš komunikační protokol (na aplikační úrovni) bude spočívat v přenosu jediného bajtu přes TCP, popř. přes UDP.
Samotná implementace klienta bude poměrně přímočará a bude se skládat z těchto kroků:
- Navázání připojení s využitím konstruktoru net.Dial(protokol, adresa), který je popsán na adrese https://golang.org/pkg/net/#Dial. Použitý protokol je specifikován řetězcem; konkrétně se může jednat o konstanty „tcp“, „tcp4“, „tcp6“, „udp“, „udp4“, „udp6“, „ip“, „ip4“, „ip6“, „unix“, „unixgram“ popř. „unixpacket“. V příkladu zvolíme „tcp“, který bude funkční v sítích s IPv4 i IPv6 (nebo pochopitelně v kombinovaných sítích).
- Přečtení n bajtů metodou Read(b []byte) (n int, err error) (konkrétní příjemce se liší podle toho, jaké připojení jsme deklarovali v konstruktoru, ovšem tato metoda bude vždy podporována). Povšimněte si, že této metodě je nutné předat řez (slice) a nikoli pole bajtů (to je nekompatibilní datový typ). Z tohoto důvodu v našem příkladu použijeme trik pole[:], kterým se vytvoří řez ukazující na celé pole (přesněji řez bude ukazovat na první prvek pole a jeho délka bude odpovídat délce pole).
- Přečtené pole bajtů se následně vytiskne, přičemž server implementovaný v rámci další kapitoly je naprogramován takovým způsobem, aby posílal jen jediný bajt.
Úplný zdrojový kód tohoto příkladu naleznete na adrese https://github.com/tisnik/go-fedora/blob/master/article11/01_simple_client.go:
package main import ( "fmt" "net" ) func main() { conn, err := net.Dial("tcp", "localhost:1234") if err != nil { println("Connection refused!") } else { var b [1]byte n, err := conn.Read(b[:]) if err != nil { println("No response!") } else { if n == 1 { fmt.Printf("Received %d byte: %v\n", n, b) } else { fmt.Printf("Received %d bytes: %v\n", n, b) } } } }
O realizovaném připojení si můžeme zjistit i další informace, například lokální i vzdálenou adresu s využitím metod Conn.LocalAddr() a Conn.RemoteAddr(). Tyto adresy převedeme do tisknutelného tvaru metodou String(). Upravený klient může vypadat následovně:
package main import ( "fmt" "net" ) func main() { conn, err := net.Dial("tcp", "localhost:1234") if err != nil { println("Connection refused!") } else { fmt.Printf("Connection established\n") fmt.Printf("Remote Address: %s \n", conn.RemoteAddr().String()) fmt.Printf("Local Address: %s \n", conn.LocalAddr().String()) var b [1]byte n, err := conn.Read(b[:]) if err != nil { println("No response!") } else { if n == 1 { fmt.Printf("Received %d byte: %v\n", n, b) } else { fmt.Printf("Received %d bytes: %v\n", n, b) } } } }
Příklad výsledku po připojení klienta k serveru popsanému v další kapitole:
Connection established Remote Address: 127.0.0.1:1234 Local Address: 127.0.0.1:38082 Received 1 byte: [0]
3. Server vracející počet realizovaných připojení
Nyní si ukažme implementaci serveru, na který se budou připojovat výše popsaní klienti. Implementace serveru je nepatrně složitější než implementace klienta, a to z toho důvodu, že server musí obsloužit větší množství klientů. V tom nejjednodušším případě použijeme „neforkujícího“ klienta, který bude implementován následujícím způsobem:
- Použijeme konstruktor net.Listen(), v němž opět specifikujeme protokol (viz předchozí kapitolu) a síťové rozhraní s portem 1234.
- S využitím příkazu defer zajistíme, že se při ukončení funkce main automaticky uzavře i otevřený port.
- Dále v nekonečné smyčce budeme čekat na připojení v metoděAccept. Jakmile se nějaký klient pokusí o připojení, vrátí tato metoda strukturu implementující mj. metody Read a Write. A právě s využitím metody Write pošleme klientovi jediný bajt obsahující hodnotu počitadla dotazů.
- Spojení se automaticky ukončí díky použití příkazu defer l.Close()
Úplný zdrojový kód tohoto příkladu vypadá následovně:
package main import ( "net" ) func main() { cnt := byte(0) l, err := net.Listen("tcp", "localhost:1234") if err != nil { println("Can't open the port!") } defer l.Close() for { conn, err := l.Accept() defer conn.Close() if err != nil { println("Connection refused!") } else { var b = []byte{cnt} cnt++ conn.Write(b) } } }
Prakticky je možné při otevírání portu vynechat část se síťovým rozhraním, takže se skript nepatrně zjednoduší:
package main import ( "net" ) func main() { cnt := byte(0) l, err := net.Listen("tcp", ":1234") if err != nil { println("Can't open the port!") } defer l.Close() for { conn, err := l.Accept() defer conn.Close() if err != nil { println("Connection refused!") } else { var b = []byte{cnt} cnt++ conn.Write(b) } } }
Ještě si vyzkoušejme, jak se bude server chovat ve chvíli, kdy bude vyřízení požadavku trvat dlouhou dobu. Upravený zdrojový kód serveru bude vypadat takto:
package main import ( "net" "time" ) func main() { cnt := byte(0) l, err := net.Listen("tcp", "localhost:1234") if err != nil { println("Can't open the port!") } defer l.Close() for { conn, err := l.Accept() println("connection accepted") time.Sleep(2 * time.Second) defer conn.Close() if err != nil { println("Connection refused!") } else { var b = []byte{cnt} cnt++ conn.Write(b) } println("connection closed") } }
V tomto případě, nezávisle na počtu spuštěných klientů, bude server vždy zpracovávat požadavek od jediného klienta:
connection accepted connection closed connection accepted connection closed
4. Server, který dokáže obsloužit větší množství klientů
Kostra předchozího serveru dokázala obsloužit v daný okamžik pouze jediného klienta. Je tomu tak z toho důvodu, že metoda l.Accept() skončila ve chvíli, kdy se k serveru připojí jediný klient a následně se musí ukončit celé tělo smyčky, aby se l.Accept() zavolala znovu:
conn, err := l.Accept() defer conn.Close() if err != nil { println("Connection refused!") } else { var b = []byte{cnt} cnt++ conn.Write(b) }
Nic nám ovšem nebrání, aby se vlastní obsluha klienta provedla v gorutině běžící paralelně s hlavní gorutinou. Na obslužnou gorutinu nikde nečekáme, takže se další volání l.Accept() provede velmi rychle:
conn, err := l.Accept() if err != nil { println("Connection refused!") } else { println("Connected") go func(c net.Conn) { var b = []byte{cnt} cnt++ c.Write(b) c.Close() }(conn) }
Podívejme se nyní na úplný zdrojový kód tohoto příkladu:
package main import ( "net" ) func main() { cnt := byte(0) l, err := net.Listen("tcp", "localhost:1234") if err != nil { println("Can't open the port!") } defer l.Close() for { conn, err := l.Accept() if err != nil { println("Connection refused!") } else { println("Connected") go func(c net.Conn) { var b = []byte{cnt} cnt++ c.Write(b) c.Close() }(conn) } } }
5. Server posílající textové zprávy
Přenosový protokol implementovaný předchozími verzemi serveru byl skutečně velmi primitivní, proto si nyní ukážeme nepatrně složitější implementaci serveru, který klientovi pošle textový řádek s ukončujícím znakem \n na konci. To je možné zajistit snadno, protože hodnota vrácená funkcí l.Accept() (l je například typ TCPListener, UnixListenr apod., obecným typem je Listener) reprezentuje handle souboru, do něhož je možné zapsat řetězec například funkcí fmt.Fprintf:
fmt.Fprintf(c, "Holla\n")
Po poslání řetězce se spojení ihned ukončí:
c.Close()
Celá obsluha klienta je samozřejmě realizována ve vlastní gorutině:
for { conn, err := l.Accept() if err != nil { println("Connection refused!") } go func(c net.Conn) { fmt.Fprintf(c, "Holla\n") c.Close() }(conn) }
Alternativně (což je možná korektnější) by bylo zapsat gorutinu s využitím příkazu defer:
go func(c net.Conn) { defer c.Close() fmt.Fprintf(c, "Holla\n") }(conn)
conn, err := l.Accept() defer conn.close() if err != nil { println("Connection refused!") } go func(c net.Conn) { fmt.Fprintf(c, "Holla\n") }(conn)
Zdrojový kód serveru naleznete na adrese https://github.com/tisnik/go-fedora/blob/master/article11/05_text_client.go:
package main import ( "fmt" "net" ) func main() { l, err := net.Listen("tcp", "localhost:1234") if err != nil { println("Can't open the port!") } defer l.Close() for { conn, err := l.Accept() if err != nil { println("Connection refused!") } go func(c net.Conn) { fmt.Fprintf(c, "Holla\n") c.Close() }(conn) } }
Předností je, že tento server můžeme otestovat s využitím běžného Telnetu:
$ telnet localhost 1234 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Holla Connection closed by foreign host.
6. Klient pracující v textovém režimu
Úprava klienta takovým způsobem, aby akceptoval celý textový řádek popř. větší množství řádků, je nepatrně složitější, než tomu bylo u serveru, protože je nutné nějakým způsobem „rozluštit“ sekvenci bajtů poslaných serverem. Můžeme zde využít například již známý balíček bufio, s nímž jsme se seznámili v předchozím článku v rámci popisu bufferování standardního výstupu. Nyní ovšem použijeme metodu Reader.ReadString(), které se předá znak používaný pro ukončení jednotlivých záznamů. A v našem případě je záznamem celý textový řádek, takže oddělovačem bude znak \n:
status, err := bufio.NewReader(conn).ReadString('\n')
Tato metoda vrátí příznak chyby v případě, že celá sekvence nekončí oddělovacím/ukončovacím znakem.
Podívejme se nyní na úplný zdrojový kód klienta, který naleznete na adrese https://github.com/tisnik/go-fedora/blob/master/article11/06_text_server.go:
package main import ( "bufio" "fmt" "net" ) func main() { conn, err := net.Dial("tcp", "localhost:1234") if err != nil { println("Connection refused!") } else { fmt.Fprintf(conn, "Hello") status, err := bufio.NewReader(conn).ReadString('\n') println(status, err) if err != nil { println("No response!") } else { println(status) } } }
7. Získání IP adresy pro specifikované hostname
V balíčku net nalezneme kromě dalších užitečných věcí i definici datového typu pojmenovaného příhodně IP. Interně se jedná o řez bajtů, který reprezentuje IP adresu, ať již podle standardu IPv4 nebo IPv6. IP adresy je možné vytisknout čitelným způsobem díky existenci metody String definované právě pro typ IP:
func (ip IP) String() string
Dále se nám může v aplikacích hodit funkce pojmenovaná LookupIP, které se předá řetězec obsahující například doménové jméno a návratovými hodnotami je dvojice: řez s prvky typu IP a příznak chyby. Řez se vrací z toho důvodu, že doménovému jménu může odpovídat více IP adres – typicky jedna IPv4 a jedna IPv6.
Opět si ukažme přímočarý způsob použití funkce LookupIP v následujícím příkladu:
package main import ( "fmt" "net" ) func performLookup(address string) { ip, err := net.LookupIP(address) if err != nil { println("Lookup failure") } else { fmt.Printf("%v\n", ip) } } func main() { performLookup("localhost") performLookup("root.cz") performLookup("google.com") }
Po překladu a spuštění tohoto příkladu by se na standardním výstupu měly objevit tyto informace:
[127.0.0.1] [91.213.160.188 2001:67c:68::76] [172.217.19.110 2a00:1450:400d:804::200e]
8. Další funkce používané v souvislosti s IP adresami
Další užitečnou funkcí z balíčku net je funkce nazvaná ParseIP, které se předá řetězec s různými reprezentacemi IP adresy a výsledkem je hodnota typu IP (řez bajtů). Tato funkce je užitečná především pro IPv6 adresy, které se mnohdy zapisují zkráceným způsobem, například „2041:0000:140F::875B:131B“ namísto „2041:0000:140F:0000:0000:0000:875B:131B“ (v prvním případě je použita „čtyřtečka“), ve druhém jsou explicitně zapsány všechny octety.
Tuto funkci si vyzkoušíme snadno:
package main import ( "fmt" "net" ) func parseIP(address string) { ip := net.ParseIP(address) if ip == nil { println("ParseIP failure") } else { fmt.Printf("%v\n", ip) } } func main() { parseIP("127.0.0.1") parseIP("1000::68") parseIP("fe80::224:d7ff:fe83:1f28") parseIP("fe80:0000:0000:0000:224:d7ff:fe83:1f28") parseIP("fe80:0000:0000:0000:0000:0000:0000:0001") }
Výsledky by měly vypadat takto:
127.0.0.1 1000::68 fe80::224:d7ff:fe83:1f28 fe80::224:d7ff:fe83:1f28 fe80::1
A v posledním příkladu tohoto typu je ukázáno použití konstruktoru IPv4, kterému se předá čtveřice bajtů a výsledkem je hodnota typu IP, tj. plnohodnotná IP adresa:
package main import ( "fmt" "net" ) func main() { fmt.Printf("%v\n", net.IPv4(127, 0, 0, 1)) fmt.Printf("%v\n", net.IPv4(192, 168, 10, 3)) }
Příklad výstupu:
127.0.0.1 192.168.10.3
9. Balíček http
Konečně se dostáváme k popisu balíčku net/http. Jak již název tohoto balíčku napovídá, obsahuje funkce a nové datové typy určené pro práci s protokolem HTTP, a to jak pro klienty, kteří posílají dotazy (request) na servery, tak i pro implementaci vlastních serverů zpracovávajících dotazy a vytvářejících odpovědi (response). Ostatně právě existence tohoto balíčku měla poměrně velký vliv na oblíbenost programovacího jazyka Go pro tvorbu síťově orientovaných aplikací.
10. Poslání HTTP dotazu a základní zpracování odpovědi serveru
První příklad použití balíčku net/http, s nímž se dnes seznámíme, bude volat funkci pojmenovanou Get, která posílá na zvolený server (a jeho endpoint) dotaz HTTP metodou GET. Použití této funkce je velmi snadné – funkci se pouze předá celá URL a výsledkem je hodnota typu Response a příznak chyby:
response, err := http.Get("http://httpbin.org/uuid")
Hodnota typu Response obsahuje mj. i stavový kód (200, 301, 400, 404 atd. atd.), stavový kód reprezentovaný řetězcem („200 OK“) a taktéž tělo odpovědi (body). S obsahem těla odpovědi můžeme pracovat jako s datovým tokem, takže ho můžeme přečíst funkcí ioutil.ReadAll, převést výsledek na řetězec a ten vytisknout:
raw_body, err := ioutil.ReadAll(response.Body) body := string(raw_body) println(body)
Příklad použití funkce Get je ukázán v následujícím demonstračním příkladu:
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { response, err := http.Get("http://httpbin.org/uuid") if err != nil { println("Connection refused") } defer response.Body.Close() fmt.Printf("Status: %s\n", response.Status) fmt.Printf("Content length: %d\n", response.ContentLength) raw_body, err := ioutil.ReadAll(response.Body) if err != nil { println("Response body can't be read") } else { body := string(raw_body) println(body) } }
Po spuštění tohoto příkladu by se na standardním výstupu měly objevit zhruba tyto řádky (samotné UUID bude ovšem takřka se stoprocentní jistotou odlišné):
Status: 200 OK Content length: 53 { "uuid": "2dc037d0-fec2-4101-9b64-cb93e2e02bf9" }
11. Zobrazení hlaviček vrácených v odpovědi HTTP serveru
Předchozí příklad si můžeme nepatrně vylepšit tak, že vytiskneme i všechny hlavičky, které jsou poslány společně s odpovědí serveru:
for name, headers := range response.Header { name = strings.ToLower(name) for _, h := range headers { fmt.Printf("%v: %v\n", name, h) } }
Následuje výpis zdrojového kódu tohoto příkladu:
package main import ( "fmt" "net/http" "strings" ) func main() { response, err := http.Get("http://httpbin.org/uuid") if err != nil { println("Connection refused") } defer response.Body.Close() fmt.Printf("Status: %s\n", response.Status) fmt.Printf("Content length: %d\n", response.ContentLength) for name, headers := range response.Header { name = strings.ToLower(name) for _, h := range headers { fmt.Printf("%v: %v\n", name, h) } } }
Po překladu a spuštění příkladu získáme tento výstup obsahující jak stavový kód HTTP (200 OK), tak i délku těla odpovědi a všechny hlavičky:
Status: 200 OK Content length: 53 access-control-allow-credentials: true via: 1.1 vegur connection: keep-alive server: gunicorn/19.9.0 date: Sun, 10 Feb 2019 20:12:21 GMT content-type: application/json content-length: 53 access-control-allow-origin: *
12. Vytvoření jednoduchého HTTP serveru
Nejužitečnější vlastností balíčku net/http je jeho podpora pro vytvoření skutečného HTTP serveru, a to doslova na několika řádcích programového kódu. Základem je funkce HandleFunc, která nám umožňuje zaregistrovat obslužnou funkci (handler) v případě, že je server volán s určitým URL (endpointem). Můžeme si například zaregistrovat handler pro endpoint /:
http.HandleFunc("/", mainEndpoint)
Hlavička funkce HandleFunc vypadá takto:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
Povšimněte si, že druhým parametrem této funkce je jiná funkce (onen handler) s hlavičkou:
func MujHandler(ResponseWriter, *Request)
Konkrétně může implementace našeho handleru poslat na výstup (typu ResponseWriter) jednoduchý text, který bude zaslán klientovi v celé HTTP odpovědi (s hlavičkami, stavovým kódem, délkou atd. atd.):
func mainEndpoint(writer http.ResponseWriter, request *http.Request) { io.WriteString(writer, "Hello world!\n") }
Následně již stačí server spustit na určeném portu:
http.ListenAndServe(":8000", nil)
Úplná implementace jednoduchého HTTP serveru může vypadat takto:
package main import ( "io" "net/http" ) func mainEndpoint(writer http.ResponseWriter, request *http.Request) { io.WriteString(writer, "Hello world!\n") } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
K otestování tohoto příkladu využijeme například utilitku curl:
$ curl localhost:8000 Hello world!
Popř. pro podrobnější výstup:
$ curl -v localhost:8000 * Rebuilt URL to: localhost:8000/ * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8000 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8000 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 13 Feb 2019 19:31:17 GMT < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 < Hello world! * Connection #0 to host localhost left intact
13. Server s měnitelným stavem a problémy, které to způsobí
V další implementaci serveru jsou zaregistrovány dva handlery pro endpointy / a /counter:
http.HandleFunc("/", mainEndpoint) http.HandleFunc("/counter", counterEndpoint)
Druhý handler vrátí klientovi obsah počitadla požadavků (počitadlo je postupně zvyšováno o jedničku):
var counter int func counterEndpoint(writer http.ResponseWriter, request *http.Request) { counter++ fmt.Fprintf(writer, "Counter: %d\n", counter) }
Následuje výpis zdrojového kódu tohoto příkladu:
package main import ( "fmt" "io" "net/http" ) var counter int func mainEndpoint(writer http.ResponseWriter, request *http.Request) { io.WriteString(writer, "Hello world!\n") } func counterEndpoint(writer http.ResponseWriter, request *http.Request) { counter++ fmt.Fprintf(writer, "Counter: %d\n", counter) } func main() { http.HandleFunc("/", mainEndpoint) http.HandleFunc("/counter", counterEndpoint) http.ListenAndServe(":8000", nil) }
Opět si ukažme příklad odpovědí serveru při posílání požadavků nástrojem curl:
$ curl localhost:8000 Hello world! $ curl localhost:8000/counter Counter: 1 $ curl localhost:8000/counter Counter: 2 $ curl localhost:8000/counter Counter: 3
14. Role mutexů v programovacím jazyce Go
Ve skutečnosti ovšem není předchozí příklad naprogramován korektně, protože jednotlivé dotazy klientů jsou automaticky zpracovávány v samostatných gorutinách (tudíž paralelně) a počitadlo je „pouze“ proměnná typu int. Paralelní přístup k této proměnné by tedy měl být synchronizován. K tomu využijeme takzvané mutexy z balíčku sync.
Vytvoření mutexu:
var mutex = &sync.Mutex{}
V handleru se přístup k proměnné counter, včetně jejího zvýšení o jedničku, provede uvnitř mutexu, který zajistí přístup pouze jediné gorutiny v daném okamžiku:
func counterEndpoint(writer http.ResponseWriter, request *http.Request) { mutex.Lock() counter++ fmt.Fprintf(writer, "Counter: %d\n", counter) mutex.Unlock() }
15. Úprava HTTP serveru takovým způsobem, aby používal mutexy
Korektně naprogramovaný server s počitadlem požadavků (tedy se stavem) bude vypadat takto:
package main import ( "fmt" "io" "net/http" "sync" ) var counter int var mutex = &sync.Mutex{} func mainEndpoint(writer http.ResponseWriter, request *http.Request) { io.WriteString(writer, "Hello world!\n") } func counterEndpoint(writer http.ResponseWriter, request *http.Request) { mutex.Lock() counter++ fmt.Fprintf(writer, "Counter: %d\n", counter) mutex.Unlock() } func main() { http.HandleFunc("/", mainEndpoint) http.HandleFunc("/counter", counterEndpoint) http.ListenAndServe(":8000", nil) }
Otestování základní funkcionality:
$ for i in {1..10};do curl localhost:8000/counter;done Counter: 1 Counter: 2 Counter: 3 Counter: 4 Counter: 5 Counter: 6 Counter: 7 Counter: 8 Counter: 9 Counter: 10
16. HTTP server nabízející statický obsah (soubory)
V případě, že potřebujeme vytvořit HTTP server, který bude klientům posílat statický obsah (nějaké soubory), můžeme implementaci napsat doslova na několik programových řádků, jak je to ostatně patrné i při pohledu na zdrojový kód dnešního předposledního demonstračního příkladu. Ve volané funkci http.FileServer(http.Dir()) lze specifikovat adresář se statickým obsahem:
package main import ( "net/http" ) func main() { http.Handle("/", http.FileServer(http.Dir(""))) http.ListenAndServe(":8000", nil) }
Po spuštění tohoto serveru:
$ go run 15_file_server.go
Lze získat seznam souborů:
$ curl localhost:8000 <pre> <a href="01B_simple_client_headers.go">01B_simple_client_headers.go</a> <a href="01_simple_client.go">01_simple_client.go</a> <a href="02B_simple_server_no_localhost.go">02B_simple_server_no_localhost.go</a> <a href="02_simple_server.go">02_simple_server.go</a> <a href="03_slow_server.go">03_slow_server.go</a> <a href="04_multi_connection_server.go">04_multi_connection_server.go</a> <a href="05_text_client.go">05_text_client.go</a> <a href="06_text_server.go">06_text_server.go</a> <a href="07_lookup.go">07_lookup.go</a> <a href="08_parse_ip.go">08_parse_ip.go</a> <a href="09_ipv4_constructor.go">09_ipv4_constructor.go</a> <a href="10_http_get.go">10_http_get.go</a> <a href="11_http_print_headers.go">11_http_print_headers.go</a> <a href="12_http_server.go">12_http_server.go</a> <a href="13_http_server_with_state.go">13_http_server_with_state.go</a> <a href="14_http_server_with_state_mutex.go">14_http_server_with_state_mutex.go</a> <a href="15_file_server.go">15_file_server.go</a> <a href="16_custom_server.go">16_custom_server.go</a> </pre>
Samozřejmě lze získat přímo obsah nějakého souboru, například:
$ curl localhost:8000/01_simple_client.go import ( "fmt" "net" ) func main() { conn, err := net.Dial("tcp", "localhost:1234") if err != nil { println("Connection refused!") } else { var b [1]byte n, err := conn.Read(b[:]) if err != nil { println("No response!") } else { if n == 1 { fmt.Printf("Received %d byte: %v\n", n, b) } else { fmt.Printf("Received %d bytes: %v\n", n, b) } } } }
17. Kombinovaný HTTP server poskytující statický i dynamický obsah
Dnešní poslední demonstrační příklad vznikl kombinací dvou HTTP serverů, které jsme si ukázali v předchozích kapitolách. Nový server nabízí jak statický obsah, tak i dva endpointy – jeden z nich vrací odpověď „Hello world!“, druhý pak textovou zprávu s celkovým počtem dotazů, které na tento endpoint přišly. Všechny tři endpointy jsou zaregistrovány takto:
http.HandleFunc("/", mainEndpoint) http.HandleFunc("/counter", counterEndpoint) http.HandleFunc("/files/", filesEndpoint)
Úpravou prošla zejména funkce, která vrací statický obsah, protože z URL odstraníme prefix „/files/“:
func filesEndpoint(writer http.ResponseWriter, request *http.Request) { url := request.URL.Path[len("/files/"):] println("Serving file from URL: " + url) http.ServeFile(writer, request, url) }
Úplný zdrojový kód dnešního posledního demonstračního příkladu naleznete na adrese https://github.com/tisnik/go-root/blob/master/article11/16_custom_server.go:
package main import ( "fmt" "io" "net/http" "sync" ) var counter int var mutex = &sync.Mutex{} func mainEndpoint(writer http.ResponseWriter, request *http.Request) { io.WriteString(writer, "Hello world!\n") } func counterEndpoint(writer http.ResponseWriter, request *http.Request) { mutex.Lock() counter++ fmt.Fprintf(writer, "Counter: %d\n", counter) mutex.Unlock() } func filesEndpoint(writer http.ResponseWriter, request *http.Request) { url := request.URL.Path[len("/files/"):] println("Serving file from URL: " + url) http.ServeFile(writer, request, url) } func main() { http.HandleFunc("/", mainEndpoint) http.HandleFunc("/counter", counterEndpoint) http.HandleFunc("/files/", filesEndpoint) http.ListenAndServe(":8000", nil) }
18. Repositář s demonstračními příklady
Zdrojové kódy všech dnes popsaných demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má doslova několik kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Go Mutex Tutorial
https://tutorialedge.net/golang/go-mutex-tutorial/ - Creating A Simple Web Server With Golang
https://tutorialedge.net/golang/creating-simple-web-server-with-golang/ - Building a Web Server in Go
https://thenewstack.io/building-a-web-server-in-go/ - How big is the pipe buffer?
https://unix.stackexchange.com/questions/11946/how-big-is-the-pipe-buffer - How to turn off buffering of stdout in C
https://stackoverflow.com/questions/7876660/how-to-turn-off-buffering-of-stdout-in-c - setbuf(3) – Linux man page
https://linux.die.net/man/3/setbuf - setvbuf(3) – Linux man page (stejný obsah jako předchozí stránka)
https://linux.die.net/man/3/setvbuf - Select waits on a group of channels
https://yourbasic.org/golang/select-explained/ - Rob Pike: Simplicity is Complicated (video)
http://www.golang.to/posts/dotgo-2015-rob-pike-simplicity-is-complicated-youtube-16893 - Algorithms to Go
https://yourbasic.org/ - Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů
https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu/ - Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů: vlastní filtry a lexery
https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu-vlastni-filtry-a-lexery/ - Go Defer Simplified with Practical Visuals
https://blog.learngoprogramming.com/golang-defer-simplified-77d3b2b817ff - 5 More Gotchas of Defer in Go — Part II
https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa - The Go Blog: Defer, Panic, and Recover
https://blog.golang.org/defer-panic-and-recover - The defer keyword in Swift 2: try/finally done right
https://www.hackingwithswift.com/new-syntax-swift-2-defer - Swift Defer Statement
https://andybargh.com/swift-defer-statement/ - Modulo operation (Wikipedia)
https://en.wikipedia.org/wiki/Modulo_operation - Node.js vs Golang: Battle of the Next-Gen Languages
https://www.hostingadvice.com/blog/nodejs-vs-golang/ - The Go Programming Language (home page)
https://golang.org/ - GoDoc
https://godoc.org/ - Go (programming language), Wikipedia
https://en.wikipedia.org/wiki/Go_(programming_language) - Go Books (kniha o jazyku Go)
https://github.com/dariubs/GoBooks - The Go Programming Language Specification
https://golang.org/ref/spec - Go: the Good, the Bad and the Ugly
https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/ - Package builtin
https://golang.org/pkg/builtin/ - Package fmt
https://golang.org/pkg/fmt/ - The Little Go Book (další kniha)
https://github.com/dariubs/GoBooks - The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
https://www.safaribooksonline.com/library/view/the-go-programming/9780134190570/ebook_split010.html - Learning Go
https://www.miek.nl/go/ - Go Bootcamp
http://www.golangbootcamp.com/ - Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
http://www.informit.com/store/programming-in-go-creating-applications-for-the-21st-9780321774637 - Introducing Go (Build Reliable, Scalable Programs)
http://shop.oreilly.com/product/0636920046516.do - Learning Go Programming
https://www.packtpub.com/application-development/learning-go-programming - The Go Blog
https://blog.golang.org/ - Getting to Go: The Journey of Go's Garbage Collector
https://blog.golang.org/ismmkeynote - Go (programovací jazyk, Wikipedia)
https://cs.wikipedia.org/wiki/Go_(programovac%C3%AD_jazyk) - Rychle, rychleji až úplně nejrychleji s jazykem Go
https://www.root.cz/clanky/rychle-rychleji-az-uplne-nejrychleji-s-jazykem-go/ - Installing Go on the Raspberry Pi
https://dave.cheney.net/2012/09/25/installing-go-on-the-raspberry-pi - How the Go runtime implements maps efficiently (without generics)
https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics - Niečo málo o Go – Golang (slovensky)
http://golangsk.logdown.com/ - How Many Go Developers Are There?
https://research.swtch.com/gophercount - Most Popular Technologies (Stack Overflow Survery 2018)
https://insights.stackoverflow.com/survey/2018/#most-popular-technologies - Most Popular Technologies (Stack Overflow Survery 2017)
https://insights.stackoverflow.com/survey/2017#technology - JavaScript vs. Golang for IoT: Is Gopher Winning?
https://www.iotforall.com/javascript-vs-golang-iot/ - The Go Programming Language: Release History
https://golang.org/doc/devel/release.html - Go 1.11 Release Notes
https://golang.org/doc/go1.11 - Go 1.10 Release Notes
https://golang.org/doc/go1.10 - Go 1.9 Release Notes (tato verze je stále používána)
https://golang.org/doc/go1.9 - Go 1.8 Release Notes (i tato verze je stále používána)
https://golang.org/doc/go1.8 - Go on Fedora
https://developer.fedoraproject.org/tech/languages/go/go-installation.html - Writing Go programs
https://developer.fedoraproject.org/tech/languages/go/go-programs.html - The GOPATH environment variable
https://tip.golang.org/doc/code.html#GOPATH - Command gofmt
https://tip.golang.org/cmd/gofmt/ - The Go Blog: go fmt your code
https://blog.golang.org/go-fmt-your-code - C? Go? Cgo!
https://blog.golang.org/c-go-cgo - Spaces vs. Tabs: A 20-Year Debate Reignited by Google’s Golang
https://thenewstack.io/spaces-vs-tabs-a-20-year-debate-and-now-this-what-the-hell-is-wrong-with-go/ - 400,000 GitHub repositories, 1 billion files, 14 terabytes of code: Spaces or Tabs?
https://medium.com/@hoffa/400–000-github-repositories-1-billion-files-14-terabytes-of-code-spaces-or-tabs-7cfe0b5dd7fd - Gofmt No Longer Allows Spaces. Tabs Only
https://news.ycombinator.com/item?id=7914523 - Why does Go „go fmt“ uses tabs instead of whitespaces?
https://www.quora.com/Why-does-Go-go-fmt-uses-tabs-instead-of-whitespaces - Interactive: The Top Programming Languages 2018
https://spectrum.ieee.org/static/interactive-the-top-programming-languages-2018 - Go vs. Python
https://www.peterbe.com/plog/govspy - PackageManagementTools
https://github.com/golang/go/wiki/PackageManagementTools - A Tour of Go: Type inference
https://tour.golang.org/basics/14 - Go Slices: usage and internals
https://blog.golang.org/go-slices-usage-and-internals - Go by Example: Slices
https://gobyexample.com/slices - What is the point of slice type in Go?
https://stackoverflow.com/questions/2098874/what-is-the-point-of-slice-type-in-go - The curious case of Golang array and slices
https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335 - Introduction to Slices in Golang
https://www.callicoder.com/golang-slices/ - Golang: Understanding ‚null‘ and nil
https://newfivefour.com/golang-null-nil.html - What does nil mean in golang?
https://stackoverflow.com/questions/35983118/what-does-nil-mean-in-golang - nils In Go
https://go101.org/article/nil.html - Go slices are not dynamic arrays
https://appliedgo.net/slices/ - Go-is-no-good (nelze brát doslova)
https://github.com/ksimka/go-is-not-good - Rust vs. Go
https://news.ycombinator.com/item?id=13430108 - Seriál Programovací jazyk Rust
https://www.root.cz/serialy/programovaci-jazyk-rust/ - Modern garbage collection: A look at the Go GC strategy
https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e - Go GC: Prioritizing low latency and simplicity
https://blog.golang.org/go15gc - Is Golang a good language for embedded systems?
https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems - Running GoLang on an STM32 MCU. A quick tutorial.
https://www.mickmake.com/post/running-golang-on-an-mcu-a-quick-tutorial - Go, Robot, Go! Golang Powered Robotics
https://gobot.io/ - Emgo: Bare metal Go (language for programming embedded systems)
https://github.com/ziutek/emgo - UTF-8 history
https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt - Less is exponentially more
https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html - Should I Rust, or Should I Go
https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9 - Setting up and using gccgo
https://golang.org/doc/install/gccgo - Elastic Tabstops
http://nickgravgaard.com/elastic-tabstops/ - Strings, bytes, runes and characters in Go
https://blog.golang.org/strings - Datový typ
https://cs.wikipedia.org/wiki/Datov%C3%BD_typ - Seriál o programovacím jazyku Rust: Základní (primitivní) datové typy
https://www.root.cz/clanky/programovaci-jazyk-rust-nahrada-c-nebo-slepa-cesta/#k09 - Seriál o programovacím jazyku Rust: Vytvoření „řezu“ z pole
https://www.root.cz/clanky/prace-s-poli-v-programovacim-jazyku-rust/#k06 - Seriál o programovacím jazyku Rust: Řezy (slice) vektoru
https://www.root.cz/clanky/prace-s-vektory-v-programovacim-jazyku-rust/#k05 - Printf Format Strings
https://www.cprogramming.com/tutorial/printf-format-strings.html - Java: String.format
https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#format-java.lang.String-java.lang.Object…- - Java: format string syntax
https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html#syntax - Selectors
https://golang.org/ref/spec#Selectors - Calling Go code from Python code
http://savorywatt.com/2015/09/18/calling-go-code-from-python-code/ - Go Data Structures: Interfaces
https://research.swtch.com/interfaces - How to use interfaces in Go
http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go - Interfaces in Go (part I)
https://medium.com/golangspec/interfaces-in-go-part-i-4ae53a97479c - Part 21: Goroutines
https://golangbot.com/goroutines/ - Part 22: Channels
https://golangbot.com/channels/ - [Go] Lightweight eventbus with async compatibility for Go
https://github.com/asaskevich/EventBus - What about Trait support in Golang?
https://www.reddit.com/r/golang/comments/8mfykl/what_about_trait_support_in_golang/ - Don't Get Bitten by Pointer vs Non-Pointer Method Receivers in Golang
https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/ - Control Flow
https://en.wikipedia.org/wiki/Control_flow - Structured programming
https://en.wikipedia.org/wiki/Structured_programming - Control Structures
https://www.golang-book.com/books/intro/5 - Control structures – Go if else statement
http://golangtutorials.blogspot.com/2011/06/control-structures-if-else-statement.html - Control structures – Go switch case statement
http://golangtutorials.blogspot.com/2011/06/control-structures-go-switch-case.html - Control structures – Go for loop, break, continue, range
http://golangtutorials.blogspot.com/2011/06/control-structures-go-for-loop-break.html - Single Function Exit Point
http://wiki.c2.com/?SingleFunctionExitPoint - Entry point
https://en.wikipedia.org/wiki/Entry_point - Why does Go have a GOTO statement?!
https://www.reddit.com/r/golang/comments/kag5q/why_does_go_have_a_goto_statement/ - Effective Go
https://golang.org/doc/effective_go.html - GoClipse: an Eclipse IDE for the Go programming language
http://goclipse.github.io/ - GoClipse Installation
https://github.com/GoClipse/goclipse/blob/latest/documentation/Installation.md#installation