Ako hovorí internet

Príbeh komunikácie

Zamysleli ste sa niekedy nad tým, ako vlastne hovorí internet? Ako jeden počítač „komunikuje“ s iným počítačom cez internet?

Keď ľudia navzájom komunikujú, používame slová naviazané na zdanlivo zmysluplné vety. Vety majú zmysel iba preto, lebo sme sa zhodli na význame týchto viet. Definovali sme takpovediac komunikačný protokol.

Ukázalo sa, že počítače medzi sebou hovoria podobným spôsobom cez internet. Ale, predbiehame sa. Ľudia používajú na komunikáciu ústa, poďme zistiť, čo je ústa počítača ako prvé.

Zadajte The Socket

Zásuvka je jedným z najzákladnejších pojmov v počítačovej vede. Celé siete vzájomne prepojených zariadení môžete vytvoriť pomocou zásuviek.

Rovnako ako všetky ostatné veci v informatike, aj zásuvka je veľmi abstraktný pojem. Takže skôr ako definovať, čo je soket, je oveľa jednoduchšie definovať, čo soket robí.

Čo teda robí zásuvka? Pomáha dvom počítačom navzájom komunikovať. Ako to robí? Má definované dve metódy, volané send()a recv()pre odosielanie a prijímanie.

Dobre, to je všetko super, ale čo robiť send()a recv()vlastne posielať a prijímať? Keď ľudia pohybujú ústami, vymieňajú si slová. Keď zásuvky používajú svoje metódy, vymieňajú si bity a bajty.

Poďme si ilustrovať metódy na príklade. Povedzme, že máme dva počítače, A a B. Počítač A sa snaží niečo povedať Počítaču B. Preto sa Počítač B snaží načúvať tomu, čo hovorí Počítač A. Takto by to vyzeralo.

Čítanie vyrovnávacej pamäte

Vyzerá trochu zvláštne, však? Po prvé, oba počítače ukazujú na stredovú lištu s názvom „vyrovnávacia pamäť“.

Čo je to rezerva? Vyrovnávacia pamäť je zásobník pamäte. Je to miesto, kde sú uložené údaje o každom počítači a sú pridelené jadrom.

Ďalej, prečo obaja poukazujú na rovnaký nárazník? To vlastne nie je celkom presné. Každý počítač má svoju vlastnú vyrovnávaciu pamäť pridelenú vlastným jadrom a sieť prenáša údaje medzi dvoma samostatnými vyrovnávacími pamäťami. Nechcem sa tu ale podrobne zaoberať sieťou, takže budeme predpokladať, že obidva počítače majú prístup k rovnakej vyrovnávacej pamäti umiestnenej „niekde v prázdnote medzi“.

Dobre, keď vieme, ako to vyzerá vizuálne, abstrahujme to do kódu.

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

Tento útržok kódu robí presne to isté, čo predstavuje obrázok vyššie. Až na jednu zaujímavosť, nehovoríme computerB.recv(data). Namiesto toho namiesto údajov zadáme zdanlivo náhodné číslo.

Dôvod je jednoduchý. Dáta po sieti sa prenášajú do bitov. Preto keď prijímame v počítači B, určujeme počet bitov, ktoré sme ochotní prijať v ktoromkoľvek danom časovom okamihu.

Prečo som vybral 1024 bajtov na príjem naraz? Žiadny konkrétny dôvod. Spravidla je najlepšie určiť počet bajtov, ktoré by ste dostali v sile 2. Vybral som 1024, čo je 2¹⁰.

Ako to teda zistí vyrovnávacia pamäť? Počítač A zapisuje alebo odosiela všetky údaje, ktoré sú s ním uložené, do medzipamäte. Počítač B sa rozhodne prečítať alebo prijať prvých 1024 bajtov toho, čo je uložené v tejto vyrovnávacej pamäti.

Dobre, úžasné! Ako však tieto dva počítače vedia navzájom komunikovať? Napríklad, keď počítač A zapisuje do tejto medzipamäte, ako vie, že ho počítač B zdvihne? Aby som to preformuloval, ako môže zabezpečiť, aby spojenie medzi dvoma počítačmi malo jedinečný buffer?

