Cómo habla Internet

Una historia de comunicación

¿Alguna vez te has preguntado cómo habla realmente Internet? ¿Cómo "habla" una computadora con otra a través de Internet?

Cuando las personas se comunican entre sí, usamos palabras encadenadas en oraciones aparentemente significativas. Las oraciones solo tienen sentido porque hemos acordado un significado para estas oraciones. Hemos definido un protocolo de comunicación, por así decirlo.

Resulta que las computadoras se comunican entre sí de manera similar a través de Internet. Pero nos estamos adelantando. La gente usa la boca para comunicarse, primero averigüemos qué es la boca de la computadora.

Entrar en el enchufe

El zócalo es uno de los conceptos más fundamentales en informática. Puede construir redes completas de dispositivos interconectados mediante sockets.

Como todas las demás cosas en informática, un socket es un concepto muy abstracto. Entonces, en lugar de definir qué es un socket, es mucho más fácil definir lo que hace un socket.

Entonces, ¿qué hace un enchufe? Ayuda a dos computadoras a comunicarse entre sí. ¿Como hace esto? Tiene dos métodos definidos, llamado send()y recv()para enviar y recibir respectivamente.

Está bien, eso es todo muy bien, pero ¿qué send()y recv()en realidad enviar y recibir? Cuando la gente mueve la boca, intercambian palabras. Cuando los sockets usan sus métodos, intercambian bits y bytes.

Ilustremos los métodos con un ejemplo. Digamos que tenemos dos computadoras, A y B. La computadora A está tratando de decirle algo a la computadora B. Por lo tanto, la computadora B está tratando de escuchar lo que la computadora A está diciendo. Así es como se vería.

Leer el búfer

Parece un poco extraño, ¿no? Por un lado, ambas computadoras apuntan a una barra en el medio, titulada 'búfer'.

¿Qué es el búfer? El búfer es una pila de memoria. Es donde se almacenan los datos de cada computadora y es asignado por el kernel.

A continuación, ¿por qué ambos apuntan al mismo búfer? Bueno, eso no es del todo exacto en realidad. Cada computadora tiene su propio búfer asignado por su propio núcleo y la red transporta los datos entre los dos búferes separados. Pero no quiero entrar en detalles de la red aquí, por lo que asumiremos que ambas computadoras tienen acceso al mismo búfer ubicado "en algún lugar en el vacío entre".

Bien, ahora que sabemos cómo se ve esto visualmente, vamos a abstraerlo en código.

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

Este fragmento de código hace exactamente lo mismo que representa la imagen de arriba. Excepto por una curiosidad, no decimos computerB.recv(data). En cambio, especificamos un número aparentemente aleatorio en lugar de los datos.

La razón es simple. Los datos a través de una red se transmiten en bits. Por lo tanto, cuando recibimos en la computadora B, especificamos el número de bits que estamos dispuestos a recibir en un momento dado.

¿Por qué elegí 1024 bytes para recibirlos a la vez? Sin motivo específico. Por lo general, es mejor especificar la cantidad de bytes que recibiría en una potencia de 2. Elegí 1024, que es 2¹⁰.

Entonces, ¿cómo lo resuelve el búfer? Bueno, la computadora A escribe o envía los datos almacenados en el búfer. La computadora B decide leer o recibir los primeros 1024 bytes de lo que está almacenado en ese búfer.

¡De acuerdo, genial! Pero, ¿cómo saben estas dos computadoras que se comunican entre sí? Por ejemplo, cuando la computadora A escribe en este búfer, ¿cómo sabe que la computadora B lo va a recoger? Para reformular eso, ¿cómo se puede asegurar que una conexión entre dos computadoras tenga un búfer único?

Portar a direcciones IP

La imagen de arriba muestra las mismas dos computadoras en las que hemos estado trabajando todo junto con un detalle más agregado. Hay un montón de números listados en frente de cada computadora a lo largo de una barra.

Considere la barra larga frente a cada computadora como el enrutador que conecta una computadora específica a Internet. Los números que aparecen en cada barra se denominan puertos . Su computadora tiene miles de puertos disponibles en este momento. Cada puerto permite una conexión de enchufe. Solo he mostrado 6 puertos en la imagen de arriba, pero entiendes la idea.

Los puertos por debajo de 255 generalmente están reservados para llamadas al sistema y conexiones de bajo nivel. En general, es recomendable abrir una conexión en un puerto de 4 dígitos altos, como 8000. No he dibujado el búfer en la imagen de arriba, pero puede suponer que cada puerto tiene su propio búfer.

La barra en sí también tiene un número asociado. Este número se llama dirección IP. La dirección IP tiene varios puertos asociados. Piense en ello de la siguiente manera:

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

Genial, configuremos una conexión en un puerto específico entre la computadora A y la computadora 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)

Aquí está el código para 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'))

Parece que hemos avanzado un poco en términos del código, pero lo analizaré. Sabemos que tenemos dos computadoras, A y B. Por lo tanto, necesitamos una para enviar datos y otra para recibir datos.

He seleccionado arbitrariamente A para enviar datos y B para recibir datos. En esta línea computerA.connect((‘127.0.0.1’, 8000), estoy haciendo que la computadora A se conecte al puerto 8000 en la dirección IP 127.0.0.1.

Nota: 127.0.0.1 normalmente significa localhost, que hace referencia a su máquina

Luego, para la computadora B, lo estoy vinculando al puerto 8000 en la dirección IP 127.0.0.1. Probablemente se esté preguntando por qué tengo la misma dirección IP para dos computadoras diferentes.

Eso es porque estoy haciendo trampa. Estoy usando una computadora para demostrar cómo se pueden usar los sockets (básicamente me conecto desde y hacia la misma computadora por simplicidad). Normalmente, dos computadoras diferentes tendrían dos direcciones IP diferentes.

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:

La longitud de la cadena será de 2 bytes, seguida de la cadena real, que tendrá una longitud variable. Dependerá de la longitud de la cadena enviada en el paquete anterior, y alternaremos entre enviar las longitudes de la cadena y la cadena en sí. EOT significa End Of Transmission, y enviar la cadena 'end' significa que no hay más datos para enviar.

Nota: Antes de continuar, quiero señalar algo. Este es un protocolo muy simple y estúpido. Si desea ver cómo se ve un protocolo bien diseñado, no busque más que el protocolo HTTP.

Codifiquemos esto. He incluido comentarios en el código siguiente, por lo que se explica por sí mismo.

Genial, tenemos un cliente en ejecución. A continuación, necesitamos el servidor.

Quiero explicar algunas líneas específicas de código en las esencias anteriores. El primero, del clientSocket.pyarchivo.

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

Okay, we covered quite a bit so far. Namely, what are sockets, how we use them and how to design a very simple and stupid protocol. If you want to learn more about how sockets work, I highly recommend reading Beej’s Guide To Network Programming. That e-book has a lot of great stuff in it.

You can of course take what you read in this article so far, and apply it to more complex problems like streaming images from a RaspberryPi camera to your computer. Have fun with it!

If you want to, you can follow me on Twitter or GitHub. You can also check out my blog here. I’m always available if you want to reach out to me!

Originally published at //redixhumayun.github.io/networking/2019/02/14/how-the-internet-speaks.html on February 14, 2019.