El servidor de catàleg de BiblioTech ja funciona. La Marta, en Diego i la Nuria s'hi connecten alhora des dels seus llocs i consulten el catàleg amb BTCP/1 sobre TCP. Però hi ha un detall que espatlla la festa: cada lloc de treball té l'adreça del servidor escrita a mà al seu bibliotech.properties. El dia que la màquina del servidor canviï d'IP, algú haurà d'anar lloc per lloc a corregir un fitxer de configuració.
Existeix una solució elegant: que els llocs preguntin a tota la xarxa local on és BiblioTech i el servidor respongui. Però TCP no pot fer això. TCP exigeix que sàpigues a qui vols connectar-te abans de connectar; no existeix l'operació "envia això a tothom i a veure qui contesta".
UDP sí que pot. I aquest és el tema d'aquesta lliçó: l'altre protocol de transport, el que no garanteix res —ni lliurament, ni ordre, ni unicitat, ni control de flux— i que precisament per això pot fer coses que TCP no pot. Entendràs quan aquesta manca de garanties és un problema i quan és exactament el que vols, cometràs (i evitaràs) els dos errors clàssics que comet tothom, i donaràs a BiblioTech dues capacitats noves: descobriment automàtic del servidor a la xarxa local i telemetria contínua que no bloqueja ningú i a la qual no li importa perdre algun paquet.
Contingut
- UDP a la pràctica: què es perd i què es guanya
- Quan UDP és l'elecció correcta
DatagramPacket: el sobreDatagramSocket: la bústia- Enviar i rebre: el client-servidor d'eco
- Error clàssic 1: reutilitzar el paquet de recepció sense restaurar la longitud
- Error clàssic 2: fer servir
buffer.lengthen lloc depacket.getLength() - Mida del datagrama, MTU i fragmentació
- Pèrdua, duplicació i desordre: la demostració
- Fiabilitat a mà: seqüència, temps límit i reintent
- Difusió (broadcast)
- Multidifusió (multicast) amb
MulticastSocket - BiblioTech: el servei de descobriment
- BiblioTech: l'emissor de telemetria
- El mateix cas resolt amb TCP i amb UDP
- Errors Comuns i Consells
- Exercicis
- UDP a la pràctica: què es perd i què es guanya
Amb TCP tenies un tub. Amb UDP tens una bústia de correus: hi fiques un sobre, l'envies, i esperes que arribi.
graph LR
subgraph TCP
A["Client"] ---|connexio establerta| B["Servidor"]
end
subgraph UDP
C["Emissor"] -.->|sobre 1| D["Receptor"]
C -.->|sobre 2 - perdut| E["X"]
C -.->|sobre 3| D
end
El que perds respecte de TCP:
| Garantia perduda | Què significa al teu codi |
|---|---|
| Lliurament | Un datagrama pot desaparèixer i ningú no t'ho diu. No hi ha excepció, no hi ha avís |
| Ordre | El paquet 3 pot arribar abans que el 2 |
| Unicitat | Un paquet pot arribar dues vegades (retransmès per un router) |
| Control de flux | Pots saturar el receptor sense adonar-te'n |
| Control de congestió | Pots empitjorar una xarxa congestionada |
| Detecció de caiguda del parell | No hi ha connexió, així que no hi ha res que es trenqui |
El que guanyes:
| Avantatge | Detall |
|---|---|
| Sense establiment | Envies a l'instant. TCP gasta un viatge d'anada i tornada abans del primer byte útil |
| Capçalera mínima | 8 bytes davant de 20 o més de TCP |
| Fronteres de missatge | Si envies 100 bytes, l'altre rep 100 bytes o res. Mai 60. S'ha acabat el problema del delimitador |
| Sense estat | El servidor no manté res per client: pot atendre'n milers amb un sol socket |
| Un a molts | Difusió i multidifusió, impossibles en TCP |
| Sense bloqueig de capçalera de línia | Un paquet perdut no endarrereix els següents |
De totes elles, les dues que més canvien el disseny són les fronteres de missatge —que fan innecessària tota la feina de delimitadors de 09-02— i la capacitat d'un a molts, que és literalment impossible amb TCP.
- Quan UDP és l'elecció correcta
La pregunta que decideix és sempre la mateixa: una dada vella encara val, o cal esperar-la sí o sí?
| Cas real | Per què UDP |
|---|---|
| DNS | Una pregunta, una resposta, cap en un datagrama. Si es perd, es repeteix. Establir una connexió TCP per a 60 bytes seria absurd |
| Vídeo i àudio en directe | Un fotograma perdut és un parpelleig de 40 ms. Retransmetre'l arribaria tard i a més aturaria tota la resta |
| Videojocs d'acció | La posició del jugador fa 200 ms ja no interessa: interessa la d'ara. Retransmetre una posició vella és pitjor que perdre-la |
| Telemetria i mètriques | Milers de mesures per segon; perdre'n una no canvia res. I l'emissor no s'ha de bloquejar mai per culpa del receptor |
| Descobriment en xarxa local | Requereix difusió. TCP no pot |
| Sincronització de rellotge (NTP) | La latència ha de ser mínima i predictible; el protocol ja tolera pèrdues |
| Registre remot (syslog) | Un servidor de logs caigut no ha de bloquejar les aplicacions que li escriuen |
| QUIC i HTTP/3 | Implementen la seva pròpia fiabilitat sobre UDP per evitar el bloqueig de capçalera de línia de TCP |
I el criteri negatiu, igual d'important: si acabes implementant confirmacions, retransmissions, números de seqüència i control de flux sobre UDP, has escrit una versió pitjor de TCP. Tria UDP quan no necessitis les garanties, no quan vulguis estalviar-te dotze bytes de capçalera.
Hi ha una excepció legítima a aquesta regla, i és quan necessites fiabilitat parcial i a la teva mida: per exemple, retransmetre només els fotogrames clau d'un vídeo, o garantir l'ordre però no el lliurament. Això TCP no ho ofereix —és tot o res— i és la raó per la qual QUIC existeix.
DatagramPacket: el sobre
DatagramPacket: el sobreUn DatagramPacket és un sobre: conté les dades, la seva longitud, i —segons per a què el facis servir— una adreça i un port.
La classe es fa servir de dues formes completament diferents, i confondre-les és l'origen de la majoria dels problemes.
Per enviar: dades + destí
byte[] dades = "ON ES BIBLIOTECH?".getBytes(StandardCharsets.UTF_8);
DatagramPacket paquet = new DatagramPacket(
dades, // el contingut
dades.length, // quants bytes d'aquest array
InetAddress.getByName("192.168.1.50"), // on va
9091); // a quin portPer rebre: una memòria intermèdia buida
byte[] buffer = new byte[1024];
// Sense adreca ni port: els omplira receive() amb els del REMITENT.
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
socket.receive(paquet); // bloqueja fins que arriba alguna cosa
// Ara el paquet SI que te remitent, i sabem quants bytes han arribat.
InetAddress remitent = paquet.getAddress();
int portRemitent = paquet.getPort();
int bytesRebuts = paquet.getLength();Mètodes de DatagramPacket
| Mètode | Significat |
|---|---|
getData() |
L'array de bytes. Compte: l'array complet, no només el rebut |
getLength() |
Quants bytes són vàlids. La peça que tothom oblida |
getOffset() |
Des de quina posició de l'array comencen les dades vàlides |
getAddress() / getPort() |
El destí (en enviar) o el remitent (en rebre) |
setData(byte[]) |
Canvia l'array |
setLength(int) |
Canvia la longitud vàlida. Clau per reutilitzar paquets |
getSocketAddress() |
Adreça i port junts, com a SocketAddress |
La distinció entre getData().length (la mida de la memòria intermèdia) i getLength() (els bytes que realment han arribat) és l'origen del segon error clàssic, i el tractem a l'apartat 7.
DatagramSocket: la bústia
DatagramSocket: la bústiaUn DatagramSocket és la bústia per la qual surten i entren els sobres. A diferència de Socket, no representa una connexió amb ningú: representa un punt de la màquina pel qual s'envia i es rep.
// Socket EFIMER: el sistema assigna un port lliure.
// Serveix per enviar i per rebre respostes al que s'ha enviat.
DatagramSocket client = new DatagramSocket();
// Socket lligat a un port CONEGUT: es el que fa un servidor UDP.
DatagramSocket servidor = new DatagramSocket(9091);
// Lligat a un port i a una interficie concreta.
DatagramSocket local = new DatagramSocket(9091, InetAddress.getByName("127.0.0.1"));La diferència essencial amb Socket i ServerSocket
| TCP | UDP | |
|---|---|---|
| Classe del client | Socket |
DatagramSocket |
| Classe del servidor | ServerSocket i un Socket per client |
DatagramSocket (un de sol, per a tots) |
| Objectes per client | Un | Cap: no hi ha estat per client |
| Com se sap qui parla | És la connexió | És a cada paquet: getAddress() |
| Fils necessaris | Un per connexió | Un, o uns pocs |
Un servidor UDP no necessita un pool de fils per client, perquè no hi ha clients: hi ha paquets solts. Un sol fil en un bucle de receive() pot atendre milers de màquines. És una arquitectura radicalment més simple, a canvi que tu t'encarreguis de tot el que TCP feia per tu.
Mètodes principals
| Mètode | Què fa |
|---|---|
send(DatagramPacket) |
Envia. Gairebé mai no bloqueja i gairebé mai no falla, encara que el destí no existeixi |
receive(DatagramPacket) |
Bloqueja fins que arriba un datagrama |
setSoTimeout(int ms) |
Fa que receive llanci SocketTimeoutException. Imprescindible |
connect(InetAddress, int) |
Filtra: només s'envia i es rep d'aquell parell. No estableix connexió |
disconnect() |
Desfà el filtre |
setBroadcast(boolean) |
Permet enviar a l'adreça de difusió |
getLocalPort() |
El port que se t'ha assignat |
close() |
Allibera el port. Implementa Closeable |
Sobre
send: el silenci absolut. Això sorprèn tothom.send()normalment no llança excepció encara que el destí estigui apagat, no existeixi o descarti el paquet. El sistema lliura el datagrama a la xarxa i se n'oblida. Si envies a192.168.1.250i allà no hi ha res, el teu codi no se n'assabenta de res. L'única senyal possible és un missatge ICMP de "port inabastable" que pot arribar i provocar unaPortUnreachableExceptiona l'operació següent — però només si el socket estàconnect-at, i ni tan sols està garantit. Amb UDP, l'èxit desend()no significa absolutament res.
Sobre
connecten UDP: no és el que sembla.DatagramSocket.connect(adr, port)no fa cap salutació, ni estableix res, ni envia ni un sol paquet. Només instal·la un filtre local: a partir d'aquísendpot ometre el destí ireceivedescarta els paquets de qualsevol altre remitent. És útil per dos motius: evita que un tercer t'injecti paquets falsos, i permet rebrePortUnreachableException. És una comoditat i una defensa, no una connexió.
- Enviar i rebre: el client-servidor d'eco
Comencem amb el parell més simple que funciona, per fixar la mecànica.
El servidor d'eco
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Servidor d'eco UDP. Un sol fil, un sol socket, tots els clients.
* No hi ha connexions, no hi ha pool, no hi ha estat per client.
*/
public class ServidorEcoUdp implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(ServidorEcoUdp.class.getName());
/** Mida de la memoria intermedia de recepcio. Veure l'apartat 8 sobre l'MTU. */
private static final int MAXIM_DATAGRAMA = 1400;
private final int port;
private DatagramSocket socket;
private volatile boolean executant = false;
public ServidorEcoUdp(int port) {
this.port = port;
}
public void executar() throws SocketException {
socket = new DatagramSocket(port);
executant = true;
LOG.info(() -> "Servidor d'eco UDP al port " + port);
// UNA memoria intermedia i UN paquet, reutilitzats a tot el bucle:
// crear un array de 1400 bytes per cada paquet rebut
// generaria brossa innecessaria a milers per segon.
byte[] buffer = new byte[MAXIM_DATAGRAMA];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
while (executant) {
try {
// IMPRESCINDIBLE abans de cada receive: restaurar la longitud.
// Si el paquet anterior tenia 20 bytes, getLength() val 20
// i nomes es rebrien 20 bytes del seguent. Apartat 6.
paquet.setLength(buffer.length);
socket.receive(paquet); // BLOQUEJA fins que arriba alguna cosa
// getLength() -> el que REALMENT ha arribat, no buffer.length.
String text = new String(paquet.getData(), paquet.getOffset(),
paquet.getLength(), StandardCharsets.UTF_8);
LOG.info(() -> "De " + paquet.getSocketAddress() + " ("
+ paquet.getLength() + " bytes): " + text);
// Responem AL REMITENT, que ve al mateix paquet.
byte[] resposta = ("ECO: " + text).getBytes(StandardCharsets.UTF_8);
DatagramPacket tornada = new DatagramPacket(
resposta, resposta.length,
paquet.getAddress(), paquet.getPort());
socket.send(tornada);
} catch (SocketException e) {
if (!executant) {
LOG.info("Servidor d'eco UDP aturat");
return;
}
LOG.log(Level.SEVERE, "Fallada del socket UDP", e);
return;
} catch (IOException e) {
// Una fallada amb UN paquet no pot tombar el servidor.
LOG.log(Level.WARNING, "Error processant un datagrama", e);
}
}
}
@Override
public void close() {
executant = false;
if (socket != null) {
// Tancar el socket desbloqueja el receive(), igual que tancar
// el ServerSocket desbloquejava l'accept() a 09-03.
socket.close();
}
}
public static void main(String[] args) throws SocketException {
ServidorEcoUdp servidor = new ServidorEcoUdp(9095);
Runtime.getRuntime().addShutdownHook(new Thread(servidor::close));
servidor.executar();
}
}El client d'eco
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
/** Client d'eco UDP amb temps limit. */
public class ClientEcoUdp {
private static final int MAXIM_DATAGRAMA = 1400;
public static void main(String[] args) throws IOException {
String host = args.length > 0 ? args[0] : "localhost";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 9095;
// Socket efimer: el sistema ens dona un port lliure.
// try-with-resources: DatagramSocket es Closeable.
try (DatagramSocket socket = new DatagramSocket()) {
// OBLIGATORI. Sense aixo, si la resposta es perd
// -i amb UDP pot perdre's- el receive() no torna mai.
socket.setSoTimeout(2000);
InetAddress desti = InetAddress.getByName(host);
for (String missatge : new String[]{"Hola", "BiblioTech", "Refactorització"}) {
byte[] dades = missatge.getBytes(StandardCharsets.UTF_8);
socket.send(new DatagramPacket(dades, dades.length, desti, port));
System.out.println("-> " + missatge + " (" + dades.length + " bytes)");
byte[] buffer = new byte[MAXIM_DATAGRAMA];
DatagramPacket resposta = new DatagramPacket(buffer, buffer.length);
try {
socket.receive(resposta);
System.out.println("<- " + new String(resposta.getData(),
resposta.getOffset(), resposta.getLength(),
StandardCharsets.UTF_8));
} catch (SocketTimeoutException e) {
// Amb UDP aixo NO es un error del programa: es el
// funcionament normal quan alguna cosa es perd.
System.out.println("<- (sense resposta en 2 s: paquet perdut"
+ " o servidor caigut)");
}
}
}
}
}Prova en dues terminals:
# Terminal 1
java -cp classes com.nexussoftware.bibliotech.xarxa.ServidorEcoUdp
# Terminal 2
java -cp classes com.nexussoftware.bibliotech.xarxa.ClientEcoUdpTerminal 2:
-> Hola (4 bytes)
<- ECO: Hola
-> BiblioTech (10 bytes)
<- ECO: BiblioTech
-> Refactorització (16 bytes)
<- ECO: RefactoritzacióFixa't en Refactorització: són 15 caràcters però 16 bytes, perquè la ó ocupa dos bytes en UTF-8. Amb UDP, la mida es mesura en bytes, no en caràcters, i aquesta diferència importa quan calcules si alguna cosa cap en un datagrama.
També ho pots provar amb nc en mode UDP:
I una cosa que amb TCP no passaria: apaga el servidor i torna a llançar el client. No obtens ConnectException ni cap error; obtens tres temps esgotats de dos segons. send() va tenir èxit les tres vegades. Aquesta és la naturalesa d'UDP en una línia.
- Error clàssic 1: reutilitzar el paquet de recepció sense restaurar la longitud
Aquest és el bug més citat de l'API d'UDP en Java, i el seu símptoma és desconcertant: els missatges comencen a arribar truncats a partir del segon.
// CODI TRENCAT. No el copiis.
byte[] buffer = new byte[1400];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
while (true) {
socket.receive(paquet); // <-- falta setLength abans
processar(paquet);
}Què passa
receive() modifica la longitud del paquet per indicar quants bytes han arribat. I alhora, aquesta mateixa longitud és la que receive() fa servir com a límit màxim del que acceptarà a la crida següent.
Estat inicial: paquet.getLength() = 1400 (la memoria intermedia sencera)
1r receive: arriba "Hola" (4 bytes)
-> receive escriu 4 bytes i posa getLength() = 4
2n receive: el limit ara es de 4 bytes.
Arriba "BiblioTech" (10 bytes)
-> se n'accepten NOMES 4: "Bibl"
-> els 6 restants ES DESCARTEN SENSE AVIS
-> getLength() = 4
3r receive: el limit continua sent 4. I aixi per sempre.Sortida real del servidor amb el bug:
De /127.0.0.1:51234 (4 bytes): Hola
De /127.0.0.1:51234 (4 bytes): Bibl
De /127.0.0.1:51234 (4 bytes): RefaEl primer missatge arriba perfecte. Tots els següents queden retallats a la longitud del primer. I no hi ha excepció, ni avís, ni res al log: els bytes sobrants simplement es llencen.
La solució
// CORRECTE: restaurar la longitud ABANS de cada receive.
while (true) {
paquet.setLength(buffer.length); // <-- la linia que falta
socket.receive(paquet);
processar(paquet);
}Una sola línia. I si prefereixes no haver-te'n de recordar:
// Alternativa: paquet nou a cada volta. Correcte, pero genera
// brossa. En un servidor que rep milers de paquets per segon,
// la reutilitzacio amb setLength es notablement millor.
while (true) {
DatagramPacket paquet = new DatagramPacket(new byte[1400], 1400);
socket.receive(paquet);
processar(paquet);
}Regla: si reutilitzes el paquet —i ho hauries de fer—, setLength(buffer.length) abans de cada receive(). Sense excepcions.
- Error clàssic 2: fer servir
buffer.length en lloc de packet.getLength()
buffer.length en lloc de packet.getLength()El segon error té l'efecte contrari: en lloc de perdre dades, arrossegues brossa.
// CODI TRENCAT. No el copiis.
socket.receive(paquet);
String text = new String(paquet.getData(), StandardCharsets.UTF_8);
// ^^^^^^^^^^^^^^^^ l'array SENCER, 1400 bytesgetData() retorna l'array complet, de 1400 bytes. Si només n'han arribat 4, els altres 1396 són el que hi hagués abans a la memòria: zeros la primera vegada, i restes del missatge anterior a partir d'aleshores.
Amb el bug, rebent "Hola" i despres "Hi":
1r missatge: "Hola" + 1396 bytes de zeros
-> "Hola\0\0\0\0\0\0..." (sembla que funciona, enganya)
2n missatge: "Hi" + "la" (restes de l'anterior) + zeros
-> "Hila\0\0\0..." <-- DADES BARREJADES!Aquest "Hila" és una dada corrupta que cap log no delata. En un cas real —rebent el nombre de préstecs actius, o un identificador— produeix valors erronis que semblen legítims.
La solució
// CORRECTE: sempre offset i getLength().
String text = new String(
paquet.getData(),
paquet.getOffset(), // des d'on
paquet.getLength(), // quants bytes son valids
StandardCharsets.UTF_8); // charset explicit, com sempreI per copiar els bytes a un array de la mida exacta:
byte[] utils = Arrays.copyOfRange(
paquet.getData(),
paquet.getOffset(),
paquet.getOffset() + paquet.getLength());| Mètode | Retorna | Quan fer-lo servir |
|---|---|---|
paquet.getData().length |
La mida de la memòria intermèdia (1400) | Gairebé mai. És la font del bug |
paquet.getLength() |
Els bytes rebuts (4) | Sempre |
paquet.getOffset() |
Des d'on comencen | Sempre, al costat de l'anterior |
Els dos errors d'aquests dos apartats són, amb diferència, els més freqüents amb DatagramPacket. Si els evites, el 80 % dels problemes d'UDP en Java desapareixen.
- Mida del datagrama, MTU i fragmentació
Com de gran pot ser un datagrama? La resposta té tres nivells.
El límit teòric
El camp de longitud de la capçalera UDP és de 16 bits, així que el màxim absolut són 65.535 bytes, menys 8 de capçalera UDP i 20 de capçalera IP: 65.507 bytes de dades amb IPv4.
El límit pràctic: l'MTU
L'MTU (unitat màxima de transmissió) és la mida màxima de trama que admet un enllaç. A Ethernet són 1500 bytes, incloent-hi les capçaleres IP i UDP.
1500 bytes d'MTU Ethernet
- 20 bytes de capcalera IPv4 (40 si es IPv6)
- 8 bytes de capcalera UDP
= 1472 bytes de dades que caben en UNA trama EthernetSi n'envies més, IP fragmenta el datagrama en diverses trames i el receptor les reensambla. I aquí hi ha el problema:
Si es perd un sol fragment, es perd el datagrama sencer. No hi ha retransmissió de fragments: el receptor descarta el que tenia i la teva aplicació no rep res.
Amb un 1 % de pèrdua per paquet, un datagrama de 10 fragments té gairebé un 10 % de probabilitat de perdre's. La fragmentació multiplica la taxa de pèrdua.
A més, molts tallafocs descarten fragments IP per política de seguretat, amb la qual cosa els teus datagrames grans simplement no arriben mai — i el diagnòstic és infernal perquè els petits funcionen.
La recomanació pràctica
| Mida | Valoració |
|---|---|
| 512 bytes | Molt segur. És el límit del DNS clàssic, escollit per funcionar en qualsevol xarxa |
| 1400 bytes | Prudent. Cap a Ethernet fins i tot amb túnels VPN, que resten uns bytes |
| 1472 bytes | El màxim exacte d'Ethernet sense fragmentar. Sense marge |
| Més de 1500 | Fragmenta. Només en xarxes controlades |
| Més de 8192 | A més, alguns sistemes ho rebutgen directament |
BiblioTech farà servir 1400 bytes com a mida de memòria intermèdia i mantindrà els seus missatges molt per sota. Si una dada no cap en un datagrama, la resposta correcta gairebé mai és "fer el datagrama més gran": és trossejar la dada tu mateix amb números de seqüència, o fer servir TCP, que ja ho sap fer.
Un detall que mossega
Si un datagrama arriba i no cap a la teva memòria intermèdia, Java el trunca sense avisar. No hi ha excepció. Reps els primers N bytes i la resta es perd.
// Truc util per detectar truncaments: demanar un byte de mes.
// Si getLength() == buffer.length, es MOLT probable que hagi arribat
// un paquet mes gran i s'hagi truncat.
byte[] buffer = new byte[MAXIM_DATAGRAMA + 1];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
socket.receive(paquet);
if (paquet.getLength() > MAXIM_DATAGRAMA) {
LOG.warning("Datagrama de " + paquet.getLength()
+ " bytes: supera el maxim previst. Descartat.");
return;
}
- Pèrdua, duplicació i desordre: la demostració
A localhost UDP sembla perfecte: no es perd res. Això és enganyós, perquè el bucle invertit no travessa una xarxa real. Comprovarem que cal tolerar els tres fenòmens.
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
/**
* Demostra que un receptor UDP ha de tolerar perdua, duplicacio i desordre.
* L'emissor envia N paquets numerats; el receptor analitza que ha arribat.
*/
public class DemostracioFiabilitatUdp {
private static final int PORT = 9096;
private static final int TOTAL = 20;
/** Receptor: compta el que arriba i detecta forats, duplicats i desordre. */
static void receptor() throws IOException {
try (DatagramSocket socket = new DatagramSocket(PORT)) {
socket.setSoTimeout(3000); // sense aixo, si es perd l'ultim,
// el bucle no acabaria mai
byte[] buffer = new byte[512];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
Set<Integer> vistos = new HashSet<>();
int duplicats = 0;
int desordenats = 0;
int ultimVist = -1;
while (true) {
paquet.setLength(buffer.length); // error classic 1
try {
socket.receive(paquet);
} catch (SocketTimeoutException e) {
break; // 3 s sense res: donem per acabat
}
String text = new String(paquet.getData(), paquet.getOffset(),
paquet.getLength(), StandardCharsets.UTF_8); // error classic 2
int numero = Integer.parseInt(text.substring(text.indexOf('#') + 1));
if (!vistos.add(numero)) {
// add() retorna false si ja hi era: es un DUPLICAT.
duplicats++;
System.out.println(" DUPLICAT: #" + numero);
continue;
}
if (numero < ultimVist) {
// Ha arribat despres d'un de mes gran: DESORDRE.
desordenats++;
System.out.println(" DESORDENAT: #" + numero
+ " rere #" + ultimVist);
}
ultimVist = Math.max(ultimVist, numero);
}
// Els que falten son PERDUTS.
StringBuilder perduts = new StringBuilder();
for (int i = 0; i < TOTAL; i++) {
if (!vistos.contains(i)) {
perduts.append('#').append(i).append(' ');
}
}
System.out.println();
System.out.println("=== ANALISI DE " + TOTAL + " PAQUETS ===");
System.out.println("Rebuts unics : " + vistos.size());
System.out.println("Perduts : " + (TOTAL - vistos.size())
+ (perduts.length() == 0 ? "" : " -> " + perduts));
System.out.println("Duplicats : " + duplicats);
System.out.println("Desordenats : " + desordenats);
}
}
/** Emissor: envia TOTAL paquets numerats tan de pressa com pot. */
static void emissor() throws IOException, InterruptedException {
try (DatagramSocket socket = new DatagramSocket()) {
InetAddress desti = InetAddress.getByName("localhost");
for (int i = 0; i < TOTAL; i++) {
byte[] dades = ("PAQUET#" + i).getBytes(StandardCharsets.UTF_8);
socket.send(new DatagramPacket(dades, dades.length, desti, PORT));
// send() ha "tingut exit". Aixo NO significa que hagi arribat.
}
System.out.println("Enviats " + TOTAL + " paquets");
}
}
public static void main(String[] args) throws Exception {
if (args.length > 0 && args[0].equals("emissor")) {
emissor();
} else {
receptor();
}
}
}A localhost veuràs normalment zero pèrdues. Per veure el comportament real, hi ha dos camins:
Camí 1: saturar la memòria intermèdia de recepció. Puja TOTAL a 100.000 i treu qualsevol pausa. L'emissor generarà paquets més de pressa del que el receptor els consumeix, la memòria intermèdia del sistema s'omplirà i el sistema descartarà els que no hi caben, sense avisar ningú:
=== ANALISI DE 100000 PAQUETS ===
Rebuts unics : 73412
Perduts : 26588
Duplicats : 0
Desordenats : 0Un 26 % de pèrdua, a localhost, sense xarxa pel mig. Això és l'absència de control de flux: TCP hauria frenat l'emissor automàticament; UDP no frena ningú i els datagrames sobrants es llencen.
Camí 2: simular una xarxa dolenta. A Linux, amb tc (requereix administrador):
# Afegir 10% de perdua, 5% de duplicacio i retard variable al bucle invertit
sudo tc qdisc add dev lo root netem loss 10% duplicate 5% delay 20ms 10ms
# ... executar la prova ...
# Treure-ho SEMPRE en acabar
sudo tc qdisc del dev lo root DUPLICAT: #3
DESORDENAT: #7 rere #8
DUPLICAT: #11
=== ANALISI DE 20 PAQUETS ===
Rebuts unics : 18
Perduts : 2 -> #5 #14
Duplicats : 2
Desordenats : 1Aquí hi són els tres fenòmens. Qualsevol receptor UDP seriós els ha de tolerar, i el patró és sempre el mateix: numerar els missatges, descartar els repetits amb un conjunt o una finestra, i decidir conscientment què fer amb els desordenats (per a telemetria, ignorar els antics; per a un protocol de petició-resposta, descartar els que no corresponguin a la petició actual).
- Fiabilitat a mà: seqüència, temps límit i reintent
Quan necessites alguna cosa de fiabilitat però no tota la de TCP, es construeix a mà. L'esquelet és sempre aquest:
sequenceDiagram
participant C as Client
participant S as Servidor
C->>S: PETICIO id=42
Note over C: setSoTimeout(500) i espera
Note over S: (paquet perdut)
Note over C: temps esgotat -> reintent 1
C->>S: PETICIO id=42 (mateix id)
S-->>C: RESPOSTA id=42
Note over C: l'id coincideix: acceptada
Les quatre peces:
- Identificador únic per petició. Permet aparellar respostes i descartar les que no corresponen — perquè pot arribar la resposta d'un intent anterior que s'havia donat per perdut.
setSoTimeout+ reintent. Amb espera creixent, per no empitjorar una xarxa congestionada: 200 ms, 400, 800...- Límit de reintents. Reintentar per sempre és un bucle infinit disfressat.
- Idempotència. Si el servidor pot rebre la mateixa petició dues vegades, l'operació ha de poder repetir-se sense dany.
CONSULTAsí que és idempotent;PRESTARno, i aquest és un argument sòlid per deixar els préstecs a TCP.
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Logger;
/**
* Peticio-resposta fiable sobre UDP: identificador, temps limit,
* reintent amb espera creixent i descart de respostes desaparellades.
*
* ATENCIO: si necessites aixo per a tot el teu transit, fes servir TCP. Aixo te
* sentit per a intercanvis curts i solts (com fa el DNS), no com a
* substitut general de TCP.
*/
public class PeticioUdpFiable {
private static final Logger LOG = Logger.getLogger(PeticioUdpFiable.class.getName());
private static final int MAXIM_DATAGRAMA = 1400;
private final InetAddress desti;
private final int port;
private final int maximIntents;
private final int esperaInicialMs;
public PeticioUdpFiable(InetAddress desti, int port,
int maximIntents, int esperaInicialMs) {
this.desti = desti;
this.port = port;
this.maximIntents = maximIntents;
this.esperaInicialMs = esperaInicialMs;
}
/**
* Envia una peticio i espera resposta, reintentant.
* Retorna null si no hi ha hagut resposta despres de tots els intents.
*
* Format: "<id> <peticio>" -> "<id> <resposta>"
*/
public String demanar(String peticio) throws IOException {
// Identificador aleatori: aparellar respostes I dificultar que
// un tercer endevini l'id i injecti una resposta falsa.
int id = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
byte[] dades = (id + " " + peticio).getBytes(StandardCharsets.UTF_8);
try (DatagramSocket socket = new DatagramSocket()) {
// connect() en UDP no connecta: filtra. Nomes acceptarem
// paquets d'aquest desti, cosa que descarta injeccions.
socket.connect(desti, port);
int espera = esperaInicialMs;
byte[] buffer = new byte[MAXIM_DATAGRAMA];
DatagramPacket resposta = new DatagramPacket(buffer, buffer.length);
for (int intent = 1; intent <= maximIntents; intent++) {
socket.send(new DatagramPacket(dades, dades.length, desti, port));
socket.setSoTimeout(espera);
// Bucle intern: poden arribar respostes d'intents
// anteriors que donavem per perduts. Cal descartar-les
// i continuar esperant la nostra dins del mateix termini.
long limit = System.currentTimeMillis() + espera;
while (System.currentTimeMillis() < limit) {
try {
resposta.setLength(buffer.length); // error classic 1
socket.receive(resposta);
String text = new String(resposta.getData(),
resposta.getOffset(), resposta.getLength(),
StandardCharsets.UTF_8); // error classic 2
int sep = text.indexOf(' ');
if (sep < 0) {
LOG.fine("Resposta amb format invalid; descartada");
continue;
}
int idRebut = Integer.parseInt(text.substring(0, sep));
if (idRebut != id) {
// Resposta d'una altra peticio: es descarta.
LOG.fine("Resposta amb id " + idRebut
+ ", esperavem " + id + "; descartada");
continue;
}
return text.substring(sep + 1); // la nostra!
} catch (SocketTimeoutException e) {
break; // s'ha esgotat el termini d'aquest intent
} catch (NumberFormatException e) {
LOG.fine("Identificador no numeric; descartada");
}
}
LOG.info("Intent " + intent + "/" + maximIntents
+ " sense resposta (espera " + espera + " ms)");
// ESPERA CREIXENT: duplicar a cada intent. Reintentar
// al mateix ritme sobre una xarxa congestionada l'empitjora.
espera *= 2;
}
return null; // sense resposta despres de tots els intents
}
}
}Tres decisions que mereixen explicació:
El bucle intern de recepció. Quan el primer intent esgota el seu termini i llances el segon, la resposta al primer pot arribar tard. Si et limitessis a acceptar el primer paquet que arribi, te la menjaries com si fos la del segon intent. En fer servir identificador i descartar els que no coincideixen, això es resol; però cal continuar esperant dins del mateix termini després de descartar-ne un, i per això el bucle intern amb límit temporal.
L'espera creixent. És la diferència entre un client educat i un que participa a tombar una xarxa. Si mil clients reintenten cada 200 ms contra un servidor saturat, garanteixen que continuï saturat. Duplicar el termini reparteix la càrrega.
L'identificador aleatori en lloc d'un comptador. Un comptador és predictible: un tercer que sàpiga que vas pel 43 et pot enviar una resposta falsa amb id 44 abans que arribi la real. És exactament l'atac d'enverinament de memòria cau DNS. Un valor aleatori de 31 bits ho fa impracticable.
- Difusió (broadcast)
La difusió permet enviar un datagrama que reben totes les màquines de la xarxa local. És la capacitat que TCP no té i la que necessita BiblioTech.
Adreces de difusió
| Adreça | Abast |
|---|---|
255.255.255.255 |
Difusió limitada: tota la xarxa local. No travessa mai un router |
192.168.1.255 |
Difusió dirigida a la subxarxa 192.168.1.0/24 |
10.0.255.255 |
Difusió dirigida a 10.0.0.0/16 |
L'adreça dirigida es calcula posant a 1 tots els bits de host. A la xarxa de Nexus Software, 192.168.1.0/24, la difusió és 192.168.1.255.
Enviar
try (DatagramSocket socket = new DatagramSocket()) {
// OBLIGATORI. Sense aixo, enviar a una adreca de difusio
// llanca SocketException: Permission denied.
socket.setBroadcast(true);
byte[] dades = "ON ES BIBLIOTECH?".getBytes(StandardCharsets.UTF_8);
socket.send(new DatagramPacket(dades, dades.length,
InetAddress.getByName("255.255.255.255"), 9091));
}Rebre
No hi ha res d'especial: n'hi ha prou d'estar lligat al port de destí.
try (DatagramSocket socket = new DatagramSocket(9091)) {
byte[] buffer = new byte[1400];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
socket.receive(paquet); // rep tambe les difusions al 9091
}Límits de la difusió
| Límit | Conseqüència |
|---|---|
| No travessa routers | Només funciona dins de la mateixa xarxa local. És un límit de disseny, no una fallada |
| Molesta tothom | Cada màquina de la xarxa processa el paquet encara que no li interessi |
| Moltes Wi-Fi la filtren | Els punts d'accés sovint la limiten o la bloquegen |
| No existeix a IPv6 | IPv6 la va eliminar: el seu substitut és la multidifusió |
Aquest últim punt és important: la difusió és tecnologia heretada. Funciona perfectament en una xarxa IPv4 d'oficina, i per això BiblioTech la farà servir; però per a alguna cosa nova i amb vocació de durar, la multidifusió és la resposta correcta.
- Multidifusió (multicast) amb
MulticastSocket
MulticastSocketLa multidifusió és la difusió feta bé: en lloc de molestar tota la xarxa, es defineix un grup al qual les màquines interessades se subscriuen voluntàriament.
Adreces de grup
| Rang IPv4 | Ús |
|---|---|
224.0.0.0 – 224.0.0.255 |
Reservat a protocols de xarxa. No travessa routers |
224.0.1.0 – 238.255.255.255 |
Grups globals, assignats per la IANA |
239.0.0.0 – 239.255.255.255 |
Àmbit administratiu: per a ús privat. El que has de fer servir |
BiblioTech farà servir 239.10.10.10, que és al rang privat.
El TTL: fins on arriba
| TTL | Abast |
|---|---|
| 0 | Només la mateixa màquina |
| 1 | Només la xarxa local. El valor per defecte i el prudent |
| 32 | L'emplaçament |
| 255 | Sense restricció (els routers solen bloquejar-lo) |
L'API moderna
MulticastSocket tenia mètodes joinGroup(InetAddress) i leaveGroup(InetAddress) que estan obsolets des de Java 14, perquè no permeten indicar per quina interfície de xarxa cal unir-se — i en una màquina amb Wi-Fi, Ethernet i adaptadors virtuals de Docker, la interfície que tria el sistema gairebé mai no és la que vols.
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.logging.Logger;
/**
* Emissor i receptor de multidifusio per als avisos interns de BiblioTech
* ("el cataleg s'ha actualitzat", "manteniment d'aqui a 10 minuts").
*/
public class AvisosMulticast implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(AvisosMulticast.class.getName());
/** Rang 239.x.x.x: ambit administratiu, reservat a us privat. */
private static final String GRUP = "239.10.10.10";
private static final int PORT = 9097;
private static final int MAXIM_DATAGRAMA = 1400;
private MulticastSocket socket;
private InetSocketAddress grup;
private NetworkInterface interficie;
private volatile boolean executant = false;
/** S'uneix al grup i queda a punt per rebre. */
public void unirse() throws IOException {
socket = new MulticastSocket(PORT);
socket.setTimeToLive(1); // no surt de la xarxa local
grup = new InetSocketAddress(InetAddress.getByName(GRUP), PORT);
interficie = triarInterficie();
// API MODERNA (Java 14+): joinGroup(SocketAddress, NetworkInterface).
// L'antiga joinGroup(InetAddress) esta obsoleta perque no permet
// indicar la interficie, i en una maquina amb Wi-Fi + Ethernet + Docker
// el sistema gairebe mai no tria la que vols.
socket.joinGroup(grup, interficie);
executant = true;
LOG.info(() -> "Unit al grup " + GRUP + ":" + PORT
+ " per la interficie " + interficie.getName());
}
/**
* Tria una interficie activa, no de bucle invertit i amb multidifusio.
* En produccio aixo hauria de ser configurable a bibliotech.properties.
*/
private NetworkInterface triarInterficie() throws IOException {
Enumeration<NetworkInterface> interficies = NetworkInterface.getNetworkInterfaces();
while (interficies.hasMoreElements()) {
NetworkInterface ni = interficies.nextElement();
if (ni.isUp() && !ni.isLoopback() && ni.supportsMulticast()) {
return ni;
}
}
// Reserva: en una maquina sense xarxa, el bucle invertit permet
// almenys que emissor i receptor de la mateixa maquina es parlin.
return NetworkInterface.getByName("lo");
}
/** Envia un avis a tots els subscrits al grup. */
public void avisar(String missatge) throws IOException {
byte[] dades = missatge.getBytes(StandardCharsets.UTF_8);
if (dades.length > MAXIM_DATAGRAMA) {
throw new IOException("Avis massa llarg: " + dades.length + " bytes");
}
socket.send(new DatagramPacket(dades, dades.length,
InetAddress.getByName(GRUP), PORT));
LOG.info(() -> "Avis enviat al grup: " + missatge);
}
/** Bucle d'escolta. Cridar des d'un fil propi. */
public void escoltar(java.util.function.Consumer<String> alRebre) {
byte[] buffer = new byte[MAXIM_DATAGRAMA];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
while (executant) {
try {
paquet.setLength(buffer.length); // error classic 1
socket.receive(paquet);
String missatge = new String(paquet.getData(), paquet.getOffset(),
paquet.getLength(), StandardCharsets.UTF_8); // error classic 2
// COMPTE: tambe rebem els NOSTRES PROPIS avisos. Si molesta,
// es desactiva amb socket.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, false)
// o es filtra per remitent comparant amb les adreces locals.
alRebre.accept(missatge);
} catch (IOException e) {
if (!executant) {
return; // tancament ordenat
}
LOG.warning("Error rebent l'avis: " + e.getMessage());
}
}
}
@Override
public void close() {
executant = false;
if (socket != null) {
try {
socket.leaveGroup(grup, interficie); // API moderna tambe aqui
} catch (IOException e) {
LOG.fine("Fallada abandonant el grup: " + e.getMessage());
}
socket.close(); // desbloqueja el receive()
}
}
}Difusió davant de multidifusió
| Difusió | Multidifusió | |
|---|---|---|
| Qui rep | Totes les màquines de la xarxa | Només les subscrites al grup |
| Travessa routers | No | Sí, si estan configurats |
| IPv6 | No existeix | Sí |
| Configuració | Cap | Triar grup i TTL |
| Cost per a la xarxa | Alt: molesta tothom | Baix |
| Quan fer-la servir | Descobriment simple en LAN IPv4 | Tota la resta |
- BiblioTech: el servei de descobriment
Ara resolem el problema amb què obria la lliçó: que cada lloc de treball tingui la IP del servidor escrita a mà.
El protocol de descobriment
BTDP/1 - BiblioTech Discovery Protocol
======================================
Transport : UDP, difusio al port 9091
Codificacio : UTF-8
Mida maxima : 512 bytes (molt per sota de qualsevol MTU)
Peticio (lloc -> difusio):
BTDP/1 ON <idPeticio>
Resposta (servidor -> remitent, unicast):
BTDP/1 AQUI <idPeticio> <ip> <portTcp> <nomServidor>
Exemple:
-> BTDP/1 ON 748291
<- BTDP/1 AQUI 748291 192.168.1.50 9090 bibliotech-centralFixa't en dues decisions. La petició va per difusió perquè no sabem a qui preguntar; la resposta va per unicast al remitent, perquè sí que sabem a qui contestar i no cal molestar tota la xarxa. I l'identificador permet descartar respostes que no són a la nostra pregunta.
sequenceDiagram
participant P as Lloc de la Marta
participant R as Xarxa local (difusio)
participant S as Servidor BiblioTech
participant O as Altres maquines
P->>R: BTDP/1 ON 748291 (255.255.255.255:9091)
R->>S: (arriba)
R->>O: (arriba, l'ignoren)
S-->>P: BTDP/1 AQUI 748291 192.168.1.50 9090 (unicast)
Note over P: Ja se on connectar per TCP
P->>S: connexio TCP al 9090 (BTCP/1 de 09-03)
El responedor, al servidor
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Respon a les peticions de descobriment de BiblioTech.
* S'executa al costat del ServidorCataleg de 09-03, al seu propi fil.
*
* Un sol socket, un sol fil, tots els llocs de l'oficina:
* aixi es un servidor UDP, sense pool i sense estat per client.
*/
public class ResponedorDescobriment implements AutoCloseable, Runnable {
private static final Logger LOG =
Logger.getLogger(ResponedorDescobriment.class.getName());
private static final int PORT_DESCOBRIMENT = 9091;
/** 512 bytes: el limit prudent universal, el mateix que fa servir el DNS. */
private static final int MAXIM_DATAGRAMA = 512;
private static final String VERSIO = "BTDP/1";
private final int portTcp;
private final String nomServidor;
private DatagramSocket socket;
private volatile boolean executant = false;
private final AtomicLong respostes = new AtomicLong();
private final AtomicLong descartades = new AtomicLong();
public ResponedorDescobriment(int portTcp, String nomServidor) {
this.portTcp = portTcp;
this.nomServidor = nomServidor;
}
public void arrencar() throws SocketException {
socket = new DatagramSocket(PORT_DESCOBRIMENT);
executant = true;
LOG.info(() -> "Responedor de descobriment a UDP:" + PORT_DESCOBRIMENT);
}
@Override
public void run() {
// Una memoria intermedia un byte mes gran que el maxim, per detectar truncaments.
byte[] buffer = new byte[MAXIM_DATAGRAMA + 1];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
while (executant) {
try {
paquet.setLength(buffer.length); // ERROR CLASSIC 1
socket.receive(paquet);
if (paquet.getLength() > MAXIM_DATAGRAMA) {
// Un datagrama mes gran del previst: sospitos. Es descarta.
descartades.incrementAndGet();
LOG.warning("Datagrama de " + paquet.getLength()
+ " bytes descartat per mida");
continue;
}
String peticio = new String(paquet.getData(), paquet.getOffset(),
paquet.getLength(), StandardCharsets.UTF_8); // ERROR CLASSIC 2
processar(peticio.strip(), paquet.getAddress(), paquet.getPort());
} catch (SocketException e) {
if (!executant) {
LOG.info("Responedor de descobriment aturat");
return;
}
LOG.log(Level.SEVERE, "Fallada del socket de descobriment", e);
return;
} catch (IOException e) {
// Un paquet dolent no pot tombar el servei.
LOG.log(Level.WARNING, "Error processant un descobriment", e);
}
}
}
private void processar(String peticio, InetAddress remitent, int portRemitent) {
// TOT el que arriba per la xarxa no es fiable, i aqui arriba de
// QUALSEVOL de la xarxa local, sense connexio que l'identifiqui.
String[] parts = peticio.split(" ");
if (parts.length != 3 || !parts[0].equals(VERSIO)
|| !parts[1].equals("ON")) {
descartades.incrementAndGet();
// No responem al que no entenem: respondre a paquets
// arbitraris converteix aquest servei en un AMPLIFICADOR per a
// atacs de denegacio de servei reflectida.
LOG.fine(() -> "Descobriment no reconegut de " + remitent);
return;
}
String id = parts[2];
if (!identificadorValid(id)) {
descartades.incrementAndGet();
return;
}
try {
String laMevaIp = InetAddress.getLocalHost().getHostAddress();
String resposta = VERSIO + " AQUI " + id + " " + laMevaIp + " "
+ portTcp + " " + nomServidor;
byte[] dades = resposta.getBytes(StandardCharsets.UTF_8);
// UNICAST al remitent: no cal molestar tota la xarxa
// amb la resposta. La pregunta va en difusio; la resposta, no.
socket.send(new DatagramPacket(dades, dades.length,
remitent, portRemitent));
respostes.incrementAndGet();
LOG.info(() -> "Descobriment respost a " + remitent.getHostAddress()
+ " (id " + id + ")");
} catch (IOException e) {
// Si l'enviament falla, no passa res greu: el client reintentara.
LOG.log(Level.WARNING, "No s'ha pogut respondre al descobriment", e);
}
}
/** Llista blanca: nomes digits, maxim 10. Res de dades arbitraries. */
private boolean identificadorValid(String id) {
if (id.isEmpty() || id.length() > 10) {
return false;
}
for (int i = 0; i < id.length(); i++) {
if (!Character.isDigit(id.charAt(i))) {
return false;
}
}
return true;
}
@Override
public void close() {
executant = false;
if (socket != null) {
socket.close(); // desbloqueja el receive()
}
LOG.info(() -> "Descobriment: " + respostes.get() + " respostes, "
+ descartades.get() + " descartades");
}
}El cercador, al lloc de treball
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Logger;
/**
* Cerca el servidor de BiblioTech a la xarxa local per difusio.
* Evita haver de configurar la IP del servidor a cada lloc.
*/
public class CercadorServidor {
private static final Logger LOG = Logger.getLogger(CercadorServidor.class.getName());
private static final int PORT_DESCOBRIMENT = 9091;
private static final int MAXIM_DATAGRAMA = 512;
private static final String VERSIO = "BTDP/1";
private static final int MAXIM_INTENTS = 3;
/** El que es descobreix: on es el servidor. Un record de 04-07. */
public record ServidorTrobat(String ip, int portTcp, String nom) {
}
/**
* Cerca el servidor. Retorna null si ningu no respon despres dels reintents.
* @param esperaInicialMs termini del primer intent; es duplica a cadascun.
*/
public ServidorTrobat cercar(int esperaInicialMs) throws IOException {
int id = ThreadLocalRandom.current().nextInt(1, 1_000_000);
String peticio = VERSIO + " ON " + id;
byte[] dades = peticio.getBytes(StandardCharsets.UTF_8);
try (DatagramSocket socket = new DatagramSocket()) {
// Sense aixo, enviar a 255.255.255.255 llanca
// SocketException: Permission denied.
socket.setBroadcast(true);
// COMPTE: aqui NO es pot fer servir connect(), perque la resposta
// vindra de la IP del servidor i no de la de difusio a la
// qual enviem. connect() la filtraria i la descartariem.
byte[] buffer = new byte[MAXIM_DATAGRAMA + 1];
DatagramPacket resposta = new DatagramPacket(buffer, buffer.length);
InetAddress difusio = InetAddress.getByName("255.255.255.255");
int espera = esperaInicialMs;
for (int intent = 1; intent <= MAXIM_INTENTS; intent++) {
socket.send(new DatagramPacket(dades, dades.length,
difusio, PORT_DESCOBRIMENT));
LOG.info("Cercant BiblioTech a la xarxa local (intent "
+ intent + "/" + MAXIM_INTENTS + ")...");
socket.setSoTimeout(espera);
long limit = System.currentTimeMillis() + espera;
while (System.currentTimeMillis() < limit) {
try {
resposta.setLength(buffer.length); // error classic 1
socket.receive(resposta);
String text = new String(resposta.getData(),
resposta.getOffset(), resposta.getLength(),
StandardCharsets.UTF_8); // error classic 2
ServidorTrobat trobat = analitzar(text, id);
if (trobat != null) {
LOG.info(() -> "BiblioTech trobat: " + trobat);
return trobat;
}
// Resposta d'un altre id o mal formada: continuem esperant.
} catch (SocketTimeoutException e) {
break; // termini esgotat, seguent intent
}
}
espera *= 2; // espera creixent
}
LOG.warning("Cap servidor de BiblioTech no ha respost a la xarxa local");
return null;
}
}
/** Analitza la resposta VALIDANT-HO tot: ve d'un desconegut de la xarxa. */
private ServidorTrobat analitzar(String text, int idEsperat) {
String[] p = text.strip().split(" ");
if (p.length != 6 || !p[0].equals(VERSIO) || !p[1].equals("AQUI")) {
return null;
}
try {
if (Integer.parseInt(p[2]) != idEsperat) {
return null; // resposta a una altra pregunta: es descarta
}
int port = Integer.parseInt(p[4]);
if (port < 1 || port > 65535) {
LOG.warning("Port invalid a la resposta: " + port);
return null;
}
// La IP es valida intentant interpretar-la: si no es valida,
// getByName llancara i no ens connectarem a res estrany.
InetAddress.getByName(p[3]);
return new ServidorTrobat(p[3], port, p[5]);
} catch (NumberFormatException | IOException e) {
LOG.fine("Resposta de descobriment invalida: " + text);
return null;
}
}
public static void main(String[] args) throws IOException {
ServidorTrobat servidor = new CercadorServidor().cercar(500);
if (servidor == null) {
System.out.println("No s'ha trobat cap servidor.");
System.out.println("Configura la IP a ma a bibliotech.properties.");
} else {
System.out.println("Servidor: " + servidor.nom());
System.out.println("Connectar a " + servidor.ip() + ":" + servidor.portTcp());
// I a partir d'aqui, el ClientCataleg de 09-02 sobre TCP.
}
}
}Com provar-ho
# Terminal 1: el servidor (o el responedor sol)
java -cp classes com.nexussoftware.bibliotech.xarxa.ResponedorDescobriment
# Terminal 2: el lloc de treball
java -cp classes com.nexussoftware.bibliotech.xarxa.CercadorServidorTerminal 2:
INFO: Cercant BiblioTech a la xarxa local (intent 1/3)...
INFO: BiblioTech trobat: ServidorTrobat[ip=192.168.1.50, portTcp=9090, nom=bibliotech-central]
Servidor: bibliotech-central
Connectar a 192.168.1.50:9090
Terminal 1:
INFO: Descobriment respost a 192.168.1.23 (id 748291)Amb això, la configuració manual desapareix. El lloc de la Marta arrenca, pregunta a la xarxa i troba el servidor. Si demà la màquina canvia d'IP, ningú no ha de tocar res. Aquesta és la mena de cosa que TCP no pot fer.
Nota de seguretat. El descobriment per difusió és còmode i confia en qualsevol de la xarxa local. Un equip maliciós pot respondre abans que el servidor real i fer que la Marta s'hi connecti. Fixa't a més que el responedor no contesta al que no entén: contestar a paquets arbitraris el convertiria en un amplificador per a atacs de denegació de servei reflectida, en què l'atacant falsifica la IP d'origen perquè les respostes arribin a la seva víctima. En un entorn real, el descobriment serveix per trobar candidats i després es verifica el servidor amb TLS i un certificat (12-07). Sense verificació, el descobriment automàtic és una comoditat de xarxa interna, no un mecanisme de seguretat.
- BiblioTech: l'emissor de telemetria
El segon cas on UDP és l'elecció correcta: enviar cada pocs segons l'estat del servidor a un recol·lector de mètriques.
Els requisits ho deixen clar:
- No ha de bloquejar mai. El servidor està atenent la Marta i en Diego; enviar mètriques no s'hi pot interposar.
- No importa perdre alguna mesura. Se n'envia una cada 5 segons: si se'n perd una, la següent arriba de seguida.
- El recol·lector pot estar caigut. I el servidor ha de continuar funcionant igual, sense errors ni reintents.
Amb TCP, un recol·lector caigut significaria intents de connexió que triguen, reintents, i un fil entretingut en una cosa que no importa. Amb UDP, s'envia el sobre a la bústia i cap a una altra cosa.
package com.nexussoftware.bibliotech.xarxa;
import com.nexussoftware.bibliotech.servei.EstadistiquesBiblioTech;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Emissor de telemetria de BiblioTech.
*
* Envia per UDP l'estat del servei cada N segons. Fa servir el
* ScheduledExecutorService de 08-05 i NO bloqueja MAI el servidor:
* si el recolector esta caigut, els datagrames es perden i no passa res.
*
* Format (text d'una linia, estil StatsD):
* bibliotech.prestecs.actius:12|g
* bibliotech.consultes.total:8471|c
*/
public class EmissorTelemetria implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(EmissorTelemetria.class.getName());
private static final int MAXIM_DATAGRAMA = 512;
private final String hostRecolector;
private final int portRecolector;
private final int intervalSegons;
private final EstadistiquesBiblioTech estadistiques;
private final ServidorCataleg servidor;
private DatagramSocket socket;
private InetAddress desti;
private ScheduledExecutorService planificador;
private final AtomicLong enviats = new AtomicLong();
private final AtomicLong fallits = new AtomicLong();
public EmissorTelemetria(String hostRecolector, int portRecolector,
int intervalSegons,
EstadistiquesBiblioTech estadistiques,
ServidorCataleg servidor) {
this.hostRecolector = hostRecolector;
this.portRecolector = portRecolector;
this.intervalSegons = intervalSegons;
this.estadistiques = estadistiques;
this.servidor = servidor;
}
public void arrencar() throws IOException {
socket = new DatagramSocket(); // efimer: nomes enviem
// El nom es resol UNA VEGADA en arrencar, no a cada enviament:
// una resolucio DNS cada 5 segons es un malbaratament i a mes
// podria bloquejar el fil del planificador.
desti = InetAddress.getByName(hostRecolector);
planificador = Executors.newSingleThreadScheduledExecutor(r -> {
Thread f = new Thread(r, "bibliotech-telemetria");
f.setDaemon(true); // la telemetria no ha d'impedir sortir
return f;
});
// scheduleAtFixedRate de 08-05: cada intervalSegons, sense acumular.
planificador.scheduleAtFixedRate(this::emetre,
intervalSegons, intervalSegons, TimeUnit.SECONDS);
LOG.info(() -> "Telemetria cap a " + hostRecolector + ":" + portRecolector
+ " cada " + intervalSegons + " s");
}
/**
* Envia una tanda de mesures. S'executa al fil del planificador.
*
* CRITIC: aquest metode NO pot llancar res. Una excepcio que escapi
* d'una tasca de scheduleAtFixedRate CANCELLA la tasca en silenci i
* la telemetria deixa d'emetre sense que ningu se n'assabenti (08-05).
*/
private void emetre() {
try {
enviar("bibliotech.prestecs.actius:"
+ estadistiques.prestecsActius() + "|g");
enviar("bibliotech.consultes.total:"
+ estadistiques.consultesTotals() + "|c");
enviar("bibliotech.connexions.actives:"
+ servidor.connexionsActives() + "|g");
enviar("bibliotech.peticions.total:"
+ servidor.peticionsAteses() + "|c");
Runtime rt = Runtime.getRuntime();
long memoriaMb = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
enviar("bibliotech.memoria.mb:" + memoriaMb + "|g");
} catch (RuntimeException e) {
// Es registra i es continua: la tanda seguent ho tornara a intentar.
LOG.log(Level.WARNING, "Fallada emetent telemetria", e);
}
}
private void enviar(String mesura) {
byte[] dades = mesura.getBytes(StandardCharsets.UTF_8);
if (dades.length > MAXIM_DATAGRAMA) {
LOG.warning("Mesura massa llarga; descartada: " + mesura);
return;
}
try {
socket.send(new DatagramPacket(dades, dades.length,
desti, portRecolector));
enviats.incrementAndGet();
// Recorda: aquest "exit" NO significa que hagi arribat.
// Amb UDP, send() te exit encara que el recolector estigui apagat.
} catch (IOException e) {
// Gairebe mai no passa. I si passa, no importa: es telemetria.
fallits.incrementAndGet();
LOG.fine("Datagrama de telemetria no enviat: " + e.getMessage());
}
}
@Override
public void close() {
if (planificador != null) {
planificador.shutdown(); // aturada en dues fases (08-05)
try {
if (!planificador.awaitTermination(2, TimeUnit.SECONDS)) {
planificador.shutdownNow();
}
} catch (InterruptedException e) {
planificador.shutdownNow();
Thread.currentThread().interrupt();
}
}
if (socket != null) {
socket.close();
}
LOG.info(() -> "Telemetria aturada: " + enviats.get() + " datagrames enviats, "
+ fallits.get() + " fallits");
}
}Un recol·lector mínim per provar-ho
package com.nexussoftware.bibliotech.xarxa;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
/** Recolector de telemetria minim: mostra un panell a la consola. */
public class RecolectorTelemetria {
public static void main(String[] args) throws Exception {
int port = args.length > 0 ? Integer.parseInt(args[0]) : 9098;
try (DatagramSocket socket = new DatagramSocket(port)) {
System.out.println("Recolector escoltant a UDP:" + port);
byte[] buffer = new byte[512];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
Map<String, String> panell = new TreeMap<>(); // ordenat per clau
long rebuts = 0;
while (true) {
paquet.setLength(buffer.length);
socket.receive(paquet);
rebuts++;
String mesura = new String(paquet.getData(), paquet.getOffset(),
paquet.getLength(), StandardCharsets.UTF_8);
int dosPunts = mesura.indexOf(':');
int barra = mesura.indexOf('|');
if (dosPunts < 0 || barra < 0 || barra < dosPunts) {
continue; // mesura mal formada: s'ignora
}
panell.put(mesura.substring(0, dosPunts),
mesura.substring(dosPunts + 1, barra));
// Repintem el panell sencer amb cada mesura.
System.out.print("\033[H\033[2J"); // netejar la pantalla
System.out.println("=== TELEMETRIA BIBLIOTECH ===");
System.out.println("Datagrames rebuts: " + rebuts);
System.out.println();
for (Map.Entry<String, String> e : panell.entrySet()) {
System.out.printf(" %-38s %s%n", e.getKey(), e.getValue());
}
}
}
}
}=== TELEMETRIA BIBLIOTECH ===
Datagrames rebuts: 145
bibliotech.connexions.actives 3
bibliotech.consultes.total 8471
bibliotech.memoria.mb 34
bibliotech.peticions.total 12093
bibliotech.prestecs.actius 12Prova clau: atura el recol·lector amb Ctrl+C i mira el servidor. No passa absolutament res. Ni un error, ni un retard, ni una traça. El servidor continua atenent els llocs exactament igual. Torna a arrencar el recol·lector i les mesures reapareixen en cinc segons.
Això és impossible d'aconseguir amb TCP sense escriure bastant codi de reconnexió, i és precisament la raó per la qual la telemetria de gairebé tota la indústria —StatsD, la part de mètriques de Datadog, els metrics de Graphite— viatja per UDP.
- El mateix cas resolt amb TCP i amb UDP
Tanquem amb la comparació directa. El cas: consultar la disponibilitat d'un llibre.
Amb TCP (el de 09-02 i 09-03)
try (ClientCataleg client = new ClientCataleg("192.168.1.50", 9090)) {
client.connectar(); // salutacio de 3 vies + salutacio BTCP
FitxaXarxa fitxa = client.consultar("978-0000000001"); // peticio + resposta
System.out.println(fitxa.disponible());
} // SORTIR + tancament de 4 missatgesAmb UDP
PeticioUdpFiable peticio = new PeticioUdpFiable(
InetAddress.getByName("192.168.1.50"), 9099, 3, 300);
String resposta = peticio.demanar("CONSULTA 978-0000000001");
if (resposta == null) {
System.out.println("Sense resposta despres de 3 intents");
} else {
System.out.println(resposta);
}La comparació
| Criteri | TCP | UDP |
|---|---|---|
| Viatges de xarxa | 3 (establir) + 1 (consulta) + 2 (tancar) = 6 | 1 |
| Latència total en LAN (0,5 ms/viatge) | ~3 ms | ~0,5 ms |
| Latència total a 100 ms de distància | ~600 ms | ~100 ms |
| Bytes de capçalera | 20+ per segment, molts segments | 8, un sol datagrama |
| Estat al servidor | Un socket i un fil del pool | Cap |
| Clients simultanis que aguanta | Els del pool (16) | Milers amb un fil |
| Si es perd un paquet | TCP retransmet sol | Tu reintentes |
| Si la resposta no cap en 1400 bytes | Sense problema | Cal trossejar a mà |
| Fiabilitat | Del protocol | Teva |
| Codi necessari | Més al client, molt més al servidor | Menys en total |
El veredicte per a BiblioTech
| Operació | Transport | Motiu |
|---|---|---|
CONSULTA d'un ISBN |
TCP | Cap en UDP, però el catàleg ja és a TCP i la consulta va dins d'una sessió amb diverses operacions |
LLISTA completa |
TCP | La resposta supera fàcilment els 1400 bytes: trossejar-la a mà seria reinventar TCP |
PRESTAR |
TCP | No és idempotent. Un reintent podria registrar dos préstecs |
| Descobriment | UDP | Necessita difusió. TCP no pot |
| Telemetria | UDP | No ha de bloquejar ni importa la pèrdua |
| Avisos interns | UDP multicast | Un a molts, sense connexions |
La conclusió general, que val més enllà de BiblioTech: UDP no substitueix TCP, complementa TCP. Un sistema real fa servir tots dos, cadascun on encaixa. I el criteri de decisió no és la velocitat, sinó tres preguntes: la dada vella encara val?, necessito parlar amb diversos alhora? i l'operació és idempotent?
Errors Comuns i Consells
No restaurar setLength() abans de cada receive(). L'error número u. El primer missatge arriba bé i tots els següents queden truncats a aquella mateixa longitud, sense cap avís.
Fer servir paquet.getData() sense getOffset() i getLength(). L'error número dos. Arrossegues el contingut anterior de la memòria intermèdia i produeixes dades corruptes que semblen legítimes.
Creure que send() sense excepció significa que ha arribat. No significa res. send() té èxit encara que el destí estigui apagat. Si necessites saber si ha arribat, el receptor ho ha de confirmar.
No posar setSoTimeout abans d'un receive(). Amb UDP la resposta es pot perdre legítimament. Sense temps límit, el teu fil espera per sempre una resposta que no arribarà mai.
Enviar datagrames grans. Per sobre de l'MTU es fragmenten, i perdre un sol fragment perd el datagrama sencer: la fragmentació multiplica la taxa de pèrdua. A més, molts tallafocs descarten fragments. Mantén-te en 1400 bytes o, si vols estar molt segur, en 512.
No preveure el truncament. Si arriba un datagrama més gran que la teva memòria intermèdia, Java el talla en silenci. Demana un byte de més i comprova si getLength() ha arribat al màxim.
Oblidar setBroadcast(true). Enviar a una adreça de difusió sense ell llança SocketException: Permission denied, i el missatge no orienta gaire.
Fer servir connect() en un socket que espera resposta de difusió. La resposta ve de la IP real del servidor, no de la de difusió, i connect() la filtraria. Al cercador de descobriment no es pot fer servir.
Fer servir joinGroup(InetAddress). Està obsolet des de Java 14. En una màquina amb diverses interfícies —i avui gairebé totes en tenen, amb Docker, VPN o Wi-Fi més Ethernet— el sistema tria una interfície que probablement no és la teva. Fes servir joinGroup(SocketAddress, NetworkInterface).
Triar una adreça de multidifusió qualsevol. Fes servir el rang 239.x.x.x, que està reservat a ús privat. Els altres rangs estan assignats a protocols concrets.
Reimplementar TCP sobre UDP. Si acabes escrivint confirmacions, finestres, retransmissions i control de congestió, fes servir TCP: està més ben provat que el que escriuràs tu.
Reintentar sense espera creixent. Reintentar al mateix ritme contra un servidor saturat garanteix que continuï saturat. Duplica el termini a cada intent i posa-hi un límit.
Fer servir un comptador predictible com a identificador de petició. Permet a un tercer endevinar-lo i enviar una resposta falsa abans que la real. Fes servir un valor aleatori.
Respondre a paquets que no entens. Converteix el teu servei en un amplificador per a atacs de denegació de servei reflectida: l'atacant falsifica la IP d'origen i les respostes cauen sobre la seva víctima. Si no reconeixes el format, calla.
Deixar que una excepció escapi d'una tasca de scheduleAtFixedRate. La tasca es cancel·la en silenci i la telemetria deixa d'emetre sense que ningú se n'assabenti. Embolcalla el cos sencer en un try/catch.
Consell de diagnòstic. UDP és difícil de depurar precisament perquè no falla sorollosament. Tres eines: nc -u -l 9095 fa de receptor UDP i t'ensenya exactament què arriba; ss -ulnp llista els sockets UDP en escolta (la u en lloc de la t); i tcpdump -i any -n udp port 9091 -A et mostra els datagrames en vol amb el seu contingut en text. Si amb tcpdump veus el paquet sortir i no arribar, el problema és a la xarxa o en un tallafoc, no al teu codi.
Exercicis
Exercici 1: Mesurador de pèrdua i latència UDP
Escriu un parell SondaUdp (client) i ReflectorUdp (servidor) que mesurin la qualitat d'un enllaç UDP, a l'estil d'una eina de diagnòstic de xarxa.
Requisits:
- El reflector retorna cada datagrama tal qual, sense tocar-lo, i no manté cap estat.
- La sonda envia N datagrames numerats amb marca de temps en nanosegons, a un ritme configurable (paquets per segon), sense esperar la resposta de cadascun — envia i rep en moments diferents.
- Un fil a part rep les respostes i calcula la latència de cadascuna a partir de la marca de temps que torna.
- Informe: enviats, rebuts, perduts i percentatge de pèrdua; latència mínima, mitjana, mediana, p95 i màxima; i jitter (variació mitjana entre latències consecutives), que és la mètrica que decideix si un enllaç serveix per a veu o vídeo.
- Detecció de duplicats i de desordre.
- Prova amb mides de datagrama de 64, 512 i 1400 bytes, i comenta les diferències.
Exercici 2: Xat de xarxa local per multidifusió
Escriu ChatBiblioTech, un xat intern de Nexus Software sobre multidifusió, sense servidor central.
Requisits:
- Grup
239.10.10.20, port 9099, TTL 1,joinGroupamb l'API moderna i elecció explícita d'interfície. - Un fil rep i mostra els missatges; el principal llegeix de consola i envia.
- Format:
<marca> <usuari> <missatge>, amb la marca en mil·lisegons des de l'arrencada de l'aplicació (res dejava.time, que és 10-05). - Ha de filtrar els missatges propis per no veure'ls duplicats. Investiga les dues formes de fer-ho (per opció del socket i comparant el remitent amb les adreces locals) i implementa'n una, explicant l'elecció.
- Anunci d'entrada i sortida al grup (
*** Marta Ruiz s'ha unit ***). - Validació estricta de tot el que es rep: longitud, caràcters de control i sanejament abans de mostrar a consola.
- Prova amb tres instàncies a la mateixa màquina i, si pots, amb dues màquines.
Exercici 3: Transferència fiable de fitxer sobre UDP
Implementa EnviamentFiableUdp i RecepcioFiableUdp per transferir el cataleg.csv de BiblioTech sobre UDP amb fiabilitat construïda a mà, fent servir l'esquema de parada i espera.
Requisits:
- Trossejar el fitxer en blocs de 1024 bytes de dades.
- Capçalera binària per datagrama amb
DataOutputStreamsobre unByteArrayOutputStream: número de seqüència (int), indicador d'últim bloc (boolean) i longitud de dades (int). - El receptor confirma cada bloc amb un datagrama de confirmació que porta el número de seqüència rebut.
- L'emissor espera la confirmació amb
setSoTimeouti reintenta fins a 5 vegades amb espera creixent; si s'esgoten, avorta amb error. - El receptor ha de detectar blocs duplicats (una confirmació perduda fa que l'emissor reenviï un bloc que ja s'havia rebut) i reconfirmar-los sense escriure'ls dues vegades.
- Comprovació d'integritat al final amb una suma simple calculada sobre tots els bytes.
- Simula un 20 % de pèrdua artificial a l'emissor (descartant datagrames abans d'enviar-los) i comprova que la transferència es completa igualment.
- Informe final: blocs, retransmissions, duplicats detectats, bytes i temps.
- Escriu una reflexió final comparant el resultat amb fer-ho en tres línies sobre TCP.
Solucions
Solució 1
package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* Reflector UDP: retorna cada datagrama tal qual, sense interpretar-lo.
* Sense estat, sense fils, sense connexions. Un servidor UDP en la forma pura.
*/
public class ReflectorUdp {
public static void main(String[] args) throws IOException {
int port = args.length > 0 ? Integer.parseInt(args[0]) : 9100;
try (DatagramSocket socket = new DatagramSocket(port)) {
// Memoria intermedia generosa: acceptem fins a 1500 bytes sense truncar.
byte[] buffer = new byte[1500];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
long reflectits = 0;
System.out.println("Reflector UDP al port " + port);
while (true) {
paquet.setLength(buffer.length); // error classic 1
socket.receive(paquet);
// Retornem EXACTAMENT els bytes rebuts, ni un mes:
// el paquet ja porta l'adreca i el port del remitent,
// aixi que send() el retorna al seu origen sense tocar res.
socket.send(paquet);
if (++reflectits % 1000 == 0) {
System.out.println("Reflectits: " + reflectits);
}
}
}
}
}package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Sonda UDP: mesura perdua, latencia i jitter d'un enllac.
* Envia i rep en fils separats, sense esperar resposta per paquet,
* que es com es mesura de veritat un enllac.
*/
public class SondaUdp {
private final String host;
private final int port;
private final int total;
private final int midaDatagrama;
private final int perSegon;
// Estat compartit entre el fil emissor i el receptor.
private final long[] latenciesUs;
private final Set<Integer> rebuts = new HashSet<>();
private volatile int duplicats = 0;
private volatile int desordenats = 0;
private volatile int rebutsTotal = 0;
public SondaUdp(String host, int port, int total,
int midaDatagrama, int perSegon) {
this.host = host;
this.port = port;
this.total = total;
this.midaDatagrama = Math.max(midaDatagrama, 16); // capcalera minima
this.perSegon = perSegon;
this.latenciesUs = new long[total];
}
public void mesurar() throws IOException, InterruptedException {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(2000);
InetAddress desti = InetAddress.getByName(host);
CountDownLatch emissioAcabada = new CountDownLatch(1);
// --- Fil receptor ---
// Rep en parallel a l'enviament: aixi la mesura no inclou
// el temps d'"esperar el meu torn per enviar el seguent".
Thread receptor = new Thread(() -> rebre(socket, emissioAcabada),
"sonda-receptor");
receptor.start();
// --- Emissor, en aquest fil ---
long intervalNs = 1_000_000_000L / perSegon;
long seguent = System.nanoTime();
for (int i = 0; i < total; i++) {
// Capcalera binaria: numero de sequencia + marca de temps.
// ByteBuffer fa servir big-endian per defecte: l'ordre de xarxa.
ByteBuffer bb = ByteBuffer.allocate(midaDatagrama);
bb.putInt(i);
bb.putLong(System.nanoTime());
// La resta del datagrama es reblert fins a la mida demanada.
socket.send(new DatagramPacket(bb.array(), midaDatagrama,
desti, port));
// Ritme controlat: sense aixo saturariem la memoria intermedia
// del receptor i mesurariem la nostra saturacio, no la xarxa.
seguent += intervalNs;
long esperaNs = seguent - System.nanoTime();
if (esperaNs > 0) {
TimeUnit.NANOSECONDS.sleep(esperaNs);
}
}
emissioAcabada.countDown();
// Marge perque arribin les ultimes respostes en vol.
receptor.join(3000);
socket.close(); // desbloqueja el receive del receptor
informe();
}
}
private void rebre(DatagramSocket socket, CountDownLatch emissioAcabada) {
byte[] buffer = new byte[1500];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
int ultim = -1;
while (true) {
try {
paquet.setLength(buffer.length);
socket.receive(paquet);
long ara = System.nanoTime();
if (paquet.getLength() < 12) {
continue; // no hi cap ni la capcalera: s'ignora
}
ByteBuffer bb = ByteBuffer.wrap(paquet.getData(),
paquet.getOffset(), paquet.getLength());
int sequencia = bb.getInt();
long enviat = bb.getLong();
if (sequencia < 0 || sequencia >= total) {
continue; // sequencia fora de rang: paquet alie
}
synchronized (rebuts) {
if (!rebuts.add(sequencia)) {
duplicats++;
continue;
}
}
if (sequencia < ultim) {
desordenats++;
}
ultim = Math.max(ultim, sequencia);
latenciesUs[sequencia] = (ara - enviat) / 1000;
rebutsTotal++;
} catch (SocketTimeoutException e) {
// Dos segons sense res. Si l'emissio ja ha acabat, parem.
if (emissioAcabada.getCount() == 0) {
return;
}
} catch (IOException e) {
return; // socket tancat: fi
}
}
}
private void informe() {
// Nomes les latencies dels paquets que efectivament han arribat.
long[] valides = new long[rebutsTotal];
int j = 0;
for (int i = 0; i < total && j < valides.length; i++) {
if (latenciesUs[i] > 0) {
valides[j++] = latenciesUs[i];
}
}
valides = Arrays.copyOf(valides, j);
if (valides.length == 0) {
System.out.println("No s'ha rebut cap resposta.");
return;
}
// Jitter: variacio mitjana entre latencies CONSECUTIVES. Es calcula
// ABANS d'ordenar, perque depen de l'ordre temporal.
double sumaJitter = 0;
for (int i = 1; i < valides.length; i++) {
sumaJitter += Math.abs(valides[i] - valides[i - 1]);
}
double jitterUs = valides.length > 1 ? sumaJitter / (valides.length - 1) : 0;
long suma = 0;
for (long v : valides) {
suma += v;
}
long[] ordenades = valides.clone();
Arrays.sort(ordenades);
double perdua = 100.0 * (total - rebutsTotal) / total;
System.out.println();
System.out.println("=== SONDA UDP: " + host + ":" + port + " ===");
System.out.printf("%-24s %d bytes%n", "Mida de datagrama", midaDatagrama);
System.out.printf("%-24s %d/s%n", "Ritme", perSegon);
System.out.println();
System.out.printf("%-24s %d%n", "Enviats", total);
System.out.printf("%-24s %d%n", "Rebuts", rebutsTotal);
System.out.printf("%-24s %d (%.2f%%)%n", "Perduts",
total - rebutsTotal, perdua);
System.out.printf("%-24s %d%n", "Duplicats", duplicats);
System.out.printf("%-24s %d%n", "Desordenats", desordenats);
System.out.println();
System.out.println("--- LATENCIA (ms) ---");
System.out.printf("%-24s %.3f%n", "Minima", ordenades[0] / 1000.0);
System.out.printf("%-24s %.3f%n", "Mitjana",
(suma / (double) valides.length) / 1000.0);
System.out.printf("%-24s %.3f%n", "Mediana",
ordenades[ordenades.length / 2] / 1000.0);
System.out.printf("%-24s %.3f%n", "p95",
ordenades[(int) (ordenades.length * 0.95)] / 1000.0);
System.out.printf("%-24s %.3f%n", "Maxima",
ordenades[ordenades.length - 1] / 1000.0);
System.out.printf("%-24s %.3f%n", "Jitter mitja", jitterUs / 1000.0);
}
public static void main(String[] args) throws Exception {
String host = args.length > 0 ? args[0] : "localhost";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 9100;
for (int mida : new int[]{64, 512, 1400}) {
new SondaUdp(host, port, 1000, mida, 500).mesurar();
Thread.sleep(500);
}
}
}Sortida típica a localhost:
=== SONDA UDP: localhost:9100 ===
Mida de datagrama 64 bytes
Ritme 500/s
Enviats 1000
Rebuts 1000
Perduts 0 (0,00%)
Duplicats 0
Desordenats 0
--- LATENCIA (ms) ---
Minima 0,041
Mitjana 0,078
Mediana 0,069
p95 0,142
Maxima 1,884
Jitter mitja 0,031Comentaris. Quatre coses que ensenya aquest exercici.
Emetre i rebre en fils diferents és el que fa que la mesura sigui de la xarxa i no del teu programa. Si esperessis la resposta de cada paquet abans d'enviar el següent, estaries mesurant un protocol de parada i espera, i amb 500 paquets per segon el teu propi ritme seria el factor dominant.
El control de ritme amb seguent += intervalNs acumula sobre l'instant previst en lloc de dormir un interval fix. La diferència importa: dormir "20 ms" a cada volta acumula el temps de processament i el ritme real acaba sent més lent que el demanat. Calcular l'instant absolut següent corregeix la deriva.
El jitter es calcula abans d'ordenar i és la mètrica que sovint s'oblida. Un enllaç amb 50 ms de latència constant serveix perfectament per a una videotrucada; un amb 20 ms de mitjana però 40 de jitter, no, perquè el receptor no pot predir quan arribarà el fragment següent i ha de posar-hi una memòria intermèdia gran, que al seu torn afegeix retard.
I sobre les tres mides: a localhost les diferències entre 64, 512 i 1400 bytes són mínimes perquè no hi ha xarxa real. En un enllaç de veritat veuries que 1400 bytes té més latència (cal serialitzar més bits al cable) i que pujar a 1500 dispara la pèrdua per fragmentació. Val la pena provar-ho entre dues màquines si en tens ocasió.
Solució 2
package com.nexussoftware.bibliotech.xarxa;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.StandardSocketOptions;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.logging.Logger;
/**
* Xat intern de Nexus Software sobre multidifusio. Sense servidor central:
* tots son iguals, tots emeten al grup i tots hi reben.
*/
public class ChatBiblioTech implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(ChatBiblioTech.class.getName());
/** Rang 239.x.x.x: ambit administratiu, us privat. */
private static final String GRUP = "239.10.10.20";
private static final int PORT = 9099;
private static final int MAXIM_DATAGRAMA = 1400;
private static final int MAXIM_MISSATGE = 500;
private final String usuari;
private final long arrencada = System.currentTimeMillis();
private MulticastSocket socket;
private InetSocketAddress grup;
private NetworkInterface interficie;
private volatile boolean executant = false;
public ChatBiblioTech(String usuari) {
this.usuari = usuari;
}
public void arrencar() throws IOException {
socket = new MulticastSocket(PORT);
socket.setTimeToLive(1); // no surt de la xarxa local
// FILTRAT DE MISSATGES PROPIS - Opcio triada.
//
// Hi ha dues formes:
// (a) IP_MULTICAST_LOOP = false: el sistema no ens retorna
// els nostres propis enviaments. Es neta i no costa res, PERO
// tambe impedeix que DUES INSTANCIES A LA MATEIXA MAQUINA es
// vegin entre elles, perque el filtre es per maquina, no per socket.
// (b) Comparar el remitent amb les adreces locals i filtrar
// al codi. Mes feina, pero permet provar amb tres
// instancies al mateix ordinador.
//
// Triem (b) perque l'enunciat demana poder provar amb tres
// instancies locals, i a mes es el comportament que es vol
// en un xat: si obro dues finestres, vull veure les dues.
socket.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, true);
grup = new InetSocketAddress(InetAddress.getByName(GRUP), PORT);
interficie = triarInterficie();
// API moderna (Java 14+): amb NetworkInterface explicita.
socket.joinGroup(grup, interficie);
executant = true;
LOG.info(() -> "Unit a " + GRUP + ":" + PORT
+ " per " + interficie.getName());
}
private NetworkInterface triarInterficie() throws IOException {
Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces();
NetworkInterface reserva = null;
while (nis.hasMoreElements()) {
NetworkInterface ni = nis.nextElement();
if (!ni.isUp() || !ni.supportsMulticast()) {
continue;
}
if (!ni.isLoopback()) {
return ni; // preferim una interficie real
}
reserva = ni; // el bucle invertit serveix per a proves locals
}
if (reserva == null) {
throw new IOException("No hi ha cap interficie amb multidifusio");
}
return reserva;
}
/** Bucle de recepcio. S'executa al seu propi fil. */
private void rebre() {
byte[] buffer = new byte[MAXIM_DATAGRAMA];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
while (executant) {
try {
paquet.setLength(buffer.length); // error classic 1
socket.receive(paquet);
// Filtre (b): descartem el que enviem nosaltres mateixos.
if (esNostre(paquet.getAddress(), paquet.getPort())) {
continue;
}
String missatge = new String(paquet.getData(), paquet.getOffset(),
paquet.getLength(), StandardCharsets.UTF_8); // error classic 2
// TOT el que arriba pel grup ve d'un desconegut.
if (missatge.length() > MAXIM_MISSATGE) {
LOG.warning("Missatge massa llarg descartat");
continue;
}
System.out.println(sanejar(missatge));
} catch (IOException e) {
if (!executant) {
return; // tancament ordenat
}
LOG.warning("Error rebent: " + e.getMessage());
}
}
}
/**
* Un paquet es nostre si ve d'una adreca local I del nostre
* port d'origen. Comparar nomes l'adreca no bastaria: filtraria
* tambe els missatges de les altres instancies de la mateixa maquina.
*/
private boolean esNostre(InetAddress remitent, int portRemitent) {
if (portRemitent != socket.getLocalPort()) {
return false;
}
try {
Enumeration<InetAddress> propies = interficie.getInetAddresses();
while (propies.hasMoreElements()) {
if (propies.nextElement().equals(remitent)) {
return true;
}
}
} catch (Exception e) {
// Si no ho podem comprovar, preferim mostrar de mes que de menys.
return false;
}
return false;
}
public void enviar(String text) throws IOException {
long marca = (System.currentTimeMillis() - arrencada) / 1000;
String missatge = String.format("[%3ds] %-14s %s", marca, usuari, text);
byte[] dades = missatge.getBytes(StandardCharsets.UTF_8);
if (dades.length > MAXIM_DATAGRAMA) {
System.out.println("(missatge massa llarg, no enviat)");
return;
}
socket.send(new DatagramPacket(dades, dades.length,
InetAddress.getByName(GRUP), PORT));
}
/**
* Saneja abans d'imprimir a consola: un missatge del grup pot
* contenir sequencies d'escapada ANSI que manipulin el terminal
* de qui el llegeixi (esborrar la pantalla, moure el cursor, colors).
*/
private String sanejar(String text) {
StringBuilder sb = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
sb.append(Character.isISOControl(c) && c != '\t' ? '?' : c);
}
return sb.toString();
}
@Override
public void close() {
if (!executant) {
return;
}
try {
enviar("*** ha marxat ***");
} catch (IOException ignorada) {
// Marxem igualment.
}
executant = false;
try {
socket.leaveGroup(grup, interficie);
} catch (IOException e) {
LOG.fine("Fallada abandonant el grup: " + e.getMessage());
}
socket.close(); // desbloqueja el receive()
}
public static void main(String[] args) throws IOException {
String usuari = args.length > 0 ? args[0] : "Anonim";
ChatBiblioTech xat = new ChatBiblioTech(usuari);
xat.arrencar();
Thread receptor = new Thread(xat::rebre, "xat-receptor");
receptor.setDaemon(true);
receptor.start();
Runtime.getRuntime().addShutdownHook(new Thread(xat::close));
xat.enviar("*** s'ha unit ***");
System.out.println("Xat de BiblioTech. Escriu /sortir per acabar.");
try (BufferedReader teclat = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
String linia;
while ((linia = teclat.readLine()) != null) {
if (linia.equals("/sortir")) {
break;
}
if (!linia.isBlank()) {
xat.enviar(linia);
}
}
}
xat.close();
}
}Prova amb tres terminals:
java -cp classes com.nexussoftware.bibliotech.xarxa.ChatBiblioTech "Marta Ruiz"
java -cp classes com.nexussoftware.bibliotech.xarxa.ChatBiblioTech "Diego Alonso"
java -cp classes com.nexussoftware.bibliotech.xarxa.ChatBiblioTech "Nuria Vidal"Terminal de la Marta:
Xat de BiblioTech. Escriu /sortir per acabar.
[ 2s] Diego Alonso *** s'ha unit ***
[ 5s] Nuria Vidal *** s'ha unit ***
Algu te Java Eficac?
[ 12s] Diego Alonso El tinc jo, el retorno dema
[ 18s] Nuria Vidal *** ha marxat ***Comentaris. El punt interessant és el filtratge dels missatges propis, i la raó de triar l'opció (b) està explicada al codi: IP_MULTICAST_LOOP = false és més net, però el filtre l'aplica el sistema per màquina, no per socket, així que tres instàncies al mateix ordinador deixarien de veure's entre elles i l'exercici no es podria provar. La comparació manual exigeix a més comparar adreça i port d'origen, no només l'adreça: si comparessis només l'adreça, filtraries els missatges dels teus companys de màquina, que és exactament el que volíem evitar.
Fixa't també que el xat no té servidor. És un sistema d'igual a igual real, el model que es va presentar a 09-01: cada node emet al grup i rep del grup. Ningú no és especial, ningú no manté estat de ningú, i si una instància cau les altres ni se n'assabenten. A canvi, no hi ha historial, no hi ha lliurament garantit i no hi ha forma de saber qui està connectat llevat dels anuncis — que també es poden perdre. És el preu de no tenir servidor.
I el sanejar abans d'imprimir no és paranoia acadèmica: un missatge que contingui \033[2J esborra la pantalla de qui el rebi, i seqüències més elaborades poden fer coses pitjors en alguns terminals.
Solució 3
package com.nexussoftware.bibliotech.xarxa;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Logger;
/**
* Transferencia fiable de fitxer sobre UDP amb parada i espera.
*
* Format de cada datagrama de dades:
* [int ] numero de sequencia
* [byte ] 1 si es l'ultim bloc, 0 si no
* [int ] longitud de les dades
* [bytes ] dades
*
* Format de la confirmacio:
* [int ] numero de sequencia confirmat
*/
public class EnviamentFiableUdp {
private static final Logger LOG = Logger.getLogger(EnviamentFiableUdp.class.getName());
private static final int DADES_PER_BLOC = 1024;
private static final int CAPCALERA = 4 + 1 + 4; // int + byte + int
private static final int MAXIM_INTENTS = 5;
private static final int ESPERA_INICIAL_MS = 200;
/** Percentatge de datagrames que es descarten a proposit, per provar. */
private final int perduaSimulada;
private int retransmissions = 0;
public EnviamentFiableUdp(int perduaSimulada) {
this.perduaSimulada = perduaSimulada;
}
public void enviar(String host, int port, Path fitxer) throws IOException {
long mida = Files.size(fitxer);
long inici = System.currentTimeMillis();
try (DatagramSocket socket = new DatagramSocket();
InputStream entrada = Files.newInputStream(fitxer)) {
InetAddress desti = InetAddress.getByName(host);
// connect() filtra: nomes acceptem confirmacions d'aquest desti.
socket.connect(desti, port);
byte[] bloc = new byte[DADES_PER_BLOC];
byte[] buferAck = new byte[16];
DatagramPacket ack = new DatagramPacket(buferAck, buferAck.length);
int sequencia = 0;
long bytesEnviats = 0;
long sumaComprovacio = 0;
while (true) {
int llegits = entrada.read(bloc);
boolean ultim = false;
if (llegits == -1) {
// Fi del fitxer: enviem un bloc buit marcat
// com a ultim, perque el receptor sapiga que ha acabat.
llegits = 0;
ultim = true;
} else {
// Mirem si queda alguna cosa mes sense consumir-la:
// available() es orientatiu, aixi que marquem
// l'ultim amb un bloc buit final. Mes simple i segur.
for (int i = 0; i < llegits; i++) {
sumaComprovacio += bloc[i] & 0xFF;
}
bytesEnviats += llegits;
}
byte[] datagrama = construir(sequencia, ultim, bloc, llegits);
if (!enviarAmbConfirmacio(socket, desti, port,
datagrama, sequencia, ack, buferAck)) {
throw new IOException("El receptor no confirma el bloc "
+ sequencia + " despres de " + MAXIM_INTENTS + " intents");
}
if (ultim) {
break;
}
sequencia++;
}
long ms = System.currentTimeMillis() - inici;
System.out.println();
System.out.println("=== TRANSFERENCIA COMPLETADA ===");
System.out.printf("%-26s %s%n", "Fitxer", fitxer.getFileName());
System.out.printf("%-26s %d%n", "Bytes", bytesEnviats);
System.out.printf("%-26s %d%n", "Blocs", sequencia + 1);
System.out.printf("%-26s %d%n", "Retransmissions", retransmissions);
System.out.printf("%-26s %d%%%n", "Perdua simulada", perduaSimulada);
System.out.printf("%-26s %d ms%n", "Temps", ms);
System.out.printf("%-26s %d%n", "Suma de comprovacio", sumaComprovacio);
System.out.printf("%-26s %.1f KB/s%n", "Velocitat",
ms == 0 ? 0 : bytesEnviats / 1024.0 / (ms / 1000.0));
}
}
/** Construeix el datagrama amb la seva capcalera binaria en big-endian. */
private byte[] construir(int sequencia, boolean ultim, byte[] dades, int longitud)
throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(CAPCALERA + longitud);
DataOutputStream sortida = new DataOutputStream(bytes);
sortida.writeInt(sequencia);
sortida.writeBoolean(ultim);
sortida.writeInt(longitud);
sortida.write(dades, 0, longitud);
sortida.flush();
return bytes.toByteArray();
}
/** Envia i espera confirmacio, reintentant amb espera creixent. */
private boolean enviarAmbConfirmacio(DatagramSocket socket, InetAddress desti,
int port, byte[] datagrama, int sequencia,
DatagramPacket ack, byte[] buferAck)
throws IOException {
int espera = ESPERA_INICIAL_MS;
for (int intent = 1; intent <= MAXIM_INTENTS; intent++) {
if (intent > 1) {
retransmissions++;
}
// --- PERDUA SIMULADA ---
// Es descarta el datagrama ABANS d'enviar-lo, per provar que
// el mecanisme de reintent funciona sense necessitar una xarxa dolenta.
if (ThreadLocalRandom.current().nextInt(100) >= perduaSimulada) {
socket.send(new DatagramPacket(datagrama, datagrama.length,
desti, port));
} else {
LOG.fine("Perdua simulada del bloc " + sequencia);
}
socket.setSoTimeout(espera);
long limit = System.currentTimeMillis() + espera;
while (System.currentTimeMillis() < limit) {
try {
ack.setLength(buferAck.length); // error classic 1
socket.receive(ack);
if (ack.getLength() < 4) {
continue;
}
int confirmat = ByteBuffer.wrap(ack.getData(),
ack.getOffset(), ack.getLength()).getInt();
if (confirmat == sequencia) {
return true;
}
// Confirmacio d'un bloc anterior (ha arribat tard):
// es descarta i es continua esperant la nostra.
LOG.fine("Confirmacio desfasada: " + confirmat
+ ", esperavem " + sequencia);
} catch (SocketTimeoutException e) {
break; // termini esgotat: seguent intent
}
}
espera *= 2; // espera creixent
}
return false;
}
public static void main(String[] args) throws IOException {
Path fitxer = Path.of(args.length > 0 ? args[0] : "cataleg.csv");
int perdua = args.length > 1 ? Integer.parseInt(args[1]) : 20;
new EnviamentFiableUdp(perdua).enviar("localhost", 9101, fitxer);
}
}package com.nexussoftware.bibliotech.xarxa;
import java.io.IOException;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Logger;
/** Receptor de la transferencia fiable sobre UDP. */
public class RecepcioFiableUdp {
private static final Logger LOG = Logger.getLogger(RecepcioFiableUdp.class.getName());
private static final int DADES_PER_BLOC = 1024;
private static final int CAPCALERA = 4 + 1 + 4;
private static final int MAXIM_DATAGRAMA = CAPCALERA + DADES_PER_BLOC;
public void rebre(int port, Path desti) throws IOException {
Files.createDirectories(desti.getParent() == null
? Path.of(".") : desti.getParent());
try (DatagramSocket socket = new DatagramSocket(port);
OutputStream sortida = Files.newOutputStream(desti)) {
System.out.println("Esperant fitxer a UDP:" + port);
socket.setSoTimeout(30_000); // abandonem si ningu no envia
byte[] buffer = new byte[MAXIM_DATAGRAMA + 1];
DatagramPacket paquet = new DatagramPacket(buffer, buffer.length);
int esperat = 0;
int duplicats = 0;
long bytes = 0;
long sumaComprovacio = 0;
long inici = System.currentTimeMillis();
while (true) {
paquet.setLength(buffer.length); // error classic 1
socket.receive(paquet);
if (paquet.getLength() < CAPCALERA
|| paquet.getLength() > MAXIM_DATAGRAMA) {
LOG.warning("Datagrama de mida invalida descartat");
continue;
}
ByteBuffer bb = ByteBuffer.wrap(paquet.getData(),
paquet.getOffset(), paquet.getLength());
int sequencia = bb.getInt();
boolean ultim = bb.get() != 0;
int longitud = bb.getInt();
// VALIDACIO: la longitud ve de la xarxa. Sense aixo,
// un emissor malicios provoca una excepcio o alguna cosa pitjor.
if (longitud < 0 || longitud > DADES_PER_BLOC
|| longitud > bb.remaining()) {
LOG.warning("Longitud declarada invalida: " + longitud);
continue;
}
if (sequencia < esperat) {
// DUPLICAT: la nostra confirmacio anterior s'ha perdut i
// l'emissor ha reenviat. Cal RECONFIRMAR pero NO escriure
// les dades una altra vegada, o el fitxer sortiria corrupte.
duplicats++;
confirmar(socket, paquet, sequencia);
continue;
}
if (sequencia > esperat) {
// Amb parada i espera aixo no hauria de passar: significa
// que hem perdut un bloc intermedi. No confirmem, i
// l'emissor reenviara el que falta.
LOG.warning("Bloc " + sequencia + " fora d'ordre; "
+ "esperavem " + esperat);
continue;
}
// Bloc correcte i en ordre: s'escriu.
byte[] dades = new byte[longitud];
bb.get(dades);
sortida.write(dades);
for (byte b : dades) {
sumaComprovacio += b & 0xFF;
}
bytes += longitud;
confirmar(socket, paquet, sequencia);
esperat++;
if (ultim) {
sortida.flush();
long ms = System.currentTimeMillis() - inici;
System.out.println();
System.out.println("=== RECEPCIO COMPLETADA ===");
System.out.printf("%-26s %s%n", "Desat a", desti);
System.out.printf("%-26s %d%n", "Bytes", bytes);
System.out.printf("%-26s %d%n", "Blocs", esperat);
System.out.printf("%-26s %d%n", "Duplicats detectats", duplicats);
System.out.printf("%-26s %d ms%n", "Temps", ms);
System.out.printf("%-26s %d%n", "Suma de comprovacio",
sumaComprovacio);
return;
}
}
}
}
private void confirmar(DatagramSocket socket, DatagramPacket original, int sequencia)
throws IOException {
byte[] ack = ByteBuffer.allocate(4).putInt(sequencia).array();
// Es respon AL REMITENT, que ve al paquet original.
socket.send(new DatagramPacket(ack, ack.length,
original.getAddress(), original.getPort()));
}
public static void main(String[] args) throws IOException {
new RecepcioFiableUdp().rebre(9101, Path.of("rebuts", "cataleg.csv"));
}
}Prova amb 20 % de pèrdua simulada:
Terminal 1 (receptor):
Esperant fitxer a UDP:9101
=== RECEPCIO COMPLETADA ===
Desat a rebuts/cataleg.csv
Bytes 2847
Blocs 4
Duplicats detectats 1
Temps 834 ms
Suma de comprovacio 291476
Terminal 2 (emissor):
=== TRANSFERENCIA COMPLETADA ===
Fitxer cataleg.csv
Bytes 2847
Blocs 4
Retransmissions 3
Perdua simulada 20%
Temps 841 ms
Suma de comprovacio 291476Comentaris. Les sumes de comprovació coincideixen: la transferència és correcta malgrat el 20 % de pèrdua. Tres punts mereixen atenció.
La detecció de duplicats és imprescindible i la seva lògica no és òbvia. Un duplicat no passa perquè la xarxa dupliqui paquets: passa perquè la nostra confirmació s'ha perdut. L'emissor no la va rebre, va esgotar el termini i va reenviar un bloc que nosaltres ja havíem escrit. Si l'escrivíssim una altra vegada, el fitxer sortiria amb blocs repetits. I fixa't que cal reconfirmar-lo: si ens limitéssim a ignorar-lo, l'emissor continuaria reenviant fins a esgotar els intents i avortaria una transferència que en realitat anava bé.
L'espera creixent domina el temps. 841 ms per a 2847 bytes és una velocitat ridícula, i no és culpa de la xarxa: són les esperes de 200, 400 i 800 ms dels reintents. Parada i espera és l'esquema de fiabilitat més simple i el més lent, perquè només hi ha un bloc en vol alhora. TCP fa servir una finestra lliscant —molts blocs en vol simultàniament, confirmats acumulativament— i per això assoleix velocitats ordres de magnitud més grans.
I la reflexió que demanava l'enunciat. Aquest parell de classes suma unes 250 línies de codi acuradament pensat: numeració, confirmacions, temps límit, reintents amb espera creixent, detecció de duplicats, validació de longituds i comprovació d'integritat. L'equivalent amb TCP és:
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), 3000);
Files.copy(fitxer, socket.getOutputStream());
socket.shutdownOutput();
}Quatre línies, més ràpid, més robust i sense ni un sol bug propi. I el que hem escrit encara no té control de congestió, ni finestra lliscant, ni reordenació, ni protecció davant d'un emissor que satura el receptor.
Aquesta és exactament la lliçó de la secció 2: si acabes implementant fiabilitat sobre UDP, gairebé sempre hauries d'estar fent servir TCP. L'excepció legítima —i la raó d'existir de QUIC— és quan necessites fiabilitat parcial o a la teva mida, cosa que TCP no ofereix perquè és tot o res. Però per transferir un fitxer, TCP guanya sense discussió.
Conclusió
Has après l'altre transport, i amb ell BiblioTech ha guanyat dues capacitats que TCP simplement no pot donar.
Saps què perds amb UDP —lliurament, ordre, unicitat, control de flux, control de congestió i qualsevol noció que l'altre extrem continuï viu— i què guanyes: enviament immediat sense establiment, capçalera de 8 bytes, fronteres de missatge que fan innecessària tota la feina de delimitadors de 09-02, absència total d'estat per client i, sobretot, la capacitat de parlar amb molts alhora. I tens el criteri per triar, que no és la velocitat sinó tres preguntes: la dada vella encara val?, necessito un a molts? i l'operació és idempotent?
Manages les dues classes i la seva asimetria: DatagramPacket és el sobre —amb dos usos completament diferents, dades més destí per enviar i memòria intermèdia buida per rebre— i DatagramSocket és la bústia, una de sola per a tots els clients, sense connexions, sense pool i sense estat. Amb els dos advertiments que defineixen el protocol: send() gairebé mai no falla encara que el destí estigui apagat, així que el seu èxit no significa res; i connect() no connecta, només instal·la un filtre local que resulta útil com a defensa contra injeccions.
Has vist demostrats els dos errors clàssics, que junts causen la majoria dels problemes d'UDP en Java: no cridar setLength(buffer.length) abans de cada receive(), que trunca tots els missatges a la longitud del primer sense ni un avís; i fer servir getData() sense getOffset() i getLength(), que arrossega restes del missatge anterior i produeix dades corruptes que semblen legítimes.
Entens la mida dels datagrames: el màxim teòric de 65.507 bytes no serveix de res davant de l'MTU real de 1500, perquè passar d'aquí obliga a fragmentar, i perdre un sol fragment perd el datagrama sencer — la fragmentació multiplica la pèrdua i molts tallafocs descarten fragments directament. D'aquí les xifres prudents: 512 bytes per estar segur en qualsevol xarxa, 1400 com a límit pràctic. I saps que un datagrama que no cap a la teva memòria intermèdia es trunca en silenci, amb el truc de demanar un byte de més per detectar-ho.
Has comprovat amb les teves pròpies mesures que cal tolerar pèrdua, duplicació i desordre — amb un 26 % de pèrdua a localhost, sense xarxa pel mig, tan bon punt l'emissor supera el receptor: això és l'absència de control de flux. I saps construir fiabilitat a mà quan cal, amb les seves quatre peces: identificador aleatori per petició per aparellar respostes i evitar injeccions, setSoTimeout amb reintent i espera creixent, límit d'intents, i idempotència de l'operació — l'argument decisiu pel qual PRESTAR es queda a TCP.
Coneixes la difusió, que arriba a tota la xarxa local, no travessa routers i no existeix a IPv6; i la multidifusió, que és la seva versió ben feta: grups als quals un se subscriu voluntàriament, rang privat 239.x.x.x, TTL 1 per no sortir de la xarxa local, i l'API moderna joinGroup(SocketAddress, NetworkInterface) en lloc de l'obsoleta, perquè en qualsevol màquina amb Docker, VPN o Wi-Fi més Ethernet la interfície que tria el sistema no és la que vols.
I BiblioTech ha guanyat peces de veritat. ResponedorDescobriment i CercadorServidor implementen BTDP/1: els llocs de treball pregunten per difusió «on és BiblioTech?» i el servidor respon per unicast amb la seva adreça i el seu port TCP — la configuració manual de la IP a cada lloc ha desaparegut, i això era literalment impossible amb TCP. EmissorTelemetria envia mètriques cada pocs segons amb el ScheduledExecutorService de 08-05, sense bloquejar mai el servidor i sense que li importi que el recol·lector estigui caigut: el pots apagar amb Ctrl+C i el servidor ni s'immuta. Més ServidorEcoUdp, PeticioUdpFiable, AvisosMulticast, RecolectorTelemetria i les classes dels exercicis: SondaUdp amb la seva mesura de jitter, ChatBiblioTech sense servidor central, i el parell de transferència fiable que demostra —en 250 línies davant de 4— per què reimplementar TCP sobre UDP gairebé mai no és bona idea.
I tens el veredicte clar per a BiblioTech, que resumeix la lliçó: UDP no substitueix TCP, el complementa. Consulta, llista i préstec van per TCP perquè necessiten fiabilitat, respostes grans i no són idempotents; descobriment, telemetria i avisos van per UDP perquè necessiten un a molts, no han de bloquejar i toleren la pèrdua. Un sistema real fa servir tots dos.
Fins aquí has treballat al nivell del transport: bytes, sockets, datagrames, protocols que has dissenyat tu. A la lliçó següent, URL i HttpURLConnection, puges un nivell. Deixaràs d'inventar protocols i faràs servir el que ja domina el món: HTTP. Veuràs l'anatomia d'una URL i les classes URL i URI amb les seves diferències, i per què codificar els paràmetres amb URLEncoder és imprescindible tan bon punt apareix un accent o un espai. Veuràs HTTP explicat a fons —petició, resposta, mètodes, codis d'estat, capçaleres— amb un intercanvi real mostrat en text pla, i amb la connexió que ho uneix tot: això és exactament el que viatja pel socket de 09-02, només que ara el protocol l'ha dissenyat un altre i l'entén mig planeta. Aprendràs URL.openStream() per al cas simple i HttpURLConnection per al control real —mètodes, capçaleres, temps límit obligatoris, codis d'estat, i l'error clàssic de no llegir getErrorStream() quan arriba un 404—, i descarregaràs un fitxer binari a disc combinant-ho amb el mòdul 7. Amb una valoració honesta per endavant: HttpURLConnection és una API vella, verbosa i plena de paranys, i per això Java 11 va portar una de nova que veuràs a 09-06 — però continua viva en muntanyes de codi heretat i cal saber llegir-la. BiblioTech començarà a parlar amb el món exterior: consultarà un servei de metadades per ISBN i es descarregarà la portada d'un llibre.
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
