Section author: Vedran Miletić
Python: međuprocesna komunikacija: utičnice¶
BSD utičnice ili POSIX utičnice (engl. sockets), poznate i pod nazivima Berkeley sockets i Unix sockets, su de facto standardno sučelje za međuprocesnu komunikaciju lokalno i putem računalne mreže
koriste model komunikacije klijent-poslužitelj bez obzira izvodi li se komunikacija izvodi lokalno ili putem računalne mreže
koriste se na operacijskim sustavima sličnim Unixu i na Windowsima
nastaju kao aplikacijsko programsko sučelje za pristup TCP-u i IP-u 1983. godine kad Bill Joy i kolege iz Computer Systems Research Group s Kalifornijskog sveučilišta u Berkeleyu izbacuju 4.2BSD; BSD je u to doba razvojna platforma za TCP/IP, o čemu detaljnije govori Marshall Kirk McKusick u svojim izlaganjima o povijesti razvoja Unixa na Berkeleyu:
u predavanju MeetBSD 2018: Dr. Kirk McKusick - A Narrative History of BSD
u predavanju A Narrative History of BSD
u predavanju Kirk McKusick - A Narrative History of BSD
u predavanju A Narrative History of BSD, Dr. Kirk McKusick
standardizirane su 2008. godine uz male razlike u imenovanju funkcija i preimenovane u POSIX utičnice
podjela prema adresiranju (
AF_*
)Unix domain sockets koriste datotečni sustav (obitelj adresa
AF_UNIX
)Unix network sockets koriste IPv4 (obitelj adresa
AF_INET
) i IPv6 (obitelj adresaAF_INET6
)
podjela prema pouzdanosti (
SOCK_*
)datagramski (tip
SOCK_DGRAM
), kod TCP/IP-a koriste se za UDPtokovni (tip
SOCK_STREAM
), kod TCP/IP-a koriste se za TCP
module socket
(službena dokumentacija) nudi pristup BSD socket sučeljukoristi objekte tipa
bytes
u komunikaciji; kad šaljete objekte tipastr
, za konverziju između tipovastr
ibytes
koriste sestr.encode()
ibytes.decode()
s1 = 'Bill Joy' b1 = s1.encode() # b1 ima vrijednost b'Bill Joy' b2 = b'uti\xc4\x8dnica' # u kodiranju UTF-8 0xc4 0x8d (c48d) je 'č' s2 = b2.decode() # s2 ima vrijednost 'utičnica'
socket.AF_UNIX
mrežne utičnice formalno koriste datotečni sustav kao prostor adresa (interna implementacija je efikasnija od pisanja i čitanja iz datoteka)procesi međusobno mogu slati podatke i opisnike datoteka; mi ćemo se ograničiti na slanje podataka, a Keith Packard u članku fd-passing objašnjava kako se opisnici datoteka šalju putem utičnica i koja je primjena tih tehnika u X Window Systemu
socket.AF_INET
mrežne utičnice koriste IPv4, asocket.AF_INET6
mrežne utičnice koriste IPv6 kao prostor adresaprimjer para IPv4 adrese i TCP/UDP vrata je oblika
('127.0.0.1', 5000)
primjer para IPv6 adrese i TCP/UDP vrata je oblika
('::1', 5000)
za IPv4 i IPv6 možemo koristiti i domene umjesto adresa, npr.
('localhost', 5000)
socket.SOCK_DGRAM
su datagramske utičniceomogućuju jednosmjernu komunikaciju kod koje klijent šalje, a poslužitelj prima prima podatke
nema mogućnosti baratanja s više klijenata, svi podaci stižu na jednu utičnicu bez obzira koji od klijenata ih šalje
nema osiguranja da će poslani podaci stići
socket.bind(address)
se povezuje na adresuaddress
(poslužiteljska strana)socket.recv(size)
čita podatke s utičnice do veličinesize
i njih vraćasocket.recvfrom(size)
čita podatke s utičnice do veličinesize
i vraća uređeni par(data, address)
socket.close()
zatvara utičnicu; potrebno je napraviti nakon završetka rada s utičnicom, slično kao kod datoteka# poslužiteljska strana, pokreće se prva import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(('localhost', 5000)) podaci = sock.recv(1024) sock.close() pozdrav = podaci.decode() print(pozdrav)
ako želimo koristiti datoteke umjesto IP adresa, u primjeru poslužitelja iznad treba promijeniti dvije linije
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sock.bind('socket1') # ovu datoteku će biti potrebno ručno izbrisati nakon završetka izvođenja programa
socket.connect(address)
se povezuje na adresuaddress
(klijentska strana)socket.send(data)
šalje podatkedata
na adresu na koju je utičnica povezana i vraća veličinu poslanih podatakasocket.sendto(data, address)
šalje podatkedata
na adresuaddress
i vraća veličinu poslanih podataka# klijentska strana, pokreće se nakon pokretanja poslužitelja import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('localhost', 5000)) pozdrav = "Pozdrav svijetu!" podaci = pozdrav.encode() sock.send(podaci) sock.close()
ako želimo koristiti datoteke umjesto IP adresa, u primjeru klijenta iznad treba promijeniti dvije linije
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sock.bind('socket1') # ovu datoteku će biti potrebno ručno izbrisati nakon završetka izvođenja programa
Zadatak
Napišite komunikaciju klijenta i poslužitelja tako da klijent pošalje podatak koji korisnik unese (umjesto fiksnog niza znakova pokazanog u primjeru).
Dodajte zatim još jedan unos podataka i izvedite dva slanja na strani klijenta i dva primanja na strani poslužitelja.
socket.SOCK_STREAM
su tokovne utičniceomogućuju dvosmjernu komunikaciju
garantiraju dostavu poruka
postoji konekcija dvaju strana
poslužiteljska utičnica stvara utičnice na strani poslužitelja za klijente koji se povezuju
socket.listen(backlog)
poslužiteljska utičnica osluškuje za povezivanja od strane klijenata; prima klijente koji se žele povezati dok broj ne naraste dobacklog
klijenata, a nakon toga odbija nova povezivanja sve do prihvaćanja dotad primljenih klijenata korištenjem funkcijesocket.accept()
socket.accept()
prihvaća klijenta i vraća uređeni par(socket object, address info)
za svakog klijenta;socket object
se koristi za komunikaciju s točno tim klijentom; kad se koristisocket.AF_UNIX
,address info
je prazan# poslužiteljska strana, pokreće se prva import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 5000)) sock.listen(1) clisock, addr = sock.accept() while True: podaci = clisock.recv(1024) if not podaci: break pozdrav = podaci.decode() print(pozdrav) poslani_podaci = pozdrav.encode() clisock.send(poslani_podaci) clisock.close() sock.close()
klijentska utičnica se povezuje na poslužitelj isto kao kod datagramskih
# klijentska strana, pokreće se nakon pokretanja poslužitelja import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', 5000)) pozdrav = "Pozdrav svijetu!" podaci = pozdrav.encode() sock.send(podaci) primljeni_podaci = sock.recv(1024) primljeni_pozdrav = primljeni_podaci.decode() print(primljeni_pozdrav) sock.close()
Zadatak
Preradite kod tako da korisnik na strani klijenta unosi dva broja koja se zatim odvojeno šalju poslužitelju; poslužitelj ih prima i ispisuje njihov zbroj. Pripazite kojeg tipa su podaci kojima baratate i izvedite konverziju gdje je potrebno.
Preradite kod tako da poslužitelj klijentu šalje zbroj koji onda klijent ispisuje.
Zadatak
Napišite poslužiteljsku i klijentsku stranu aplikacije za dvosmjernu komunikaciju koristeći datagramske utičnice. Na klijentskoj strani korisnik unosi niz znakova.
U slučaju da niz znakova počinje znakom
0
, poslužitelj kao odgovor vraća ostatak niza.U slučaju da niz znakova počinje znakom
a
, poslužitelj kao odgovor vraća duljinu ostatka niza.
U preostalim slučajevima niz se ne šalje. Klijentska aplikacija prekida nakon jednog slanja i primanja, a poslužiteljska nakon jednog primanja i slanja.
poslužiteljska aplikacija može primiti više od jednog klijenta korištenjem poziva
select()
iz modulaselect
za provjeru postoji li novi klijent ili novi podaci za čitanje na nekoj od postojećih klijentskih utičnicaobzirom da se za komunikaciju sa svakim od klijenata koristi posebna utičnica, vrlo je lako razlikovati poruke različitih klijenata
# poslužiteljska strana import socket import select sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 5000)) sockets = [sock] # ova lista će nam trebat ispod za select() while True: sock.listen(1) readready, writeready, exceptready = select.select(sockets, [], []) for readysock in readready: if readysock == sock: # klijent se želi povezati clisock, addr = readysock.accept() sockets.append(clisock) else: # neki od klijenata ima podatke za razmijeniti while True: podaci = readysock.recv(1024) if not podaci: break pozdrav = podaci.decode() print(pozdrav) poslani_podaci = pozdrav.encode() readysock.send(poslani_podaci) readysock.close() sock.close()