Obsah
1. Komunikace přes unixové sokety v programovacím jazyce Go
2. Základní nástroje, které využijeme: netstat, nc a lsof
3. Komunikace přes TCP sokety (stream)
4. Komunikace přes UDP sokety (datagram)
5. „Jmenné prostory“ TCP a UDP
6. Použití Unixových soketů (stream i datagram)
7. Realizace jednoduchého klienta komunikujícího přes TCP sokety v jazyku Go
8. Základní kostra serveru komunikujícího přes TCP sokety v jazyku Go
9. Vylepšení klienta i serveru – uzavírání prostředků a reakce na případné chyby
10. Simulace zpomalené reakce serveru
11. Otestování chování serveru
12. Server akceptující větší množství klientů realizovaný pomocí gorutin
13. Otestování chování nové implementace serveru
14. Klient komunikující přes unixové sokety naprogramovaný v Go
15. Server komunikující přes unixové sokety naprogramovaný v Go
17. Otestování komunikace mezi klientem a serverem
19. Repositář s demonstračními příklady
1. Komunikace přes unixové sokety v programovacím jazyce Go
Programovací jazyk Go se velmi často používá pro tvorbu síťových aplikací. V dnešním článku si ukážeme, jak snadno je možné v jazyku Go realizovat komunikaci mezi serverem a klienty jak přes TCP či UDP (tedy s využitím síťových rozhraní, včetně lokálního síťového rozhraní), tak i přes unixové sokety. Přitom si ukážeme i některé základní způsoby využití dalších utility: nc, lsof a netstat.
Nezávisle na tom, zda je pro komunikaci použito síťové rozhraní (včetně localhostu) nebo unixové sokety, se rozlišuje mezi tzv. spojovou službou a komunikací pomocí datagramů. Spojová služba je realizována buď s využitím protokolu TCP nebo přes unixové sokety (které podporují i využití datagramů). Před zahájením komunikace je nutné navázat spojení. Toto spojení vždy navazuje klient, který se připojuje k serveru (ovšem odpojit se může jakákoli strana). Po navázání spojení může probíhat přenos dat (obousměrný) a přitom jsou všechna přenesená data potvrzována (z pohledu programátora automaticky). Pokud nastane situace, kdy nejsou data potvrzena, jsou poslána znovu (opět bez zásahu programátora). Zaručeno je i pořadí doručení dat.
Naproti tomu existuje (rostoucí) počet aplikací, pro které je zajištění spojové služby zbytečně komplikované a v případě TCP i příliš zatěžující pro síť. Pokud není nutné zaručit pořadí zpráv nebo jejich potvrzování, lze komunikovat přes datagramy. To je případ síťové komunikace přes protokol UDP resp. (opět) přes unixové sokety (zde je ovšem pořadí zaručeno). U UDP může dojít i k duplikaci dat, zatímco u Unixových soketů většinou nikoli.
2. Základní nástroje, které využijeme: netstat, nc a lsof
V dnešním článku sice vytvoříme několik jednoduchých klientů a serverů naprogramovaných v jazyce Go, ovšem kromě toho využijeme i některé (dnes již prakticky) standardní systémové nástroje; konkrétně nástroje nazvané netstat, nc a lsof:
- netstat je nástroj, který je možné využít pro zjištění síťových připojení popř. i použití Unixových soketů. Moderní varianty tohoto nástroje podporují velké množství přepínačů, ovšem my využijeme jen malou podskupinu těchto přepínačů.
- nc neboli netcat je takřka univerzální nástroj při práci s TCP a UDP, ale (jak ostatně uvidíme dále) i s Unixovými sokety. Pomocí tohoto nástroje lze snadno realizovat TCP/UDP klienta i server, ovšem možnosti tohoto nástroje jsou ve skutečnosti mnohem větší.
- lsof je nástroj, který dokáže vypsat seznam otevřených souborů a popř. i další vlastnosti těchto souborů. Tento nástroj využijeme v souvislosti s Unixovými sokety, které se z pohledu uživatele skutečně tváří jako speciální typy souborů.
3. Komunikace přes TCP sokety (stream)
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 dále využitých Unixových soketů) 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šší vrstvě (pravděpodobně nejznámějším příkladem ze současnosti je protokol 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. Později namísto TCP/UDP použijeme unixové sokety.
Nejprve na prvním terminálu spustíme nástroj nc, který bude nakonfigurován jako server přijímající (naslouchající) na portu 1234. Aby nástroj nc naslouchal požadavkům, musíme použít přepínač -l neboli „listen“:
$ nc -l -v localhost 1234 Listening on localhost 1234
Následně ve druhém terminálu navážeme spojení se serverem naslouchajícím na portu 1234:
$ nc -v localhost 1234 Connection to localhost 1234 port [tcp/*] succeeded!
A na prvním terminálu se zobrazí informace o navázání spojení a navíc o portu otevřeném klientem:
Connection received on localhost 35636
Pokud nyní budete na tento terminál zapisovat nějaké zprávy, bude je server přijímat a opisovat je na svůj (první) terminál:
aaa bbb cc ddd
Na třetím terminálu si pak můžeme zobrazit podrobnější informace o komunikačním kanále mezi serverem a klientem:
$ netstat -tunp | grep :1234 (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) tcp 0 0 127.0.0.1:1234 127.0.0.1:35636 ESTABLISHED 231176/nc tcp 0 0 127.0.0.1:35636 127.0.0.1:1234 ESTABLISHED 232495/nc
4. Komunikace přes UDP sokety (datagram)
Nyní si vyzkoušíme podobnou komunikaci, ovšem nikoli zajištěnou s využitím TCP (stream), ale použijeme namísto toho protokol UDP (datagram). Nástroj nc komunikaci přes UDP taktéž podporuje, ovšem musíme použít přepínač -u:
$ nc -l -u -v localhost 1234 Bound on localhost 1234
V dalším terminálu se k serveru připojíme:
$ nc -u -v localhost 1234 Connection to localhost 1234 port [udp/*] succeeded!
Na terminálu serveru se taktéž vypíše informace o připojení:
Connection received on localhost 45470
A propojené porty opět získáme a necháme si vypsat nástrojem netstat:
$ netstat -tunp | grep :1234 (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) udp 0 0 127.0.0.1:45470 127.0.0.1:1234 ESTABLISHED 233385/nc udp 0 0 127.0.0.1:1234 127.0.0.1:45470 ESTABLISHED 233118/nc
5. „Jmenné prostory“ TCP a UDP
Porty TCP a UDP jsou sice identifikovány celými čísly 0..65535, ovšem jedná se o odlišné „jmenné prostory“. Proto nelze navázat spojení se serverem naslouchajícím na UDP portu 1234 z klienta pokoušejícího se připojit na TCP port 1234:
$ nc -l -u -v localhost 1234 Bound on localhost 1234 $ nc -v localhost 1234 nc: connect to localhost port 1234 (tcp) failed: Connection refused
Platí to pochopitelně i naopak:
$ nc -l -v localhost 1234 Listening on localhost 1234 $ nc -u -v localhost 1234 (proces je ihned ukončen)
6. Použití Unixových soketů (stream i datagram)
Namísto protokolu TCP či UDP můžeme pro komunikaci mezi procesy použít unixové sokety, které taktéž existují ve variantě stream a datagram (tj. způsob komunikace do značné míry odpovídá TCP resp. UDP). Unixové sokety lze využít pro komunikaci mezi procesy běžícími na jednom počítači a vlastní komunikace probíhá přes strukturu spravovanou jádrem, která je z uživatelského prostoru viditelná jako speciální typ souboru. Podívejme se nyní na způsob spuštění serveru, který pro komunikaci otevře soket se jménem /tmp/test.socket (což je onen speciální typ souboru):
$ nc -l -U -v /tmp/test.socket Bound on /tmp/test.socket Listening on /tmp/test.socket
Samotný soket, na který se mají klienti připojit, bude vypadat následovně:
$ lsof /tmp/test.socket COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nc 235825 ptisnovs 3u unix 0x0000000000000000 0t0 1288302629 /tmp/test.socket type=STREAM
Na druhém terminálu se k tomuto serveru připojíme přes unixový soket:
$ nc -U -v /tmp/test.socket
Připojení k serveru je potvrzeno na prvním terminálu zprávou:
Connection received on /tmp/test.socket
A obousměrná komunikace je nástrojem lsof nalezena takto:
$ lsof /tmp/test.socket COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nc 235825 ptisnovs 3u unix 0x0000000000000000 0t0 1288302629 /tmp/test.socket type=STREAM nc 235825 ptisnovs 4u unix 0x0000000000000000 0t0 1288302630 /tmp/test.socket type=STREAM
Podívejme se nyní na vlastnosti souboru /tmp/test.socket:
$ ls -la /tmp/test.socket srwxrwxr-x 1 ptisnovs ptisnovs 0 Apr 30 15:26 /tmp/test.socket
Z prvního znaku „s“ je patrné, že se skutečně jedná o soket.
Komunikace přes datagram soket vypadá poněkud odlišně, protože se ve skutečnosti použijí dva sokety, jeden explicitně pojmenovaný a druhý vytvořený klientem (a klientem uzavíraný):
$ nc -l -u -U -v /tmp/test.socket Bound on /tmp/test.socket $ nc -u -U -v /tmp/test.socket Bound on /tmp/nc.XXXXRAlNkA $ lsof /tmp/test.socket COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nc 238174 ptisnovs 3u unix 0x0000000000000000 0t0 1288403891 /tmp/test.socket type=DGRAM $ lsof /tmp/nc.XXXXRAlNkA COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nc 238687 ptisnovs 4u unix 0x0000000000000000 0t0 1288437146 /tmp/nc.XXXXRAlNkA type=DGRAM
7. Realizace jednoduchého klienta komunikujícího přes TCP sokety v jazyku Go
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 v tomto případě 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 demonstračního příkladu bude vypadat následovně:
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 (stále však nepřipravený pro reálný provoz) 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]
8. Základní kostra serveru komunikujícího přes TCP sokety v jazyku Go
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 takzvaný „neforkující“ server, který bude implementován následujícím způsobem a který dokáže v dané chvíli obsloužit jen jediného klienta:
- Použijeme konstruktor nazvaný 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()
Zdrojový kód takto napsaného serveru bude vypadat následovně:
package main import ( "log" "net" ) func processRequest(connection net.Conn, cnt *byte) { var buffer = []byte{*cnt} *cnt++ n, err := connection.Write(buffer) if err != nil { log.Println("Writing error", err) } else { log.Printf("Written %d byte(s)", n) } } func main() { cnt := byte(0) l, err := net.Listen("tcp", "localhost:1234") if err != nil { log.Fatal("Can't open the port!") } for { connection, err := l.Accept() if err != nil { log.Println("Connection refused!") } else { log.Println("Connection accepted") processRequest(connection, &cnt) } } }
9. Vylepšení klienta i serveru – uzavírání prostředků a reakce na případné chyby
Jak klient tak i server komunikující s využitím soketů by měly korektně (a navíc i co nejdříve) uzavírat všechny prostředky. Týká se to zejména vlastního připojení, které je v klientovi získáno funkcí net.Dial a uzavřeno by mělo být metodou Close. Vše lze realizovat například v bloku defer:
package main import ( "log" "net" ) const BufferSize = 1024 func main() { connection, err := net.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("Connection refused!") return } log.Print("Connection established, waiting for data") defer func() { log.Println("Closing connection") err := connection.Close() if err != nil { log.Println("Closing connection failed", err) } }() buffer := make([]byte, BufferSize) n, err := connection.Read(buffer) if err != nil { log.Println("No response!", err) } else { if n == 1 { log.Printf("Received %d byte: %v\n", n, buffer[:n]) } else { log.Printf("Received %d bytes: %v\n", n, buffer[:n]) } } }
U serveru by se mělo uzavřít jak každé realizované připojení, tak i vlastní síťový server zkonstruovaný funkcí net.Listen. Opět je možné použít bloky defer a výsledek by mohl vypadat následovně:
package main import ( "log" "net" ) func processRequest(connection net.Conn, cnt *byte) { defer func() { log.Println("Closing connection") err := connection.Close() if err != nil { log.Println("Closing connection failed", err) } }() var buffer = []byte{*cnt} *cnt++ n, err := connection.Write(buffer) if err != nil { log.Println("Writing error", err) } else { log.Printf("Written %d byte(s)", n) } } func main() { cnt := byte(0) l, err := net.Listen("tcp", "localhost:1234") if err != nil { log.Fatal("Can't open the port!") } defer func() { err := l.Close() if err != nil { log.Println("Listener close failed", err) } }() for { connection, err := l.Accept() if err != nil { log.Println("Connection refused!") } else { log.Println("Connection accepted") processRequest(connection, &cnt) } } }
10. Simulace zpomalené reakce serveru
Reálné implementace serverů mnohdy provádí nějakou časově náročnou činnost, což znamená, že nedokážou na požadavek klienta odpovědět ihned. Můžeme si tedy otestovat, jak se bude server i klient (popř. klienti) chovat ve chvíli, kdy bude server odpovídat se zpožděním, například se zpožděním dvou sekund. Ono zpoždění bude simulováno pozastavením činnosti serveru po akceptaci připojení klienta:
package main import ( "log" "net" "time" ) func processRequest(connection net.Conn, cnt *byte) { defer func() { log.Println("Closing connection") err := connection.Close() if err != nil { log.Println("Closing connection failed", err) } }() var buffer = []byte{*cnt} *cnt++ n, err := connection.Write(buffer) if err != nil { log.Println("Writing error", err) } else { log.Printf("Written %d byte(s)", n) } } func main() { cnt := byte(0) l, err := net.Listen("tcp", "localhost:1234") if err != nil { log.Fatal("Can't open the port!") } defer func() { err := l.Close() if err != nil { log.Println("Listener close failed", err) } }() for { connection, err := l.Accept() if err != nil { log.Println("Connection refused!") } else { log.Println("Connection accepted") time.Sleep(2 * time.Second) processRequest(connection, &cnt) } } }
11. Otestování chování serveru
Nyní server popsaný v předchozí kapitole spustíme v jednom terminálu:
$ go run slow_tcp_server.go
A následně spustíme (částečně souběžně) několik klientů ve druhém terminálu:
$ go run simple_tcp_client.go & $ go run simple_tcp_client.go & $ go run simple_tcp_client.go &
Klienti se připojí a budou očekávat odpověď od serveru:
2023/05/02 15:34:16 Connection established, waiting for data 2023/05/02 15:34:16 Connection established, waiting for data 2023/05/02 15:34:17 Connection established, waiting for data 2023/05/02 15:34:18 Received 1 byte: [1] 2023/05/02 15:34:18 Closing connection 2023/05/02 15:34:20 Received 1 byte: [2] 2023/05/02 15:34:20 Closing connection 2023/05/02 15:34:22 Received 1 byte: [3] 2023/05/02 15:34:22 Closing connection
Povšimněte si, že klienti dostávali data postupně (se zpožděním dvou sekund mezi odpověďmi), nikoli současně. To je ostatně patrné i z logů serveru:
2023/05/02 15:34:16 Connection accepted 2023/05/02 15:34:18 Written 1 byte(s) 2023/05/02 15:34:18 Closing connection 2023/05/02 15:34:18 Connection accepted 2023/05/02 15:34:20 Written 1 byte(s) 2023/05/02 15:34:20 Closing connection 2023/05/02 15:34:20 Connection accepted 2023/05/02 15:34:22 Written 1 byte(s) 2023/05/02 15:34:22 Closing connection
12. Server akceptující větší množství klientů realizovaný pomocí gorutin
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 (v níž je realizováno poslání odpovědi klientovi), aby se l.Accept() zavolala znovu:
for { connection, err := l.Accept() if err != nil { log.Println("Connection refused!") } else { log.Println("Connection accepted") processRequest(connection, &cnt) } }
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:
for { connection, err := l.Accept() if err != nil { log.Println("Connection refused!") } else { log.Println("Connection accepted") go processRequest(connection, &cnt) } }
Podívejme se nyní na takto upravený a úplný zdrojový kód demonstračního příkladu:
package main import ( "log" "net" "time" ) func processRequest(connection net.Conn, cnt *byte) { log.Println("Handling connection") time.Sleep(5 * time.Second) defer func() { log.Println("Closing connection") err := connection.Close() if err != nil { log.Println("Closing connection failed", err) } }() var buffer = []byte{*cnt} *cnt++ n, err := connection.Write(buffer) if err != nil { log.Println("Writing error", err) } else { log.Printf("Written %d byte(s)", n) } } func main() { cnt := byte(0) l, err := net.Listen("tcp", "localhost:1234") if err != nil { log.Fatal("Can't open the port!") } defer func() { err := l.Close() if err != nil { log.Println("Listener close failed", err) } }() for { connection, err := l.Accept() if err != nil { log.Println("Connection refused!") } else { log.Println("Connection accepted") go processRequest(connection, &cnt) } } }
13. Otestování chování nové implementace serveru
Opět si vyzkoušejme, jak bude server komunikovat s větším množstvím klientů. Nejdříve spustíme v prvním terminálu server:
$ go run multi_connection_tcp_server.go
A poté čtveřici klientů ve druhém terminálu:
$ go run simple_tcp_client.go & $ go run simple_tcp_client.go & $ go run simple_tcp_client.go & $ go run simple_tcp_client.go &
Klienti začnou komunikovat se serverem prakticky okamžitě:
2023/05/02 15:37:27 Connection established, waiting for data
2023/05/02 15:37:28 Connection established, waiting for data
2023/05/02 15:37:28 Connection established, waiting for data
2023/05/02 15:37:28 Connection established, waiting for data
Odpovědi ovšem všichni klienti dostanou až za pět sekund (což je prodleva zakomponovaná do serveru). Všechny odpovědi však jsou připravovány souběžně, každá ve své gorutině:
2023/05/02 15:37:32 Received 1 byte: [0] 2023/05/02 15:37:32 Closing connection 2023/05/02 15:37:33 Received 1 byte: [1] 2023/05/02 15:37:33 Closing connection 2023/05/02 15:37:33 Received 1 byte: [2] 2023/05/02 15:37:33 Closing connection 2023/05/02 15:37:33 Received 1 byte: [3] 2023/05/02 15:37:33 Closing connection
Souběžné zpracování požadavků je ostatně patrné i z logů serveru:
2023/05/02 15:37:27 Connection accepted 2023/05/02 15:37:27 Handling connection 2023/05/02 15:37:28 Connection accepted 2023/05/02 15:37:28 Handling connection 2023/05/02 15:37:28 Connection accepted 2023/05/02 15:37:28 Handling connection 2023/05/02 15:37:28 Connection accepted 2023/05/02 15:37:28 Handling connection 2023/05/02 15:37:32 Written 1 byte(s) 2023/05/02 15:37:32 Closing connection 2023/05/02 15:37:33 Written 1 byte(s) 2023/05/02 15:37:33 Closing connection 2023/05/02 15:37:33 Written 1 byte(s) 2023/05/02 15:37:33 Closing connection 2023/05/02 15:37:33 Written 1 byte(s) 2023/05/02 15:37:33 Closing connection
Nástroj netstat nám vypíše všechny čtyři v daný okamžik připojené klienty:
$ netstat -tunp | grep :1234 (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) tcp 0 0 127.0.0.1:1234 127.0.0.1:46558 ESTABLISHED 1422106/multi_conne tcp 0 0 127.0.0.1:54550 127.0.0.1:1234 ESTABLISHED 1422685/simple_tcp_ tcp 0 0 127.0.0.1:46558 127.0.0.1:1234 ESTABLISHED 1422959/simple_tcp_ tcp 0 0 127.0.0.1:1234 127.0.0.1:54550 ESTABLISHED 1422106/multi_conne tcp 0 0 127.0.0.1:1234 127.0.0.1:54564 ESTABLISHED 1422106/multi_conne tcp 0 0 127.0.0.1:1234 127.0.0.1:46566 ESTABLISHED 1422106/multi_conne tcp 0 0 127.0.0.1:46566 127.0.0.1:1234 ESTABLISHED 1423115/simple_tcp_ tcp 0 0 127.0.0.1:54564 127.0.0.1:1234 ESTABLISHED 1422821/simple_tcp_
14. Klient komunikující přes unixové sokety naprogramovaný v Go
Nyní si ukažme, jak by mohl vypadat klient, který se serverem komunikuje nikoli přes TCP či UDP (tedy přes reálné nebo lokální síťové rozhraní), ale přes standardní unixové sokety. Samotný kód klienta se nebude příliš odlišovat od TCP klienta, v němž bylo spojení „vytočeno“ funkcí:
conn, err := net.Dial("tcp", "localhost:1234")
nyní použijeme tutéž funkci, ale s odlišnými parametry:
connection, err := net.Dial("unix", SocketFileName)
kde SocktFileName je jméno speciálního souboru představujícího soket:
const SocketFileName = "/tmp/xyzzy"
Úplný kód klienta by mohl vypadat následovně:
package main import ( "log" "net" ) const BufferSize = 1024 const SocketFileName = "/tmp/xyzzy" func main() { connection, err := net.Dial("unix", SocketFileName) if err != nil { log.Fatal("Connection refused!") return } log.Print("Connection established, waiting for data") defer func() { log.Println("Closing connection") err := connection.Close() if err != nil { log.Println("Closing connection failed", err) } }() buffer := make([]byte, BufferSize) n, err := connection.Read(buffer) if err != nil { log.Println("No response!", err) } else { if n == 1 { log.Printf("Received %d byte: %v\n", n, buffer[:n]) } else { log.Printf("Received %d bytes: %v\n", n, buffer[:n]) } } }
15. Server komunikující přes unixové sokety naprogramovaný v Go
Ani zdrojový kód serveru určeného pro komunikaci přes unixové sokety se nebude příliš odlišovat od serveru, jenž komunikoval přes TCP či UDP. Ostatně podívejme se na následující zdrojový kód, z něhož je patrné, že se liší pouze parametry předávané do funkce net.Listen:
package main import ( "log" "net" ) const SocketFileName = "/tmp/xyzzy" func processRequest(connection net.Conn, cnt *byte) { defer func() { log.Println("Closing connection") err := connection.Close() if err != nil { log.Println("Closing connection failed", err) } }() var buffer = []byte{*cnt} *cnt++ n, err := connection.Write(buffer) if err != nil { log.Println("Writing error", err) } else { log.Printf("Written %d byte(s)", n) } } func main() { cnt := byte(0) l, err := net.Listen("unix", SocketFileName) if err != nil { log.Fatal("Can't open the port!") } defer func() { err := l.Close() if err != nil { log.Println("Listener close failed", err) } }() for { connection, err := l.Accept() if err != nil { log.Println("Connection refused!") } else { log.Println("Connection accepted") processRequest(connection, &cnt) } } }
Praktičtější je však server, jenž může v daný okamžik komunikovat s větším množstvím klientů. Opět zde s výhodou využijeme gorutiny:
package main import ( "log" "net" "time" ) const SocketFileName = "/tmp/xyzzy" func processRequest(connection net.Conn, cnt *byte) { log.Println("Handling connection") time.Sleep(10 * time.Second) defer func() { log.Println("Closing connection") err := connection.Close() if err != nil { log.Println("Closing connection failed", err) } }() var buffer = []byte{*cnt} *cnt++ n, err := connection.Write(buffer) if err != nil { log.Println("Writing error", err) } else { log.Printf("Written %d byte(s)", n) } } func main() { cnt := byte(0) l, err := net.Listen("unix", SocketFileName) if err != nil { log.Fatal("Can't open the port!") } defer func() { err := l.Close() if err != nil { log.Println("Listener close failed", err) } }() for { connection, err := l.Accept() if err != nil { log.Println("Connection refused!") } else { log.Println("Connection accepted") go processRequest(connection, &cnt) } } }
17. Otestování komunikace mezi klientem a serverem
Opět si otestujme, jak bude se serverem současně komunikovat větší množství klientů. Nejprve spustíme server, který by měl nabídnout komunikaci přes unixové sokety:
$ go run multi_connection_unix_socket_server.go
Dále ve druhém terminálu spustíme (prakticky v totožný okamžik) větší množství klientů:
$ go run simple_unix_socket_client.go & $ go run simple_unix_socket_client.go & $ go run simple_unix_socket_client.go & $ go run simple_unix_socket_client.go & $ go run simple_unix_socket_client.go &
Celá pětice klientů by měla ihned vypsat, že bylo navázáno spojení se serverem:
2023/05/02 16:09:21 Connection established, waiting for data 2023/05/02 16:09:21 Connection established, waiting for data 2023/05/02 16:09:22 Connection established, waiting for data 2023/05/02 16:09:22 Connection established, waiting for data 2023/05/02 16:09:22 Connection established, waiting for data
I server by měl na terminál vypsat, že se vytvořila pětice připojení:
2023/05/02 16:09:21 Connection accepted 2023/05/02 16:09:21 Handling connection 2023/05/02 16:09:21 Connection accepted 2023/05/02 16:09:21 Handling connection 2023/05/02 16:09:22 Connection accepted 2023/05/02 16:09:22 Handling connection 2023/05/02 16:09:22 Connection accepted 2023/05/02 16:09:22 Handling connection 2023/05/02 16:09:22 Connection accepted 2023/05/02 16:09:22 Handling connection
Po deseti sekundách server začne odpovídat:
2023/05/02 16:09:31 Written 1 byte(s) 2023/05/02 16:09:31 Closing connection 2023/05/02 16:09:31 Written 1 byte(s) 2023/05/02 16:09:31 Closing connection 2023/05/02 16:09:32 Written 1 byte(s) 2023/05/02 16:09:32 Closing connection 2023/05/02 16:09:32 Written 1 byte(s) 2023/05/02 16:09:32 Closing connection 2023/05/02 16:09:32 Written 1 byte(s) 2023/05/02 16:09:32 Closing connection
A právě po oněch po cca deseti sekundách by skutečně měla být (a to pochopitelně každým klientem zvlášť) přijata odpověď:
2023/05/02 16:09:31 Received 1 byte: [0] 2023/05/02 16:09:31 Closing connection 2023/05/02 16:09:31 Received 1 byte: [1] 2023/05/02 16:09:31 Closing connection 2023/05/02 16:09:32 Received 1 byte: [2] 2023/05/02 16:09:32 Closing connection 2023/05/02 16:09:32 Received 1 byte: [3] 2023/05/02 16:09:32 Closing connection 2023/05/02 16:09:32 Received 1 byte: [4] 2023/05/02 16:09:32 Closing connection
V průběhu komunikace (ještě předtím, než jsou jednotlivá spojení uzavřena) by měl nástroj lsof vypsat informace o celé pětici navázaných spojení:
$ lsof /tmp/xyzzy COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME multi_con 1452207 ptisnovs 3u unix 0x0000000000000000 0t0 2944390322 /tmp/xyzzy type=STREAM multi_con 1452207 ptisnovs 4u unix 0x0000000000000000 0t0 2944390332 /tmp/xyzzy type=STREAM multi_con 1452207 ptisnovs 11u unix 0x0000000000000000 0t0 2944381541 /tmp/xyzzy type=STREAM multi_con 1452207 ptisnovs 12u unix 0x0000000000000000 0t0 2944381558 /tmp/xyzzy type=STREAM multi_con 1452207 ptisnovs 13u unix 0x0000000000000000 0t0 2944393280 /tmp/xyzzy type=STREAM
18. Obsah druhé části článku
V navazujícím článku si ukážeme, jak lze využít komunikaci přes unixové sokety prakticky. Konkrétně bude představen způsob konfigurace oblíbeného nástroje HAProxy právě přes unixové sokety. Povšimněte si, o jak vhodnou volbu se v tomto případě ze strany autorů HAProxy jedná – u Unixových soketů je zaručeno, že komunikace bude navázána pouze lokálně; není tudíž nutné konfigurovat firewall. Navíc při přístupu k souboru představujícího soket můžeme využít klasická přístupová práva Unixu (Linuxu); opět bez nutnosti použití nějakého dalšího nástroje.
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů naprogramovaných v jazyku Go byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- What Are Unix Sockets and How Do They Work?
https://www.howtogeek.com/devops/what-are-unix-sockets-and-how-do-they-work/ - Unix domain socket
https://en.wikipedia.org/wiki/Unix_domain_socket - Interprocess Communication With Unix Sockets
https://www.baeldung.com/linux/communicate-with-unix-sockets - 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 - OSI model
https://en.wikipedia.org/wiki/OSI_model - Datagram
https://cs.wikipedia.org/wiki/Datagram - Softwarová rozhraní systémů UNIX pro přístup k síťovým službám
https://www.cs.vsb.cz/grygarek/PS/sockets.html - HAProxy: Management Guide version 2.4.22–1
https://docs.haproxy.org/2.4/management.html#9.3 - Síťový socket
https://cs.wikipedia.org/wiki/S%C3%AD%C5%A5ov%C3%BD_socket - Unix domain socket
https://cs.wikipedia.org/wiki/Unix_domain_socket - unix domain sockets vs. internet sockets
https://lists.freebsd.org/pipermail/freebsd-performance/2005-February/001143.html - What's the difference between streams and datagrams in network programming?
https://stackoverflow.com/questions/4688855/whats-the-difference-between-streams-and-datagrams-in-network-programming - Introducing TCP/IP concepts: Selecting sockets
https://www.ibm.com/docs/en/zos/2.3.0?topic=concepts-introducing-tcpip-selecting-sockets - Windows Sockets: Sokety datového proudu
https://learn.microsoft.com/cs-cz/cpp/mfc/windows-sockets-stream-sockets?view=msvc-170 - Windows Sockets: Sokety datagramů
https://learn.microsoft.com/cs-cz/cpp/mfc/windows-sockets-datagram-sockets?view=msvc-170 - Berkeley sockets
https://cs.wikipedia.org/wiki/Berkeley_sockets - Sokety a C/C++: datagramy a PF_UNIX
https://www.root.cz/clanky/sokety-a-c-datagramy-a-pf-unix/