Πώς μιλάει το Διαδίκτυο

Μια ιστορία επικοινωνίας

Αναρωτηθήκατε ποτέ πώς μιλάει πραγματικά το Διαδίκτυο; Πώς ένας υπολογιστής «μιλά» σε έναν άλλο υπολογιστή μέσω του Διαδικτύου;

Όταν οι άνθρωποι επικοινωνούν μεταξύ τους, χρησιμοποιούμε λέξεις που είναι συνδεδεμένες σε φαινομενικά σημαντικές προτάσεις. Οι προτάσεις έχουν νόημα μόνο επειδή έχουμε συμφωνήσει σε ένα νόημα για αυτές τις προτάσεις. Έχουμε ορίσει ένα πρωτόκολλο επικοινωνίας, για να το πούμε.

Αποδεικνύεται ότι οι υπολογιστές μιλούν ο ένας στον άλλο με παρόμοιο τρόπο μέσω του Διαδικτύου. Όμως, προχωράμε μπροστά μας. Οι άνθρωποι χρησιμοποιούν το στόμα τους για να επικοινωνήσουν, ας καταλάβουμε ποιο είναι το στόμα του υπολογιστή.

Εισαγάγετε την υποδοχή

Η υποδοχή είναι μια από τις πιο θεμελιώδεις έννοιες στην επιστήμη των υπολογιστών. Μπορείτε να δημιουργήσετε ολόκληρα δίκτυα διασυνδεδεμένων συσκευών χρησιμοποιώντας πρίζες.

Όπως όλα τα άλλα πράγματα στην επιστήμη των υπολογιστών, η υποδοχή είναι μια πολύ αφηρημένη έννοια. Έτσι, αντί να ορίσουμε τι είναι μια πρίζα, είναι πολύ πιο εύκολο να ορίσουμε τι κάνει μια πρίζα.

Λοιπόν, τι κάνει μια πρίζα; Βοηθά δύο υπολογιστές να επικοινωνούν μεταξύ τους. Πώς το κάνει αυτό; Έχει δύο μεθόδους καθορισμένες, καλούμενες send()και recv()για αποστολή και λήψη αντίστοιχα.

Εντάξει, αυτό είναι υπέροχο, αλλά τι να κάνεις send()και recv()να στείλεις και να λάβεις; Όταν οι άνθρωποι κινούν το στόμα τους, ανταλλάσσουν λέξεις. Όταν οι πρίζες χρησιμοποιούν τις μεθόδους τους, ανταλλάσσουν bits και byte.

Ας παρουσιάσουμε τις μεθόδους με ένα παράδειγμα. Ας υποθέσουμε ότι έχουμε δύο υπολογιστές, τον Α και τον Β. Ο υπολογιστής Α προσπαθεί να πει κάτι στον Υπολογιστή Β. Επομένως, ο Υπολογιστής Β προσπαθεί να ακούσει τι λέει ο Υπολογιστής Α. Εδώ θα μοιάζει.

Διαβάζοντας το Buffer

Φαίνεται λίγο περίεργο, έτσι δεν είναι; Για ένα, και οι δύο υπολογιστές δείχνουν μια γραμμή στη μέση, με τίτλο «buffer».

Τι είναι το buffer; Το buffer είναι μια στοίβα μνήμης. Εκεί αποθηκεύονται τα δεδομένα για κάθε υπολογιστή και κατανέμονται από τον πυρήνα.

Στη συνέχεια, γιατί και οι δύο δείχνουν το ίδιο buffer; Λοιπόν, αυτό δεν είναι απόλυτα ακριβές. Κάθε υπολογιστής έχει το δικό του buffer που διατίθεται από τον δικό του πυρήνα και το δίκτυο μεταφέρει τα δεδομένα μεταξύ των δύο ξεχωριστών buffer. Όμως, δεν θέλω να μπω σε λεπτομέρειες δικτύου εδώ, οπότε θα υποθέσουμε ότι και οι δύο υπολογιστές έχουν πρόσβαση στο ίδιο buffer τοποθετημένο «κάπου στο κενό μεταξύ».

Εντάξει, τώρα που ξέρουμε πώς μοιάζει οπτικά, ας το αφαιρέσουμε σε κώδικα.

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