Prenos do IP

Obrázok vyššie zobrazuje rovnaké dva počítače, na ktorých sme pracovali spolu, s pridaním ďalších podrobností. Pred každým počítačom je pozdĺž celej čiary uvedených niekoľko čísel.

Za dlhú lištu pred každým počítačom považujte smerovač, ktorý pripája konkrétny počítač k internetu. Čísla uvedené na každom paneli sa nazývajú porty . Váš počítač má momentálne k dispozícii tisíce portov. Každý port umožňuje pripojenie zásuvky. Na obrázku vyššie som ukázal iba 6 portov, ale máte predstavu.

Porty pod 255 sú zvyčajne vyhradené pre systémové volania a nízkoúrovňové pripojenia. Všeobecne sa odporúča otvoriť spojenie na porte s vysokými 4 číslicami, napríklad 8000. Na obrázku vyššie som nevykreslil vyrovnávaciu pamäť, ale môžete predpokladať, že každý port má svoju vlastnú vyrovnávaciu pamäť.

K samotnej lište je tiež priradené číslo. Toto číslo sa nazýva IP adresa. K IP adrese je priradených veľa portov. Myslite na to nasledujúcim spôsobom:

 127.0.0.1 / | \ / | \ / | \ 8000 8001 8002

Skvelé, vytvorme pripojenie na konkrétnom porte medzi počítačom A a počítačom 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)

Tu je kód pre 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'))

Vyzerá to, že sme z hľadiska kódu poskočili trochu vpred, ale prejdem to. Vieme, že máme dva počítače, A a B. Preto potrebujeme jeden na odosielanie údajov a druhý na príjem údajov.

