La lliçó anterior va acabar amb un full de ruta i una promesa: el primer pas per partir el monòlit de Quilòmetre Zero és aconseguir que dos processos parlin entre ells de manera fiable. Abans de triar un mecanisme d'alt nivell (RPC, gRPC, cues de missatges), cal entendre sobre què s'apuntalen tots ells: els protocols de xarxa. Tot el que veurem en aquest mòdul (i bona part del que veurem a la resta del curs) són bytes que viatgen per TCP o UDP, gairebé sempre embolcallats en HTTP. Si no s'entén què garanteix cada capa i què no, és impossible raonar sobre timeouts, duplicats, ordre de missatges o rendiment.
En aquesta lliçó construirem la primera versió, deliberadament primitiva, del servei inventari com un servidor TCP que respon a una ordre de text; enviarem posicions de furgoneta-3 per UDP i observarem què es perd; i farem una petició HTTP al monòlit per veure què hi ha sota un curl. Amb aquest material pràctic compararem TCP, UDP, HTTP (en les versions 1.1, 2 i 3) i MQTT, i fixarem criteris per triar entre ells.
Contingut
- Què és un protocol i per què s'organitza en capes
- La pila TCP/IP
- TCP: la connexió fiable i el seu preu
- UDP: rapidesa sense garanties
- HTTP/1.1, HTTP/2 i HTTP/3
- Protocols lleugers: MQTT i WebSockets
- Pràctica:
inventarisobre TCP, telemetria per UDP i una petició HTTP al monòlit - Com triar: taula comparativa
- Errors comuns i consells
- Exercicis
- Conclusió
- Què és un protocol i per què s'organitza en capes
Un protocol és un acord entre dues parts sobre tres coses: el format dels missatges (quins bytes signifiquen què), les regles d'intercanvi (qui parla primer, com es respon, què passa davant d'un error) i el significat de cada missatge. Sense un acord previ no hi ha comunicació possible: quan comandes envia bytes a inventari, tots dos han d'haver pactat d'entrada que ESTOC formatge-curat\n és una consulta i que OK 5\n n'és la resposta.
Els protocols de xarxa s'organitzen en capes perquè el problema és massa gran per resoldre'l de cop. Cada capa resol un problema concret i ofereix un servei a la capa superior, tot amagant-li els detalls de les inferiors:
- La capa d'enllaç mou trames entre dues màquines connectades físicament (Ethernet, Wi-Fi).
- La capa de xarxa (IP) mou paquets entre màquines de xarxes diferents, saltant per routers. No garanteix res: els paquets es poden perdre, duplicar o arribar desordenats.
- La capa de transport (TCP, UDP) lliura dades a un procés concret (identificat per un port) i, en el cas de TCP, hi afegeix fiabilitat i ordre.
- La capa d'aplicació (HTTP, MQTT, DNS, el protocol de gRPC...) defineix el significat dels missatges per a un ús concret.
Aquesta separació és la raó per la qual pots escriure un servidor HTTP sense saber res d'Ethernet, i també la raó per la qual les fal·làcies de la lliçó 01-04 són tan perilloses: cada capa amaga els problemes de la inferior, però no els elimina. TCP amaga que IP perd paquets a força de reintentar, i aquest reintent es tradueix en latència impredictible (fal·làcia 2).
- La pila TCP/IP
El model TCP/IP té quatre capes (el model OSI, més acadèmic, en té set; a la pràctica es parla amb la terminologia de tots dos, i convé conèixer-ne la correspondència).
| Capa TCP/IP | Capa OSI equivalent | Unitat de dades | Protocols habituals | Què identifica el destí | On viu a Quilòmetre Zero |
|---|---|---|---|---|---|
| Aplicació | Aplicació, Presentació, Sessió (5-7) | Missatge | HTTP, gRPC, MQTT, AMQP, DNS, TLS | URL, tòpic, mètode | El codi de comandes, inventari, l'app mòbil dels repartidors |
| Transport | Transport (4) | Segment (TCP) / Datagrama (UDP) | TCP, UDP, QUIC | Port (p. ex. 5001) | La biblioteca socket de Python, el runtime de gRPC |
| Xarxa (Internet) | Xarxa (3) | Paquet | IP (v4/v6), ICMP | Adreça IP | El kernel del sistema operatiu, la xarxa virtual de Docker, Kubernetes |
| Enllaç | Enllaç i Física (1-2) | Trama | Ethernet, Wi-Fi, 4G/5G | Adreça MAC | Les targetes de xarxa del servidor; el mòdem 4G de furgoneta-3 |
Quan comandes envia "ESTOC formatge-curat" a inventari, el missatge baixa per la pila a l'emissor (se li afegeixen capçaleres TCP, després IP, després Ethernet: encapsulació) i puja per la pila al receptor, on cada capa treu la seva capçalera. És útil visualitzar-ho:
flowchart LR
subgraph comandes["Node de comandes"]
A1[Aplicació: ESTOC formatge-curat] --> T1[TCP: port 5001, núm. de seqüència]
T1 --> R1[IP: 10.0.0.12 → 10.0.0.31]
R1 --> E1[Ethernet: trama]
end
E1 -. xarxa física, routers .-> E2
subgraph inventari["Node d'inventari"]
E2[Ethernet: trama] --> R2[IP]
R2 --> T2[TCP: reassembla i ordena]
T2 --> A2[Aplicació: ESTOC formatge-curat]
end
Un detall pràctic que s'oblida sovint: un port identifica un procés, no un servei lògic. Quan inventari tingui diverses rèpliques (inv-bcn, inv-vlc), cadascuna escoltarà a la seva pròpia IP i port, i caldrà alguna cosa per sobre (DNS, un balancejador, el descobriment de serveis de Kubernetes, lliçó 07-05) perquè comandes sàpiga a quina adreçar-se.
- TCP: la connexió fiable i el seu preu
TCP (Transmission Control Protocol) és el protocol de transport que fan servir HTTP, gRPC, AMQP, el protocol de PostgreSQL i gairebé tota la resta. Ofereix quatre garanties sobre IP, que no n'ofereix cap:
- Connexió. Abans d'enviar dades s'estableix un canal bidireccional entre dos extrems (IP:port ↔ IP:port). Les dades pertanyen a aquesta connexió, i la connexió es tanca explícitament.
- Fiabilitat. Cada byte enviat té un número de seqüència; el receptor confirma (ACK) el que ha rebut; el que no s'ha confirmat es retransmet. Des del punt de vista de l'aplicació, o el byte arriba, o la connexió acaba amb error.
- Ordre. Els bytes es lliuren a l'aplicació en el mateix ordre en què es van enviar, encara que els paquets IP arribin desordenats.
- Control de flux i de congestió. El receptor anuncia quant pot acceptar (finestra de recepció), i l'emissor redueix el ritme quan detecta pèrdues, que interpreta com a congestió de la xarxa.
El que TCP no és: un protocol de missatges
TCP lliura un flux de bytes, no missatges. Si comandes fa dos send() seguits de "ESTOC formatge-curat\n" i "ESTOC tomaquet-rosa\n", el receptor els pot obtenir en un sol recv(), o en tres trossos de mida arbitrària. L'aplicació és responsable de delimitar els missatges (amb un terminador com \n, amb un prefix de longitud, o amb un format autodescriptiu). És la font d'errors més freqüent en programar amb sockets i la raó per la qual existeixen els protocols d'aplicació: HTTP fa servir capçaleres i Content-Length; gRPC fa servir un prefix de longitud de 5 bytes per missatge.
El cost del handshake
Obrir una connexió TCP requereix un intercanvi de tres missatges (SYN, SYN-ACK, ACK): un viatge d'anada i tornada complet (RTT) abans de poder enviar el primer byte útil. Si a més hi ha TLS (lliçó 06-02), s'hi afegeixen un o dos RTT més. Entre dos contenidors del mateix centre de dades l'RTT és d'uns 0,5 ms i el cost és menyspreable; entre l'app d'un repartidor amb 4G a Lleida i el servidor, l'RTT pot ser de 80-100 ms, i obrir una connexió per cada missatge és un malbaratament enorme. D'aquí neix la importància de les connexions persistents (keep-alive) i dels pools de connexions: obrir poques connexions i reutilitzar-les.
Head-of-line blocking
La garantia d'ordre té un preu. Si en una connexió TCP es perd el paquet número 7, TCP reté els paquets 8, 9 i 10 (que sí que han arribat) fins que arriba la retransmissió del 7, perquè ha de lliurar els bytes en ordre. Aquest fenomen s'anomena bloqueig de cap de línia (head-of-line blocking, HOL): una pèrdua endarrereix tot el que ve darrere, encara que sigui independent. Quan diverses peticions lògiques comparteixen una connexió (com fa HTTP/2), una pèrdua en la petició de l'Anna endarrereix la petició d'en Marc. Ho reprendrem en parlar d'HTTP/3.
Quan s'assabenta TCP que l'altre extrem ha mort
Mai de manera immediata. Si el procés d'inventari mor netament, el sistema operatiu tanca la connexió i el client rep un error ràpid. Però si el servidor sencer s'apaga o un router deixa d'encaminar, no arriba cap paquet de tancament: el client continua esperant fins que les seves retransmissions esgoten un límit (minuts, per defecte) o fins que el mecanisme keepalive de TCP (desactivat per defecte, i amb intervals d'hores) ho detecta. Aquesta és la raó tècnica per la qual la lliçó 01-04 insistia que cap crida remota s'ha de fer sense timeout d'aplicació: TCP no ho farà per tu.
- UDP: rapidesa sense garanties
UDP (User Datagram Protocol) és el mínim que es pot posar sobre IP: hi afegeix els ports d'origen i destí i una suma de verificació, i res més. No hi ha connexió, no hi ha confirmacions, no hi ha retransmissió, no hi ha ordre, no hi ha control de flux. Cada sendto() és un datagrama independent que arriba sencer o no arriba.
Per a què serveix, doncs? Per als casos en què les garanties de TCP fan nosa:
- Quan una dada vella no val res. L'app de
furgoneta-3envia la seva posició GPS cada segon. Si es perd la posició de les 10:00:03, retransmetre-la a les 10:00:05 no aporta res: ja haurà arribat la de les 10:00:04. Amb TCP, aquesta retransmissió a més bloquejaria les posicions posteriors (HOL). Amb UDP, simplement es perd i la següent la substitueix. - Quan la latència importa més que la completesa. Veu, vídeo en directe, jocs, telemetria.
- Quan hi ha moltíssims missatges petits i el cost de mantenir connexions seria prohibitiu (DNS fa servir UDP per això).
- Quan es vol construir un protocol de transport propi a sobre, com fa QUIC.
A Quilòmetre Zero, la telemetria de posicions és el cas clar: milers de repartidors en campanya enviant un datagrama per segon, on perdre'n un de cada cent és irrellevant. El que no s'enviaria mai per UDP és una comanda o un pagament: allà cada missatge importa. Un matís important: l'aplicació que fa servir UDP sol acabar afegint algunes de les garanties de TCP a mida (un número de seqüència per detectar desordre, per exemple), i ho veurem al codi.
- HTTP/1.1, HTTP/2 i HTTP/3
HTTP és el protocol d'aplicació dominant, no només per a pàgines web, sinó com a transport d'API REST, de gRPC i, cada cop més, de gairebé qualsevol cosa (perquè travessa tallafocs i proxies sense problemes). Convé entendre'n les tres versions perquè les diferències tenen conseqüències arquitectòniques directes.
HTTP/1.1
Protocol de text sobre TCP. Una petició és una línia de mètode i ruta, unes capçaleres, una línia en blanc i un cos opcional; la resposta, una línia d'estat, capçaleres i cos. És el que veuràs a l'apartat 7 en fer la petició al monòlit. Les seves limitacions:
- Una petició en vol per connexió. El client envia una petició i ha d'esperar la resposta completa abans d'enviar la següent per aquesta connexió (el pipelining existeix a l'especificació, però mai no ha funcionat bé a la pràctica). Els navegadors ho compensen obrint 6 connexions per domini; els clients d'API, amb pools.
- Capçaleres repetides i en text. Cada petició envia
Host,User-Agent,Accept, cookies... sense comprimir. En una API que fa milers de crides petites, les capçaleres poden pesar més que els cossos. - Keep-alive (connexions persistents) és el comportament per defecte i mitiga el cost del handshake, però no el problema de la serialització de peticions.
HTTP/2
Mateix significat (mètodes, codis, capçaleres), format diferent: binari i amb multiplexació. Una única connexió TCP transporta múltiples fluxos (streams) simultanis i independents: la petició d'estoc de l'Anna i la d'en Marc viatgen intercalades a la mateixa connexió, sense esperar-se. A més, comprimeix les capçaleres (HPACK) i permet prioritzar fluxos. És la base de gRPC (lliçó 02-03) precisament per la multiplexació i perquè els fluxos permeten streaming en totes dues direccions.
El seu punt feble és el que ja coneixem: com que tots els fluxos comparteixen una connexió TCP, el bloqueig de cap de línia de TCP els afecta tots. En xarxes amb pèrdues (mòbils), HTTP/2 pot rendir pitjor que HTTP/1.1 amb diverses connexions.
HTTP/3 i QUIC
HTTP/3 resol aquest problema canviant el transport: en lloc de TCP fa servir QUIC, un protocol construït sobre UDP que reimplementa fiabilitat, ordre i control de congestió, però per flux, no per connexió. Si es perd un paquet del flux de l'Anna, només s'endarrereix el flux de l'Anna. QUIC a més integra TLS 1.3 al handshake (una connexió segura en 1 RTT, o 0 RTT si es reprèn) i sobreviu a canvis d'IP (la furgoneta que passa de Wi-Fi a 4G manté la connexió). Està desplegat als grans navegadors i CDN; en la comunicació entre serveis dins d'un centre de dades el seu avantatge és menor, perquè allà gairebé no hi ha pèrdues.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Format | Text | Binari (trames) | Binari (trames) |
| Transport | TCP | TCP | QUIC sobre UDP |
| Peticions simultànies per connexió | 1 | Moltes (multiplexació) | Moltes (multiplexació) |
| Compressió de capçaleres | No | HPACK | QPACK |
| Bloqueig de cap de línia | A nivell HTTP (una petició bloqueja la següent) | Només a nivell TCP | Eliminat (per flux) |
| Handshake amb TLS | 2-3 RTT | 2-3 RTT | 1 RTT (0 RTT si es reprèn) |
| Ús típic a Quilòmetre Zero | API REST pública, monòlit actual | gRPC entre serveis | Apps mòbils via CDN |
- Protocols lleugers: MQTT i WebSockets
Hi ha escenaris en què HTTP és massa pesant o el seu model petició-resposta no hi encaixa.
MQTT és un protocol de publicació/subscripció dissenyat per a dispositius amb recursos limitats i xarxes poc fiables (el seu origen és a la telemetria d'oleoductes per satèl·lit). Un broker central rep missatges publicats en tòpics jeràrquics (km0/repartiment/furgoneta-3/posicio) i els reenvia a qui s'hi hagi subscrit (amb comodins: km0/repartiment/+/posicio). Les seves capçaleres ocupen 2 bytes, manté una única connexió TCP persistent (de manera que un missatge no paga handshake), defineix tres nivells de qualitat de servei (QoS 0: com a molt una vegada; QoS 1: almenys una vegada; QoS 2: exactament una vegada, amb un intercanvi de quatre missatges) i inclou el concepte de last will, un missatge que el broker publica si el dispositiu es desconnecta bruscament. Per a l'app dels repartidors és una alternativa seriosa a UDP cru: paga una mica més de bytes a canvi de travessar qualsevol xarxa mòbil i de tenir un broker que reparteix les posicions. Aquí només el presentem; el disseny d'un sistema de missatgeria en temps real amb MQTT i la propagació fins al mapa de la clienta es desenvolupa a la lliçó 08-02.
WebSockets converteix una connexió HTTP en un canal bidireccional persistent entre navegador i servidor, perquè el servidor pugui enviar sense que el client pregunti. És el que farà servir la pantalla de seguiment de l'Anna per veure moure's furgoneta-3 sense recarregar. També es desenvolupa a la lliçó 08-02; aquí n'hi ha prou de saber que existeix, que va sobre TCP i que comença com una petició HTTP normal amb la capçalera Upgrade: websocket.
- Pràctica:
inventari sobre TCP, telemetria per UDP i una petició HTTP al monòlit
inventari sobre TCP, telemetria per UDP i una petició HTTP al monòlitTocarem les tres coses amb la biblioteca estàndard de Python. L'objectiu no és escriure codi de producció (per a això hi ha les lliçons següents), sinó veure amb les mans què fa cada protocol. Tot el codi va a km0/serveis/, que fins ara era buida.
7.1 Servidor TCP d'inventari
Primera versió del servei inventari extret del monòlit: un servidor que accepta connexions i respon a l'ordre ESTOC <producte>. Definim un protocol d'aplicació mínim: missatges de text UTF-8 acabats en \n; respostes OK <unitats> o ERR <motiu>.
# km0/serveis/inventari/tcp_servidor.py
import socket
import threading
HOST = "0.0.0.0" # escoltar a totes les interfícies del contenidor
PORT = 5001
# Estoc inicial: els cinc productes de l'esquema 001_esquema_inicial.sql,
# identificats per un slug URL-safe. Més endavant vindrà de PostgreSQL.
ESTOC = {
"tomaquet-rosa": 120,
"carbasso": 80,
"formatge-curat": 5,
"formatge-fresc": 30,
"vi-crianca": 200,
}
cadenat = threading.Lock() # diversos fils llegiran ESTOC alhora
def llegir_linia(conn):
"""Llegeix bytes de la connexió fins a trobar '\\n'.
TCP lliura un flux de bytes, no missatges: un recv() pot retornar
mitja ordre o dues ordres juntes. Delimitar és feina nostra.
"""
dades = b""
while not dades.endswith(b"\n"):
tros = conn.recv(1024)
if not tros: # b"" vol dir que el client ha tancat
return None
dades += tros
return dades.decode("utf-8").strip()
def processar(linia):
parts = linia.split()
if len(parts) == 2 and parts[0] == "ESTOC":
with cadenat:
unitats = ESTOC.get(parts[1])
if unitats is None:
return "ERR producte desconegut"
return f"OK {unitats}"
return "ERR ordre no reconeguda"
def atendre(conn, adreca):
"""Bucle d'una connexió: llegeix l'ordre, respon, repeteix fins al tancament."""
print(f"[inventari] connexió de {adreca}")
with conn:
while True:
linia = llegir_linia(conn)
if linia is None:
break
resposta = processar(linia)
conn.sendall((resposta + "\n").encode("utf-8"))
print(f"[inventari] {adreca} desconnectat")
def main():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as servidor:
# Permet reiniciar el servidor sense esperar que el SO alliberi el port
servidor.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
servidor.bind((HOST, PORT))
servidor.listen()
print(f"[inventari] escoltant a {HOST}:{PORT}")
while True:
conn, adreca = servidor.accept() # bloqueja fins que arriba un client
threading.Thread(target=atendre, args=(conn, adreca), daemon=True).start()
if __name__ == "__main__":
main()Explicació de les parts clau:
socket.AF_INET, socket.SOCK_STREAMvol dir "IPv4, TCP". AmbSOCK_DGRAMseria UDP.bindassocia el socket a una adreça i un port;listenel posa en mode servidor;acceptbloqueja fins que un client completa el handshake i retorna un socket nou dedicat a aquesta connexió. El socket original continua escoltant.- Cada connexió s'atén en un fil, perquè
recvbloqueja: si atenguéssim els clients en sèrie, el segon esperaria que el primer es desconnectés. A la lliçó 01-02 vam veure l'alternativa ambasyncio; aquí fem servir fils per simplicitat. llegir_liniaés la part més important de tot el fitxer. Acumula bytes fins a veure el terminador. Si l'elimines i confies que "unrecv= un missatge", el servidor funcionarà a les teves proves locals i fallarà en producció quan la xarxa fragmenti un missatge o n'ajunti dos.sendall(i nosend) garanteix que s'envien tots els bytes;sendpot enviar-ne només una part i retornar quants n'ha enviat.
7.2 Client TCP a comandes
# km0/serveis/comandes/tcp_client.py
import socket
class ClientInventari:
"""Client del protocol de text d'inventari. Reutilitza una connexió."""
def __init__(self, host="localhost", port=5001, timeout=2.0):
# create_connection fa el handshake TCP; timeout acota tant la
# connexió com cada recv/send posteriors (lliçó 01-04).
self.sock = socket.create_connection((host, port), timeout=timeout)
self.buffer = b""
def consultar_estoc(self, producte):
self.sock.sendall(f"ESTOC {producte}\n".encode("utf-8"))
while b"\n" not in self.buffer:
tros = self.sock.recv(1024)
if not tros:
raise ConnectionError("inventari ha tancat la connexió")
self.buffer += tros
linia, _, self.buffer = self.buffer.partition(b"\n")
resposta = linia.decode("utf-8")
if resposta.startswith("OK "):
return int(resposta[3:])
raise ValueError(f"inventari ha respost: {resposta}")
def tancar(self):
self.sock.close()
if __name__ == "__main__":
client = ClientInventari()
for producte in ["formatge-curat", "tomaquet-rosa", "pernil"]:
try:
print(producte, "->", client.consultar_estoc(producte))
except (ValueError, ConnectionError, socket.timeout) as e:
print(producte, "-> error:", e)
client.tancar()Sortida esperada:
formatge-curat -> 5 tomaquet-rosa -> 120 pernil -> error: inventari ha respost: ERR producte desconegut
Fixa't en dues coses. Primera: la connexió s'obre una vegada i es fa servir per a les tres consultes (keep-alive fet a mà); obrir-la a cada consulta triplicaria els handshakes. Segona: el buffer del client conserva els bytes sobrants d'un recv, per si ha arribat part de la resposta següent. Prova d'aturar el servidor amb Ctrl+C mentre el client espera: veuràs un ConnectionError immediat perquè el sistema operatiu ha tancat la connexió. Ara prova de bloquejar el port amb un tallafocs o de desconnectar la xarxa: el client no veurà res fins que salti el timeout de 2 segons. És la diferència entre una fallada crash neta i una omissió (lliçó 01-02).
7.3 Telemetria de furgoneta-3 per UDP
Ara l'app del repartidor. Envia un datagrama JSON per segon amb la seva posició i un número de seqüència. Per simular una xarxa mòbil, l'emissor "perd" un 10 % dels datagrames abans d'enviar-los.
# km0/serveis/repartiment/furgoneta_udp.py
import json
import random
import socket
import time
DESTI = ("localhost", 5002)
REPARTIDOR = "furgoneta-3"
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP: sense connect ni handshake
lat, lon = 41.9794, 2.8214 # sortida del mercat de Girona
for seq in range(1, 31):
lat += random.uniform(-0.0005, 0.0005)
lon += random.uniform(-0.0005, 0.0005)
posicio = {"repartidor": REPARTIDOR, "seq": seq,
"lat": round(lat, 5), "lon": round(lon, 5), "ts": time.time()}
if random.random() < 0.10:
print(f"(simulant la pèrdua del datagrama {seq})")
else:
# Cada sendto és un datagrama independent: arriba sencer o no arriba.
sock.sendto(json.dumps(posicio).encode("utf-8"), DESTI)
time.sleep(1)I el receptor a repartiment, que detecta forats a la seqüència i descarta posicions que arriben tard:
# km0/serveis/repartiment/receptor_posicions.py
import json
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 5002))
print("[repartiment] esperant posicions a UDP 5002")
ultim_seq = {} # repartidor -> últim número de seqüència acceptat
while True:
dades, origen = sock.recvfrom(4096) # un datagrama complet per crida
pos = json.loads(dades)
rep, seq = pos["repartidor"], pos["seq"]
anterior = ultim_seq.get(rep, 0)
if seq <= anterior:
print(f"[repartiment] {rep}: el datagrama {seq} ha arribat tard, descartat")
continue
if seq > anterior + 1:
print(f"[repartiment] {rep}: perduts {seq - anterior - 1} datagrames")
ultim_seq[rep] = seq
print(f"[repartiment] {rep} a ({pos['lat']}, {pos['lon']}) seq={seq}")Punts importants:
- No hi ha
listen,acceptniconnect.recvfromretorna un datagrama complet i l'adreça de qui l'ha enviat. A diferència de TCP, aquí sí que hi ha missatges amb límits: no calllegir_linia. - El número de seqüència l'afegim nosaltres: UDP no el dona. Amb ell detectem pèrdues i desordre, i decidim què fer (ignorar la pèrdua i descartar el que arriba tard, que és la política correcta per a posicions). Estem reconstruint part de TCP, la part que ens interessa, sense pagar per la que no.
- El datagrama de
furgoneta-3fa uns 100 bytes. Per TCP+HTTP/1.1, amb capçaleres, passaria de 300, i cada enviament nou des d'una connexió tancada afegiria un handshake. Per MQTT (apartat 6) serien uns 110 bytes sobre una connexió ja oberta: un compromís intermedi que veurem a 08-02.
7.4 Una petició HTTP al monòlit
Per acabar, parlem amb el monòlit que vam deixar engegat a 01-06, però mirant el que curl amaga.
# km0/serveis/comandes/http_salut.py
import http.client
import json
conn = http.client.HTTPConnection("localhost", 8000, timeout=3)
conn.request("GET", "/salut", headers={"Accept": "application/json",
"User-Agent": "km0-comandes/0.1"})
resp = conn.getresponse()
print("Estat:", resp.status, resp.reason) # 200 OK
print("Capçaleres:", dict(resp.getheaders()))
cos = json.loads(resp.read())
print("Cos:", cos) # {'estat': 'ok', 'productes': 5}
conn.close()El que viatja per la connexió TCP cap al port 8000 és exactament aquest text (pots veure'l amb curl -v http://localhost:8000/salut o amb nc localhost 8000 escrivint-lo a mà):
I la resposta:
Fixa't en Content-Length: és la manera com HTTP/1.1 resol el problema de delimitar missatges sobre el flux de bytes de TCP (el mateix que vam resoldre amb \n a 7.1). I fixa't que, comparat amb el nostre protocol de text, HTTP afegeix capçaleres que aquí ocupen més que la dada útil: és el preu de la interoperabilitat universal. Per a una crida interna entre comandes i inventari que es repeteixi 1.200 vegades per segon (lliçó 01-03), aquest preu pesa, i és una de les raons per les quals la lliçó 02-03 farà servir gRPC sobre HTTP/2 amb cossos binaris.
- Com triar: taula comparativa
| Criteri | TCP (socket cru) | UDP | HTTP (1.1/2) | MQTT |
|---|---|---|---|---|
| Garantia de lliurament | Sí (o error) | No | Sí (heretada de TCP) | Configurable (QoS 0/1/2) |
| Ordre | Sí | No | Sí | Sí per tòpic i QoS>0 |
| Connexió | Sí, persistent | No | Sí (keep-alive) | Sí, persistent i lleugera |
| Sobrecàrrega per missatge | Molt baixa (20 B de capçalera TCP) | Mínima (8 B) | Alta a 1.1, mitjana a 2 | Molt baixa (2 B) |
| Model | Flux bidireccional | Datagrames | Petició-resposta | Publicació/subscripció |
| Delimitació de missatges | Feina de l'aplicació | Nativa | Nativa (capçaleres) | Nativa |
| Travessa proxies/tallafocs | Depèn del port | Sovint bloquejat | Sempre | Normalment (o sobre WebSockets) |
| Interoperabilitat | Requereix protocol propi | Requereix protocol propi | Universal | Àmplia (IoT) |
| Quan fer-lo servir a Quilòmetre Zero | Mai directament: sempre amb un protocol a sobre | Telemetria de posicions si es controla la xarxa | API públiques (REST) i gRPC entre serveis | Telemetria de repartidors en xarxes mòbils (08-02) |
Criteris de tria, per ordre d'importància:
- Cada missatge importa? Si és que sí, descarta UDP i MQTT QoS 0.
- És petició-resposta o és un flux d'esdeveniments? Petició-resposta → HTTP/gRPC (02-02, 02-03). Flux cap a molts interessats → missatgeria (02-04) o MQTT.
- Qui hi ha a l'altra banda? Un navegador o un tercer → HTTP. Un servei propi → gRPC. Un dispositiu amb bateria i 4G → MQTT.
- Quants missatges per segon i de quina mida? A partir de milers per segon amb cossos petits, la sobrecàrrega d'HTTP/1.1 en text es nota; HTTP/2 binari o MQTT la redueixen.
- Només llavors mira el rendiment brut. Gairebé mai és el factor decisiu i gairebé sempre és el primer que es discuteix.
Errors Comuns i Consells
- Suposar que un
recvretorna un missatge. És l'error número u amb TCP. Funciona en local i falla en producció. Delimita sempre (terminador, prefix de longitud o format autodescriptiu) o fes servir un protocol d'aplicació que ho faci per tu. - Confiar que TCP detectarà que el servidor ha mort. Només ho detecta si el procés tanca la connexió. Davant d'un servidor apagat o una xarxa tallada, el teu client esperarà minuts sense timeout d'aplicació.
- Obrir una connexió per petició. Cada handshake costa un RTT (més amb TLS). Reutilitza connexions: pools als clients HTTP, canals persistents a gRPC, una connexió per procés a MQTT.
- Fer servir UDP "perquè és més ràpid" per a dades que importen. Una comanda perduda no és més ràpida, és una comanda perduda. UDP és per a dades que caduquen.
- Afegir a UDP tota la fiabilitat de TCP a mà. Si acabes implementant retransmissions, ordre i control de flux, fes servir TCP (o QUIC). UDP només compensa si necessites menys garanties.
- Ignorar la mida de les capçaleres HTTP. En crides internes molt freqüents, mesurar bytes per petició revela sorpreses. HTTP/2 i els cossos binaris existeixen per això.
- Consell: aprèn a fer servir
curl -v,nc(netcat) itcpdump/Wireshark. Veure els bytes reals que viatgen resol en minuts discussions que en abstracte duren hores. - Consell: documenta el protocol d'aplicació encara que sigui de text i de tres línies, com hem fet amb
ESTOC/OK/ERR. El dia que un altre equip escrigui un client, ho agrairà; i el dia que el vulguis canviar, sabràs què es trenca.
Exercicis
Exercici 1: Ampliar el protocol d'inventari
Afegeix al servidor TCP l'ordre RESERVAR <producte> <quantitat>, que descompti estoc si n'hi ha prou i respongui OK <restant>, o ERR estoc insuficient en cas contrari. Tingues en compte que diversos fils poden reservar alhora el mateix producte (recorda l'últim formatge de l'Anna i en Marc, lliçó 01-05). Després, des del client, llança 4 reserves de 2 unitats de formatge-curat des de 4 fils simultanis i comprova que l'estoc mai no queda negatiu.
Exercici 2: Fragmentació de missatges
Modifica tcp_client.py perquè enviï les tres ordres ESTOC de cop amb un únic sendall (concatenades amb \n) i després llegeixi les tres respostes. Comprova si el servidor les processa correctament quan arriben juntes i explica per què, i raona què passaria si llegir_linia s'hagués escrit com return conn.recv(1024).decode().strip().
Exercici 3: Triar protocol
Per a cada flux de Quilòmetre Zero indica quin protocol de transport/aplicació faries servir (TCP cru, UDP, HTTP/1.1, HTTP/2, MQTT) i justifica-ho amb els criteris de l'apartat 8:
- L'app de la Llúcia consulta el catàleg de la Formatgeria Montblanc.
comandesconsulta l'estoc ainventari1.200 vegades per segon en campanya.furgoneta-3envia la seva posició cada segon mentre travessa zones amb cobertura intermitent entre Lleida i Tarragona.- Un productor puja 40 fotos dels seus productes des de la seva oficina.
- Un sensor de temperatura de la cambra frigorífica del Celler Roure Alt informa cada minut, amb bateria.
Solucions
Solució 1:
def processar(linia):
parts = linia.split()
if len(parts) == 2 and parts[0] == "ESTOC":
with cadenat:
unitats = ESTOC.get(parts[1])
return "ERR producte desconegut" if unitats is None else f"OK {unitats}"
if len(parts) == 3 and parts[0] == "RESERVAR":
producte = parts[1]
try:
quantitat = int(parts[2])
except ValueError:
return "ERR quantitat no numèrica"
if quantitat <= 0:
return "ERR la quantitat ha de ser positiva"
with cadenat: # comprovar i descomptar de manera atòmica
unitats = ESTOC.get(producte)
if unitats is None:
return "ERR producte desconegut"
if unitats < quantitat:
return "ERR estoc insuficient"
ESTOC[producte] = unitats - quantitat
return f"OK {ESTOC[producte]}"
return "ERR ordre no reconeguda"La clau és que la comprovació i el descompte passen dins del mateix with cadenat. Si comprovessis fora del cadenat i descomptessis a dins, dos fils podrien veure "en queden 5", i tots dos descomptar-ne 2... i després un tercer i un quart també: l'estoc quedaria en -3. Amb 4 reserves concurrents de 2 unitats sobre 5, exactament dues tindran èxit (restant 3, després 1) i dues rebran ERR estoc insuficient. Client de prova:
import threading
def reservar():
c = ClientInventari()
c.sock.sendall(b"RESERVAR formatge-curat 2\n")
print(c.sock.recv(1024).decode().strip())
c.tancar()
fils = [threading.Thread(target=reservar) for _ in range(4)]
for f in fils: f.start()
for f in fils: f.join()Fixa't que, en un sol procés, el cadenat resol el problema; quan hi hagi rèpliques inv-bcn i inv-vlc, cap cadenat en memòria no servirà: aquest és el problema del Mòdul 3.
Solució 2:
self.sock.sendall(b"ESTOC formatge-curat\nESTOC tomaquet-rosa\nESTOC pernil\n")
for _ in range(3):
while b"\n" not in self.buffer:
self.buffer += self.sock.recv(1024)
linia, _, self.buffer = self.buffer.partition(b"\n")
print(linia.decode())Funciona perquè llegir_linia del servidor acumula fins al primer \n i... aquí hi ha una trampa: tal com està escrita, llegir_linia comprova dades.endswith(b"\n"), i si les tres ordres arriben en un sol recv, dades acaba en \n i conté tres línies; processar veurà "ESTOC formatge-curat\nESTOC tomaquet-rosa\nESTOC pernil" i respondrà ERR ordre no reconeguda una sola vegada, deixant el client esperant dues respostes que no arribaran mai (fins al timeout). La versió correcta del servidor ha de conservar un buffer per connexió i extreure'n línies amb partition, exactament com fa el client. És un exemple perfecte que "funciona a les meves proves" no vol dir "gestiona bé el flux de bytes". Amb la versió ingènua conn.recv(1024).decode().strip(), a més, una ordre que arribés partida en dos recv produiria dos errors ERR ordre no reconeguda i cap resultat.
Solució 3:
- HTTP (1.1 o 2, segons el client): petició-resposta, client extern (app mòbil o navegador), interoperabilitat i memòria cau d'HTTP. Amb una CDN al davant, HTTP/3 millora l'experiència mòbil.
- HTTP/2 (gRPC): cada missatge importa (descarta UDP), petició-resposta síncrona (descarta missatgeria), servei propi, volum alt amb missatges petits (la multiplexació i el format binari redueixen la sobrecàrrega). Lliçó 02-03.
- MQTT (o UDP si la xarxa fos pròpia): les dades caduquen (una posició vella no val res), xarxa mòbil amb pèrdues, dispositiu amb bateria. MQTT amb QoS 0 dona pràcticament l'eficiència d'UDP, però manté una connexió que travessa les xarxes dels operadors mòbils i es reconnecta sola; i el broker reparteix les posicions a qui les necessiti (08-02).
- HTTP: cada foto importa (res d'UDP), és una transferència petició-resposta, va des d'un navegador; HTTP/2 permet pujar les 40 en paral·lel per una sola connexió.
- MQTT amb QoS 1: dispositiu restringit, xarxa possiblement inestable, missatges petits i infreqüents en què sí que importa que arribin (una temperatura fora de rang és una alerta). El last will avisaria si el sensor es desconnecta.
Conclusió
Aquesta lliçó ha baixat fins als fonaments sobre els quals s'apuntala tota la resta. Hem vist que un protocol és un acord de format, regles i significat, i que la pila TCP/IP reparteix el problema en capes que amaguen, però no eliminen, les fallades de la capa inferior. TCP ens dona connexió, fiabilitat, ordre i control de flux a canvi del handshake, del bloqueig de cap de línia i d'una detecció de fallades que mai no és immediata; UDP ens treu tot això i per això resulta ideal per a les posicions de furgoneta-3, on una dada vella no val res. HTTP/1.1 és text i una petició per connexió; HTTP/2 multiplexa fluxos sobre una connexió binària i és la base de gRPC; HTTP/3 s'apuntala en QUIC sobre UDP per eliminar el bloqueig de cap de línia. MQTT i WebSockets queden presentats i remesos a la lliçó 08-02. Amb el codi hem comprovat que TCP és un flux de bytes que exigeix delimitar missatges, que un client sense timeout es penja davant d'un servidor que desapareix sense tancar, i que les capçaleres HTTP poden pesar més que la dada.
El servidor TCP d'inventari amb el seu protocol ESTOC/OK/ERR funciona, però és incòmode: cada operació nova exigeix inventar un format, escriure un parser a tots dos extrems i traduir errors a mà. El que voldríem és escriure inventari.consultar_estoc("formatge-curat") al codi de comandes i que algú s'encarregués de convertir-ho en bytes, enviar-ho, esperar la resposta i retornar-la com un valor de Python. Aquesta idea, amb els seus avantatges i els seus paranys, és el tema de la lliçó següent: RPC i RMI.
Curs d'Arquitectures Distribuïdes
Mòdul 1: Introducció als Sistemes Distribuïts
- Conceptes Bàsics de Sistemes Distribuïts
- Models de Sistemes Distribuïts
- Avantatges i Desafiaments dels Sistemes Distribuïts
- Les Fal·làcies de la Computació Distribuïda
- Temps, Rellotges i Ordenació d'Esdeveniments
- Del Monòlit a la Plataforma Distribuïda: el Cas Quilòmetre Zero
Mòdul 2: Comunicació en Sistemes Distribuïts
- Protocols de Comunicació
- RPC i RMI
- gRPC i Serialització de Dades
- Missatgeria i Cues de Missatges
- Patrons de Comunicació Asíncrona
Mòdul 3: Consistència i Replicació
- Models de Consistència
- El Teorema CAP i PACELC
- Algorismes de Consens
- Replicació de Dades
- Transaccions Distribuïdes i Sagues
Mòdul 4: Emmagatzematge Distribuït
- Particionament de Dades i Hashing Consistent
- Sistemes de Fitxers Distribuïts
- Emmagatzematge d'Objectes
- Bases de Dades Distribuïdes
- Memòries Cau Distribuïdes
Mòdul 5: Computació Distribuïda
- Models de Computació Distribuïda
- MapReduce i Hadoop
- Spark i Computació en Memòria
- Processament de Fluxos de Dades
- Planificació de Treballs i Pipelines de Dades
Mòdul 6: Seguretat en Sistemes Distribuïts
- Autenticació i Autorització
- Xifratge i Protecció de Dades
- Gestió d'Identitats
- Seguretat entre Serveis: mTLS i Gestió de Secrets
- Passarel·les d'API, Limitació de Taxa i Auditoria
Mòdul 7: Monitoratge i Manteniment
- Monitoratge de Sistemes Distribuïts
- Logs Centralitzats i Traçabilitat Distribuïda
- Gestió de Fallades i Recuperació
- Patrons de Resiliència: Timeouts, Reintents i Circuit Breaker
- Automatització i Orquestració
- Proves en Sistemes Distribuïts i Enginyeria del Caos