Αυτό το απόσπασμα κώδικα κάνει το ίδιο ακριβώς που αντιπροσωπεύει η παραπάνω εικόνα. Εκτός από μια περιέργεια, δεν λέμε computerB.recv(data). Αντ 'αυτού, καθορίζουμε έναν φαινομενικά τυχαίο αριθμό στη θέση των δεδομένων.

Ο λόγος είναι απλός. Τα δεδομένα μέσω δικτύου μεταδίδονται σε bit. Επομένως, όταν λαμβάνουμε recv στον υπολογιστήB, καθορίζουμε τον αριθμό των bit που είμαστε πρόθυμοι να λάβουμε σε οποιαδήποτε δεδομένη χρονική στιγμή.

Γιατί επέλεξα 1024 byte για να λάβω ταυτόχρονα; Δεν υπάρχει συγκεκριμένος λόγος. Συνήθως είναι καλύτερο να καθορίσετε τον αριθμό των byte που θα λάβετε με ισχύ 2. Διάλεξα το 1024 που είναι 2¹⁰.

Λοιπόν, πώς το καταλαβαίνει αυτό; Λοιπόν, ο Υπολογιστής Α γράφει ή στέλνει όσα δεδομένα αποθηκεύονται μαζί του στο buffer. Ο Υπολογιστής Β αποφασίζει να διαβάσει ή να λάβει τα πρώτα 1024 byte του τι είναι αποθηκευμένο σε αυτό το buffer.

Εντάξει, φοβερό! Αλλά, πώς αυτοί οι δύο υπολογιστές ξέρουν να μιλούν μεταξύ τους; Για παράδειγμα, όταν ο Υπολογιστής Α γράφει σε αυτό το buffer, πώς ξέρει ότι ο Υπολογιστής Β πρόκειται να τον πάρει; Για να το επαναλάβω, πώς μπορεί να διασφαλίσει ότι μια σύνδεση μεταξύ δύο υπολογιστών έχει ένα μοναδικό buffer;

Μεταφορά σε IP

Η παραπάνω εικόνα δείχνει τους ίδιους δύο υπολογιστές με τους οποίους εργαζόμασταν όλοι μαζί με μια ακόμη λεπτομέρεια που προστέθηκε. Υπάρχει μια δέσμη αριθμών που εμφανίζονται μπροστά από κάθε υπολογιστή κατά μήκος ενός φραγμού.

Θεωρήστε ότι η μεγάλη γραμμή μπροστά από κάθε υπολογιστή είναι ο δρομολογητής που συνδέει έναν συγκεκριμένο υπολογιστή στο Διαδίκτυο. Αυτοί οι αριθμοί που αναφέρονται σε κάθε γραμμή ονομάζονται θύρες . Ο υπολογιστής σας διαθέτει χιλιάδες θύρες διαθέσιμες αυτήν τη στιγμή. Κάθε θύρα επιτρέπει σύνδεση υποδοχής. Έχω δείξει μόνο 6 θύρες στην παραπάνω εικόνα, αλλά έχετε την ιδέα.

Οι θύρες κάτω των 255 προορίζονται γενικά για κλήσεις συστήματος και συνδέσεις χαμηλού επιπέδου. Συνιστάται γενικά να ανοίξετε μια σύνδεση σε μια θύρα με τα υψηλά 4-ψηφία, όπως το 8000. Δεν έχω σχεδιάσει το buffer στην παραπάνω εικόνα, αλλά μπορείτε να υποθέσετε ότι κάθε θύρα έχει το δικό της buffer.

Η ίδια η γραμμή έχει επίσης έναν αριθμό που σχετίζεται με αυτό. Αυτός ο αριθμός ονομάζεται διεύθυνση IP. Η διεύθυνση IP έχει μια δέσμη θυρών που σχετίζονται με αυτήν. Σκεφτείτε το με τον ακόλουθο τρόπο:

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

Τέλεια, ας δημιουργήσουμε μια σύνδεση σε μια συγκεκριμένη θύρα μεταξύ του υπολογιστή A και του υπολογιστή 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)

Εδώ είναι ο κωδικός για 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'))

Φαίνεται ότι έχουμε προχωρήσει λίγο από την άποψη του κώδικα, αλλά θα το κάνω. Γνωρίζουμε ότι έχουμε δύο υπολογιστές, τους Α και Β. Επομένως, χρειαζόμαστε έναν για να στείλουμε δεδομένα και έναν για να λαμβάνουμε δεδομένα.

