A la lliçó anterior vas dissenyar BTCP/1, el protocol de consulta del catàleg de BiblioTech: un protocol de text, sobre TCP, al port 9090, amb missatges delimitats per salt de línia i respostes amb codis de tres dígits. Ara toca implementar-lo. I la bona notícia és que la major part de la feina ja la saps fer.
Un socket és un extrem d'una conversa entre dos programes. Un cop establert, un Socket de Java et lliura un InputStream i un OutputStream. Exactament els mateixos que vas fer servir al mòdul 7 per llegir i escriure fitxers. Els mateixos decoradors, el mateix InputStreamReader amb charset explícit, el mateix BufferedReader, el mateix try-with-resources. Un cop connectat, la xarxa és E/S que ja saps fer.
El que és nou, i el que ocupa aquesta lliçó, és tot el que envolta aquesta E/S: com s'estableix la connexió i amb quin temps límit, quines opcions té un socket i quan importen, com es tanca bé —i què significa tancar-ne només la meitat—, què et diu cada excepció de xarxa sobre el que ha passat realment, i un error concret que cometen absolutament tots els que comencen i que deixa l'altre extrem esperant per sempre.
En acabar tindràs el client de BiblioTech funcionant contra un servidor que faràs tu a mà amb nc. El servidor de veritat arriba a 09-03.
Contingut
- El socket i els fluxos del mòdul 7
- La classe
Socket: connectar connectamb temps límit iInetSocketAddress- Escriure i llegir text sobre el socket
- El bug número u: oblidar el buidatge
- El protocol per línies i el problema del delimitador
- Tancament correcte i
try-with-resources - Mitja connexió:
shutdownOutputishutdownInput - Opcions del socket
setSoTimeoutiSocketTimeoutExceptionsetTcpNoDelayi l'algorisme de Nagle- Les excepcions de xarxa i què significa cadascuna
- Dades binàries amb
DataOutputStreamiDataInputStream - Per què no enviar objectes serialitzats a un client no fiable
- BiblioTech: el client de catàleg complet
- Errors Comuns i Consells
- Exercicis
- El socket i els fluxos del mòdul 7
Abans de res, la idea que estructura tota la lliçó:
graph LR
subgraph CLIENT
A["El teu codi"] --> B["PrintWriter"]
B --> C["BufferedWriter"]
C --> D["OutputStreamWriter<br/>UTF-8"]
D --> E["socket.getOutputStream()"]
end
E -->|bytes per la xarxa| F
subgraph SERVIDOR
F["socket.getInputStream()"] --> G["InputStreamReader<br/>UTF-8"]
G --> H["BufferedReader"]
H --> I["El teu codi"]
end
Aquest diagrama és exactament el del mòdul 7, amb una sola diferència: on abans hi havia un FileOutputStream i un FileInputStream, ara hi ha un socket. Tota la resta —el patró Decorador, el pont OutputStreamWriter entre bytes i caràcters, la memòria intermèdia, el charset explícit— és idèntica.
| Concepte del mòdul 7 | El seu equivalent en xarxa |
|---|---|
new FileInputStream(fitxer) |
socket.getInputStream() |
new FileOutputStream(fitxer) |
socket.getOutputStream() |
| El fitxer existeix o no | El servidor accepta o rebutja |
read() retorna -1 en arribar al final |
read() retorna -1 quan l'altre tanca |
| El fitxer està complet en obrir-lo | Les dades arriben a poc a poc |
| Tancar allibera un descriptor | Tancar acaba la conversa |
Les dues files del final són les que marquen la diferència real. Un fitxer està sencer quan l'obres; un socket et va lliurant bytes a mesura que arriben, i una lectura et pot retornar menys del que demanaves simplement perquè la resta encara viatja pel cable. Aquesta és l'arrel del problema del delimitador que veuràs a l'apartat 6.
- La classe
Socket: connectar
Socket: connectarjava.net.Socket representa un socket TCP del costat del client. La forma més directa de fer-lo servir és el seu constructor, que connecta durant la construcció:
// El constructor CONNECTA. Si torna, ja estas connectat.
// Si no pot, llanca excepcio i l'objecte no arriba a existir mai.
Socket socket = new Socket("localhost", 9090);Aquest constructor fa tres coses de cop:
- Resol el nom
localhosta una adreça (DNS o/etc/hosts). - Crea el socket i li assigna un port efímer local.
- Executa la salutació de tres vies contra el port 9090 del destí.
I pot fallar en qualsevol de les tres, sempre amb una IOException o subclasse:
import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.net.UnknownHostException;
try (Socket socket = new Socket("localhost", 9090)) {
System.out.println("Connectat a : " + socket.getInetAddress().getHostAddress());
System.out.println("Port remot : " + socket.getPort());
System.out.println("Adreca local : " + socket.getLocalAddress().getHostAddress());
System.out.println("Port local : " + socket.getLocalPort());
} catch (UnknownHostException e) {
// Fallada al pas 1: el nom no s'ha pogut resoldre.
System.err.println("No existeix l'equip: " + e.getMessage());
} catch (ConnectException e) {
// Fallada al pas 3: la maquina ha respost RST. No hi ha ningu escoltant.
System.err.println("Connexio rebutjada: no hi ha servidor en aquest port");
} catch (IOException e) {
// Qualsevol altra fallada de xarxa.
System.err.println("Fallada de xarxa: " + e.getMessage());
}Sortida si hi ha un servidor escoltant:
Aquí tens la quàdrupla de la lliçó anterior feta codi: (127.0.0.1:51422) parlant amb (127.0.0.1:9090). El port local no l'has triat tu: l'ha assignat el sistema operatiu del seu rang efímer.
Mètodes de consulta útils
| Mètode | Retorna |
|---|---|
getInetAddress() |
L'adreça de l'altre extrem |
getPort() |
El port de l'altre extrem |
getLocalAddress() / getLocalPort() |
Els teus |
getRemoteSocketAddress() |
Tots dos de l'altre extrem, com a SocketAddress |
isConnected() |
Si alguna vegada es va connectar — no si continua viu |
isClosed() |
Si tu el vas tancar — no si l'altre va tancar |
isInputShutdown() / isOutputShutdown() |
Si has tancat mitja connexió |
Avís important sobre
isConnected(). És el parany més citat de l'API.isConnected()retornatruedes del moment en què la connexió es va establir, i continua retornanttrueencara que l'altre extrem hagi caigut, perquè TCP no avisa de res fins que intentes fer servir el socket. No existeix cap mètode que respongui "continua viu l'altre?". L'única forma de saber-ho és intentar llegir o escriure i veure què passa: unread()que retorna-1significa que l'altre va tancar ordenadament; unaSocketExceptionsignifica que es va trencar. Qualsevol codi que depengui d'isConnected()per decidir si encara hi ha conversa està malament.
connect amb temps límit i InetSocketAddress
connect amb temps límit i InetSocketAddressEl constructor new Socket(host, port) té un problema seriós: no admet temps límit de connexió. Si la màquina destí no respon —està apagada, o hi ha un tallafoc que descarta els paquets en silenci— el constructor es queda bloquejat fins que el sistema operatiu es rendeix, i aquest termini per defecte pot ser de més d'un minut a Linux, fins i tot de diversos minuts.
Un minut bloquejant el fil del menú de BiblioTech no és acceptable. La solució és separar la creació de la connexió:
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
// 1. Es crea el socket SENSE connectar (constructor sense arguments).
Socket socket = new Socket();
// 2. Es descriu el desti amb un InetSocketAddress: parell (host, port).
InetSocketAddress desti = new InetSocketAddress("192.168.1.50", 9090);
// 3. Es connecta amb temps limit explicit en milisegons.
try {
socket.connect(desti, 3000); // 3 segons com a maxim
} catch (SocketTimeoutException e) {
// El termini s'ha esgotat: la maquina no ha respost al SYN.
// Fallada TRANSITORIA: pot tenir sentit reintentar.
System.err.println("El servidor no respon en 3 s");
}InetSocketAddress és simplement el parell (adreça, port) del qual parlàvem a 09-01. Té dues formes de construir-se, i la diferència importa:
// Resol el nom ARA. Si no resol, isUnresolved() sera true
// (compte: NO llanca excepcio aqui; la llancara connect()).
InetSocketAddress a = new InetSocketAddress("servidor.nexussoftware.local", 9090);
// No resol res: deixa el nom tal qual perque el resolgui
// qui connecti (util amb proxies SOCKS, que resolen ells).
InetSocketAddress b = InetSocketAddress.createUnresolved("servidor.local", 9090);
// Amb una InetAddress ja resolta, sense consultar DNS.
InetSocketAddress c = new InetSocketAddress(InetAddress.getLoopbackAddress(), 9090);Regla ferma per a la resta del mòdul: en codi de producció, no facis servir mai el constructor que connecta. Fes servir sempre new Socket() + connect(desti, tempsLimit). I no confonguis els dos temps límit que existeixen, perquè són diferents i necessites tots dos:
| Temps límit | Es fixa amb | Cobreix |
|---|---|---|
| De connexió | connect(addr, ms) |
Quant s'espera que s'estableixi la connexió |
| De lectura | setSoTimeout(ms) |
Quant s'espera que arribin dades a cada read() |
Un socket pot connectar en 5 ms i després quedar-se una hora sense rebre res. El primer no et protegeix del segon.
- Escriure i llegir text sobre el socket
Amb la connexió feta, entrem en terreny conegut. Per a BTCP/1, que és un protocol de text per línies en UTF-8, la pila correcta és aquesta:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("localhost", 9090), 3000);
socket.setSoTimeout(10_000);
// SORTIDA: el teu text -> caracters -> bytes UTF-8 -> xarxa
// PrintWriter dona println() i format
// BufferedWriter acumula per no fer una escriptura de xarxa per caracter
// OutputStreamWriter es el PONT caracters -> bytes, amb charset explicit
PrintWriter escriptor = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)),
true); // <-- autoFlush: buida a cada println()
// ENTRADA: xarxa -> bytes UTF-8 -> caracters -> linies
BufferedReader lector = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String salutacio = lector.readLine(); // "200 BIBLIOTECH BTCP/1"
System.out.println("S: " + salutacio);
escriptor.println("CONSULTA 978-0000000001");
System.out.println("C: CONSULTA 978-0000000001");
String resposta = lector.readLine(); // "200 OK 978-...|Java Eficac|LLIBRE|true"
System.out.println("S: " + resposta);
escriptor.println("SORTIR");
System.out.println("S: " + lector.readLine()); // "221 ADEU"
}Cada capa hi és per una raó concreta:
| Classe | Per què hi és |
|---|---|
OutputStreamWriter(..., UTF_8) |
El pont de caràcters a bytes. El charset explícit és obligatori: sense ell es fa servir el de la plataforma, i client i servidor poden no coincidir |
BufferedWriter |
Sense ell, cada caràcter seria potencialment un paquet de xarxa. Amb ell, s'acumula i s'envia de cop |
PrintWriter |
Aporta println(), que afegeix el delimitador de línia del protocol, i printf() |
InputStreamReader(..., UTF_8) |
El pont invers, amb el mateix charset |
BufferedReader |
Aporta readLine(), que acumula bytes fins a trobar el salt de línia. És la peça que resol el problema de les fronteres de missatge |
Sobre
PrintWriteri les excepcions.PrintWriterno llançaIOException: s'empassa els errors i els exposa a través decheckError(). És còmode per escriure a consola i perillós en xarxa, perquè una fallada d'escriptura pot passar desapercebuda. Si necessites assabentar-te'n, comprovaescriptor.checkError()després d'escriure, o fes servirBufferedWriterdirectament ambwrite()inewLine(), que sí que llancen. Al client de BiblioTech comprovaremcheckError().
Sobre el delimitador de línia.
println()escriu el separador de la plataforma:\na Linux i macOS,\r\na Windows. Un protocol de xarxa no pot dependre de en quin sistema corre cada extrem. La bona notícia és quereadLine()accepta\n,\ri\r\nindistintament, així que a la pràctica interopera; però si el protocol exigeix\r\nestricte (HTTP ho exigeix), cal escriure'l a mà ambprint("...\r\n"). A BTCP/1 hem especificat\n, i per ser rigorosos el client l'escriurà explícitament.
- El bug número u: oblidar el buidatge
Aquest és l'error que comet tothom la primera vegada, i mereix una demostració completa perquè el seu símptoma és enganyós: no hi ha excepció, no hi ha error, no hi ha res al log. El programa simplement es queda aturat per sempre.
El codi trencat
// CLIENT TRENCAT - no el copiis, es l'exemple del que NO s'ha de fer.
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("localhost", 9090), 3000);
PrintWriter escriptor = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)));
// ^^^^^ falta el "true" de l'autoFlush
BufferedReader lector = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
escriptor.println("CONSULTA 978-0000000001"); // Es queda a la MEMORIA INTERMEDIA.
String resposta = lector.readLine(); // <-- ES BLOQUEJA AQUI PER SEMPRE
System.out.println(resposta);
}Què ha passat exactament
sequenceDiagram
participant App as Codi client
participant Buf as BufferedWriter
participant Xar as Xarxa
participant Srv as Servidor
App->>Buf: println("CONSULTA ...")
Note over Buf: 24 bytes desats en memoria.<br/>La memoria intermedia es de 8192 bytes: encara hi cap mes,<br/>aixi que NO s'envia res.
App->>Xar: readLine()
Note over App,Xar: El client espera una resposta...
Note over Srv: ...a una peticio que no ha arribat mai.
Note over App,Srv: INTERBLOQUEIG. Tots dos esperen l'altre.<br/>Sense excepcio. Sense traca. Per sempre.
El BufferedWriter té una memòria intermèdia de 8192 caràcters per defecte. Els seus 24 caràcters hi caben de sobres, així que fa el que se li ha demanat: esperar a tenir-ne més abans de gastar una operació de xarxa. En un fitxer això és una optimització sense conseqüències, perquè en tancar el fitxer es buida i tot arriba. En xarxa és un interbloqueig, perquè l'altre extrem està esperant aquests bytes per poder respondre.
I fixa't en el detall cruel: el mateix codi funcionaria amb un missatge de més de 8192 caràcters, perquè la memòria intermèdia es desbordaria i s'enviaria sola. És la mena de bug que "funciona a la meva màquina" amb dades grans i falla en producció amb dades petites.
Les tres formes d'arreglar-ho
// FORMA 1 (recomanada per a protocols de linia): autoFlush a PrintWriter.
// El segon argument true fa que println(), printf() i format()
// buidin automaticament. COMPTE: print() SENSE salt de linia NO buida.
PrintWriter escriptor = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)),
true);
escriptor.println("CONSULTA 978-0000000001"); // s'envia sol
// FORMA 2: buidatge explicit. Obligatoria si escrius amb print() sense salt,
// o si el protocol fa servir \r\n i escrius el terminador a ma.
escriptor.print("CONSULTA 978-0000000001\n");
escriptor.flush(); // <-- imprescindible
// FORMA 3: BufferedWriter directe, que a mes SI que llanca IOException.
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
bw.write("CONSULTA 978-0000000001");
bw.write('\n');
bw.flush();La regla que cal memoritzar: en xarxa, després d'enviar alguna cosa que espera resposta, cal buidar. Sense excepcions. Si fas servir autoFlush, assegura't d'escriure sempre amb println i no amb print, perquè autoFlush només actua a println, printf i format — un print("SORTIR\n") amb autoFlush activat no buida, i tornes a tenir el mateix interbloqueig amb un codi que aparentment ho fa bé.
El símptoma reconeixible. Si el teu programa de xarxa "es queda penjat" sense error, sospita primer d'això. El segon sospitós és no haver posat
setSoTimeout, que converteix el bloqueig etern en unaSocketTimeoutExceptiondiagnosticable. Amb les dues coses ben fetes, aquest bug es torna impossible de patir en silenci.
- El protocol per línies i el problema del delimitador
A 09-01 vam veure que TCP és un flux de bytes sense fronteres de missatge. Convé veure ara exactament què significa això al codi.
// El que fa el client:
escriptor.println("CONSULTA 111");
escriptor.println("LLISTA");
// El que pot llegir el servidor amb un read() sobre bytes crus:
// Cas A: "CONSULTA 111\nLLISTA\n" (els dos missatges junts)
// Cas B: "CONSULTA 111\n" despres "LLISTA\n" (un i un)
// Cas C: "CONSU" despres "LTA 111\nLLI" despres "STA\n"Els tres casos són legals i passen de veritat, segons la mida de les memòries intermèdies, l'MTU de la xarxa i l'algorisme de Nagle. Un servidor que suposi el cas B funciona a localhost i falla en producció.
BufferedReader.readLine() ho resol per tu. Internament manté la seva pròpia memòria intermèdia, i quan li demanes una línia continua llegint del socket tantes vegades com calgui fins a trobar un salt de línia. Retorna la línia sense el terminador.
String linia;
while ((linia = lector.readLine()) != null) {
// Aqui 'linia' es SEMPRE un missatge complet del protocol.
// readLine() ja ha resolt el trossejat per tu.
processar(linia);
}
// Sortim del bucle quan readLine() retorna null:
// l'altre extrem ha tancat el seu costat de la connexio.Dos advertiments sobre readLine():
- Retorna
nullal final del flux, cosa que en xarxa significa "l'altre ha tancat". No comprovar-ho produeix unNullPointerExceptionamb el primer client que es desconnecti, i és un clàssic absolut. El buclewhile ((linia = lector.readLine()) != null)no és un adorn estilístic. - Bloqueja fins a trobar el salt de línia. Si l'altre extrem envia
"CONSULTA 111"sense\ni es queda callat, el teureadLine()no torna. D'aquí elsetSoTimeout.
I una limitació de disseny: readLine() no té límit de longitud. Un client maliciós pot enviar cent megabytes sense ni un sol salt de línia i el teu servidor els acumularà en memòria fins a rebentar. És un vector de denegació de servei real. Al servidor de 09-03 hi posarem una lectura acotada.
Quan el delimitador no serveix
El delimitador de línia funciona si el delimitador no pot aparèixer a les dades. Per a BTCP/1 és cert: ni un ISBN ni un títol no porten salts de línia. Però si haguessis d'enviar un text multilínia —la ressenya d'un llibre, per exemple— tindries dues sortides: escapar els salts (\n com a dos caràcters) o canviar a longitud prèvia:
// Longitud previa: primer quants bytes venen, despres els bytes.
byte[] dades = ressenya.getBytes(StandardCharsets.UTF_8);
sortidaDades.writeInt(dades.length); // 4 bytes en big-endian
sortidaDades.write(dades); // les dades, sense restriccions
sortidaDades.flush();
// En llegir:
int longitud = entradaDades.readInt();
if (longitud < 0 || longitud > MAXIM_PERMES) {
// IMPRESCINDIBLE: si no, un client malicios demana un array de 2 GB
// i provoca un OutOfMemoryError amb 4 bytes de peticio.
throw new ProtocolException("Longitud declarada invalida: " + longitud);
}
byte[] dades = entradaDades.readNBytes(longitud);
String ressenya = new String(dades, StandardCharsets.UTF_8);Fixa't en la comprovació del límit. Sempre que llegeixis una longitud que ve de la xarxa, valida-la abans de reservar memòria amb ella. És una de les vulnerabilitats més antigues i més repetides que existeixen.
- Tancament correcte i
try-with-resources
try-with-resourcesSocket implementa Closeable, així que va en un try-with-resources sense discussió:
try (Socket socket = new Socket()) {
socket.connect(desti, 3000);
// ... conversa ...
} // socket.close() garantit, tambe si salta una excepcioUn detall que estalvia codi: tancar el Socket tanca també els seus fluxos, i tancar qualsevol dels seus fluxos tanca el socket. Estan units. Per això no cal —ni convé— declarar el PrintWriter i el BufferedReader al try-with-resources:
// INNECESSARI i a mes enganyos: dona a entendre que son recursos
// independents quan en realitat els tres son el mateix socket.
try (Socket socket = new Socket();
PrintWriter escriptor = new PrintWriter(...);
BufferedReader lector = new BufferedReader(...)) {Pitjor encara, aquest codi té un problema real d'ordre: try-with-resources tanca en ordre invers, així que tancaria el lector, després l'escriptor —que intentarà buidar la seva memòria intermèdia sobre un socket ja tancat— i després el socket. Declara només el Socket.
Ara bé, hi ha una cosa que el tancament automàtic no fa bé per tu: buidar el que queda pendent abans de tancar quan l'ordre importa. close() sobre un BufferedWriter sí que buida; però si el socket es tanca primer per una altra via, el que estigui pendent es perd. Amb autoFlush i println no tens aquest problema, cosa que és una altra raó per fer-lo servir.
- Mitja connexió:
shutdownOutput i shutdownInput
shutdownOutput i shutdownInputUna connexió TCP té dos sentits independents, i es poden tancar per separat. És el que a 09-01 vam veure reflectit en el fet que el tancament són quatre missatges i no tres.
| Operació | Què fa | Què veu l'altre extrem |
|---|---|---|
socket.shutdownOutput() |
Tanca el teu sentit d'escriptura. Envia FIN | El seu read() retorna -1; el seu readLine() retorna null |
socket.shutdownInput() |
Tanca el teu sentit de lectura. El que arribi es descarta | Res immediat; si continua escrivint, acabarà amb error |
socket.close() |
Tanca els dos sentits i allibera el descriptor | El seu read() retorna -1, i les seves escriptures fallaran |
El cas d'ús clàssic de shutdownOutput és un protocol en què el client ho envia tot, després el servidor respon tot, i el senyal de "he acabat d'enviar" és el final del flux:
// Client que puja un fitxer al servidor i despres espera el resum.
try (Socket socket = new Socket()) {
socket.connect(desti, 3000);
// 1. Enviar el contingut complet.
try (OutputStream sortida = socket.getOutputStream()) {
// COMPTE: aquest try-with-resources tancaria el socket sencer. Malament.
}
}Escrit així està malament, precisament pel que hem vist: tancar el flux tanca el socket. La forma correcta és:
try (Socket socket = new Socket()) {
socket.connect(desti, 3000);
// 1. Enviar tot el contingut i BUIDAR.
OutputStream sortida = socket.getOutputStream();
Files.copy(Path.of("cataleg.csv"), sortida);
sortida.flush();
// 2. Dir "he acabat de parlar" SENSE tancar la connexio.
// El servidor veura fi de flux i sabra que pot processar.
socket.shutdownOutput();
// 3. Continuar escoltant: el sentit de lectura continua obert.
BufferedReader lector = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String resum;
while ((resum = lector.readLine()) != null) {
System.out.println("S: " + resum);
}
} // ara si, tancament completSense shutdownOutput(), el servidor esperaria més dades indefinidament i el client esperaria una resposta: el mateix interbloqueig de l'apartat 5, per una altra causa.
A BTCP/1 no el necessitem, perquè el protocol té una ordre explícita de comiat (SORTIR) i respostes delimitades. Però convé conèixer-lo, perquè molts protocols reals —i diversos exercicis d'aquest mòdul— el fan servir.
- Opcions del socket
Un socket té opcions que modifiquen el comportament de la pila TCP del sistema operatiu. Aquestes són les que importen de veritat:
| Opció | Mètode | Valor típic | Per a què |
|---|---|---|---|
| Temps límit de lectura | setSoTimeout(int ms) |
5.000-30.000 | La més important. Evita bloquejos eterns a read() |
| Sense retard (Nagle) | setTcpNoDelay(boolean) |
true en protocols interactius |
Envia immediatament els missatges petits |
| Mantenir viu | setKeepAlive(boolean) |
true en connexions llargues |
Detecta parells morts en connexions ocioses |
| Reutilitzar adreça | setReuseAddress(boolean) |
true en servidors |
Permet tornar a escoltar en un port en TIME_WAIT |
| Memòria de recepció | setReceiveBufferSize(int) |
per defecte | Ajust fi per a enllaços d'alta latència |
| Memòria d'enviament | setSendBufferSize(int) |
per defecte | Igual |
| Demora en tancar | setSoLinger(boolean, int) |
desactivat | Controla què passa amb les dades pendents en tancar |
| Prioritat de trànsit | setTrafficClass(int) |
per defecte | Suggeriment de qualitat de servei a la xarxa |
Tres avisos:
- Gairebé totes s'han de fixar abans de fer servir el socket, i algunes abans de connectar.
setSoTimeoutes pot canviar en qualsevol moment i afecta les lectures següents. - Les mides de memòria intermèdia són suggeriments. El sistema operatiu et pot donar una altra cosa; comprova amb
getReceiveBufferSize()el que realment tens. Llevat que estiguis optimitzant un enllaç concret d'alta latència i alta amplada de banda, no les toquis. setSoLingerés un parany. AmbsetSoLinger(true, 0)el tancament envia unRSTen lloc d'unFIN, descartant les dades pendents. Es fa servir de vegades per alliberar ports ràpid, però provocaSocketException: Connection reseta l'altre extrem i pèrdua de dades. No el facis servir llevat que sàpigues exactament per què.
setSoTimeout i SocketTimeoutException
setSoTimeout i SocketTimeoutExceptionÉs l'opció que més vegades et salvarà, així que mereix apartat propi.
Per defecte, un read() sobre un socket bloqueja indefinidament. Si l'altre extrem no envia res i tampoc no tanca —perquè se li ha acabat la bateria del portàtil, o perquè un tallafoc intermedi ha descartat la connexió sense avisar— el teu fil es queda aturat. Per sempre. Sense excepció.
socket.setSoTimeout(10_000); // 10 segons per lectura
try {
String linia = lector.readLine();
if (linia == null) {
System.out.println("L'altre extrem ha tancat ordenadament");
} else {
processar(linia);
}
} catch (SocketTimeoutException e) {
// Han passat 10 s sense arribar ni un sol byte.
// MOLT IMPORTANT: el socket CONTINUA VALID. Es pot reintentar la lectura.
System.out.println("Sense dades en 10 s; el socket continua utilitzable");
}Tres propietats que cal tenir clares:
- El termini és per operació de lectura, no total. Si demanes una línia llarga que arriba a poc a poc, el comptador es reinicia amb cada tros que arriba. Deu segons significa "deu segons sense rebre res", no "deu segons com a màxim".
- Després de l'excepció, el socket continua sent vàlid. A diferència de gairebé qualsevol altra excepció de xarxa,
SocketTimeoutExceptionno invalida la connexió: pots tornar a cridarreadLine(). Això permet el patró d'"espera amb comprovació periòdica":
socket.setSoTimeout(1000); // sondeig cada segon
while (!Thread.currentThread().isInterrupted() && executant) {
try {
String linia = lector.readLine();
if (linia == null) {
break; // l'altre ha tancat
}
processar(linia);
} catch (SocketTimeoutException e) {
// Un segon sense dades: aprofitem per comprovar
// el marcador de cancellacio del modul 8 i tornem a esperar.
continue;
}
}Aquest patró és el que fa que un socket bloquejant sigui cancel·lable, i connecta directament amb el protocol d'interrupció de 08-02: sense ell, un fil bloquejat a read() ignora les interrupcions —read() no és interrompible— i l'aturada ordenada de la teva aplicació es queda esperant un fil que no tornarà.
- Un valor de 0 significa infinit, que és el valor per defecte.
setSoTimeout(0)no és "sense espera": és "espera per sempre".
setTcpNoDelay i l'algorisme de Nagle
setTcpNoDelay i l'algorisme de NagleEl 1984, John Nagle va observar que les sessions interactives (teclejar en una consola remota) generaven un paquet de 41 bytes per cada caràcter premut: 1 byte de dades i 40 de capçaleres. Un malbaratament del 97 %.
La seva solució, l'algorisme de Nagle, està activada per defecte a totes les piles TCP i funciona així: si hi ha dades petites ja enviades i sense confirmar, no enviïs més dades petites; acumula-les fins que arribi la confirmació o fins a omplir un segment complet.
És una bona idea per a transferències massives. És un problema per a protocols de petició-resposta amb missatges petits, sobretot combinat amb una altra optimització anomenada confirmació retardada (el receptor espera fins a 200 ms abans de confirmar, per si pot confirmar diverses coses juntes). Les dues juntes produeixen retards artificials de desenes o centenars de mil·lisegons:
Sense TCP_NODELAY, protocol interactiu:
Client: envia "CONSULTA 111\n" (13 bytes) --> surt de seguida (no hi ha res pendent)
Servidor: rep, processa, respon
Client: envia "LLISTA\n" (7 bytes) --> ESPERA: hi ha un enviament sense confirmar
... fins a 200 ms de retard artificialPer a BTCP/1, que és exactament un protocol interactiu de missatges curts, el correcte és desactivar-lo:
El nom confon: setTcpNoDelay(true) significa "activa l'opció NODELAY", és a dir, desactiva l'algorisme de Nagle. true = enviar immediatament.
| Tipus de trànsit | setTcpNoDelay |
Motiu |
|---|---|---|
| Petició-resposta interactiva (BTCP, Redis, jocs) | true |
La latència importa més que l'eficiència |
| Transferència de fitxers grans | false (per defecte) |
Els segments ja van plens; Nagle no fa nosa |
| Streaming en temps real | true |
Igual que l'interactiu |
Nota. Nagle només actua sobre dades que tu no has agrupat. Si el teu codi ja escriu cada missatge de cop amb un
flush()per missatge —com faautoFlush—, i els missatges són grans, Nagle amb prou feines es nota. El cas dolorós és escriure un missatge en diversos trossos petits amb diversosflush(). La conclusió pràctica: agrupa tu el que puguis i posasetTcpNoDelay(true)en protocols interactius.
- Les excepcions de xarxa i què significa cadascuna
Totes descendeixen d'IOException, així que un sol catch (IOException e) les captura totes. Però tractar-les per igual malbarata informació valuosíssima: cadascuna et diu alguna cosa diferent sobre el que ha passat, i sobre si té sentit reintentar.
graph TD
A["IOException"] --> B["SocketException"]
A --> C["UnknownHostException"]
A --> D["EOFException"]
A --> E["InterruptedIOException"]
B --> F["ConnectException"]
B --> G["BindException"]
B --> H["NoRouteToHostException"]
E --> I["SocketTimeoutException"]
| Excepció | Què ha passat realment | Reintentar? | Reacció recomanada |
|---|---|---|---|
UnknownHostException |
El nom no resol: no existeix o el DNS falla | No (llevat de fallada temporal de DNS) | Error de configuració. Registrar i avisar l'usuari |
ConnectException |
Vas arribar a la màquina i va respondre RST: no hi ha ningú escoltant en aquest port |
No immediatament | El servidor no està arrencat o és un altre port |
SocketTimeoutException (en connectar) |
Ningú no ha respost al SYN: màquina caiguda o tallafoc que descarta |
Sí, amb espera creixent | Transitori. Reintentar 2-3 vegades |
SocketTimeoutException (en llegir) |
Connexió viva però l'altre no envia res | Depèn | El socket continua vàlid: reintentar la lectura o abandonar |
NoRouteToHostException |
No hi ha camí cap a aquella xarxa | No | Problema d'encaminament o tallafoc |
BindException |
El port local ja està ocupat | No | ss -tlnp per veure qui el té (típic en servidors, 09-03) |
SocketException: Connection reset |
L'altre extrem ha enviat RST: ha caigut, o ha tancat bruscament, o ha rebutjat el que s'ha enviat |
Sí, un cop | La connexió és morta: cal obrir-ne una altra |
SocketException: Broken pipe |
Has escrit en una connexió que l'altre ja ha tancat | Sí, un cop | Igual |
EOFException |
Fi de flux inesperat llegint amb DataInputStream |
No en aquell socket | L'altre ha tancat a mitja missatge: dades incompletes |
I una distinció que cal tenir molt clara:
| Situació | Com es manifesta |
|---|---|
| L'altre ha tancat ordenadament | read() retorna -1; readLine() retorna null. No hi ha excepció |
| L'altre ha caigut o ha tancat bruscament | SocketException: Connection reset |
| L'altre és viu però callat | SocketTimeoutException, si has posat setSoTimeout |
| L'altre és viu però callat i no hi has posat temps límit | Res. Silenci. El teu fil bloquejat per sempre |
Gestió completa, aplicant l'estratègia per capes de 06-07:
package com.nexussoftware.bibliotech.xarxa;
import com.nexussoftware.bibliotech.excepcio.BiblioTechException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Tradueix les excepcions de xarxa a excepcions del domini de BiblioTech
* i decideix si la fallada es transitoria (mereix reintent) o permanent.
* Aquesta es la FRONTERA D'ERRORS de 06-07 aplicada a la xarxa.
*/
public final class TraductorErrorsXarxa {
private static final Logger LOG = Logger.getLogger(TraductorErrorsXarxa.class.getName());
private TraductorErrorsXarxa() {
}
/** Resultat de classificar una fallada de xarxa. */
public record Classificacio(String missatgeUsuari, boolean transitori) {
}
public static Classificacio classificar(IOException e, String desti) {
if (e instanceof UnknownHostException) {
LOG.severe("Nom no resoluble: " + desti);
return new Classificacio(
"No es troba el servidor de cataleg. Revisa la configuracio.", false);
}
if (e instanceof ConnectException) {
// Compte amb l'ordre: ConnectException es subclasse de SocketException,
// aixi que s'ha de comprovar ABANS.
LOG.severe("Connexio rebutjada per " + desti + ": el servidor no esta arrencat");
return new Classificacio(
"El servidor de cataleg no esta disponible.", false);
}
if (e instanceof SocketTimeoutException) {
LOG.warning("Temps esgotat amb " + desti);
return new Classificacio(
"El servidor de cataleg triga massa a respondre.", true);
}
if (e instanceof NoRouteToHostException) {
LOG.severe("Sense ruta cap a " + desti);
return new Classificacio(
"No hi ha connexio amb la xarxa del servidor.", false);
}
if (e instanceof SocketException) {
// "Connection reset", "Broken pipe" i similars.
LOG.warning("Connexio trencada amb " + desti + ": " + e.getMessage());
return new Classificacio(
"S'ha perdut la connexio amb el cataleg.", true);
}
LOG.log(Level.SEVERE, "Fallada de xarxa no classificada amb " + desti, e);
return new Classificacio("Error de comunicacio amb el cataleg.", false);
}
/** Converteix la fallada en l'excepcio de domini, conservant la causa. */
public static BiblioTechException traduir(IOException e, String desti) {
Classificacio c = classificar(e, desti);
return new BiblioTechException(c.missatgeUsuari(), e);
}
}Fixa't en l'ordre de les comprovacions: ConnectException i BindException són subclasses de SocketException, així que si comprovessis SocketException primer, les capturaries totes allà i perdries la distinció. El mateix passaria amb un catch escrit en mal ordre — amb la diferència que allà el compilador t'avisaria; aquí no.
- Dades binàries amb
DataOutputStream i DataInputStream
DataOutputStream i DataInputStreamBTCP/1 és de text, però no tots els protocols ho són. Quan cal enviar números, booleans o blocs binaris, DataOutputStream i DataInputStream del mòdul 7 funcionen sobre un socket exactament igual que sobre un fitxer, i amb un avantatge afegit: escriuen en big-endian, l'ordre de bytes de xarxa, així que interoperen amb programes escrits en altres llenguatges sense conversió.
package com.nexussoftware.bibliotech.xarxa;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* Variant binaria de l'enviament d'una fitxa de material.
* Es fa servir quan el volum importa mes que la llegibilitat.
*/
public final class ProtocolBinari {
/** Limit defensiu: cap camp de text legitim no passa d'aixo. */
private static final int MAXIM_TEXT = 4096;
private ProtocolBinari() {
}
public static void enviarFitxa(Socket socket, String isbn, String titol,
int pagines, boolean disponible) throws IOException {
// Fem servir memoria intermedia: sense BufferedOutputStream, cada writeInt
// seria potencialment una operacio de xarxa independent.
DataOutputStream sortida = new DataOutputStream(
new BufferedOutputStream(socket.getOutputStream()));
escriureText(sortida, isbn); // longitud (int) + bytes UTF-8
escriureText(sortida, titol);
sortida.writeInt(pagines); // 4 bytes, big-endian
sortida.writeBoolean(disponible); // 1 byte
sortida.flush(); // imprescindible, com sempre
}
public static String rebreTitol(Socket socket) throws IOException {
DataInputStream entrada = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
String isbn = llegirText(entrada);
String titol = llegirText(entrada);
int pagines = entrada.readInt();
boolean disponible = entrada.readBoolean();
return titol + " (" + isbn + ", " + pagines + " pag., "
+ (disponible ? "disponible" : "prestat") + ")";
}
/** Escriu un text amb longitud previa: 4 bytes de mida + bytes UTF-8. */
private static void escriureText(DataOutputStream sortida, String text)
throws IOException {
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
sortida.writeInt(bytes.length);
sortida.write(bytes);
}
/** Llegeix un text amb longitud previa, VALIDANT la longitud declarada. */
private static String llegirText(DataInputStream entrada) throws IOException {
int longitud = entrada.readInt();
// Sense aquesta comprovacio, un client malicios envia l'int 2_000_000_000
// i provoquem un OutOfMemoryError amb una peticio de 4 bytes.
if (longitud < 0 || longitud > MAXIM_TEXT) {
throw new IOException("Longitud de text invalida: " + longitud);
}
// readNBytes llegeix EXACTAMENT n bytes o llanca EOFException si el
// flux acaba abans. Un read(byte[]) sol podria llegir-ne menys
// i deixar-te amb dades incompletes sense assabentar-te'n: error classic.
byte[] bytes = entrada.readNBytes(longitud);
if (bytes.length < longitud) {
throw new java.io.EOFException("Flux acabat a mitja text");
}
return new String(bytes, StandardCharsets.UTF_8);
}
}Els mètodes de DataOutputStream escriuen mides fixes i conegudes, cosa que fa el protocol predictible:
| Mètode | Bytes | Notes |
|---|---|---|
writeByte |
1 | |
writeShort |
2 | big-endian |
writeInt |
4 | big-endian |
writeLong |
8 | big-endian |
writeFloat / writeDouble |
4 / 8 | IEEE 754 |
writeBoolean |
1 | 0 o 1 |
writeUTF |
2 + n | UTF modificat, màxim 65535 bytes |
Sobre
writeUTF/readUTF. Són còmodes perquè fan la longitud prèvia per tu, però fan servir una variant pròpia d'UTF-8 ("UTF modificat") que no és UTF-8 estàndard, i estan limitats a 65535 bytes. Interoperen bé entre programes Java, i malament amb qualsevol altre llenguatge. Si l'altre extrem pot no ser Java, fes la longitud prèvia a mà com a l'exemple.
- Per què no enviar objectes serialitzats a un client no fiable
A 07-05 vas aprendre Serializable, serialVersionUID i els riscos de deserialitzar dades no fiables. És temptador aplicar la serialització als sockets, perquè sembla que ho resol tot de cop:
// TEMPTADOR I PERILLOS. No facis aixo en un servidor de xarxa.
ObjectOutputStream sortida = new ObjectOutputStream(socket.getOutputStream());
sortida.writeObject(llibre);
sortida.flush();
// I a l'altre costat:
ObjectInputStream entrada = new ObjectInputStream(socket.getInputStream());
Llibre llibre = (Llibre) entrada.readObject(); // <-- AQUI HI HA EL FORATEl problema és que readObject() construeix objectes arbitraris abans que puguis comprovar de quin tipus són. La conversió (Llibre) passa després d'haver deserialitzat. Si un atacant envia un flux acuradament construït, readObject instanciarà les classes que ell indiqui i executarà el seu codi de deserialització (readObject privat, readResolve, finalize) — fent servir classes que ja són al teu classpath. És la família de vulnerabilitats coneguda com a gadget chains, i ha causat forats d'execució remota de codi en productes molt coneguts.
| Risc | Descripció |
|---|---|
| Execució de codi | Cadenes de classes del classpath encadenades per executar ordres del sistema |
| Denegació de servei | Un flux petit que expandeix a estructures gegants o objectes amb hashCode quadràtic |
| Acoblament | Els dos extrems han de tenir les mateixes classes i versions compatibles |
| No interoperable | Només Java parla aquest format |
Regles pràctiques:
- No deserialitzis mai objectes Java que vinguin d'una xarxa no fiable. Ni tan sols d'una xarxa interna: la xarxa interna de Nexus Software inclou el portàtil que algú va connectar al Wi-Fi de convidats.
- Fes servir un format de dades, no un format d'objectes: text delimitat (com BTCP/1), CSV, JSON o binari amb longitud prèvia. Tots ells produeixen dades, que després tu converteixes en objectes amb codi teu i validat.
- Si per herència no tens més remei, fes servir
ObjectInputFilter(Java 9+) per restringir quines classes es permet deserialitzar:
ObjectInputStream entrada = new ObjectInputStream(socket.getInputStream());
// Llista blanca estricta: nomes aquestes classes, maxim 1000 objectes,
// maxim 100 KB, profunditat maxima 10. Tota la resta, rebutjat.
entrada.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
"com.nexussoftware.bibliotech.domini.Llibre;"
+ "java.lang.String;"
+ "maxdepth=10;maxarray=1000;maxbytes=102400;"
+ "!*")); // el !* final rebutja tot el que no esta llistatTot i així, la recomanació oficial d'Oracle i del mateix equip de Java és clara: la serialització nativa no s'ha de fer servir com a protocol de xarxa. BiblioTech no la farà servir. BTCP/1 envia text, i el text es converteix en Llibre amb un mètode teu que valida cada camp.
- BiblioTech: el client de catàleg complet
És hora de posar-ho tot junt. Escriurem ClientCataleg, la classe que permet a qualsevol lloc de treball de Nexus Software parlar BTCP/1 amb el servidor.
Disseny
sequenceDiagram
participant M as MenuBiblioTech
participant C as ClientCataleg
participant S as Servidor BTCP/1
M->>C: connectar()
C->>S: TCP connect (3 s de limit)
S-->>C: 200 BIBLIOTECH BTCP/1
C->>C: comprova la salutacio i la versio
M->>C: consultar("978-0000000001")
C->>S: CONSULTA 978-0000000001
S-->>C: 200 OK 978-...|Java Eficac|LLIBRE|true
C-->>M: Resultat.exit(Fitxa)
M->>C: close()
C->>S: SORTIR
S-->>C: 221 ADEU
C->>C: socket.close()
ClientCataleg implementa AutoCloseable, així que es fa servir en un try-with-resources com qualsevol altre recurs de BiblioTech, i el seu close() és cortès: s'acomiada amb SORTIR abans de tancar.
package com.nexussoftware.bibliotech.xarxa;
import com.nexussoftware.bibliotech.excepcio.BiblioTechException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Client del protocol BTCP/1 de BiblioTech.
*
* Parla amb el servidor de cataleg definit a 09-01 i implementat a 09-03.
* Implementa AutoCloseable: s'acomiada amb SORTIR i tanca el socket.
*
* NO es segur per a diversos fils: un socket es una conversa, i dos fils
* escrivint alhora entrellacarien les seves peticions. Un fil, un client.
*/
public class ClientCataleg implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(ClientCataleg.class.getName());
private static final int LIMIT_CONNEXIO_MS = 3_000;
private static final int LIMIT_LECTURA_MS = 10_000;
private static final String VERSIO_ESPERADA = "BTCP/1";
/** Sostre de linies d'una resposta multiple: defensa contra un servidor hostil. */
private static final int MAXIM_LINIES = 10_000;
private final String host;
private final int port;
private Socket socket;
private BufferedReader lector;
private PrintWriter escriptor;
public ClientCataleg(String host, int port) {
this.host = host;
this.port = port;
}
/** Fitxa d'un material tal com la retorna el protocol. */
public record FitxaXarxa(String isbn, String titol, String tipus, boolean disponible) {
/** Converteix una linia "isbn|titol|tipus|disponible" en una fitxa. */
static FitxaXarxa desdeLinia(String linia) throws BiblioTechException {
// -1 a split conserva els camps buits del final;
// sense ell, "111|Titol|LLIBRE|" donaria 3 camps en lloc de 4.
String[] camps = linia.split("\\|", -1);
if (camps.length != 4) {
throw new BiblioTechException(
"Resposta del servidor amb format invalid: " + linia);
}
return new FitxaXarxa(camps[0], camps[1], camps[2],
Boolean.parseBoolean(camps[3]));
}
}
// ---------------------------------------------------------------
// Connexio
// ---------------------------------------------------------------
/** Connecta i verifica la salutacio del servidor. */
public void connectar() throws BiblioTechException {
try {
socket = new Socket();
// Protocol interactiu de missatges curts: sense Nagle.
socket.setTcpNoDelay(true);
// Temps limit de CONNEXIO: mai el constructor que connecta.
socket.connect(new InetSocketAddress(host, port), LIMIT_CONNEXIO_MS);
// Temps limit de LECTURA: diferent de l'anterior, i igual d'obligatori.
socket.setSoTimeout(LIMIT_LECTURA_MS);
// La pila de fluxos del modul 7, amb charset EXPLICIT en tots dos sentits.
escriptor = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(),
StandardCharsets.UTF_8)),
true); // autoFlush: cada println() s'envia
lector = new BufferedReader(
new InputStreamReader(socket.getInputStream(),
StandardCharsets.UTF_8));
// El servidor parla primer: comprovem que es qui diem
// i que parla la nostra versio abans d'enviar res.
String salutacio = lector.readLine();
if (salutacio == null) {
throw new BiblioTechException(
"El servidor ha tancat la connexio sense saludar");
}
if (!salutacio.startsWith("200 ") || !salutacio.contains(VERSIO_ESPERADA)) {
throw new BiblioTechException(
"Salutacio inesperada del servidor: '" + salutacio + "'");
}
LOG.info(() -> "Connectat al cataleg " + host + ":" + port
+ " (" + salutacio + ")");
} catch (IOException e) {
tancarSocketSilenciosament();
throw TraductorErrorsXarxa.traduir(e, host + ":" + port);
}
}
// ---------------------------------------------------------------
// Operacions del protocol
// ---------------------------------------------------------------
/** CONSULTA <isbn>. Retorna null si el material no existeix (404). */
public FitxaXarxa consultar(String isbn) throws BiblioTechException {
validarArgument(isbn, "l'ISBN");
String resposta = intercanviar("CONSULTA " + isbn);
if (resposta.startsWith("200 OK ")) {
return FitxaXarxa.desdeLinia(resposta.substring("200 OK ".length()));
}
if (resposta.startsWith("404 ")) {
return null; // "no trobat" es un resultat, no un error
}
throw errorDeProtocol(resposta);
}
/** LLISTA. Retorna totes les fitxes del cataleg. */
public List<FitxaXarxa> llistar() throws BiblioTechException {
String capcalera = intercanviar("LLISTA");
if (!capcalera.startsWith("201 LLISTA ")) {
throw errorDeProtocol(capcalera);
}
int anunciades = enterDe(capcalera.substring("201 LLISTA ".length()).strip(), capcalera);
if (anunciades < 0 || anunciades > MAXIM_LINIES) {
throw new BiblioTechException(
"El servidor anuncia un nombre de linies fora de rang: " + anunciades);
}
List<FitxaXarxa> fitxes = new ArrayList<>(anunciades);
try {
String linia;
while ((linia = lector.readLine()) != null) {
if (linia.equals(".")) {
break; // fi de la resposta multiple
}
if (fitxes.size() >= MAXIM_LINIES) {
throw new BiblioTechException(
"El servidor envia mes linies de les permeses");
}
fitxes.add(FitxaXarxa.desdeLinia(linia));
}
if (linia == null) {
throw new BiblioTechException(
"El servidor ha tancat la connexio a mitja llista");
}
} catch (SocketTimeoutException e) {
throw new BiblioTechException(
"El servidor ha deixat de respondre a mitja llista", e);
} catch (IOException e) {
throw TraductorErrorsXarxa.traduir(e, host + ":" + port);
}
// El nombre anunciat es redundant a proposit: si no quadra,
// hi ha un problema i es millor saber-ho que ignorar-ho.
if (fitxes.size() != anunciades) {
LOG.warning("El servidor va anunciar " + anunciades + " linies i n'ha enviat "
+ fitxes.size());
}
return fitxes;
}
/** PRESTAR <isbn> <empleat>. Retorna true si s'ha registrat el prestec. */
public boolean prestar(String isbn, String empleat) throws BiblioTechException {
validarArgument(isbn, "l'ISBN");
validarArgument(empleat, "l'empleat");
String resposta = intercanviar("PRESTAR " + isbn + " " + empleat);
if (resposta.startsWith("200 ")) {
return true;
}
if (resposta.startsWith("409 ") || resposta.startsWith("404 ")) {
LOG.info(() -> "Prestec rebutjat: " + resposta);
return false;
}
throw errorDeProtocol(resposta);
}
// ---------------------------------------------------------------
// Nucli: enviar una linia i llegir una resposta
// ---------------------------------------------------------------
private String intercanviar(String peticio) throws BiblioTechException {
if (socket == null || socket.isClosed()) {
throw new BiblioTechException("El client no esta connectat");
}
try {
// El protocol especifica \n, no el separador de la plataforma:
// per aixo l'escrivim a ma en lloc de fer servir println(String).
escriptor.print(peticio + "\n");
escriptor.flush(); // OBLIGATORI: print() no dispara autoFlush
// PrintWriter s'empassa les IOException; checkError() es l'unica
// forma d'assabentar-se que l'escriptura ha fallat.
if (escriptor.checkError()) {
throw new BiblioTechException(
"Fallada en enviar la peticio al servidor de cataleg");
}
String resposta = lector.readLine();
if (resposta == null) {
throw new BiblioTechException(
"El servidor ha tancat la connexio sense respondre a: " + peticio);
}
LOG.fine(() -> "C: " + peticio + " -> S: " + resposta);
return resposta;
} catch (SocketTimeoutException e) {
throw new BiblioTechException(
"El servidor no ha respost en " + LIMIT_LECTURA_MS + " ms", e);
} catch (IOException e) {
throw TraductorErrorsXarxa.traduir(e, host + ":" + port);
}
}
// ---------------------------------------------------------------
// Validacio i utilitats
// ---------------------------------------------------------------
/**
* Un argument amb salt de linia trenca el protocol: injectaria
* una peticio addicional. Validar la SORTIDA es tan important com
* validar l'entrada.
*/
private void validarArgument(String valor, String nom) throws BiblioTechException {
if (valor == null || valor.isBlank()) {
throw new BiblioTechException("Falta " + nom);
}
if (valor.indexOf('\n') >= 0 || valor.indexOf('\r') >= 0) {
throw new BiblioTechException(
"Valor invalid per a " + nom + ": conte salts de linia");
}
}
private int enterDe(String text, String context) throws BiblioTechException {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
throw new BiblioTechException(
"Numero invalid a la resposta del servidor: '" + context + "'", e);
}
}
private BiblioTechException errorDeProtocol(String resposta) {
return new BiblioTechException(
"Resposta inesperada del servidor de cataleg: " + resposta);
}
private void tancarSocketSilenciosament() {
if (socket != null) {
try {
socket.close();
} catch (IOException ignorada) {
// Tancant ja no hi ha res a salvar.
}
}
}
// ---------------------------------------------------------------
// Tancament
// ---------------------------------------------------------------
@Override
public void close() {
if (socket == null || socket.isClosed()) {
return;
}
try {
// Comiat cortes: el servidor pot distingir un tancament
// ordenat d'una caiguda i alliberar recursos amb tranquillitat.
escriptor.print("SORTIR\n");
escriptor.flush();
// Li donem poc marge: si no contesta, tanquem igualment.
socket.setSoTimeout(1_000);
String adeu = lector.readLine();
LOG.fine(() -> "Comiat del servidor: " + adeu);
} catch (IOException e) {
// En tancar, una fallada no canvia res: es registra i es continua.
LOG.log(Level.FINE, "Fallada durant el comiat", e);
} finally {
tancarSocketSilenciosament();
LOG.info(() -> "Connexio amb " + host + ":" + port + " tancada");
}
}
}Programa de prova
package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.excepcio.BiblioTechException;
import com.nexussoftware.bibliotech.xarxa.ClientCataleg;
import com.nexussoftware.bibliotech.xarxa.ClientCataleg.FitxaXarxa;
import java.util.List;
/** Prova manual del client BTCP/1 contra un servidor. */
public class ProvaClientCataleg {
public static void main(String[] args) {
String host = args.length > 0 ? args[0] : "localhost";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 9090;
// ClientCataleg es AutoCloseable: try-with-resources com sempre.
try (ClientCataleg client = new ClientCataleg(host, port)) {
client.connectar();
System.out.println("--- CONSULTA d'un ISBN existent ---");
FitxaXarxa fitxa = client.consultar("978-0000000001");
if (fitxa == null) {
System.out.println("No trobat");
} else {
System.out.printf("%s - %s [%s] %s%n",
fitxa.isbn(), fitxa.titol(), fitxa.tipus(),
fitxa.disponible() ? "disponible" : "prestat");
}
System.out.println();
System.out.println("--- CONSULTA d'un ISBN inexistent ---");
System.out.println(client.consultar("000-0000000000") == null
? "No trobat (correcte)" : "Inesperat");
System.out.println();
System.out.println("--- LLISTA ---");
List<FitxaXarxa> cataleg = client.llistar();
for (FitxaXarxa f : cataleg) {
System.out.println(" " + f.isbn() + " " + f.titol());
}
System.out.println("Total: " + cataleg.size());
System.out.println();
System.out.println("--- PRESTAR ---");
boolean ok = client.prestar("978-0000000002", "Diego_Alonso");
System.out.println(ok ? "Prestec registrat" : "Prestec rebutjat");
} catch (BiblioTechException e) {
// Frontera d'errors: aqui ja no hi ha sockets, nomes domini.
System.err.println("ERROR: " + e.getMessage());
if (e.getCause() != null) {
System.err.println("Causa tecnica: " + e.getCause());
}
}
}
}Com provar-ho sense servidor: nc com a servidor manual
El servidor de veritat s'escriu a 09-03. Mentrestant, fes tu de servidor. Aquesta és la millor forma d'entendre un protocol, perquè veus la conversa lletra a lletra.
Terminal 1 — actua de servidor al port 9090:
(En algunes versions de netcat cal nc -l -p 9090. Si no tens nc, ncat d'Nmap o socat -v TCP-LISTEN:9090,reuseaddr - serveixen igual.)
Terminal 2 — llança el client:
javac -d classes $(find src -name "*.java")
java -cp classes com.nexussoftware.bibliotech.presentacio.ProvaClientCatalegAra, a la terminal 1, veuràs aparèixer el que envia el client i podràs teclejar les respostes a mà. La sessió completa és aquesta (el que tecleges tu va marcat):
TERMINAL 1 (tu, fent de servidor amb nc -l 9090)
-----------------------------------------------------
200 BIBLIOTECH BTCP/1 <-- TECLEGES TU (i Enter)
CONSULTA 978-0000000001 (l'envia el client)
200 OK 978-0000000001|Java Eficac|LLIBRE|true <-- TECLEGES TU
CONSULTA 000-0000000000
404 NO TROBAT 000-0000000000 <-- TECLEGES TU
LLISTA
201 LLISTA 3 <-- TECLEGES TU
978-0000000001|Java Eficac|LLIBRE|true <-- TECLEGES TU
978-0000000002|Patrons de Disseny|LLIBRE|false <-- TECLEGES TU
978-0000000003|Refactoritzacio|LLIBRE|true <-- TECLEGES TU
. <-- TECLEGES TU
PRESTAR 978-0000000002 Diego_Alonso
409 NO DISPONIBLE 978-0000000002 <-- TECLEGES TU
SORTIR
221 ADEU <-- TECLEGES TUTERMINAL 2 (el client Java)
-----------------------------------------------------
--- CONSULTA d'un ISBN existent ---
978-0000000001 - Java Eficac [LLIBRE] disponible
--- CONSULTA d'un ISBN inexistent ---
No trobat (correcte)
--- LLISTA ---
978-0000000001 Java Eficac
978-0000000002 Patrons de Disseny
978-0000000003 Refactoritzacio
Total: 3
--- PRESTAR ---
Prestec rebutjatExperiments que val la pena fer ara mateix, perquè ensenyen més que deu pàgines de teoria:
- No teclegis la salutació. El client esperarà 10 segons i fallarà amb "El servidor no ha respost". És
setSoTimeoutfent la seva feina: sense ell, esperaria per sempre. - Tecleja una salutació dolenta, per exemple
HOLA. El client rebutja la connexió amb "Salutació inesperada". Verificar la versió abans de parlar evita converses absurdes amb el servidor equivocat. - Tanca
ncamb Ctrl+C a mitja llista. El client detecta elnulldereadLine()i dóna "El servidor ha tancat la connexió a mitja llista", no unNullPointerException. - No arrenquis
ncen absolut. Obtens "El servidor de catàleg no està disponible", que és la traducció deConnectException. Compara-la amb connectar a una IP inexistent de la teva xarxa (192.168.1.250), que dóna temps esgotat després de 3 segons: rebutjat i sense resposta són coses diferents, i ara les distingeixes. - Tecleja un títol amb accents, com
Refactorització. Arriba bé perquè els dos extrems fan servir UTF-8. Prova d'arrencar la JVM amb-Dfile.encoding=ISO-8859-1i veuràs que continua arribant bé, precisament perquè el charset és explícit al codi i no depèn de la plataforma. Aquest és el motiu de la regla del mòdul 7.
Errors Comuns i Consells
No buidar la memòria intermèdia després d'enviar. El bug número u, ja demostrat. Símptoma: el programa es penja sense error. Fes servir autoFlush amb println, o flush() explícit. I recorda que autoFlush no actua amb print().
Fer servir new Socket(host, port) en producció. No admet temps límit de connexió i pot bloquejar més d'un minut. Fes servir sempre new Socket() + connect(addr, ms).
Confondre el temps límit de connexió amb el de lectura. Són dues coses diferents i necessites totes dues. connect(addr, 3000) no et protegeix d'un read() etern.
No posar setSoTimeout. Converteix qualsevol problema de l'altre extrem en un fil bloquejat per sempre. En un servidor amb pool acotat, uns quants clients zombis esgoten el pool i el servei mor en silenci, sense ni una sola línia d'error.
Confiar en isConnected(). Retorna true des que es va connectar, encara que l'altre extrem porti hores apagat. L'única forma de saber si continua viu és intentar fer servir el socket.
No comprovar el null de readLine(). Produeix NullPointerException amb el primer client que es desconnecti. El while ((linia = lector.readLine()) != null) és obligatori.
Fer servir el charset per defecte. new InputStreamReader(socket.getInputStream()) sense charset funciona a la teva màquina i corromp els accents tan bon punt l'altre extrem té una altra configuració. Sempre StandardCharsets.UTF_8.
Suposar que un read() retorna el missatge complet. TCP no té fronteres de missatge. O fas servir readLine(), que ja ho resol, o implementes longitud prèvia. No suposis mai que un enviament equival a una lectura.
Llegir sense límit de mida. Un readLine() sobre una línia de cent megabytes esgota la memòria, i un readInt() de longitud seguit d'un new byte[longitud] sense validar és una denegació de servei de quatre bytes. Valida sempre les longituds que vénen de la xarxa.
Declarar els fluxos al try-with-resources juntament amb el socket. Tanca en ordre invers i l'escriptor intenta buidar-se sobre un socket ja tancat. Declara només el Socket.
Enviar objectes serialitzats per la xarxa. readObject() construeix objectes abans que puguis validar-los. Envia dades, no objectes.
No validar el que escrius al protocol. Si el nom d'empleat porta un salt de línia, injecta una petició extra. Validar la sortida és tan necessari com validar l'entrada — és el mateix tipus de fallada que una injecció SQL.
Consell d'or per depurar. Quan alguna cosa no funcioni, posa't al mig. nc -l 9090 fa de servidor i t'ensenya exactament què envia el teu client, byte a byte. Si la teva petició no hi apareix, no l'has buidada. Si apareix deformada, tens un problema de charset o de delimitador. Dos minuts amb nc estalvien dues hores de System.out.println.
Exercicis
Exercici 1: Client d'eco amb mesura de latència
Escriu una classe ClientEco que es connecti a un servidor d'eco (un que retorna tal qual el que rep), li enviï N línies i mesuri el temps d'anada i tornada de cadascuna.
Requisits:
- Temps límit de connexió de 2 s i de lectura de 5 s.
setTcpNoDelay(true), i un mode alternatiu ambfalseper comparar.- Ha d'informar de latència mínima, mitjana i màxima en mil·lisegons amb dos decimals.
- Ha de verificar que el que s'ha rebut coincideix amb el que s'ha enviat i comptar les discrepàncies.
- Gestió diferenciada de
ConnectException,SocketTimeoutExceptioni la resta.
Prova'l contra nc -l 9090 (hauràs de fer d'eco a mà) o, millor, contra un servidor d'eco d'una línia: while true; do nc -l 9090 -c cat; done a Linux.
Exercici 2: Detector de serveis
Escriu una classe DetectorServeis amb un mètode escanejar(String host, int inici, int fi, int limitMs) que comprovi quins ports d'un rang tenen alguna cosa escoltant, i intenti identificar el servei.
Requisits:
- Per a cada port, intenta connectar amb temps límit curt (200-500 ms).
- Classifica el resultat en tres estats:
OBERT(ha connectat),TANCAT(ConnectException) iFILTRAT(temps esgotat, típic d'un tallafoc que descarta). - Per als oberts, intenta llegir una línia de salutació durant 500 ms; si el servei saluda (SMTP, FTP, SSH i BTCP ho fan), mostra-la.
- Mostra el nom habitual del servei segons la taula de ports ben coneguts de 09-01.
- Imprimeix una taula i un resum.
Prova'l contra localhost amb un nc -l 9090 aixecat, i explica en un comentari per què escanejar ports de màquines alienes sense permís és, a més de descortès, il·legal a molts països.
Exercici 3: Transferència binària amb shutdownOutput
Escriu un parell client/servidor mínim per transferir el fitxer cataleg.csv de BiblioTech:
EmissorFitxer: connecta, envia el nom del fitxer i la seva mida ambDataOutputStream(longitud prèvia, validada), després el contingut en blocs de 8 KB, cridashutdownOutput()i espera una línia de resum del receptor.ReceptorFitxer: amb unServerSocketmínim (pots anticipar el just de 09-03:new ServerSocket(9091)iaccept()), llegeix el nom i la mida, valida que la mida sigui raonable (màxim 10 MB) i que el nom no contingui/ni..(recorregut de camins), desa el contingut arebuts/amb NIO.2 i respon amb un resum.
Requisits: charset explícit on hi hagi text, validació estricta de tot el que arriba, try-with-resources i logging amb java.util.logging. Explica en un comentari per què és imprescindible el shutdownOutput().
Solucions
Solució 1
package com.nexussoftware.bibliotech.xarxa;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
/**
* Client d'eco que mesura la latencia d'anada i tornada.
* Serveix per comparar l'efecte de setTcpNoDelay i per mesurar la xarxa.
*/
public class ClientEco {
private final String host;
private final int port;
private final boolean senseNagle;
public ClientEco(String host, int port, boolean senseNagle) {
this.host = host;
this.port = port;
this.senseNagle = senseNagle;
}
/** Resultat d'una tanda de mesures. */
public record Mesura(int enviades, int discrepancies,
double minMs, double mitjanaMs, double maxMs) {
}
public Mesura mesurar(int repeticions) throws IOException {
try (Socket socket = new Socket()) {
// Opcions ABANS de fer servir el socket.
socket.setTcpNoDelay(senseNagle);
socket.connect(new InetSocketAddress(host, port), 2_000);
socket.setSoTimeout(5_000);
PrintWriter escriptor = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(),
StandardCharsets.UTF_8)),
true);
BufferedReader lector = new BufferedReader(
new InputStreamReader(socket.getInputStream(),
StandardCharsets.UTF_8));
double min = Double.MAX_VALUE;
double max = 0;
double suma = 0;
int discrepancies = 0;
int completades = 0;
for (int i = 1; i <= repeticions; i++) {
String missatge = "PING-" + i + "-BiblioTech";
long t0 = System.nanoTime();
escriptor.println(missatge); // autoFlush: s'envia ja
String eco = lector.readLine(); // esperem el retorn
long t1 = System.nanoTime();
if (eco == null) {
System.out.println("El servidor ha tancat rere " + completades + " ecos");
break;
}
double ms = (t1 - t0) / 1_000_000.0;
min = Math.min(min, ms);
max = Math.max(max, ms);
suma += ms;
completades++;
// L'eco ha de ser identic: si no, hi ha un problema de
// charset, de delimitador o de desincronitzacio del protocol.
if (!missatge.equals(eco)) {
discrepancies++;
System.out.println(" DISCREPANCIA: he enviat '" + missatge
+ "' i he rebut '" + eco + "'");
}
System.out.printf(" %2d) %.2f ms%n", i, ms);
}
if (completades == 0) {
return new Mesura(0, 0, 0, 0, 0);
}
return new Mesura(completades, discrepancies, min, suma / completades, max);
}
}
public static void main(String[] args) {
String host = args.length > 0 ? args[0] : "localhost";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 9090;
for (boolean senseNagle : new boolean[]{true, false}) {
System.out.println("=== setTcpNoDelay(" + senseNagle + ") ===");
try {
Mesura m = new ClientEco(host, port, senseNagle).mesurar(10);
System.out.printf(
" Ecos: %d Discrepancies: %d%n"
+ " min %.2f ms mitjana %.2f ms max %.2f ms%n%n",
m.enviades(), m.discrepancies(), m.minMs(), m.mitjanaMs(), m.maxMs());
} catch (ConnectException e) {
// No hi ha ningu escoltant: no te sentit reintentar.
System.err.println(" No hi ha servidor d'eco a " + host + ":" + port);
System.err.println(" Arrenca'l amb: while true; do nc -l "
+ port + " -c cat; done");
return;
} catch (SocketTimeoutException e) {
// Transitori: la maquina no respon a temps.
System.err.println(" Temps esgotat parlant amb " + host);
} catch (IOException e) {
System.err.println(" Fallada de xarxa: " + e);
}
}
}
}Sortida a localhost contra un servidor d'eco real:
=== setTcpNoDelay(true) ===
1) 1,84 ms
2) 0,21 ms
...
Ecos: 10 Discrepancies: 0
min 0,14 ms mitjana 0,31 ms max 1,84 msComentaris. Tres observacions. La primera mesura és sempre la més lenta (1,84 ms davant de 0,2 ms): és el cost que la JVM carregui les classes i que el primer enviament travessi camins encara no escalfats; qualsevol mesura de xarxa ha de descartar les primeres iteracions o almenys no prendre-les com a representatives. La segona: a localhost la diferència entre setTcpNoDelay(true) i false és pràcticament nul·la, perquè el bucle invertit no aplica Nagle igual que un enllaç real; per veure l'efecte de Nagle calen dues màquines, i allà les diferències poden ser de 40 ms per missatge. I la tercera: la comprovació que l'eco coincideix amb el que s'ha enviat no és paranoia; és la forma de detectar que el protocol s'ha desincronitzat, que és la fallada més difícil de diagnosticar quan apareix.
Solució 2
package com.nexussoftware.bibliotech.xarxa;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* Detector de serveis: comprova quins ports d'un rang tenen alguna cosa escoltant.
*
* AVIS LEGAL I ETIC: escanejar ports de maquines alienes sense autoritzacio
* escrita es, a molts paisos, un delicte d'acces no autoritzat a sistemes
* informatics, i a practicament tots es motiu de bloqueig per part del
* proveidor. Aquesta eina esta pensada UNICAMENT per a localhost i per a
* maquines propies de Nexus Software amb autoritzacio del departament de
* sistemes. Un escaneig genera transit i alertes en qualsevol IDS modern.
*/
public class DetectorServeis {
/** Estat d'un port. Un enum com els de 04-07. */
public enum Estat {
OBERT("hi ha un servei escoltant"),
TANCAT("la maquina respon RST: no hi ha ningu en aquest port"),
FILTRAT("sense resposta: tallafoc que descarta, o maquina caiguda");
private final String descripcio;
Estat(String descripcio) {
this.descripcio = descripcio;
}
public String descripcio() {
return descripcio;
}
}
private static final Map<Integer, String> SERVEIS = new HashMap<>();
static {
SERVEIS.put(21, "FTP");
SERVEIS.put(22, "SSH");
SERVEIS.put(25, "SMTP");
SERVEIS.put(53, "DNS");
SERVEIS.put(80, "HTTP");
SERVEIS.put(443, "HTTPS");
SERVEIS.put(3306, "MySQL");
SERVEIS.put(5432, "PostgreSQL");
SERVEIS.put(6379, "Redis");
SERVEIS.put(8080, "HTTP alternatiu");
SERVEIS.put(9090, "BiblioTech BTCP/1");
SERVEIS.put(9091, "BiblioTech descobriment");
}
public void escanejar(String host, int inici, int fi, int limitMs) {
System.out.printf("Escanejant %s ports %d-%d (limit %d ms)%n%n",
host, inici, fi, limitMs);
System.out.printf("%-8s %-10s %-24s %s%n", "PORT", "ESTAT", "SERVEI", "SALUTACIO");
System.out.println("-".repeat(90));
int oberts = 0, tancats = 0, filtrats = 0;
for (int port = inici; port <= fi; port++) {
Estat estat = provar(host, port, limitMs);
switch (estat) {
case OBERT -> oberts++;
case TANCAT -> tancats++;
case FILTRAT -> filtrats++;
}
// Nomes mostrem els interessants: una taula amb 65535 TANCAT
// no ajuda ningu.
if (estat == Estat.OBERT) {
String salutacio = llegirSalutacio(host, port, 500);
System.out.printf("%-8d %-10s %-24s %s%n",
port, estat,
SERVEIS.getOrDefault(port, "(desconegut)"),
salutacio == null ? "(no saluda)" : salutacio);
} else if (estat == Estat.FILTRAT) {
System.out.printf("%-8d %-10s %-24s %s%n",
port, estat,
SERVEIS.getOrDefault(port, "(desconegut)"), "");
}
}
System.out.println("-".repeat(90));
System.out.printf("Oberts: %d Tancats: %d Filtrats: %d%n",
oberts, tancats, filtrats);
for (Estat e : Estat.values()) {
System.out.printf(" %-10s %s%n", e, e.descripcio());
}
}
/** Intenta connectar i classifica el resultat pel TIPUS d'excepcio. */
private Estat provar(String host, int port, int limitMs) {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), limitMs);
return Estat.OBERT;
} catch (SocketTimeoutException e) {
// Ningu no ha respost al SYN: normalment un tallafoc amb politica DROP.
return Estat.FILTRAT;
} catch (ConnectException e) {
// La maquina ha respost RST: es viva, pero aquell port no te servei.
// ES INFORMACIO VALUOSA: distingeix "maquina caiguda" de "port tancat".
return Estat.TANCAT;
} catch (IOException e) {
// NoRouteToHost i similars.
return Estat.FILTRAT;
}
}
/**
* Molts protocols saluden en connectar (SMTP, FTP, SSH, BTCP/1).
* Un temps esgotat aqui NO es una fallada: significa que el servei
* espera que parlis tu primer (com HTTP).
*/
private String llegirSalutacio(String host, int port, int limitMs) {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), limitMs);
socket.setSoTimeout(limitMs);
BufferedReader lector = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String linia = lector.readLine();
if (linia == null) {
return null;
}
// Retallem: una salutacio enorme no cap a la taula, i a mes
// no volem bolcar dades arbitraries de la xarxa a la consola.
return linia.length() > 45 ? linia.substring(0, 45) + "..." : linia;
} catch (IOException e) {
return null;
}
}
public static void main(String[] args) {
DetectorServeis detector = new DetectorServeis();
// NOMES localhost per defecte: escanejar una altra maquina requereix autoritzacio.
detector.escanejar("localhost", 9080, 9100, 300);
}
}Sortida amb un nc -l 9090 aixecat:
Escanejant localhost ports 9080-9100 (limit 300 ms)
PORT ESTAT SERVEI SALUTACIO
------------------------------------------------------------------------------------------
9090 OBERT BiblioTech BTCP/1 (no saluda)
------------------------------------------------------------------------------------------
Oberts: 1 Tancats: 20 Filtrats: 0Comentaris. El valor pedagògic d'aquest exercici és a la classificació en tres estats, que és exactament el que fa nmap. ConnectException i SocketTimeoutException signifiquen coses molt diferents i confondre-les és l'error de diagnòstic més comú: "rebutjat" prova que la màquina és viva i que el paquet va arribar i va tornar; "temps esgotat" no prova res, perquè pot ser una màquina apagada, una ruta trencada o un tallafoc amb política de descart silenciós. Fixa't també que nc -l no saluda —espera que parlis tu—, i per això llegirSalutacio retorna null sense que això sigui un error: un temps esgotat llegint la salutació és informació, no fallada. I observa el detall de retallar la salutació a 45 caràcters: no bolquis mai sense límit a la consola dades que vénen de la xarxa, perquè poden contenir seqüències d'escapada de terminal.
Solució 3
package com.nexussoftware.bibliotech.xarxa;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Logger;
/**
* Emissor d'un fitxer del cataleg per TCP.
*
* Protocol (binari, amb longitud previa):
* [int ] longitud del nom en bytes UTF-8
* [bytes] nom
* [long ] mida del contingut en bytes
* [bytes] contingut
* -- shutdownOutput --
* [linia] resum del receptor, en text UTF-8
*/
public class EmissorFitxer {
private static final Logger LOG = Logger.getLogger(EmissorFitxer.class.getName());
private static final int BLOC = 8192;
public void enviar(String host, int port, Path fitxer) throws IOException {
if (!Files.isRegularFile(fitxer)) {
throw new IOException("No es un fitxer: " + fitxer);
}
long mida = Files.size(fitxer);
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), 3_000);
socket.setSoTimeout(30_000);
DataOutputStream sortida = new DataOutputStream(
new BufferedOutputStream(socket.getOutputStream()));
// --- Capcalera: nom amb longitud previa ---
// Enviem NOMES el nom del fitxer, mai el cami complet:
// el receptor decideix on desar.
byte[] nom = fitxer.getFileName().toString()
.getBytes(StandardCharsets.UTF_8);
sortida.writeInt(nom.length);
sortida.write(nom);
sortida.writeLong(mida);
// --- Contingut en blocs ---
long enviats = 0;
try (InputStream entradaFitxer =
new BufferedInputStream(Files.newInputStream(fitxer))) {
byte[] bufer = new byte[BLOC];
int llegits;
while ((llegits = entradaFitxer.read(bufer)) != -1) {
// read pot retornar MENYS de BLOC: cal escriure
// exactament 'llegits' bytes, mai bufer.length.
sortida.write(bufer, 0, llegits);
enviats += llegits;
}
}
sortida.flush(); // el buidatge de sempre, abans de tancar el sentit
LOG.info(() -> "Enviats " + mida + " bytes de " + fitxer.getFileName());
// --- shutdownOutput: IMPRESCINDIBLE ---
// El receptor llegeix fins a esgotar el flux. Sense aquest tancament de mig
// sentit, el receptor continuaria esperant mes bytes indefinidament
// i nosaltres esperariem el seu resum: interbloqueig perfecte.
// No podem fer servir socket.close() perque encara hem de LLEGIR.
socket.shutdownOutput();
// --- Resum del receptor (el sentit de lectura continua obert) ---
BufferedReader lector = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String resum = lector.readLine();
System.out.println("Receptor: " + (resum == null ? "(sense resposta)" : resum));
if (enviats != mida) {
LOG.warning("El fitxer ha canviat de mida durant l'enviament");
}
}
}
public static void main(String[] args) throws IOException {
Path fitxer = Path.of(args.length > 0 ? args[0] : "cataleg.csv");
new EmissorFitxer().enviar("localhost", 9091, fitxer);
}
}package com.nexussoftware.bibliotech.xarxa;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Receptor del fitxer. Servidor minim d'una sola connexio:
* la versio concurrent i completa s'escriu a 09-03.
*/
public class ReceptorFitxer {
private static final Logger LOG = Logger.getLogger(ReceptorFitxer.class.getName());
private static final int MAXIM_NOM = 255;
private static final long MAXIMA_MIDA = 10L * 1024 * 1024; // 10 MB
private static final Path DESTI = Path.of("rebuts");
public void escoltar(int port) throws IOException {
Files.createDirectories(DESTI);
try (ServerSocket servidor = new ServerSocket(port)) {
LOG.info(() -> "Receptor escoltant al port " + port);
// accept() bloqueja fins que arriba una connexio i retorna
// un Socket JA connectat. Tot el detall, a 09-03.
try (Socket socket = servidor.accept()) {
socket.setSoTimeout(30_000);
LOG.info(() -> "Connexio de " + socket.getRemoteSocketAddress());
atendre(socket);
}
}
}
private void atendre(Socket socket) throws IOException {
DataInputStream entrada = new DataInputStream(socket.getInputStream());
String resum;
try {
// --- Nom, amb longitud previa VALIDADA ---
int longitudNom = entrada.readInt();
if (longitudNom <= 0 || longitudNom > MAXIM_NOM) {
throw new IOException("Longitud de nom invalida: " + longitudNom);
}
byte[] bytesNom = entrada.readNBytes(longitudNom);
if (bytesNom.length < longitudNom) {
throw new EOFException("Flux tallat llegint el nom");
}
String nom = new String(bytesNom, StandardCharsets.UTF_8);
// --- Sanejament del nom: RECORREGUT DE CAMINS ---
// Sense aixo, un emissor malicios envia "../../etc/cron.d/porta"
// i escrivim on ell vulgui. Es una vulnerabilitat classica.
if (nom.contains("/") || nom.contains("\\")
|| nom.contains("..") || nom.isBlank()) {
throw new IOException("Nom de fitxer no permes: " + nom);
}
// --- Mida, tambe validada ---
long mida = entrada.readLong();
if (mida < 0 || mida > MAXIMA_MIDA) {
throw new IOException("Mida fora de rang: " + mida);
}
// --- Contingut ---
Path desti = DESTI.resolve(nom).normalize();
// Cinturo i tirants: comprovem que el resultat continua dins
// del directori previst, per si alguna cosa se'ns ha escapat abans.
if (!desti.startsWith(DESTI)) {
throw new IOException("Cami resultant fora del directori: " + desti);
}
long rebuts = 0;
try (OutputStream sortidaFitxer =
new BufferedOutputStream(Files.newOutputStream(desti))) {
byte[] bufer = new byte[8192];
int llegits;
// Llegim fins a fi de flux: es el shutdownOutput() de l'emissor
// el que provoca aquest -1. Sense ell, aquest bucle no acabaria.
while ((llegits = entrada.read(bufer)) != -1) {
if (rebuts + llegits > mida) {
throw new IOException(
"L'emissor envia mes bytes dels anunciats");
}
sortidaFitxer.write(bufer, 0, llegits);
rebuts += llegits;
}
}
if (rebuts != mida) {
throw new EOFException("S'han anunciat " + mida
+ " bytes i n'han arribat " + rebuts);
}
long totalRebuts = rebuts;
LOG.info(() -> "Desat " + desti + " (" + totalRebuts + " bytes)");
resum = "200 OK " + nom + " " + rebuts + " bytes";
} catch (IOException e) {
LOG.log(Level.WARNING, "Transferencia rebutjada", e);
resum = "400 REBUTJAT " + e.getMessage();
}
// --- Resposta de text, amb charset explicit ---
BufferedWriter escriptor = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
escriptor.write(resum);
escriptor.write('\n');
escriptor.flush(); // sense aixo, l'emissor esperaria per sempre
}
public static void main(String[] args) throws IOException {
new ReceptorFitxer().escoltar(9091);
}
}Prova en dues terminals:
# Terminal 1
java -cp classes com.nexussoftware.bibliotech.xarxa.ReceptorFitxer
# Terminal 2
java -cp classes com.nexussoftware.bibliotech.xarxa.EmissorFitxer cataleg.csvTerminal 1:
INFO: Receptor escoltant al port 9091
INFO: Connexio de /127.0.0.1:52104
INFO: Desat rebuts/cataleg.csv (2847 bytes)
Terminal 2:
INFO: Enviats 2847 bytes de cataleg.csv
Receptor: 200 OK cataleg.csv 2847 bytesComentaris. Quatre punts que mereixen quedar-se.
El shutdownOutput() és el cor de l'exercici. El receptor llegeix amb while ((llegits = entrada.read(bufer)) != -1), i aquest -1 només apareix quan l'emissor tanca el seu sentit d'escriptura. Si l'emissor fes socket.close() en el seu lloc, el -1 també arribaria, però l'emissor hauria tancat també el seu sentit de lectura i mai no podria rebre el resum. Mitja connexió és exactament l'eina per a "he acabat de parlar, però continuo escoltant".
La validació del nom no és un adorn. Files.newOutputStream(DESTI.resolve("../../../etc/passwd")) escriu on l'atacant vulgui si el procés té permisos. Es comprova dues vegades: rebutjant els caràcters perillosos i verificant després amb startsWith que el camí normalitzat continua dins del directori previst. En seguretat, la redundància és correcta.
La validació de la mida tanca l'altre forat: sense ella, un emissor que anuncia 500 GB omple el disc. I fixa't que es comprova dues vegades: el límit abans de començar, i durant el bucle que no arribin més bytes dels anunciats. Un emissor pot mentir a la capçalera.
I el read(bufer) que pot retornar menys del que s'ha demanat apareix als dos costats: l'emissor escriu sortida.write(bufer, 0, llegits) i mai sortida.write(bufer). Escriure la memòria intermèdia sencera quan només s'han llegit 300 bytes envia 7892 bytes de brossa. És el mateix error que al mòdul 7, i en xarxa fa més mal perquè el fitxer arriba corrupte i no te n'assabentes fins que algú l'obre.
Conclusió
Has obert la teva primera connexió des de Java, i amb ella has descobert el que anunciava la lliçó anterior: un cop connectat, la xarxa és l'E/S del mòdul 7. getInputStream() i getOutputStream() et retornen els mateixos fluxos, amb els mateixos decoradors, el mateix InputStreamReader amb charset explícit i el mateix try-with-resources. El que és nou no és llegir i escriure: és tot el que envolta aquesta lectura i aquesta escriptura.
Saps connectar bé: mai amb el constructor new Socket(host, port), que pot bloquejar més d'un minut sense temps límit, sinó amb new Socket() seguit de connect(new InetSocketAddress(host, port), ms). I tens clar que hi ha dos temps límit diferents i calen tots dos: el de connexió, que fixes a connect, i el de lectura, que fixes amb setSoTimeout i que és el que evita que un client zombi et deixi un fil bloquejat per sempre. Saps a més que SocketTimeoutException és l'única excepció de xarxa que no invalida el socket, i que això permet el patró de sondeig amb comprovació de cancel·lació que fa interrompible un socket bloquejant.
Has vist demostrat el bug número u dels principiants: escriure en un BufferedWriter i quedar-te esperant una resposta que no arriba mai, perquè els teus vint-i-quatre caràcters continuen en una memòria intermèdia de vuit mil i no han sortit a la xarxa. Sense excepció, sense traça, sense res. I coneixes les tres formes d'evitar-ho, amb el matís que salva vides: autoFlush només actua amb println, printf i format, mai amb print.
Entens per què existeix readLine() i quin problema resol: TCP és un flux de bytes sense fronteres de missatge, i el que tu escrius en dos enviaments es pot llegir en un, en dos o en set. BufferedReader acumula fins al delimitador i et lliura missatges complets. Amb els dos advertiments que l'acompanyen: retorna null quan l'altre tanca —i no comprovar-ho és un NullPointerException garantit—, i no té límit de longitud, cosa que el converteix en un vector de denegació de servei si no l'acotes. I saps quan el delimitador no serveix i cal passar a longitud prèvia, validant sempre la longitud declarada abans de reservar memòria amb ella.
Saps tancar bé: només el Socket al try-with-resources, perquè tancar qualsevol dels seus fluxos el tanca a ell; i coneixes la mitja connexió, amb shutdownOutput() per dir "he acabat de parlar, però continuo escoltant", que és l'única forma de resoldre els protocols que acaben l'enviament amb el final del flux.
Manages les opcions del socket, i sobretot les dues que importen: setSoTimeout, ja comentada, i setTcpNoDelay(true) per desactivar l'algorisme de Nagle en protocols interactius de missatges curts, on la combinació de Nagle amb la confirmació retardada produeix retards artificials de fins a dos-cents mil·lisegons. I saps quines no tocar: les mides de memòria intermèdia i, sobretot, setSoLinger.
Distingeixes cada excepció de xarxa i el que significa: UnknownHostException és configuració, ConnectException és que vas arribar a la màquina però no hi ha ningú en aquell port, SocketTimeoutException és que ningú no ha respost, SocketException: Connection reset és que l'altre ha caigut, i el tancament ordenat no llança cap excepció: es manifesta com a -1 o null. Amb la regla que ordena el catch: ConnectException i BindException són subclasses de SocketException i cal comprovar-les abans. I saps traduir tot això a BiblioTechException a la frontera, classificant entre transitori —mereix reintent— i permanent —reintentar només gasta CPU—, tal com vas aprendre a 06-07.
Saps enviar dades binàries amb DataOutputStream/DataInputStream, que a més escriuen en big-endian i per tant interoperen; i saps per què no has d'enviar mai objectes serialitzats a un client no fiable: readObject() construeix objectes arbitraris abans que la teva conversió de tipus pugui dir res, i això és una via d'execució remota de codi. La regla és enviar dades, no objectes.
I BiblioTech ha guanyat tres classes: ClientCataleg, que parla BTCP/1 complet —connecta amb temps límit, verifica la salutació i la versió, consulta, llista, presta, valida els seus propis arguments contra la injecció de salts de línia, acota les respostes del servidor i s'acomiada amb SORTIR al seu close()—; TraductorErrorsXarxa, la frontera que converteix cada excepció de xarxa en un missatge de domini amb la seva classificació de transitorietat; i DiagnosticXarxa de la lliçó anterior. Ho has provat contra nc -l 9090 fent tu de servidor a mà, que és la millor forma que existeix d'entendre un protocol, i has comprovat en directe què passa quan el servidor no saluda, quan saluda malament, quan tanca a mitges i quan no hi és.
Però el teu client encara no té amb qui parlar de veritat. Fins ara el servidor eres tu, teclejant respostes en una terminal.
A la lliçó següent, ServerSocket, escriuràs l'altre extrem. Veuràs ServerSocket i el seu bind a un port, la cua de connexions pendents, i accept() com el mètode bloquejant que et lliura un Socket ja connectat. Començaràs per un servidor seqüencial, comprovaràs en directe el seu límit —el segon client espera que el primer acabi— i el resoldràs amb l'ExecutorService del mòdul 8: pool acotat, ThreadFactory amb noms per als logs, aturada en dues fases i tancament del ServerSocket per desbloquejar l'accept. Aquí és on tot el mòdul 8 rendeix de veritat, i on entendràs per què el CatalegConcurrent amb el seu ConcurrentHashMap ja estava preparat per a això sense saber-ho. En acabar, el servidor de catàleg de BiblioTech acceptarà la Marta, en Diego i la Nuria alhora, validarà tot el que arribi per la xarxa, respondrà amb codis, expulsarà els clients inactius i s'apagarà ordenadament — i ho provaràs amb telnet localhost 9090.
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
