En tancar el mòdul 8, BiblioTech havia deixat d'esperar: importa el catàleg sense bloquejar el menú, envia dos-cents avisos amb un pool acotat, protegeix el seu estat amb ConcurrentHashMap i genera informes amb cadenes de CompletableFuture. I tot i així continuava tenint un límit que cap quantitat de fils no pot trencar: tot passa dins d'una sola màquina. La Marta Ruiz només consulta el catàleg si s'asseu davant d'aquell ordinador. En Diego Alonso, des d'una altra planta, no pot. La Nuria Vidal no pot demanar a un servei extern les metadades d'una novetat.
Aquest mòdul canvia això. Però abans d'escriure la primera línia de codi de xarxa convé entendre què hi ha sota d'una connexió, perquè el codi de xarxa és l'únic codi que escriuràs en què la fallada no és una possibilitat remota: és l'estat normal del sistema. Un fitxer local gairebé sempre és on el vas deixar; una màquina a l'altre costat de la xarxa pot estar apagada, saturada, darrere d'un tallafoc, amb l'adreça canviada o simplement lenta, i el teu programa s'ha de comportar raonablement en tots aquests casos.
Aquesta lliçó no té gairebé codi de xarxa —això comença a 09-02— però és la que fa que la resta del mòdul tingui sentit. Entendràs el model client-servidor, el model per capes, què és realment una adreça IP i un port, com un nom es converteix en una adreça, en què es diferencien TCP i UDP i per què aquesta diferència decideix el disseny de la teva aplicació, i com es dissenya un protocol. Al final dissenyarem —sobre paper— el protocol de consulta de catàleg de BiblioTech, que implementaràs en les dues lliçons següents.
Nota sobre l'enfocament. Aquesta lliçó explica conceptes de xarxes des del punt de vista de qui els programarà, no des del de qui administrarà una infraestructura. No hi trobaràs taules d'encaminament ni subxarxes al detall; hi trobaràs exactament el que necessites saber perquè el teu codi de xarxa no et sorprengui.
Contingut
- Què significa que dos programes es comuniquin
- El model client-servidor i el model d'igual a igual
- El model per capes: el sobre dins del sobre
- Adreces IP: IPv4, IPv6,
localhost, privades i públiques - Ports: per què no n'hi ha prou amb l'adreça
- Què és un socket
- DNS: d'un nom a una adreça
InetAddressa la pràctica- TCP davant d'UDP
- La salutació de tres vies, control de flux i retransmissió
- Quan cal triar cadascun
- Què és un protocol d'aplicació i com es dissenya
- El protocol de BiblioTech
- Latència davant d'amplada de banda
- Eines de diagnòstic
- Què pot fallar en una xarxa
- Errors Comuns i Consells
- Exercicis
- Què significa que dos programes es comuniquin
Fins ara, quan dues parts del teu programa intercanviaven informació, compartien memòria. Un fil escrivia en un ConcurrentHashMap i un altre hi llegia; el problema era la coordinació (panys, visibilitat, atomicitat), però la dada hi era, a la mateixa memòria, accessible en nanosegons.
Quan dos programes es comuniquen per xarxa, res d'això no es compleix:
- No comparteixen memòria. No hi ha objectes comuns. Només es poden enviar bytes.
- No comparteixen temps. El que envies triga a arribar, i de vegades no arriba.
- No comparteixen confiança. L'altre extrem pot ser un programa diferent, escrit en un altre llenguatge, o directament un atacant.
- No comparteixen vida. L'altre pot morir a mitja conversa sense avisar-te.
D'aquestes quatre, la primera és la que més conseqüències té en el dia a dia: per la xarxa només hi viatgen bytes. Un Llibre de BiblioTech no viatja. El que viatja és una seqüència de bytes que, per acord previ entre els dos programes, tots dos saben interpretar com un llibre. Aquest acord previ té nom: protocol.
En memoria (mateix proces): En xarxa (dos processos):
Llibre llibre = cataleg [67][79][78][83][85][76][84][65]...
.cercar(isbn); C O N S U L T A
// Es una referencia. // Es una sequencia de bytes que
// L'objecte ja existeix. // algu ha d'interpretar.Tot el mòdul 9 consisteix, en el fons, a aprendre a convertir informació en bytes, enviar-los per un tub i reconstruir-la a l'altre costat sabent que el tub es pot trencar en qualsevol moment.
- El model client-servidor i el model d'igual a igual
Hi ha dues formes bàsiques d'organitzar qui parla amb qui.
Client-servidor
Un programa —el servidor— es queda esperant peticions en una adreça coneguda. Altres programes —els clients— prenen la iniciativa, es connecten a aquella adreça, demanen alguna cosa i reben una resposta.
graph TD
C1["Client<br/>portatil de la Marta"] -->|CONSULTA 978-0000000001| S["Servidor BiblioTech<br/>192.168.1.50 port 9090"]
C2["Client<br/>PC del Diego"] -->|LLISTA| S
C3["Client<br/>mobil de la Nuria"] -->|PRESTAR 978-0000000002| S
S -->|200 OK ...| C1
S -->|200 OK ...| C2
S -->|404 NO TROBAT| C3
Característiques:
- Asimètric. El servidor no truca als clients; espera. El client no espera; truca.
- El servidor té adreça estable i coneguda. El client no cal que en tingui.
- El servidor sol atendre'n molts alhora. Aquí és on el mòdul 8 es torna imprescindible.
- És el model de tot el web, del correu, de les bases de dades i del 95 % del que programaràs.
BiblioTech serà un servidor: una màquina de Nexus Software amb el catàleg, i tots els llocs de treball com a clients.
Igual a igual (peer-to-peer)
Tots els programes són alhora client i servidor: cadascun pot iniciar una conversa i cadascun pot atendre.
graph LR
A["Node A"] <--> B["Node B"]
B <--> C["Node C"]
A <--> C
C <--> D["Node D"]
A <--> D
Característiques:
- Simètric. No hi ha un punt central.
- Resistent: si cau un node, els altres continuen.
- Complex: descobriment de nodes, consistència, seguretat. Molt més difícil de programar.
- Usos reals: BitTorrent, blockchains, alguns sistemes de missatgeria i —a petita escala— el descobriment en xarxa local que veuràs a 09-04, on els llocs de treball es busquen entre ells sense que ningú els hagi dit on és el servidor.
En Java, tots dos models es construeixen amb les mateixes peces (Socket i ServerSocket): un node P2P és simplement un programa que té les dues.
- El model per capes: el sobre dins del sobre
Quan escrius una línia de text en un socket, aquesta línia no viatja tal qual pel cable. Viatja embolcallada, diverses vegades, com una carta ficada en un sobre que al seu torn va dins d'un altre sobre, que va dins d'una saca.
Imagina't que la Marta Ruiz vol enviar al Diego una nota que diu CONSULTA 978-0000000001:
- Escriu la nota (les dades de la teva aplicació).
- La fica en un sobre amb "Per a: departament de Sistemes, despatx 12" — això identifica el programa destinatari dins de l'edifici. És la capa de transport, i el número de despatx és el port.
- Aquest sobre el fica dins d'un altre amb "Per a: edifici Nexus, carrer Major 3" — això identifica la màquina. És la capa de xarxa, i l'adreça és la IP.
- I aquest sobre el fica a la saca del camió que fa el trajecte fins a la següent oficina de correus. És la capa d'enllaç, i la saca només serveix per a aquell tram concret: a cada oficina es canvia de saca, però els sobres interiors no es toquen.
En arribar, s'obre la saca, s'obre el sobre exterior, s'obre l'interior i es llegeix la nota. Cada capa només entén el seu propi sobre i tracta tot el que hi ha dins com a contingut opac.
graph TD
subgraph EMISSOR
A1["Aplicacio<br/>CONSULTA 978-0000000001"] --> A2["Transport<br/>afegeix ports, sequencia"]
A2 --> A3["Xarxa<br/>afegeix IP origen i desti"]
A3 --> A4["Enllac<br/>afegeix MAC, trama"]
end
A4 -->|bits pel cable| B4
subgraph RECEPTOR
B4["Enllac<br/>treu la trama"] --> B3["Xarxa<br/>treu la capcalera IP"]
B3 --> B2["Transport<br/>treu la capcalera TCP"]
B2 --> B1["Aplicacio<br/>CONSULTA 978-0000000001"]
end
Taula de capes del model TCP/IP
| Capa | Què afegeix | Què identifica | Protocols típics | On la veus en Java |
|---|---|---|---|---|
| Aplicació | Les dades amb significat | L'operació que vols | HTTP, HTTPS, FTP, SMTP, DNS, SSH, el teu protocol | PrintWriter, BufferedReader, HttpClient |
| Transport | Port origen i destí, números de seqüència, sumes de control | El programa dins de la màquina | TCP, UDP | Socket, ServerSocket, DatagramSocket |
| Xarxa | Adreça IP origen i destí, TTL | La màquina a Internet | IP, ICMP (ping), ARP |
InetAddress |
| Enllaç | Adreça MAC, comprovació d'errors del tram | La targeta de xarxa al tram local | Ethernet, Wi-Fi, PPP | NetworkInterface (poc) |
Sobre el model OSI de set capes. Potser has sentit a parlar de set capes (física, enllaç, xarxa, transport, sessió, presentació, aplicació). El model OSI és un model de referència acadèmic; el que realment implementa Internet és el TCP/IP de quatre capes de la taula. Conèixer OSI és útil per a entrevistes i per a parlar amb gent d'infraestructura, però per a programar n'hi ha prou amb les quatre.
La conseqüència pràctica de tot això és enorme i convé dir-la explícitament: tu només programes la capa d'aplicació. Quan a 09-02 escriguis escriptor.println("CONSULTA " + isbn), el sistema operatiu s'encarrega de tota la resta. No veuràs ni una sola capçalera TCP ni una sola adreça MAC. El que sí que veuràs són les conseqüències d'aquestes capes: que les dades arribin trossejades, que hi hagi un retard, que una connexió es talli a mitges. Entendre el model és entendre per què passen aquestes coses.
- Adreces IP: IPv4, IPv6,
localhost, privades i públiques
localhost, privades i públiquesUna adreça IP identifica una interfície de xarxa d'una màquina. N'hi ha dues versions en ús.
IPv4
Quatre números de 0 a 255 separats per punts: 192.168.1.50. Són 32 bits, és a dir, uns 4.300 milions d'adreces possibles — que es van esgotar fa anys, i per això existeix IPv6 (i NAT, més avall).
IPv6
Vuit grups de quatre dígits hexadecimals separats per dos punts: 2001:0db8:85a3:0000:0000:8a2e:0370:7334. Són 128 bits, un nombre d'adreces absurdament gran. Es permet abreujar els zeros:
2001:0db8:85a3:0000:0000:8a2e:0370:7334
2001:db8:85a3::8a2e:370:7334 <- mateixa adreca, abreujada
^^
:: substitueix el grup mes llarg de zeros (nomes un cop)En Java, InetAddress gestiona les dues de forma transparent: Inet4Address i Inet6Address són subclasses seves i gairebé mai no les necessitaràs distingir.
Detall pràctic. En una URL, una adreça IPv6 s'escriu entre claudàtors per no confondre els
:amb el separador de port:http://[::1]:8080/catalogo.
localhost i 127.0.0.1
L'adreça 127.0.0.1 (i el seu equivalent IPv6, ::1) és la interfície de bucle invertit: es refereix sempre a la mateixa màquina. El nom localhost hi apunta.
L'important per a tu: els paquets enviats a localhost no surten a la xarxa. Ni tan sols toquen la targeta de xarxa; el sistema operatiu els retorna internament. Per això pots provar tot aquest mòdul al teu ordinador sense necessitar dues màquines, sense connexió a Internet i amb latència gairebé zero. Tots els exemples del mòdul s'executen contra localhost.
Privades davant de públiques
Certs rangs estan reservats per a xarxes privades i no són encaminables a Internet: qualsevol router públic descarta paquets adreçats a ells.
| Rang | Notació | Ús típic |
|---|---|---|
10.0.0.0 – 10.255.255.255 |
10.0.0.0/8 |
Xarxes corporatives grans |
172.16.0.0 – 172.31.255.255 |
172.16.0.0/12 |
Xarxes mitjanes, Docker |
192.168.0.0 – 192.168.255.255 |
192.168.0.0/16 |
Gairebé tots els routers domèstics |
127.0.0.0 – 127.255.255.255 |
127.0.0.0/8 |
Bucle invertit (la mateixa màquina) |
169.254.0.0 – 169.254.255.255 |
169.254.0.0/16 |
Autoconfiguració quan falla el DHCP |
La xarxa interna de Nexus Software és 192.168.1.0/24, i el servidor de BiblioTech viurà a 192.168.1.50. Això vol dir que qualsevol empleat dins de l'oficina s'hi pot connectar, però ningú des de fora — cosa que, per a un catàleg intern, és exactament el que es vol.
Nota sobre NAT. El teu ordinador de casa té una adreça privada (
192.168.1.23), però navegues per Internet. Això funciona gràcies al NAT (traducció d'adreces de xarxa): el router substitueix la teva adreça privada per la seva única adreça pública en sortir, i anota en una taula quina connexió pertany a qui per poder retornar les respostes. La conseqüència que t'afectarà com a programador és aquesta: darrere d'un NAT pots iniciar connexions cap a fora, però ningú de fora no pot iniciar una connexió cap a tu, perquè el router no sabria a quina màquina interna lliurar-la. Per això els servidors necessiten adreça pública o regles explícites de redirecció de ports, i per això els sistemes P2P han de fer malabars per travessar el NAT.
- Ports: per què no n'hi ha prou amb l'adreça
Si la IP identifica la màquina, per què cal alguna cosa més?
Perquè en una màquina hi ha molts programes que fan servir la xarxa alhora. Al teu ordinador ara mateix hi pot haver un navegador amb quinze pestanyes, un client de correu, un servidor de desenvolupament i BiblioTech. Quan arriba un paquet a l'adreça de la màquina, el sistema operatiu necessita saber a quin de tots ells lliurar-lo.
El port és aquest número. Tornant a l'analogia: la IP és el carrer i el número de l'edifici; el port és el número de despatx.
- És un número de 16 bits sense signe: de 0 a 65535.
- Els ports 0-1023 són ben coneguts i en sistemes tipus Unix requereixen privilegis d'administrador per escoltar-los.
- Els ports 1024-49151 són registrats: assignats a aplicacions concretes però sense privilegis.
- Els ports 49152-65535 són efímers: els que el sistema assigna automàticament a les teves connexions sortints.
Ports ben coneguts
| Port | Protocol | Per a què serveix |
|---|---|---|
| 20 / 21 | FTP | Transferència de fitxers (dades / control) |
| 22 | SSH | Consola remota segura, scp, git sobre SSH |
| 25 | SMTP | Enviament de correu entre servidors |
| 53 | DNS | Resolució de noms |
| 80 | HTTP | Web sense xifrar |
| 443 | HTTPS | Web xifrat amb TLS |
| 3306 | MySQL | Base de dades |
| 5432 | PostgreSQL | Base de dades |
| 6379 | Redis | Memòria cau en memòria |
| 8080 | HTTP alternatiu | Servidors de desenvolupament, Tomcat |
| 8443 | HTTPS alternatiu | Igual, amb TLS |
Que HTTP visqui al 80 és el que permet escriure http://exemple.com sense port: el navegador suposa el 80 perquè l'esquema http ho implica. Si escrius http://exemple.com:8080, estàs dient explícitament un altre despatx.
BiblioTech farà servir el port 9090 per al seu servidor de catàleg (TCP) i el 9091 per al seu servei de descobriment (UDP). Són ports alts, sense privilegis, i no xoquen amb res habitual.
El port 0 és especial. Si demanes el port 0 en crear un
ServerSocket, el sistema te n'assigna un de lliure qualsevol i després el pots consultar ambgetLocalPort(). És la tècnica estàndard en proves automatitzades per no xocar amb ports ocupats — la veuràs al mòdul 11 en parlar de JUnit.
- Què és un socket
Ara es pot definir amb precisió la paraula que dóna nom a la lliçó següent.
Un socket és un extrem d'una comunicació, identificat pel parell (adreça IP, port). Una connexió TCP queda identificada de forma única per quatre valors: IP origen, port origen, IP destí, port destí.
Connexio de la Marta al servidor de BiblioTech:
(192.168.1.23 : 51422) <-----> (192.168.1.50 : 9090)
^socket del client ^socket del servidor
port efimer, l'escull port fix i conegut
el sistema operatiu l'escull el programadorAquesta és la raó per la qual mil clients es poden connectar al port 9090 del mateix servidor sense conflicte: encara que el destí sigui idèntic en els mil casos, l'origen (IP + port efímer) és diferent en cadascun, així que les quàdruples són diferents i el sistema operatiu mai no confon una connexió amb una altra. És un dubte molt freqüent en començar: no, el servidor no necessita "un port per client".
En Java, un socket TCP es representa amb la classe java.net.Socket, i l'objecte que accepta connexions entrants al servidor és java.net.ServerSocket. La sorpresa agradable —i el motiu que el mòdul 7 fos un requisit— és el que un Socket t'ofereix un cop connectat:
Un InputStream i un OutputStream. Exactament els mateixos que vas fer servir per llegir i escriure fitxers. Els mateixos decoradors (BufferedReader, InputStreamReader, PrintWriter), la mateixa necessitat de declarar el charset, el mateix try-with-resources. La xarxa, un cop connectada, és E/S que ja saps fer. L'única cosa nova és com s'estableix i com es trenca aquesta connexió.
- DNS: d'un nom a una adreça
Ningú no escriu 142.250.185.78 al navegador. Escriu un nom. El DNS (Sistema de Noms de Domini) és la guia telefònica distribuïda que tradueix noms a adreces.
sequenceDiagram
participant App as El teu programa
participant SO as Resolutor del SO
participant DNS as Servidor DNS
App->>SO: InetAddress.getByName("metadades.nexussoftware.local")
SO->>SO: mira /etc/hosts i la memoria cau
SO->>DNS: consulta el nom (UDP port 53)
DNS-->>SO: 192.168.1.60
SO-->>App: InetAddress(192.168.1.60)
Punts que importen en programar:
- La resolució triga. És una crida de xarxa que pot afegir desenes o centenars de mil·lisegons la primera vegada. Si fas
getByNamedins d'un bucle, ho pagues cada cop. - Es guarda en memòria cau. El sistema operatiu i la mateixa JVM emmagatzemen resultats. La JVM té la propietat
networkaddress.cache.ttlper controlar-ho; el valor per defecte en aplicacions sense gestor de seguretat sol ser 30 segons. - Pot fallar. Un nom inexistent produeix
UnknownHostException, que és unaIOException. Cal capturar-la. - Un nom pot tenir diverses adreces (balanceig de càrrega): per això existeix
getAllByName, que retorna un array. - El fitxer
/etc/hosts(a Windows,C:\Windows\System32\drivers\etc\hosts) té prioritat sobre el DNS. És un truc habitual per provar en local un nom de producció.
InetAddress a la pràctica
InetAddress a la pràcticajava.net.InetAddress és la classe que representa una adreça IP en Java. No té constructor públic: s'obté mitjançant mètodes estàtics de fàbrica.
| Mètode | Què fa |
|---|---|
InetAddress.getByName(String) |
Resol un nom o interpreta una IP literal |
InetAddress.getAllByName(String) |
Totes les adreces associades a un nom |
InetAddress.getLocalHost() |
L'adreça de la màquina actual |
InetAddress.getLoopbackAddress() |
127.0.0.1 / ::1, sense consultar res |
getHostAddress() |
L'adreça en text ("192.168.1.50") |
getHostName() |
El nom, fent resolució inversa si cal |
getCanonicalHostName() |
El nom canònic complet |
isReachable(int millis) |
Intenta comprovar si respon, amb temps límit |
isLoopbackAddress() |
Si és 127.x.x.x o ::1 |
isSiteLocalAddress() |
Si és una adreça privada |
Escriurem la primera classe de xarxa del mòdul: una eina de diagnòstic que Nexus Software farà servir per comprovar on és i què abasta cada lloc de treball.
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.logging.Logger;
/**
* Diagnostic de xarxa de BiblioTech: respon a "on soc i a qui abasto".
* No obre cap connexio: nomes resol noms i consulta interficies.
*/
public final class DiagnosticXarxa {
private static final Logger LOG = Logger.getLogger(DiagnosticXarxa.class.getName());
private DiagnosticXarxa() {
// Classe d'utilitats: no s'instancia.
}
/** Mostra l'adreca d'aquesta maquina i les de les seves interficies. */
public static void mostrarMaquinaLocal() {
try {
InetAddress local = InetAddress.getLocalHost();
System.out.println("Nom d'aquesta maquina : " + local.getHostName());
System.out.println("Adreca principal : " + local.getHostAddress());
System.out.println("Es privada : " + local.isSiteLocalAddress());
} catch (UnknownHostException e) {
// Passa si la maquina no te nom resoluble. No es fatal.
LOG.warning("No s'ha pogut determinar el nom local: " + e.getMessage());
}
System.out.println();
System.out.println("Interficies de xarxa disponibles:");
try {
Enumeration<NetworkInterface> interficies = NetworkInterface.getNetworkInterfaces();
while (interficies.hasMoreElements()) {
NetworkInterface ni = interficies.nextElement();
if (!ni.isUp()) {
continue; // Ignorem les interficies apagades.
}
Enumeration<InetAddress> adreces = ni.getInetAddresses();
while (adreces.hasMoreElements()) {
InetAddress adr = adreces.nextElement();
System.out.printf(" %-12s %-40s %s%n",
ni.getName(),
adr.getHostAddress(),
adr.isLoopbackAddress() ? "(bucle invertit)" : "");
}
}
} catch (java.net.SocketException e) {
LOG.warning("No s'han pogut llistar les interficies: " + e.getMessage());
}
}
/**
* Resol un nom a totes les seves adreces.
* Retorna false si el nom no existeix, en lloc de propagar l'excepcio:
* per a una eina de diagnostic, "no resol" es un resultat, no un error.
*/
public static boolean resoldre(String nom) {
long inici = System.nanoTime();
try {
InetAddress[] adreces = InetAddress.getAllByName(nom);
long ms = (System.nanoTime() - inici) / 1_000_000;
System.out.println("Resolucio de '" + nom + "' en " + ms + " ms:");
for (InetAddress adr : adreces) {
System.out.println(" -> " + adr.getHostAddress());
}
return true;
} catch (UnknownHostException e) {
System.out.println("Resolucio de '" + nom + "': NO RESOL");
return false;
}
}
/**
* Comprova si un equip respon dins del temps indicat.
*
* ATENCIO: isReachable fa servir ICMP (com ping) si el proces te privilegis,
* i si no, intenta connectar al port 7 (echo). Molts tallafocs bloquegen
* totes dues coses, aixi que un false NO demostra que la maquina estigui caiguda.
*/
public static boolean abastable(String nom, int milisegons) {
try {
InetAddress adr = InetAddress.getByName(nom);
boolean ok = adr.isReachable(milisegons);
System.out.printf("%-30s %s%n", nom, ok ? "ABASTABLE" : "sense resposta");
return ok;
} catch (IOException e) {
System.out.printf("%-30s ERROR: %s%n", nom, e.getMessage());
return false;
}
}
public static void main(String[] args) {
System.out.println("=== DIAGNOSTIC DE XARXA - BiblioTech ===\n");
mostrarMaquinaLocal();
System.out.println();
resoldre("localhost");
resoldre("servidor-que-no-existeix.nexussoftware.local");
System.out.println();
abastable("localhost", 1000);
}
}Sortida típica:
=== DIAGNOSTIC DE XARXA - BiblioTech ===
Nom d'aquesta maquina : lloc-marta
Adreca principal : 192.168.1.23
Es privada : true
Interficies de xarxa disponibles:
lo 127.0.0.1 (bucle invertit)
lo 0:0:0:0:0:0:0:1 (bucle invertit)
eth0 192.168.1.23
eth0 fe80:0:0:0:a00:27ff:fe4e:66a1
Resolucio de 'localhost' en 2 ms:
-> 127.0.0.1
Resolucio de 'servidor-que-no-existeix.nexussoftware.local': NO RESOL
localhost ABASTABLETres detalls que convé fixar:
getLocalHost()pot llançarUnknownHostExceptionen màquines mal configurades (contenidors sense entrada a/etc/hosts, sobretot). No és un cas teòric; passa sovint a Docker.isReachableno ésping. És un intent de comprovació que depèn de privilegis i de tallafocs. Unfalsesignifica "no ho he aconseguit", no "la màquina està caiguda". Mai no basis una decisió important només en el seu resultat.- La resolució d'un nom inexistent pot trigar segons, no mil·lisegons, perquè el resolutor reintenta. Una altra raó per no ficar-la en un bucle calent.
- TCP davant d'UDP
Sobre la capa de xarxa (IP) hi viuen dos protocols de transport, i triar entre ells és la primera decisió de disseny de qualsevol aplicació de xarxa.
TCP: orientat a connexió i fiable
TCP (Protocol de Control de Transmissió) ofereix al teu programa la il·lusió d'un tub continu i fiable entre els dos extrems, com si fos un fitxer obert per tots dos costats.
Garanteix:
- Lliurament: si un segment es perd, es retransmet. Si és impossible lliurar-lo, se't notifica amb un error.
- Ordre: els bytes arriben en el mateix ordre en què es van enviar.
- Sense duplicats: els segments repetits es descarten.
- Control de flux: si el receptor va lent, l'emissor frena automàticament.
- Control de congestió: si la xarxa està saturada, TCP redueix el ritme per no empitjorar-la.
A canvi: cal establir la connexió abans d'enviar res (cosa que costa un viatge d'anada i tornada), hi ha capçaleres més grans, i les garanties introdueixen retards — si un segment es perd, tot el que ve després espera que arribi el retransmès (bloqueig de capçalera de línia).
UDP: sense connexió i no fiable
UDP (Protocol de Datagrames d'Usuari) és gairebé transparent: agafa el teu bloc de bytes, li posa ports i una suma de control, i el deixa anar a la xarxa.
No garanteix res:
- Un datagrama es pot perdre i ningú no t'avisa.
- Poden arribar desordenats.
- Pot arribar duplicat.
- No hi ha control de flux: pots saturar el receptor sense adonar-te'n.
A canvi: no hi ha establiment (envies a l'instant), la capçalera és de 8 bytes davant dels 20+ de TCP, i cada missatge es conserva com a unitat (si envies 100 bytes, l'altre rep 100 bytes o res, mai 60). I permet enviar a molts alhora amb difusió i multidifusió, cosa que TCP no pot fer per definició.
Taula comparativa
| Criteri | TCP | UDP |
|---|---|---|
| Connexió prèvia | Sí (salutació de tres vies) | No |
| Lliurament garantit | Sí | No |
| Ordre garantit | Sí | No |
| Sense duplicats | Sí | No |
| Control de flux i congestió | Sí | No |
| Mida de capçalera | 20 bytes o més | 8 bytes |
| Unitat de dades | Flux de bytes (sense fronteres) | Datagrama (amb fronteres) |
| Latència inicial | 1 viatge d'anada i tornada abans d'enviar | Zero |
| Un a molts | No | Sí (difusió, multidifusió) |
| Classe en Java | Socket / ServerSocket |
DatagramSocket / MulticastSocket |
- La salutació de tres vies, control de flux i retransmissió
La salutació de tres vies
Abans que viatgi ni un sol byte de les teves dades, TCP estableix la connexió amb tres missatges:
sequenceDiagram
participant C as Client (Marta)
participant S as Servidor BiblioTech
Note over C,S: Establiment
C->>S: SYN (vull connectar, la meva sequencia comenca en x)
S->>C: SYN + ACK (d'acord, la meva en y, confirmo la teva x)
C->>S: ACK (confirmo la teva y)
Note over C,S: Connexio establerta: ja es poden enviar dades
C->>S: CONSULTA 978-0000000001
S->>C: 200 OK Java Eficac
Note over C,S: Tancament
C->>S: FIN
S->>C: ACK
S->>C: FIN
C->>S: ACK
Conseqüències pràctiques:
- Obrir una connexió costa un viatge d'anada i tornada complet. Si el servidor és a 80 ms, només l'establiment són 80 ms abans d'enviar un byte útil. Per això les biblioteques HTTP modernes reutilitzen connexions (ho veuràs a 09-06 amb
HttpClient) i per això obrir un socket nou per a cada petició és un error de rendiment clàssic. - El servidor respon
SYN+ACKencara que el teu programa no hagi cridataccept(). El sistema operatiu manté una cua de connexions ja establertes esperant que el teu codi les reculli: és elbacklogde 09-03. - Si el port està tancat, el sistema respon amb
RSTen lloc deSYN+ACK, i en Java això es converteix enConnectException: Connection refused— immediata, no un temps esgotat. Distingir "rebutjat" (hi ha una màquina, no hi ha ningú escoltant) de "temps esgotat" (no hi ha resposta: màquina caiguda o tallafoc que descarta en silenci) és diagnòstic pur. - El tancament són quatre missatges, no tres, perquè cada sentit es tanca per separat. D'aquí surt el concepte de mitja connexió (
shutdownOutput) que veuràs a 09-02.
Retransmissió
Cada segment enviat ha de ser confirmat. Si l'emissor no rep confirmació dins d'un termini estimat, reenvia. Aquest termini es recalcula constantment mesurant el temps d'anada i tornada.
Emissor Receptor
|--- segment 1 ---------------->| ok
|<-- ACK 1 ---------------------|
|--- segment 2 -----X | perdut a la xarxa
| (espera... sense ACK) |
|--- segment 2 (reenviament) -->| ok
|<-- ACK 2 ---------------------|El que veus des de Java: res. El teu read() simplement triga una mica més. Aquesta és exactament la proposta de valor de TCP, i també la raó per la qual un read() sense setSoTimeout es pot quedar aturat molt més del que t'imagines.
Control de flux
El receptor anuncia a cada confirmació quant espai li queda a la seva memòria intermèdia (la finestra). Si anuncia zero, l'emissor s'atura fins que s'alliberi lloc.
El que veus des de Java: si l'altre extrem no llegeix, el teu write() acabarà bloquejant-se. És una sorpresa habitual: molta gent creu que escriure en un socket no bloqueja mai perquè "només es copia a una memòria intermèdia". Es copia a una memòria intermèdia, sí, però aquesta memòria és finita i s'omple si l'altre no consumeix.
- Quan cal triar cadascun
| Situació | Elecció | Per què |
|---|---|---|
| Transferir un fitxer | TCP | Un byte perdut corromp tot el fitxer |
| Consultar el catàleg de BiblioTech | TCP | La resposta ha d'arribar completa i en ordre |
| Petició web (HTTP/1.1, HTTP/2) | TCP | Igual |
| Base de dades | TCP | Igual, i a més la sessió té estat |
| Vídeo o àudio en directe | UDP | Un fotograma perdut és un parpelleig; esperar-ne la retransmissió seria pitjor |
| Videojoc d'acció | UDP | La posició de fa 200 ms ja no serveix; interessa la d'ara |
| Consulta DNS | UDP | Una pregunta, una resposta, cap en un datagrama; si es perd, es repeteix |
| Telemetria, mètriques | UDP | Milers per segon; perdre'n alguna és irrellevant |
| Descobriment en xarxa local | UDP | Necessita difusió, i TCP no pot fer difusió |
| Sincronització de rellotge (NTP) | UDP | Latència mínima; el protocol ja tolera pèrdues |
La regla mental que funciona: la dada de fa un moment encara val, o cal esperar-la sí o sí? Si retransmetre una dada vella és pitjor que perdre-la, UDP. Si perdre-la trenca el significat, TCP.
El cas híbrid. HTTP/3 i QUIC, els protocols web més recents, funcionen sobre UDP i implementen pel seu compte fiabilitat, ordre i control de congestió, precisament per alliberar-se del bloqueig de capçalera de línia de TCP. És la prova que "no fiable" no significa "pitjor": significa "les garanties les poses tu, a la teva mida".
BiblioTech farà servir tots dos: TCP per al catàleg (09-02 i 09-03) i UDP per al descobriment i la telemetria (09-04).
- Què és un protocol d'aplicació i com es dissenya
Un protocol d'aplicació és l'acord sobre quins bytes signifiquen què. TCP et dóna un tub de bytes fiable, però un tub de bytes no diu on acaba un missatge i comença el següent, ni què és una petició i què una resposta. Això ho poses tu.
Text davant de binari
| Aspecte | Protocol de text | Protocol binari |
|---|---|---|
| Llegibilitat | Es depura amb telnet i nc |
Requereix eines |
| Mida | Més gran | Més petita |
| Velocitat d'anàlisi | Menor | Major |
| Facilitat d'implementació | Alta | Mitjana/baixa |
| Problemes de codificació | Sí (charset, salts de línia) | No |
| Exemples | HTTP/1.1, SMTP, IMAP, Redis | HTTP/2, gRPC, protocols de jocs |
Per aprendre i per a la majoria d'aplicacions internes, text. És el que farem: podràs parlar amb el servidor de BiblioTech fent servir telnet, que és una ajuda pedagògica impagable.
El problema central: on acaba un missatge?
TCP és un flux de bytes sense fronteres. Si el client fa dues escriptures, el servidor les pot rebre en una sola lectura, o partides en tres. Això s'anomena de vegades "el problema de l'enganxat de missatges" i és la causa número u de bugs subtils en protocols casolans.
El client escriu: "CONSULTA 111\n" i despres "LLISTA\n"
El servidor pot llegir: "CONSULTA 111\nLLISTA\n" (tot junt)
o: "CONSUL" ... "TA 111\nLLI" ... "STA\n" (partit)Hi ha tres solucions clàssiques:
| Tècnica | Com funciona | Avantatges | Inconvenients |
|---|---|---|---|
| Delimitador | Un byte o seqüència marca el final (\n) |
Simplíssim, llegible | El delimitador no pot aparèixer a les dades |
| Longitud prèvia | Primer N bytes amb la mida, després el cos | Dades binàries sense restricció | Cal llegir en dues fases; cal limitar N |
| Tancament de connexió | El missatge acaba quan es tanca | Trivial | Una sola resposta per connexió |
HTTP fa servir les tres alhora: delimitador (\r\n) per a les capçaleres, Content-Length (longitud prèvia) per al cos, i a HTTP/1.0 el tancament de connexió.
En Java, la solució per delimitador de línia és gairebé de franc: BufferedReader.readLine() ja s'encarrega d'acumular fins a trobar el salt de línia, i PrintWriter.println() d'afegir-lo. Per això els protocols de línia són tan còmodes i per això el de BiblioTech serà un d'ells.
Compte amb
readLine(). Retornanullquan l'altre extrem tanca. Aquestnullés l'única senyal de fi de conversa que tindràs, i no comprovar-lo produeix unNullPointerExceptional primer client que es desconnecti. Ho veuràs demostrat a 09-02.
Les cinc preguntes de tot protocol
Quan en dissenyis un, contesta explícitament:
- Qui parla primer? (el client demana, o el servidor saluda?)
- On acaba cada missatge? (delimitador, longitud, tancament)
- Quina codificació de caràcters? (UTF-8, sempre, explícit)
- Com se senyalitza un error? (codis, com HTTP; o text lliure, que és pitjor)
- Com acaba la conversa? (ordre de comiat, tancament, temps d'inactivitat)
- El protocol de BiblioTech
Amb això, ja podem dissenyar el protocol que implementaràs a 09-02 (client) i 09-03 (servidor). Es dirà BTCP (BiblioTech Catalog Protocol), versió 1.
Especificació
BTCP/1 - Protocol de consulta de cataleg de BiblioTech
=======================================================
Transport : TCP, port 9090
Codificacio : UTF-8
Delimitador : una linia per missatge, acabada en \n
Inicia : el servidor envia una linia de salutacio en connectar
Fi : el client envia SORTIR, o el servidor tanca per inactivitat (60 s)
--- SALUTACIO (servidor -> client, en connectar) ---
200 BIBLIOTECH BTCP/1
--- PETICIONS (client -> servidor) ---
CONSULTA <isbn> Dades d'un material per ISBN
LLISTA Tots els materials del cataleg
PRESTAR <isbn> <empleat> Registrar un prestec
SORTIR Acabar la conversa
--- RESPOSTES (servidor -> client) ---
Tota resposta comenca per un codi numeric de 3 digits
seguit d'un text. Les respostes de diverses linies indiquen
quantes n'hi ha i acaben amb una linia que conte nomes un punt.
200 OK <dades> Exit, resposta d'una linia
201 LLISTA <n> Exit, segueixen n linies i despres "."
400 PETICIO INVALIDA <per que>
404 NO TROBAT <isbn>
409 NO DISPONIBLE <isbn>
500 ERROR INTERN
221 ADEU Resposta a SORTIR; el servidor tanca
--- FORMAT D'UN MATERIAL ---
<isbn>|<titol>|<tipus>|<disponible>
Exemple: 978-0000000001|Java Eficac|LLIBRE|trueUna sessió d'exemple
S: 200 BIBLIOTECH BTCP/1
C: CONSULTA 978-0000000001
S: 200 OK 978-0000000001|Java Eficac|LLIBRE|true
C: LLISTA
S: 201 LLISTA 3
S: 978-0000000001|Java Eficac|LLIBRE|true
S: 978-0000000002|Patrons de Disseny|LLIBRE|false
S: 978-0000000003|Refactoritzacio|LLIBRE|true
S: .
C: PRESTAR 978-0000000002 Diego Alonso
S: 409 NO DISPONIBLE 978-0000000002
C: CONSULTA 999
S: 404 NO TROBAT 999
C: DEMANA UN CAFE
S: 400 PETICIO INVALIDA ordre desconeguda: DEMANA
C: SORTIR
S: 221 ADEU
[el servidor tanca la connexio]Fixa't en les decisions i en per què es prenen:
- Codis numèrics de tres dígits. Copiats descaradament d'HTTP i SMTP, i per bones raons: el client pot decidir amb el primer dígit (2 = bé, 4 = culpa teva, 5 = culpa meva) sense analitzar el text, i el text es pot canviar o traduir sense trencar ningú.
|com a separador de camps. Escollit perquè no apareix ni en títols ni en ISBN. Si hi pogués aparèixer, caldria escapar-lo — el mateix problema que ja vas resoldre amb el CSV a 07-07.- Una resposta per petició. Simplifica enormement el client: escriu una línia, llegeix una resposta.
- La resposta múltiple anuncia la seva mida i a més es tanca amb
.. El número permet reservar i detectar truncaments; el punt final permet acabar encara que el número fos erroni. És redundant a propòsit. - El servidor saluda primer. Així el client confirma que ha arribat al lloc correcte i a un servidor que parla la seva versió del protocol abans d'enviar res.
- Hi ha una ordre de comiat. Sense ella, l'única forma d'acabar netament seria tancar el socket, i el servidor no podria distingir un tancament ordenat d'una caiguda.
Aquest és el contracte. A 09-02 escriuràs el client contra ell (provant-lo amb nc mentre no existeixi el servidor), i a 09-03 el servidor multiclient que l'implementa.
- Latència davant d'amplada de banda
Dues magnituds diferents que es confonen constantment i que governen el rendiment de tot el que programis en xarxa.
| Latència | Amplada de banda | |
|---|---|---|
| Què mesura | Quant triga el primer byte a arribar | Quants bytes per segon hi caben |
| Unitat | mil·lisegons | Mbit/s, MB/s |
| Analogia | Quant triga el camió a arribar | Quant cap al camió |
| Ho limita | La distància física i els salts | La capacitat de l'enllaç |
| Es millora amb | Acostar les dades (memòries cau, CDN) | Contractar més línia |
La clau: l'amplada de banda es pot comprar; la latència, no. Està limitada per la velocitat de la llum. Madrid–Nova York són uns 5.800 km; anada i tornada per fibra són com a mínim uns 60 ms, i a la pràctica 90-120 ms. Cap optimització del teu codi no baixarà d'aquí.
Ordres de magnitud que convé tenir interioritzats:
| Operació | Temps aproximat |
|---|---|
| Accés a memòria RAM | 100 nanosegons |
| Lectura d'un SSD | 100 microsegons (1.000×) |
| Anada i tornada a la mateixa xarxa local | 0,5 mil·lisegons (5.000×) |
| Anada i tornada a un servidor del mateix país | 10-30 mil·lisegons |
| Anada i tornada intercontinental | 100-200 mil·lisegons (2.000.000×) |
Compara la primera fila amb l'última: la xarxa és un milió de vegades més lenta que la memòria. D'aquí surten tres regles de disseny que veuràs aplicades tot el mòdul:
- Minimitza el nombre de viatges, no el nombre de bytes. Una petició que retorna 100 registres guanya a cent peticions d'un registre, encara que moguin el mateix.
- Solapa les esperes. Si has de fer deu consultes independents, fes-les en paral·lel. Això és exactament el que vas aprendre al mòdul 8 i el que faràs a 09-06 amb
sendAsynciallOf. - Reutilitza les connexions. Obrir-ne una costa un viatge complet. És la raó de ser del pool intern d'
HttpClient.
Nota: l'ordre de bytes de xarxa. Quan envies un
intde 4 bytes, hi va primer el byte més significatiu o el menys significatiu? Diferents arquitectures de CPU trien diferent (big-endian davant de little-endian), així que els protocols de xarxa en fixen un: big-endian, també anomenat ordre de bytes de xarxa. La bona notícia per a tu és queDataOutputStream.writeInt()iDataInputStream.readInt()de Java ja escriuen i llegeixen en big-endian, iByteBufferfa servir big-endian per defecte, així que si fas servir aquestes classes als dos extrems no ho has de pensar. Només importa si parles amb un programa en C que hagi escrit bytes crus, cas en què ell haurà de fer servirhtonl/ntohl.
- Eines de diagnòstic
Programar xarxes sense eines de diagnòstic és programar a cegues. Aquestes cinc resolen la majoria dels dubtes, i les faràs servir durant tot el mòdul.
ping — arriba res fins a aquella màquina?
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.041 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.055 ms
--- localhost ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3060ms
rtt min/avg/max/mdev = 0.041/0.050/0.061/0.008 msMesura latència i pèrdua. Un ping que falla no demostra que la màquina estigui caiguda: moltíssims tallafocs bloquegen ICMP per política. I un ping que funciona no demostra que el teu servei estigui viu: només que la màquina respon.
traceroute / tracert — per on va?
Mostra cada salt intermedi amb la seva latència. Serveix per localitzar on es degrada una connexió.
ss / netstat — qui està escoltant en quin port?
ss -tlnp # Linux modern: TCP, escoltant, numeric, amb proces
netstat -an | grep 9090 # Portable, mes anticState Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 50 0.0.0.0:9090 0.0.0.0:* users:(("java",pid=4711,fd=7))Aquesta és l'eina que resol l'error més freqüent del mòdul: BindException: Address already in use. Et diu exactament quin procés té ocupat el teu port. Si és una execució anterior del teu propi servidor que no vas tancar, la mates i llestos.
nc (netcat) — la navalla suïssa
Actua de client o de servidor TCP/UDP genèric. És com tenir un Socket i un ServerSocket a la línia d'ordres.
# Com a servidor: escolta al 9090 i mostra el que arribi.
nc -l 9090
# Com a client: connecta i et deixa escriure linies a ma.
nc localhost 9090
# Comprovar si un port esta obert, sense enviar res.
nc -zv localhost 9090A 09-02 escriuràs el client de BiblioTech abans de tenir servidor, i el provaràs contra nc -l 9090 fent tu de servidor a mà. És la millor forma d'entendre un protocol.
telnet — client de protocols de text
Igual que nc per a TCP, però present des de fa dècades i amb eco local, cosa que fa més còmode teclejar. A 09-03 provaràs el servidor de BiblioTech amb ell.
curl — client HTTP
curl -v https://example.com
curl -X POST -H "Content-Type: application/json" -d '{"isbn":"978-0000000001"}' http://localhost:8080/api
curl -i -s -o /dev/null -w "%{http_code}\n" http://localhost:9090/A 09-05 i 09-06 el faràs servir per comparar el que fa el teu codi Java amb el que fa un client de referència. L'opció -v mostra les capçaleres enviades i rebudes, que és justament el que necessites veure.
- Què pot fallar en una xarxa
Acaba aquesta lliçó amb el que més et marcarà com a programador de xarxa. Existeix una llista cèlebre de fal·làcies de la computació distribuïda: suposicions que tothom fa al principi i que són totes falses.
| Fal·làcia | Realitat | Conseqüència al teu codi |
|---|---|---|
| La xarxa és fiable | Els paquets es perden i els cables es desconnecten | Tota operació de xarxa va en try/catch |
| La latència és zero | És un milió de vegades la de la memòria | Minimitza viatges; mai en un bucle calent |
| L'amplada de banda és infinita | Està compartida i és limitada | No enviïs el que no necessites |
| La xarxa és segura | Qualsevol del camí pot llegir i modificar | TLS, validar tota l'entrada |
| La topologia no canvia | Canvia constantment | No guardis en memòria cau una IP per sempre |
| Hi ha un sol administrador | N'hi ha molts, amb polítiques diferents | Els tallafocs et bloquejaran sense avisar |
| El cost de transport és zero | Serialitzar i deserialitzar costa CPU | Formats i mides importen |
| La xarxa és homogènia | No ho és | No suposis res de l'altre extrem |
De totes elles, la primera és la que ha de canviar la teva forma d'escriure codi. El codi de xarxa no falla ocasionalment: falla sempre, i l'única pregunta és amb quina freqüència.
Això connecta directament amb el mòdul 6. Totes les classes de xarxa del JDK llancen IOException o subclasses seves, i són excepcions comprovades — el compilador t'obliga a tractar-les. Això no és una molèstia burocràtica: és el disseny del llenguatge dient-te la veritat sobre el món.
// El que NO s'ha de fer mai, per comode que sembli.
try {
socket.connect(adreca);
} catch (IOException e) {
// Silenci. El programa continua com si res.
}
// El que correspon, amb l'estrategia per capes de 06-07.
try {
socket.connect(adreca, TEMPS_LIMIT_MS);
} catch (SocketTimeoutException e) {
// Fallada TRANSITORIA: la maquina pot estar nomes saturada.
// Te sentit reintentar amb espera creixent.
LOG.warning("Temps esgotat connectant a " + adreca + "; es reintentara");
throw new BiblioTechException("El servidor de cataleg no respon", e);
} catch (ConnectException e) {
// Fallada PERMANENT mentre no canvii res: no hi ha ningu escoltant.
// Reintentar en bucle nomes gasta CPU.
LOG.severe("Connexio rebutjada per " + adreca + ": el servidor no esta arrencat");
throw new BiblioTechException("El servidor de cataleg no esta disponible", e);
} catch (IOException e) {
// Qualsevol altra fallada de xarxa.
LOG.log(Level.SEVERE, "Fallada de xarxa en connectar", e);
throw new BiblioTechException("Error de comunicacio amb el cataleg", e);
}Tres idees que arrossegaràs tot el mòdul:
- Distingeix transitori de permanent. Un temps esgotat mereix reintent; una connexió rebutjada o un nom inexistent, no. Reintentar l'irreintentable és un antipatró car.
- Posa sempre un temps límit. Sense
setSoTimeoutnisetConnectTimeout, unread()es pot quedar aturat fins que el sistema operatiu es rendeixi, i això poden ser hores. Un fil bloquejat per sempre és un fil perdut, i amb un pool acotat, un pool que s'esgota. - Tradueix la fallada al teu domini a la frontera. Que
CatalegRemotllanciBiblioTechExceptioni noSocketExceptionés el que permet que el menú no sàpiga res de sockets — exactament l'estratègia per capes de 06-07.
Errors Comuns i Consells
Confondre IP amb port, o creure que el servidor necessita un port per client. Ja ho hem vist: la connexió s'identifica per la quàdrupla completa. Mil clients, un sol port de servidor.
Guardar en memòria cau una InetAddress per a tota la vida del programa. Les adreces canvien: DHCP, migracions, balancejadors. Resol el nom quan el necessitis i deixa que la memòria cau del sistema faci la seva feina, en lloc de guardar tu la IP en un static final.
Fer servir getLocalHost() per saber "la meva adreça pública". No ho és. Retorna l'adreça de la màquina a la seva xarxa, que darrere d'un NAT és privada. Per conèixer la teva adreça pública cal que t'ho digui algú de fora.
Suposar que isReachable() equival a ping. Depèn de privilegis i tallafocs. Fes-lo servir com a pista, mai com a decisió.
Triar UDP "perquè és més ràpid". UDP és més ràpid en el cas feliç. Si necessites fiabilitat i la implementes a sobre, acabaràs escrivint una versió pitjor de TCP. Tria UDP quan no necessitis les garanties, no quan vulguis estalviar-te els 20 bytes de capçalera.
Dissenyar un protocol sense decidir on acaba un missatge. El bug apareixerà el dia que un missatge es parteixi en dues lectures, que és el dia que tinguis trànsit real. Decideix el delimitador abans d'escriure la primera línia.
No fixar la codificació al protocol. Si el client escriu en UTF-8 i el servidor llegeix en la codificació per defecte de la plataforma, Java Eficaç arribarà com a Java Eficaç. Al protocol es documenta, i al codi es posa explícit. Sempre.
Escriure codi de xarxa sense temps límit. El cas que ho demostra: un servidor el read() del qual es bloqueja perquè el client se'n va anar sense tancar (se li va acabar la bateria del portàtil). Sense setSoTimeout, aquell fil del pool no torna mai. Amb deu clients així, el pool de deu fils està mort i el servidor deixa d'acceptar ningú, sense ni un sol error al log.
Provar només a localhost i donar per fet que funciona. A localhost la latència és zero, no hi ha pèrdues, no hi ha MTU petita i no hi ha tallafocs. Molts bugs de xarxa només apareixen fora. Prova també entre dues màquines reals quan puguis, i com a mínim tingues present què t'està amagant el bucle invertit.
Consell transversal. Quan alguna cosa no funcioni, diagnostica capa a capa i de baix a dalt: la màquina respon (ping)? el port està escoltant (ss -tlnp)? s'hi pot connectar (nc -zv)? el protocol respon el que esperes (telnet o curl -v)? Cada pregunta descarta una capa sencera i el problema apareix en dos minuts en lloc de en dues hores.
Exercicis
Exercici 1: Inventari de xarxa de Nexus Software
Escriu una classe InventariXarxa amb un mètode analitzar(String... noms) que, per a cada nom rebut:
- el resolgui a una o diverses adreces, mesurant el temps de resolució en mil·lisegons;
- indiqui de cada adreça si és IPv4 o IPv6, si és de bucle invertit i si és privada;
- si no resol, ho indiqui sense llançar excepció.
Ha d'imprimir una taula alineada i acabar amb un resum de quants noms van resoldre i quants no. Prova'l amb localhost, 127.0.0.1, la teva pròpia màquina i un nom inventat.
Exercici 2: Analitzador del protocol BTCP
Escriu una classe AnalitzadorBtcp amb un mètode estàtic String descriure(String linia) que rebi una línia del protocol BTCP/1 definit en aquesta lliçó i retorni una descripció llegible. Ha de:
- distingir peticions (
CONSULTA,LLISTA,PRESTAR,SORTIR) de respostes (comencen per tres dígits); - per a una resposta, indicar la família segons el primer dígit (2 = èxit, 4 = error del client, 5 = error del servidor);
- validar que
CONSULTAporta exactament un argument iPRESTARalmenys dos; - retornar una descripció d'error per a qualsevol cosa que no encaixi.
Escriu un main que el provi amb la sessió d'exemple de la secció 13. No facis servir l'API de Streams.
Exercici 3: Estimador de cost de xarxa
Escriu una classe EstimadorXarxa amb un mètode estimar(int nombrePeticions, int bytesPerPeticio, int latenciaMs, double ampladaBandaMbps) que calculi i compari tres estratègies per portar les dades de N materials del catàleg:
- Seqüencial: N peticions, una darrere l'altra.
- Paral·lela amb 8 connexions: N peticions repartides en 8 vies simultànies.
- Per lots: 1 sola petició que retorna els N materials.
Per a cadascuna, calcula el temps total (latència + transferència) i mostra'l en una taula amb el factor de millora respecte de la seqüencial. Considera que cada petició seqüencial costa un viatge d'anada i tornada complet (la latència) més el temps de transferir els seus bytes.
Solucions
Solució 1
package com.nexussoftware.bibliotech.xarxa;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* Inventari de xarxa: resol una llista de noms i classifica les seves adreces.
* Eina de diagnostic interna de Nexus Software.
*/
public class InventariXarxa {
/** Resultat de resoldre un nom. Es fa servir nomes per al resum final. */
private int resolts = 0;
private int fallits = 0;
public void analitzar(String... noms) {
System.out.printf("%-40s %-42s %-6s %-8s %-8s %s%n",
"NOM", "ADRECA", "VER", "BUCLE", "PRIVADA", "MS");
System.out.println("-".repeat(115));
for (String nom : noms) {
analitzarUn(nom);
}
System.out.println("-".repeat(115));
System.out.printf("Resolts: %d Fallits: %d Total: %d%n",
resolts, fallits, resolts + fallits);
}
private void analitzarUn(String nom) {
long inici = System.nanoTime();
InetAddress[] adreces;
try {
// getAllByName retorna TOTES les adreces del nom:
// un servei balancejat en pot tenir diverses.
adreces = InetAddress.getAllByName(nom);
} catch (UnknownHostException e) {
// No resoldre es un resultat valid per a una eina de
// diagnostic: s'informa i es continua, no es propaga.
long ms = (System.nanoTime() - inici) / 1_000_000;
System.out.printf("%-40s %-42s %-6s %-8s %-8s %d%n",
nom, "NO RESOL", "-", "-", "-", ms);
fallits++;
return;
}
long ms = (System.nanoTime() - inici) / 1_000_000;
resolts++;
boolean primera = true;
for (InetAddress adr : adreces) {
// La versio es dedueix del tipus concret: Inet4Address o Inet6Address
// son les dues uniques subclasses d'InetAddress al JDK.
String versio;
if (adr instanceof Inet4Address) {
versio = "IPv4";
} else if (adr instanceof Inet6Address) {
versio = "IPv6";
} else {
versio = "?";
}
System.out.printf("%-40s %-42s %-6s %-8s %-8s %s%n",
primera ? nom : "", // el nom nomes a la 1a fila
adr.getHostAddress(),
versio,
adr.isLoopbackAddress() ? "si" : "no",
adr.isSiteLocalAddress() ? "si" : "no",
primera ? String.valueOf(ms) : "");
primera = false;
}
}
public static void main(String[] args) {
InventariXarxa inventari = new InventariXarxa();
// Si ens passen noms per linia d'ordres, fem servir aquests.
if (args.length > 0) {
inventari.analitzar(args);
return;
}
String lamevaMaquina;
try {
lamevaMaquina = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
lamevaMaquina = "localhost"; // Reserva raonable si la maquina no te nom.
}
inventari.analitzar(
"localhost",
"127.0.0.1",
lamevaMaquina,
"servidor-inexistent.nexussoftware.local");
}
}Sortida típica:
NOM ADRECA VER BUCLE PRIVADA MS
-------------------------------------------------------------------------------------------------------------
localhost 127.0.0.1 IPv4 si no 3
127.0.0.1 127.0.0.1 IPv4 si no 0
lloc-marta 192.168.1.23 IPv4 no si 1
servidor-inexistent.nexussoftware.local NO RESOL - - - 41
-------------------------------------------------------------------------------------------------------------
Resolts: 3 Fallits: 1 Total: 4Comentaris. Fixa't en tres coses de la sortida real: 127.0.0.1 resol en 0 ms perquè no hi ha consulta DNS (és una IP literal, només s'interpreta); el nom inexistent triga 41 ms, molt més que els que sí que resolen, perquè el resolutor consulta i espera abans de rendir-se — en una xarxa mal configurada això pot ser de segons; i localhost és de bucle invertit però no privada, perquè isSiteLocalAddress() només mira els rangs 10/8, 172.16/12 i 192.168/16.
Solució 2
package com.nexussoftware.bibliotech.xarxa;
/**
* Analitzador didactic del protocol BTCP/1 de BiblioTech.
* Converteix una linia del protocol en una descripcio llegible.
* Es fa servir per depurar traces de sessions capturades amb nc o telnet.
*/
public final class AnalitzadorBtcp {
private AnalitzadorBtcp() {
}
public static String descriure(String linia) {
if (linia == null || linia.isBlank()) {
return "LINIA BUIDA (no valida en BTCP/1)";
}
String neta = linia.strip();
// Una resposta comenca sempre per tres digits.
if (esCodiResposta(neta)) {
return descriureResposta(neta);
}
// El marcador de fi de resposta multiple.
if (neta.equals(".")) {
return "FI de resposta multiple";
}
return descriurePeticio(neta);
}
/** Comprova que els tres primers caracters son digits. */
private static boolean esCodiResposta(String linia) {
if (linia.length() < 3) {
return false;
}
for (int i = 0; i < 3; i++) {
if (!Character.isDigit(linia.charAt(i))) {
return false;
}
}
// Despres dels tres digits ha de venir el final o un espai.
return linia.length() == 3 || linia.charAt(3) == ' ';
}
private static String descriureResposta(String linia) {
String codi = linia.substring(0, 3);
String text = linia.length() > 4 ? linia.substring(4) : "";
// El primer digit determina la familia: la gracia dels codis
// numerics es poder decidir sense analitzar el text.
String familia = switch (codi.charAt(0)) {
case '2' -> "EXIT";
case '4' -> "ERROR DEL CLIENT";
case '5' -> "ERROR DEL SERVIDOR";
default -> "FAMILIA DESCONEGUDA";
};
String detall = switch (codi) {
case "200" -> "resposta correcta d'una linia";
case "201" -> "segueixen n linies i despres un punt";
case "221" -> "comiat; el servidor tancara";
case "400" -> "la peticio no s'enten";
case "404" -> "el material no existeix al cataleg";
case "409" -> "el material existeix pero no esta disponible";
case "500" -> "fallada interna del servidor";
default -> "codi no definit en BTCP/1";
};
return "RESPOSTA " + codi + " [" + familia + "] " + detall
+ (text.isEmpty() ? "" : " -> '" + text + "'");
}
private static String descriurePeticio(String linia) {
// split amb limit 2 en PRESTAR no serviria: necessitem separar
// l'ordre i despres la resta, perque el nom de l'empleat
// pot portar espais ("Diego Alonso").
int primerEspai = linia.indexOf(' ');
String ordre = primerEspai < 0 ? linia : linia.substring(0, primerEspai);
String resta = primerEspai < 0 ? "" : linia.substring(primerEspai + 1).strip();
switch (ordre) {
case "CONSULTA":
if (resta.isEmpty()) {
return "PETICIO INVALIDA: CONSULTA requereix un ISBN";
}
if (resta.contains(" ")) {
return "PETICIO INVALIDA: CONSULTA admet un sol argument, rebut '"
+ resta + "'";
}
return "PETICIO CONSULTA del material amb ISBN " + resta;
case "LLISTA":
if (!resta.isEmpty()) {
return "PETICIO INVALIDA: LLISTA no admet arguments";
}
return "PETICIO LLISTA: cataleg complet";
case "PRESTAR":
int espai = resta.indexOf(' ');
if (espai < 0) {
return "PETICIO INVALIDA: PRESTAR requereix ISBN i empleat";
}
String isbn = resta.substring(0, espai);
String empleat = resta.substring(espai + 1).strip();
if (empleat.isEmpty()) {
return "PETICIO INVALIDA: falta l'empleat a PRESTAR";
}
return "PETICIO PRESTAR de l'ISBN " + isbn + " a '" + empleat + "'";
case "SORTIR":
if (!resta.isEmpty()) {
return "PETICIO INVALIDA: SORTIR no admet arguments";
}
return "PETICIO SORTIR: fi de la conversa";
default:
return "PETICIO INVALIDA: ordre desconeguda '" + ordre + "'";
}
}
public static void main(String[] args) {
String[] sessio = {
"200 BIBLIOTECH BTCP/1",
"CONSULTA 978-0000000001",
"200 OK 978-0000000001|Java Eficac|LLIBRE|true",
"LLISTA",
"201 LLISTA 3",
"978-0000000001|Java Eficac|LLIBRE|true",
".",
"PRESTAR 978-0000000002 Diego Alonso",
"409 NO DISPONIBLE 978-0000000002",
"CONSULTA 999",
"404 NO TROBAT 999",
"DEMANA UN CAFE",
"400 PETICIO INVALIDA ordre desconeguda: DEMANA",
"CONSULTA",
"CONSULTA 111 222",
"SORTIR",
"221 ADEU"
};
for (String linia : sessio) {
System.out.printf("%-55s | %s%n", linia, descriure(linia));
}
}
}Comentaris. Tres decisions mereixen atenció. La primera: esCodiResposta no es limita a mirar tres dígits, sinó que exigeix que després vingui un espai o el final de línia; sense això, la línia de dades 978-0000000001|Java Eficac|... comença per tres dígits i es classificaria com a resposta. La segona: a PRESTAR no es fa servir split(" ") perquè el nom de l'empleat porta espais; es busca el primer separador a mà i la resta es pren sencera. I la tercera: fixa't que la línia de dades 978-...|Java Eficac|... cau al default de descriurePeticio i es marca com a ordre desconeguda. És correcte: fora del context d'una resposta 201, aquesta línia no és vàlida. Analitzar un protocol amb estat exigeix recordar en quin punt de la conversa ets, i això és justament el que farà el client real de 09-02.
Solució 3
package com.nexussoftware.bibliotech.xarxa;
/**
* Estimador del cost de xarxa de les diferents formes de portar N materials
* del cataleg. Serveix per justificar per que les peticions per lots guanyen.
*/
public class EstimadorXarxa {
/** Resultat d'una estrategia. Un record simple (04-07). */
public record Estimacio(String nom, double milisegons) {
}
public void estimar(int nombrePeticions, int bytesPerPeticio,
int latenciaMs, double ampladaBandaMbps) {
// Temps de transferir els bytes d'UNA peticio.
// Mbps son megabits per segon: cal passar bytes a bits (x8)
// i megabits a bits (x1.000.000).
double bitsPerPeticio = bytesPerPeticio * 8.0;
double bitsPerMs = ampladaBandaMbps * 1_000_000.0 / 1000.0;
double msTransferenciaUna = bitsPerPeticio / bitsPerMs;
// --- Estrategia 1: sequencial ---
// Cada peticio costa un viatge complet (latencia) mes la seva transferencia.
double sequencial = nombrePeticions * (latenciaMs + msTransferenciaUna);
// --- Estrategia 2: parallela amb 8 connexions ---
// Les 8 vies avancen alhora, aixi que el temps es el de la via
// mes carregada: es reparteixen les peticions arrodonint cap amunt.
int vies = 8;
int perVia = (nombrePeticions + vies - 1) / vies;
double parallela = perVia * (latenciaMs + msTransferenciaUna);
// --- Estrategia 3: un sol lot ---
// Un unic viatge d'anada i tornada, i tota la transferencia junta.
double lot = latenciaMs + msTransferenciaUna * nombrePeticions;
Estimacio[] resultats = {
new Estimacio("Sequencial (" + nombrePeticions + " viatges)", sequencial),
new Estimacio("Parallela x8 (" + perVia + " viatges per via)", parallela),
new Estimacio("Per lots (1 viatge)", lot)
};
System.out.printf("N=%d bytes/peticio=%d latencia=%d ms amplada=%.1f Mbps%n",
nombrePeticions, bytesPerPeticio, latenciaMs, ampladaBandaMbps);
System.out.println();
System.out.printf("%-36s %12s %10s%n", "ESTRATEGIA", "TEMPS (ms)", "MILLORA");
System.out.println("-".repeat(60));
for (Estimacio e : resultats) {
double millora = sequencial / e.milisegons();
System.out.printf("%-36s %12.1f %9.1fx%n", e.nom(), e.milisegons(), millora);
}
System.out.println();
System.out.printf("Bytes totals moguts en els tres casos: %d (identic)%n",
(long) nombrePeticions * bytesPerPeticio);
}
public static void main(String[] args) {
EstimadorXarxa estimador = new EstimadorXarxa();
System.out.println("=== Cas 1: xarxa local de Nexus Software ===");
estimador.estimar(100, 200, 1, 1000.0);
System.out.println();
System.out.println("=== Cas 2: servei extern de metadades (un altre continent) ===");
estimador.estimar(100, 200, 120, 50.0);
}
}Sortida:
=== Cas 1: xarxa local de Nexus Software ===
N=100 bytes/peticio=200 latencia=1 ms amplada=1000,0 Mbps
ESTRATEGIA TEMPS (ms) MILLORA
------------------------------------------------------------
Sequencial (100 viatges) 100,2 1,0x
Parallela x8 (13 viatges per via) 13,0 7,7x
Per lots (1 viatge) 1,2 86,2x
Bytes totals moguts en els tres casos: 20000 (identic)
=== Cas 2: servei extern de metadades (un altre continent) ===
N=100 bytes/peticio=200 latencia=120 ms amplada=50,0 Mbps
ESTRATEGIA TEMPS (ms) MILLORA
------------------------------------------------------------
Sequencial (100 viatges) 12003,2 1,0x
Parallela x8 (13 viatges per via) 1560,4 7,7x
Per lots (1 viatge) 123,2 97,5x
Bytes totals moguts en els tres casos: 20000 (identic)Comentaris. Aquest és l'exercici més important de la lliçó, perquè el resultat és contraintuïtiu i decideix dissenys. Els tres casos mouen exactament els mateixos 20.000 bytes, i tanmateix el més lent triga 12 segons i el més ràpid 123 mil·lisegons: una diferència de gairebé cent vegades. El que canvia no és el volum, sinó el nombre de viatges.
Compara a més els dos escenaris. A la xarxa local, la seqüencial triga 100 ms — molest però suportable. Contra un servei a 120 ms de distància, la mateixa estratègia triga dotze segons, i cap augment d'amplada de banda no ho arregla: la transferència real són 0,2 ms dels 120,2 de cada viatge. Per això les dues tècniques que funcionen són agrupar (una petició en lloc de cent) i solapar (vuit alhora), i per això el mòdul 8 era requisit d'aquest. A 09-06 faràs exactament l'estratègia paral·lela amb sendAsync i allOf.
Conclusió
Aquesta lliçó ha posat els fonaments de tot el mòdul. Ja no escriuràs codi de xarxa a cegues.
Saps què significa realment que dos programes es comuniquin: no comparteixen memòria, ni temps, ni confiança, ni vida, i per la xarxa només hi viatgen bytes que tots dos extrems han de saber interpretar. Coneixes els dos models d'organització —client-servidor, asimètric i amb adreça coneguda, que és el de BiblioTech i el del 95 % del que programaràs; i d'igual a igual, simètric i resistent però molt més complex— i saps que en Java tots dos es construeixen amb les mateixes peces.
Entens el model per capes com a sobres dins de sobres: la d'aplicació porta les teves dades, la de transport identifica el programa amb el port, la de xarxa identifica la màquina amb la IP, i la d'enllaç només cobreix el tram següent. I saps la conseqüència pràctica: tu només programes la capa d'aplicació, però pateixes les conseqüències de totes les altres.
Manages les adreces IP —IPv4 de 32 bits, IPv6 de 128, 127.0.0.1 com a bucle invertit que ni tan sols toca la targeta de xarxa, i els rangs privats que fan que Nexus Software funcioni a 192.168.1.0/24 amb NAT per sortir— i els ports: 16 bits, de 0 a 65535, amb els ben coneguts per sota de 1024 i els efímers per sobre de 49152. Amb la idea que més dubtes resol: un socket és el parell (IP, port), una connexió TCP és una quàdrupla, i per això mil clients caben en un sol port de servidor.
Saps com un nom es converteix en adreça a través del DNS, que triga, es guarda en memòria cau i pot fallar amb UnknownHostException; i has escrit la teva primera classe de xarxa, DiagnosticXarxa, que resol noms, llista interfícies i comprova abast, amb els tres advertiments que l'acompanyen: getLocalHost() falla en contenidors, isReachable() no és ping, i resoldre un nom inexistent pot trigar segons.
I sobretot has entès la decisió que obre aquest mòdul: TCP davant d'UDP. TCP et dóna un tub fiable i ordenat a canvi d'una salutació de tres vies que costa un viatge sencer, de retransmissions que fan que les teves lectures triguin més sense que te n'assabentis, i d'un control de flux que pot arribar a bloquejar les teves escriptures si l'altre no llegeix. UDP no et dóna res d'això, i per això és el correcte quan la dada vella ja no val: vídeo, jocs, telemetria, DNS i —el que farà servir BiblioTech— descobriment en xarxa local per difusió, cosa que TCP simplement no pot fer.
Saps què és un protocol d'aplicació, per què el text guanya en claredat i el binari en mida, i quin és el seu problema central: TCP no té fronteres de missatge, així que cal posar-les amb un delimitador, amb una longitud prèvia o amb el tancament de la connexió. I n'has dissenyat un de complet, BTCP/1: salutació del servidor, ordres CONSULTA, LLISTA, PRESTAR i SORTIR, respostes amb codis de tres dígits la primera xifra dels quals ja diu si va anar bé, respostes múltiples que anuncien la seva mida i es tanquen amb un punt, UTF-8 explícit i delimitador de línia.
Tens interioritzada la diferència entre latència —que no es pot comprar perquè la limita la velocitat de la llum— i amplada de banda, amb la xifra que ho resumeix tot: la xarxa és un milió de vegades més lenta que la memòria, i d'aquí surten les tres regles que governaran el teu codi: minimitza viatges, solapa esperes, reutilitza connexions. El teu propi estimador t'ha ensenyat que els mateixos 20.000 bytes triguen dotze segons o cent vint-i-tres mil·lisegons segons com els demanis.
Coneixes les cinc eines amb què es diagnostica de veritat —ping, traceroute, ss, nc i curl— i el mètode: capa a capa, de baix a dalt. I acceptes la premissa que fa bo un programador de xarxa: el codi de xarxa no falla ocasionalment, falla sempre, així que s'escriu suposant la fallada, distingint el transitori del permanent, posant temps límit a tot i traduint la fallada de xarxa a una BiblioTechException a la frontera, tal com vas aprendre a 06-07.
A la lliçó següent, Sockets, s'acaba la teoria. Obriràs la teva primera connexió des de Java amb la classe Socket, i descobriràs una cosa que ja intueixes: un cop connectat, getInputStream() i getOutputStream() et retornen exactament els fluxos del mòdul 7, amb els mateixos decoradors, el mateix charset explícit i el mateix try-with-resources. Veuràs com connectar amb temps límit, com tancar mitja connexió, què significa cada excepció de xarxa i quines opcions té un socket. I veuràs demostrat, en directe, el bug número u de tothom que comença amb sockets: oblidar el buidatge de la memòria intermèdia i deixar l'altre extrem esperant per sempre. En acabar tindràs el client de BiblioTech parlant BTCP/1 — provat contra un nc -l 9090 en què faràs tu de servidor a mà, mentre arriba el servidor de veritat a 09-03.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