Έχω επιλέξει αυθαίρετα Α για αποστολή δεδομένων και Β για λήψη δεδομένων. Σε αυτήν τη γραμμή computerA.connect((‘127.0.0.1’, 8000), κάνω υπολογιστήΑ σύνδεση με τη θύρα 8000 στη διεύθυνση IP 127.0.0.1.

Σημείωση: Το 127.0.0.1 σημαίνει συνήθως localhost, που αναφέρεται στο μηχάνημά σας

Στη συνέχεια, για το computerB, το κάνω να συνδέεται στη θύρα 8000 στη διεύθυνση IP 127.0.0.1. Τώρα, πιθανώς αναρωτιέστε γιατί έχω την ίδια διεύθυνση IP για δύο διαφορετικούς υπολογιστές.

Αυτό γιατί εξαπατώ. Χρησιμοποιώ έναν υπολογιστή για να δείξω πώς μπορείτε να χρησιμοποιήσετε πρίζες (βασικά συνδέω από και προς τον ίδιο υπολογιστή για λόγους απλότητας). Συνήθως, δύο διαφορετικοί υπολογιστές θα έχουν δύο διαφορετικές διευθύνσεις 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:

Το μήκος της συμβολοσειράς θα είναι 2 bytes, ακολουθούμενο από το ίδιο το ίδιο το string που θα έχει μεταβλητό μήκος. Θα εξαρτηθεί από το μήκος της συμβολοσειράς που έχει σταλεί στο προηγούμενο πακέτο και θα εναλλάσσουμε μεταξύ της αποστολής των μήκους των συμβολοσειρών και της ίδιας της συμβολοσειράς. Το EOT σημαίνει End Of Transmission και η αποστολή της συμβολοσειράς «end» σημαίνει ότι δεν υπάρχουν άλλα δεδομένα για αποστολή.

Σημείωση: Πριν συνεχίσουμε, θέλω να επισημάνω κάτι. Αυτό είναι ένα πολύ απλό και ανόητο πρωτόκολλο. Εάν θέλετε να δείτε πώς μοιάζει ένα καλά σχεδιασμένο πρωτόκολλο, μην ψάχνετε περισσότερο από το πρωτόκολλο HTTP.

Ας το κωδικοποιήσουμε. Έχω συμπεριλάβει σχόλια στον παρακάτω κώδικα, οπότε είναι αυτονόητο.

Τέλεια, έχουμε έναν πελάτη να τρέχει. Στη συνέχεια, χρειαζόμαστε τον διακομιστή.

Θέλω να εξηγήσω μερικές συγκεκριμένες γραμμές κώδικα στις παραπάνω βάσεις. Το πρώτο, από το clientSocket.pyαρχείο.

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

Εντάξει, έχουμε καλύψει αρκετά μέχρι τώρα. Δηλαδή, ποιες είναι οι πρίζες, πώς τις χρησιμοποιούμε και πώς να σχεδιάσουμε ένα πολύ απλό και ανόητο πρωτόκολλο. Αν θέλετε να μάθετε περισσότερα για το πώς λειτουργούν οι πρίζες, συνιστώ ανεπιφύλακτα να διαβάσετε τον Οδηγό για τον προγραμματισμό δικτύου του Beej. Αυτό το ηλεκτρονικό βιβλίο έχει πολλά υπέροχα πράγματα σε αυτό.

Φυσικά, μπορείτε να πάρετε όσα έχετε διαβάσει σε αυτό το άρθρο μέχρι στιγμής και να το εφαρμόσετε σε πιο περίπλοκα προβλήματα, όπως ροή εικόνων από μια κάμερα RaspberryPi στον υπολογιστή σας. Καλή διασκέδαση με αυτό!

Εάν θέλετε, μπορείτε να με ακολουθήσετε στο Twitter ή στο GitHub. Μπορείτε επίσης να δείτε το ιστολόγιό μου εδώ. Είμαι πάντα διαθέσιμος αν θέλετε να επικοινωνήσετε μαζί μου!

Αρχικά δημοσιεύθηκε στη διεύθυνση //redixhumayun.github.io/networking/2019/02/14/how-the-internet-speaks.html στις 14 Φεβρουαρίου 2019.