O poveste de comunicare

V-ați întrebat vreodată cum vorbește de fapt Internetul? Cum un „computer” vorbește cu un alt computer prin Internet?

Când oamenii comunică unii cu alții, folosim cuvinte strânse în propoziții aparent semnificative. Frazele au sens doar pentru că am convenit asupra unui sens pentru aceste propoziții. Am definit un protocol de comunicare, ca să spunem așa.

Se pare că computerele vorbesc între ele într-un mod similar prin internet. Dar, ne depășim. Oamenii își folosesc gura pentru a comunica, să ne dăm seama care este gura computerului mai întâi.

Intrați în soclu

Soclul este unul dintre cele mai fundamentale concepte în informatică. Puteți construi rețele întregi de dispozitive interconectate folosind prize.

Ca toate celelalte lucruri din informatică, o priză este un concept foarte abstract. Deci, mai degrabă decât să definim ce este un socket, este mult mai ușor să definim ce face un socket.

Deci, ce face o priză? Ajută două computere să comunice între ele. Cum face asta? Are două metode definite, numite send() și recv() pentru trimitere și respectiv primire.

Bine, totul este grozav, dar ce faci send() și recv() trimite și primește de fapt? Când oamenii își mișcă gura, fac schimb de cuvinte. Când socket-urile își folosesc metodele, schimbă biți și octeți.

Să ilustrăm metodele cu un exemplu. Să presupunem că avem două computere, A și B. Computerul A încearcă să îi spună ceva computerului B. Prin urmare, computerul B încearcă să asculte ceea ce spune computerul A. Iată cum ar arăta asta.

Cum vorbeste internetul
Priză cu tampon

Citind tamponul

Pare puțin ciudat, nu-i așa? În primul rând, ambele computere indică o bară din mijloc, intitulată „tampon”.

Ce este bufferul? Tamponul este o stivă de memorie. Acolo sunt stocate datele pentru fiecare computer și sunt alocate de nucleu.

Apoi, de ce indică amândoi același tampon? Ei bine, asta nu este chiar precis. Fiecare computer are propriul buffer alocat de propriul nucleu și rețeaua transportă datele între cele două buffere separate. Dar nu vreau să intru aici în detaliile rețelei, așa că vom presupune că ambele computere au acces la același buffer plasat „undeva în golul dintre”.

Bine, acum, când știm cum arată acest lucru din punct de vedere vizual, să îl abstractizăm în cod.

#Computer A sends data computerA.send(data) 
#Computer B receives data computerB.recv(1024)

Acest fragment de cod face exact același lucru pe care îl reprezintă imaginea de mai sus. Cu excepția unei curiozități, nu spunem computerB.recv(data). În schimb, specificăm un număr aparent aleatoriu în locul datelor.

Motivul este simplu. Datele printr-o rețea sunt transmise în biți. Prin urmare, atunci când recv în computerB, specificăm numărul de biți suntem dispuși să primim în orice moment dat.

De ce am ales 1024 octeți pentru a primi simultan? Niciun motiv specific. De obicei, cel mai bine este să specificați numărul de octeți pe care l-ați primi într-o putere de 2. Am ales 1024, care este 2¹⁰.

Deci, cum își dă seama bufferul? Ei bine, computerul A scrie sau trimite orice date stocate cu acesta în buffer. Computerul B decide să citească sau să primească primii 1024 de octeți din ceea ce este stocat în acel tampon.

Bine, minunat! Dar, de unde știu aceste două computere să vorbească între ele? De exemplu, când computerul A scrie în acest buffer, de unde știe că computerul B îl va prelua? Pentru a reformula acest lucru, cum se poate asigura că o conexiune între două computere are un buffer unic?

Portarea în IP-uri

1612017614 400 Cum vorbeste internetul
Porturi și IP-uri ale computerelor

Imaginea de mai sus arată aceleași două computere la care am lucrat tot împreună cu încă un detaliu adăugat. Există o grămadă de numere listate în fața fiecărui computer de-a lungul unei bare.

Luați în considerare că bara lungă din fața fiecărui computer este routerul care conectează un computer specific la internet. Acele numere enumerate pe fiecare bară sunt numite porturi. Computerul dvs. are mii de porturi disponibile pe acesta chiar acum. Fiecare port permite o conexiune socket. Am afișat doar 6 porturi în imaginea de mai sus, dar îți iese ideea.

Porturile sub 255 sunt rezervate în general pentru apeluri de sistem și conexiuni de nivel scăzut. În general, este recomandabil să deschideți o conexiune pe un port cu 4 cifre mari, cum ar fi 8000. Nu am desenat bufferul în imaginea de mai sus, dar puteți presupune că fiecare port are propriul buffer.