Svojvoľne som vybral A na zasielanie dát a B na príjem dát. V tomto riadku computerA.connect((‘127.0.0.1’, 8000)robím computerA pripojenie k portu 8000 na IP adrese 127.0.0.1.

Poznámka: 127.0.0.1 zvyčajne znamená localhost, ktorý odkazuje na vaše zariadenie

Potom pre computerB ho spravím tak, aby sa naviazal na port 8000 na IP adrese 127.0.0.1. Pravdepodobne by vás zaujímalo, prečo mám rovnakú adresu IP pre dva rôzne počítače.

To preto, lebo podvádzam. Používam jeden počítač na demonštráciu toho, ako môžete používať zásuvky (v zásade sa kvôli zjednodušeniu pripájam z rovnakého počítača k rovnakému počítaču). Typicky by dva rôzne počítače mali dve rôzne adresy IP.

We already know that only bits can be sent as part of a data packet, which is why we encode the string before sending it over. Similarly, we decode the string on Computer B. If you decide to run the above two files locally, make sure to run computerB.py file first. If you run the computerA.py file first, you will get a connection refused error.

Serving The Clients

I’m sure its been pretty clear to many of you that what I’ve been describing so far is a very simplistic client-server model. In fact you can see that from the above image, all I’ve done is replace Computer A as the client and Computer B as the server.

There is a constant stream of communication that goes on between clients and servers. In our prior code example, we described a one shot of data transfer. Instead, what we want is a constant stream of data being sent from the client to the server. However, we also want to know when that data transfer is complete, so we know we can stop listening.

Let’s try to use an analogy to examine this further. Imagine the following conversation between two people.

Two people are trying to introduce themselves. However, they will not try to talk at the same time. Let’s assume that Raj goes first. John will then wait until Raj has finished introducing himself before he begins introducing himself. This is based on some learned heuristics but we can generally describe the above as a protocol.

Our clients and servers need a similar protocol. Or else, how would they know when it’s their turn to send packets of data?

We’ll do something simple to illustrate this. Let’s say we want to send some data which happens to be an array of strings. Let’s assume the array is as follows:

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

The above is the data that is going to be written from the client to the server. Let’s create another constraint. The server needs to accept data that is exactly equivalent to the data occupied by the string that is going to be sent across at that instant.

So, for instance, if the client is going to send across the string ‘random’, and let’s assume each character occupies 1 byte, then the string itself occupies 6 bytes. 6 bytes is then equal to 6*8 = 48 bits. Therefore, for the string ‘random’ to be transferred across sockets from the client to the server, the server needs to know that it has to access 48 bits for that specific packet of data.

This is a good opportunity to break the problem down. There are a couple of things we need to figure out first.

How do we figure out the number of bytes a string occupies in Python?

Well, we could start by figuring out the length of a string first. That’s easy, it’s just a call to len(). But, we still need to know the number of bytes occupied by a string, not just the length.

We’ll convert the string to binary first, and then find the length of the resulting binary representation. That should give us the number of bytes used.

len(‘random’.encode(‘utf-8’)) will give us what we want

How do we send the number of bytes occupied by each string to the server?

Easy, we’ll convert the number of bytes (which is an integer) into a binary representation of that number, and send it to the server. Now, the server can expect to receive the length of a string before receiving the string itself.

How does the server know when the client has finished sending all the strings?

Remember from the analogy of the conversation, there needs to be a way to know if the data transfer has completed. Computers don’t have their own heuristics they can rely on. So, we’ll provide a random rule. We’ll say that when we send across the string ‘end’, that means the server has received all the strings and can now close the connection. Of course, this means that we can’t use the string ‘end’ in any other part of our array except the very end.

Here’s the protocol we’ve designed so far:

Dĺžka reťazca bude 2 bajty a za ním bude samotný reťazec s premenlivou dĺžkou. Bude to závisieť od dĺžky reťazca odoslaného v predchádzajúcom pakete a budeme striedavo posielať dĺžky reťazcov a samotný reťazec. EOT znamená End Of Transmission a odoslanie reťazca „end“ znamená, že už nie sú potrebné ďalšie údaje na odoslanie.

Poznámka: Skôr ako budeme pokračovať, chcem na niečo poukázať. Toto je veľmi jednoduchý a hlúpy protokol. Ak chcete vidieť, ako vyzerá dobre navrhnutý protokol, nehľadajte nič iné ako protokol HTTP.

Poďme to kódovať. Do kódu som zahrnul komentáre, takže je to samé osebe.

Skvelé, máme spusteného klienta. Ďalej potrebujeme server.

Vo vyššie uvedených zoznamoch chcem vysvetliť niekoľko konkrétnych riadkov kódu. Prvý zo clientSocket.pyspisu.

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

What the above does is convert a number into bytes. The first parameter passed to the to_bytes function is the number of bytes allocated to the result of converting len_of_string to its binary representation.

The second parameter is used to decide whether to follow the Little Endian format or the Big Endian format. You can read more about it here. For now, just know that we will always stick with little for that parameter.

The next line of code I want to take a look at is:

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

We’re converting the string to a binary format using the‘utf-8’ encoding.

Next, in the serverSocket.py file:

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

The first line of code above receives 2 bytes of data from the client. Remember that when we converted the length of the string to a binary format in clientSocket.py, we decided to store the result in 2 bytes. This is why we’re reading 2 bytes here for that same data.

Next line involves converting the binary format to an integer. The byteorder here is “little”, to match the byteorder we used on the client.

If you go ahead and run the two sockets, you should see that the server will print out the strings the client sends across. We established communication!

Conclusion

Dobre, pokryli sme toho zatiaľ dosť. A to, čo sú zásuvky, ako ich používame a ako navrhnúť veľmi jednoduchý a hlúpy protokol. Ak sa chcete dozvedieť viac o tom, ako fungujú zásuvky, veľmi odporúčam prečítať si Beejovu príručku k programovaniu v sieti. Táto elektronická kniha obsahuje veľa skvelých vecí.

Môžete si samozrejme vziať to, čo ste si doteraz prečítali v tomto článku, a aplikovať to na zložitejšie problémy, ako je streamovanie obrázkov z fotoaparátu RaspberryPi do vášho počítača. Bavte sa s tým!

Ak chcete, môžete ma sledovať na Twitteri alebo GitHube. Môžete sa tiež pozrieť na môj blog tu. Som kedykoľvek k dispozícii, ak ma chcete osloviť!

Pôvodne zverejnené na //redixhumayun.github.io/networking/2019/02/14/how-the-internet-speaks.html 14. februára 2019.