Bara în sine are, de asemenea, un număr asociat. Acest număr se numește adresa IP. Adresa IP are o grămadă de porturi asociate. Gândiți-vă la asta în felul următor:

                          127.0.0.1                             / |                             /  |                             /   |                           8000  8001 8002

Excelent, să configurăm o conexiune pe un anumit port între computerul A și computerul B.

# computerA.pyimport socket 
computerA = socket.socket() 
# Connecting to localhost:8000 computerA.connect(('127.0.0.1', 8000)) string = 'abcd' encoded_string = string.encode('utf-8') computerA.send(encoded_string)

Iată codul pentru computerB.py

# computerB.py import socket 
computerB = socket.socket() 
# Listening on localhost:8000 computerB.bind(('127.0.0.1', 8000)) computerB.listen(1) 
client_socket, address = computerB.accept() data = client_socket.recv(2048) print(data.decode('utf-8'))

Se pare că am sărit puțin înainte în ceea ce privește codul, dar voi trece prin el. Știm că avem două computere, A și B. Prin urmare, avem nevoie de unul pentru a trimite date și unul pentru a primi date.

Am selectat în mod arbitrar A pentru a trimite date și B pentru a primi date. În această linie computerA.connect((‘127.0.0.1’, 8000), Fac computerA conectare la portul 8000 pe adresa IP 127.0.0.1.

Notă: 127.0.0.1 înseamnă de obicei localhost, care face referire la mașina dvs.

Apoi, pentru computerB, îl fac să se lege la portul 8000 de pe adresa IP 127.0.0.1. Acum, probabil vă întrebați de ce am aceeași adresă IP pentru două computere diferite.

Asta pentru că înșel. Folosesc un singur computer pentru a demonstra cum puteți utiliza soclurile (practic mă conectez de la și la același computer din motive de simplitate). De obicei, două computere diferite ar avea două adrese IP diferite.

Știm deja că numai biții pot fi trimiși ca parte a unui pachet de date, motiv pentru care codificăm șirul înainte de al trimite. În mod similar, decodăm șirul pe computerul B. Dacă decideți să rulați local cele două fișiere de mai sus, asigurați-vă că rulați computerB.py fișier mai întâi. Dacă rulați computerA.py fișier mai întâi, veți primi o eroare de conexiune refuzată.

Servirea clienților

1612017614 455 Cum vorbeste internetul
Transmiterea datelor între client și server

Sunt sigur că a fost destul de clar pentru mulți dintre voi că ceea ce am descris până acum este un model client-server foarte simplist. De fapt, puteți vedea că din imaginea de mai sus, tot ce am făcut este să înlocuiesc computerul A ca client și computerul B ca server.

Există un flux constant de comunicare care continuă între clienți și servere. În exemplul nostru de cod anterior, am descris o singură fotografie de transfer de date. În schimb, ceea ce ne dorim este un flux constant de date trimise de la client la server. Cu toate acestea, dorim să știm și când transferul de date este finalizat, deci știm că putem opri ascultarea.

Să încercăm să folosim o analogie pentru a examina mai departe acest lucru. Imaginați-vă următoarea conversație între două persoane.

1612017614 410 Cum vorbeste internetul

Două persoane încearcă să se prezinte. Cu toate acestea, ei nu vor încerca să vorbească în același timp. Să presupunem că Raj merge primul. John va aștepta apoi până când Raj va fi prezentat înainte de a începe să se prezinte. Aceasta se bazează pe unele euristici învățate, dar în general putem descrie cele de mai sus ca un protocol.

Clienții și serverele noastre au nevoie de un protocol similar. Sau altfel, cum ar ști când le vine rândul să trimită pachete de date?

Vom face ceva simplu pentru a ilustra acest lucru. Să presupunem că vrem să trimitem niște date care se întâmplă să fie o serie de șiruri. Să presupunem că matricea este după cum urmează:

arr = ['random', 'strings', 'that', 'need', 'to', 'be', 'transferred', 'across', 'the', 'network', 'using', 'sockets']

Cele de mai sus sunt datele care vor fi scrise de la client pe server. Să creăm o altă constrângere. Serverul trebuie să accepte date care sunt exact echivalente cu datele ocupate de șirul care urmează să fie trimis în acel moment.

Deci, de exemplu, dacă clientul va trimite peste șirul „aleatoriu” și să presupunem că fiecare caracter ocupă 1 octet, atunci șirul în sine ocupă 6 octeți. 6 octeți este apoi egal cu 6 * 8 = 48 de biți. Prin urmare, pentru ca șirul „aleatoriu” să fie transferat între socketuri de la client la server, serverul trebuie să știe că trebuie să acceseze 48 de biți pentru acel pachet specific de date.

Aceasta este o bună oportunitate de a descompune problema. Există câteva lucruri pe care trebuie să le descoperim mai întâi.

Cum ne dăm seama numărul de octeți pe care îl ocupă un șir în Python?

Ei bine, am putea începe prin a afla mai întâi lungimea unui șir. Este ușor, este doar un apel către len(). Dar, tot trebuie să știm numărul de octeți ocupați de un șir, nu doar lungimea.

Vom converti mai întâi șirul în binar și apoi vom găsi lungimea reprezentării binare rezultate. Acest lucru ar trebui să ne ofere numărul de octeți utilizați.

len(‘random’.encode(‘utf-8’)) ne va oferi ceea ce vrem

Cum trimitem numărul de octeți ocupați de fiecare șir către server?

Ușor, vom converti numărul de octeți (care este un număr întreg) într-o reprezentare binară a acelui număr și îl vom trimite la server. Acum, serverul se poate aștepta să primească lungimea unui șir înainte de a primi șirul în sine.

Cum știe serverul când clientul a terminat de trimis toate șirurile?

Amintiți-vă din analogia conversației, trebuie să existe o modalitate de a ști dacă transferul de date sa încheiat. Calculatoarele nu au propriile euristici pe care se pot baza. Deci, vom oferi o regulă aleatorie. Vom spune că atunci când trimitem șirul „sfârșit”, înseamnă că serverul a primit toate șirurile și acum poate închide conexiunea. Desigur, acest lucru înseamnă că nu putem folosi șirul „sfârșit” în nicio altă parte a matricei noastre, cu excepția finalului.

Iată protocolul pe care l-am proiectat până acum:

1612017615 202 Cum vorbeste internetul
Protocolul nostru simplist

Lungimea șirului va fi de 2 octeți, urmată de șirul propriu-zis, care va avea o lungime variabilă. Va depinde de lungimea șirului trimis în pachetul anterior și vom alterna între trimiterea lungimilor șirului și șirul în sine. EOT înseamnă End Of Transmission, iar trimiterea șirului „end” înseamnă că nu mai sunt date de trimis.

Notă: Înainte de a continua, vreau să subliniez ceva. Acesta este un protocol foarte simplu și prost. Dacă doriți să vedeți cum arată un protocol bine conceput, nu căutați mai departe decât Protocol HTTP.

Să codificăm acest lucru. Am inclus comentarii în codul de mai jos, astfel încât să se explice de la sine.

Super, avem un client care rulează. Apoi, avem nevoie de server.

Vreau să explic câteva linii specifice de cod în esențele de mai sus. Primul, din clientSocket.py fişier.

len_in_bytes = (len_of_string).to_bytes(2, byteorder="little")

Ceea ce face mai sus este să convertească un număr în octeți. Primul parametru transmis funcției to_bytes este numărul de octeți alocați rezultatului conversiei len_of_string la reprezentarea sa binară.

Al doilea parametru este utilizat pentru a decide dacă urmează formatul Little Endian sau formatul Big Endian. Puteți citi mai multe despre asta Aici. Deocamdată, știți doar că vom rămâne întotdeauna cu puțin pentru acest parametru.

Următoarea linie de cod la care vreau să arunc o privire este:

client_socket.send(string.encode(‘utf-8’))

Convertim șirul într-un format binar folosind‘utf-8’ codificare.

Apoi, în serverSocket.py fişier:

data = client_socket.recv(2) str_length = int.from_bytes(data, byteorder="little")

Prima linie de cod de mai sus primește 2 octeți de date de la client. Amintiți-vă că atunci când am convertit lungimea șirului într-un format binar în clientSocket.py, am decis să stocăm rezultatul în 2 octeți. Acesta este motivul pentru care citim aici 2 octeți pentru aceleași date.

Următoarea linie implică convertirea formatului binar într-un număr întreg. byteorder aici este „puțin”, pentru a se potrivi cu byteorder am folosit pe client.

Dacă mergeți mai departe și rulați cele două sockets, ar trebui să vedeți că serverul va imprima șirurile pe care clientul le trimite. Am stabilit comunicarea!

Concluzie

Bine, am acoperit destul de mult până acum. Și anume, ce sunt soclurile, cum le folosim și cum să proiectăm un protocol foarte simplu și prost. Dacă doriți să aflați mai multe despre modul de funcționare a soclurilor, vă recomand cu drag să citiți Ghidul Beej pentru programarea în rețea. Cartea electronică are multe lucruri grozave.

Desigur, puteți lua ceea ce ați citit în acest articol până acum și să-l aplicați la probleme mai complexe, cum ar fi transmiterea de imagini de pe o cameră RaspberryPi pe computer. Distrează-te cu el!

Dacă vrei, poți să mă urmărești mai departe Stare de nervozitate sau GitHub. Puteți consulta și blogul meu Aici. Sunt întotdeauna disponibil dacă vrei să ajungi la mine!

Publicat inițial la https://redixhumayun.github.io/networking/2019/02/14/how-the-internet-speaks.html pe 14 februarie 2019.