Cicle: Desenvolupament d’Aplicacions Multiplataforma (DAM)
Mòdul: 0490. Programació de serveis i processos
Un servei és un programa auxiliar que gestiona un conjunt de recursos i n’ofereix la funcionalitat a altres usuaris o aplicacions mitjançant un conjunt d’operacions concretes. Per exemple, un servei d’impressió permet gestionar i compartir una impressora a la xarxa; un servei de fitxers ofereix operacions de lectura, escriptura i esborrat.
Tots els serveis d’Internet segueixen el model client-servidor: el servidor espera peticions en un port conegut i els clients s’hi connecten per sol·licitar operacions.
El model TCP/IP es divideix en quatre capes. La capa d’aplicació és la que implementa els protocols d’alt nivell que farem servir:
| Capa | Protocols habituals |
|---|---|
| Aplicació | SMTP, Telnet, FTP, HTTP, NFS, SNMP, DNS |
| Transport | TCP, UDP |
| Internet | IP |
| Interfície de xarxa | Protocols de subxarxa |
Alguns dels protocols estàndard més utilitzats:
A banda dels protocols “clàssics” (FTP, Telnet, SMTP…), avui en dia molts serveis en xarxa s’exposen com a serveis web, és a dir, aplicacions accessibles mitjançant HTTP/HTTPS que intercanvien dades normalment en format XML o JSON (per exemple, API REST). Encara que en aquesta unitat ens centrarem en protocols estàndard “tradicionals” amb Apache Commons Net, val la pena tenir present que:
HttpClient, HttpURLConnection, o frameworks com Spring) per consumir i
publicar serveis web.Apache Commons Net™ és una llibreria Java que implementa el costat client de molts protocols bàsics d’Internet: FTP, SMTP, Telnet, TFTP, POP3, entre d’altres.
http://commons.apache.org/proper/commons-net/download_net.cgi..jar (per exemple commons-net-3.2.jar) al classpath del projecte.Totes les classes client de la llibreria deriven de
SocketClient, que proporciona la
funcionalitat comuna:
| Mètode | Missió |
|---|---|
connect(String host) |
Obre la connexió amb el servidor |
disconnect() |
Tanca la connexió i restaura els paràmetres |
isConnected() |
Indica si hi ha una connexió activa |
getReplyCode() |
Codi numèric de l’última resposta |
getReplyString() |
Text complet de l’última resposta |
El patró de treball és sempre el mateix:
FTPClient, TelnetClient, SMTPClient, POP3Client…).connect().disconnect()/logout().Molts d’aquests protocols (FTP, SMTP) responen amb codis numèrics de tres xifres, on el primer dígit indica el tipus de resposta:
| Primer dígit | Significat |
|---|---|
| 1yz | Resposta preliminar positiva |
| 2yz | Terminació positiva (èxit) |
| 3yz | Intermèdia positiva, cal més informació |
| 4yz | Terminació negativa transitòria |
| 5yz | Terminació negativa permanent |
Les classes FTPReply i SMTPReply ofereixen
constants i el mètode estàtic isPositiveCompletion(int codi) per comprovar-ho de manera
còmoda.
La classe FTPClient encapsula tota la
funcionalitat per emmagatzemar i recuperar fitxers d’un servidor
FTP.
import java.io.IOException;
import java.net.SocketException;
import org.apache.commons.net.ftp.*;
public class ClientFTP1 {
public static void main(String[] args) throws SocketException, IOException {
FTPClient client = new FTPClient();
String host = "ftp.rediris.es";
client.connect(host);
int codi = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(codi)) {
client.disconnect();
System.out.println("Connexió rebutjada: " + codi);
System.exit(0);
}
client.disconnect();
System.out.println("Connexió finalitzada.");
}
}Amb login(usuari, clau) ens autentiquem
(l’accés anònim es fa amb l’usuari anonymous). Un cop dins, podem llistar fitxers amb listFiles(), que retorna un array de
FTPFile:
FTPClient client = new FTPClient();
client.connect("ftp.rediris.es");
boolean sessioIniciada = client.login("anonymous", "anonymous");
if (sessioIniciada) {
System.out.println("Directori actual: " + client.printWorkingDirectory());
FTPFile[] fitxers = client.listFiles();
for (FTPFile f : fitxers) {
String tipus = f.isDirectory() ? "Directori" : (f.isFile() ? "Fitxer" : "Enllaç");
System.out.println(f.getName() + " => " + tipus);
}
client.logout();
}
client.disconnect();Mètodes habituals de FTPClient (tots poden llançar IOException):
| Mètode | Missió |
|---|---|
login(user, pass) |
Inicia sessió |
logout() |
Tanca sessió |
changeWorkingDirectory(path) |
Canvia de directori |
changeToParentDirectory() |
Puja al directori pare |
makeDirectory(path) |
Crea un directori |
removeDirectory(path) |
Elimina un directori (buit) |
setFileType(int tipus) |
FTP.BINARY_FILE_TYPE, FTP.ASCII_FILE_TYPE… |
storeFile(nom, InputStream) |
Puja un fitxer |
retrieveFile(nom, OutputStream) |
Baixa un fitxer |
deleteFile(path) |
Elimina un fitxer |
rename(antic, nou) |
Reanomena un fitxer |
Cal fixar sempre el tipus de transferència amb setFileType(FTP.BINARY_FILE_TYPE) abans de pujar o baixar
fitxers binaris (imatges, documents…), o es poden corrompre.
client.changeWorkingDirectory("/NUEVODIREC");
client.setFileType(FTP.BINARY_FILE_TYPE);
BufferedInputStream in = new BufferedInputStream(
new FileInputStream("D:\\CAPIT4\\TEXTO1.txt"));
client.storeFile("TEXTO1.txt", in);
in.close();client.changeWorkingDirectory("/NUEVODIREC/NUEVO");
BufferedOutputStream out = new BufferedOutputStream(
new FileOutputStream("D:\\CAPIT4\\TEXTO2nuevo.txt"));
if (client.retrieveFile("TEXTO2.txt", out))
System.out.println("Recuperat correctament...");
out.close();Per veure els comandaments i respostes que s’intercanvien client i
servidor, s’utilitza addProtocolCommandListener() (heretat
de SocketClient):
client.addProtocolCommandListener(
new PrintCommandListener(new PrintWriter(System.out)));
client.connect(servidor);Un client FTP amb interfície gràfica es pot estructurar amb un JFrame que mostri un JList amb el contingut
del directori actual i botons per pujar, baixar, crear i eliminar
fitxers/carpetes. Els punts clau del disseny són:
carpetaActual (directori de treball actual) i elementSeleccionat (últim fitxer o carpeta
seleccionat).listFiles() i s’omple de nou el JList amb un DefaultListModel.(DIR).changeToParentDirectory().JFileChooser, que
ofereix una finestra estàndard de selecció de fitxers/directoris.private static void actualitzarLlista(FTPFile[] fitxers, String directoriActual) {
DefaultListModel model = new DefaultListModel();
model.addElement(directoriActual); // primer element = directori actual
for (FTPFile f : fitxers) {
if (!f.getName().equals(".") && !f.getName().equals("..")) {
String nom = f.getName();
if (f.isDirectory()) nom = "(DIR) " + nom;
model.addElement(nom);
}
}
llistaVisual.setModel(model);
}A Windows es pot activar des de Panell de control → Programes →
Activar o desactivar característiques de Windows, marcant
Servidor Telnet i Client Telnet. Cal
donar accés al servei als usuaris afegint-los al grup TelnetClients:
net localgroup TelnetClients /add usuari2
net localgroup TelnetClientsA Linux es pot instal·lar un dimoni telnet (per exemple telnetd) o, més recomanable, provar-ho contra un servei
propi que escolti en un port TCP (com el servidor de fitxers de
l’apartat 7).
TelnetClientTelnetClient (que estén SocketClient) implementa el terminal virtual de xarxa (NVT)
segons la RFC 854.
TelnetClient telnet = new TelnetClient("xterm"); // o new TelnetClient() -> VT100
telnet.connect("localhost");
OutputStream out = telnet.getOutputStream();
InputStream in = telnet.getInputStream();A diferència d’FTP i SMTP, Telnet no té una estructura de comandaments/respostes fixa: el que rebem i enviem depèn del programa que hi ha a l’altra banda (un shell, un login…), per això cal fer lectures i escriptures manuals dels streams.
static void lecturaDatos() throws IOException {
byte[] buff = new byte[1024];
int nbytes = in.read(buff);
System.out.print(new String(buff, 0, nbytes));
}
static void envioDatos(String cad) {
out.println(cad);
out.flush();
}Seqüència típica per fer login i executar un comandament:
lecturaDatos(); // missatge de benvinguda + "login:"
envioDatos(login);
lecturaDatos(); // "password:"
envioDatos(password);
lecturaDatos(); // presentació + prompt
envioDatos("DIR");
lecturaDatos(); // sortida del comandament
telnet.disconnect();En un client real, l’usuari escriu per teclat mentre el servidor pot
enviar dades en qualsevol moment. La solució és llançar un fil
(Thread) dedicat a llegir contínuament el que
arriba del servidor. En comptes de limitar-se a imprimir cada tros
rebut, aquest fil pot acumular-lo en un StringBuilder
compartit i marcar quan hi ha novetats, cosa que permetria, per exemple,
refrescar una àrea de text en una interfície gràfica:
class MonitorSortida extends Thread {
private final TelnetClient connexio;
private final StringBuilder acumulat = new StringBuilder();
public MonitorSortida(TelnetClient connexio) {
this.connexio = connexio;
}
public synchronized String contingutActual() {
return acumulat.toString();
}
@Override
public void run() {
InputStream entrada = connexio.getInputStream();
byte[] buff = new byte[512];
int nbytes;
try {
while ((nbytes = entrada.read(buff)) > 0) {
String tros = new String(buff, 0, nbytes);
System.out.print(tros);
synchronized (this) {
acumulat.append(tros);
}
}
} catch (IOException e) {
System.err.println("El servidor ha tancat la connexió: " + e.getMessage());
}
}
}TelnetClient telnet = new TelnetClient("xterm");
telnet.connect("localhost");
MonitorSortida monitor = new MonitorSortida(telnet);
monitor.start();
OutputStream sortida = telnet.getOutputStream();
BufferedReader teclat = new BufferedReader(new InputStreamReader(System.in));
String ordre;
while ((ordre = teclat.readLine()) != null && !ordre.equalsIgnoreCase("sortir")) {
sortida.write((ordre + "\n").getBytes());
sortida.flush();
}
telnet.disconnect();Aquest patró de “fil de lectura + bucle d’escriptura al fil principal” el reutilitzarem després per al nostre propi servidor.
Abans de programar-ho, val la pena entendre el protocol enviant un correu manualment via Telnet al port 25:
| Ordre | Missió |
|---|---|
HELO/EHLO |
Obre la sessió |
MAIL FROM: origen |
Indica el remitent |
RCPT TO: destí |
Indica el destinatari |
DATA |
Comença el cos del missatge (acaba amb una línia amb només un punt .) |
QUIT |
Tanca la sessió |
HELO
MAIL FROM: jo@brianda.net
RCPT TO: jo@brianda.net
DATA
from: jo@brianda.net
to: jo@brianda.net
subject: proves
Hola, això és una prova.
Adeu.
.
QUITSMTPClientSMTPClient (estén SMTP)
encapsula tota aquesta lògica:
import org.apache.commons.net.smtp.*;
SMTPClient client = new SMTPClient();
client.connect("localhost"); // port 25 per defecte
int respuesta = client.getReplyCode();
if (!SMTPReply.isPositiveCompletion(respuesta)) {
client.disconnect();
System.exit(1);
}Mètodes principals:
| Mètode | Missió |
|---|---|
login() / login(host) |
Envia HELO |
setSender(address) |
Envia MAIL FROM |
addRecipient(address) |
Envia RCPT TO |
sendMessageData() |
Envia DATA i retorna un Writer per
escriure la capçalera i el cos |
completePendingCommand() |
Finalitza la transacció i comprova l’èxit |
sendSimpleMessage(remitent, destinatari, missatge) |
Enviament ràpid d’un missatge senzill |
logout() |
Envia QUIT |
verify(username) |
Comprova si un usuari/adreça és vàlida (comandament VRFY) |
Enviament senzill:
client.login();
client.sendSimpleMessage("jo@localhost.es", "algu@exemple.com",
"Hola.\nEnviant salutacions.\nAdeu.");
client.logout();SimpleSMTPHeaderUn missatge enviat sense capçalera adequada pot arribar com a spam o
ser rebutjat. La classe SimpleSMTPHeader
construeix una capçalera mínima vàlida:
SimpleSMTPHeader cabecera =
new SimpleSMTPHeader("jo@localhost.es", "desti1@exemple.com", "Assumpte");
cabecera.addCC("desti2@exemple.com");
client.setSender("jo@localhost.es");
client.addRecipient("desti1@exemple.com");
client.addRecipient("desti2@exemple.com");
Writer writer = client.sendMessageData();
if (writer == null) { System.out.println("Fallada en enviar DATA."); System.exit(1); }
writer.write(cabecera.toString());
writer.write("Hola.\nEnviant salutacions.\nAdeu.");
writer.close();
if (!client.completePendingCommand()) {
System.out.println("Fallada en finalitzar la transacció.");
}
client.logout();Per enviar correu amb un proveïdor real (per exemple Gmail: smtp.gmail.com, port 587 amb STARTTLS, o 465 amb SSL
directe) calen classes amb suport de seguretat:
SMTPSClient: SMTP sobre SSL/TLS.
Constructor SMTPSClient() (mode explícit, negocia STARTTLS després de connectar) o SMTPSClient(true) (mode implícit, SSL/TLS des del
principi).AuthenticatingSMTPClient: afegeix el
mètode auth() per autenticar-se.AuthenticatingSMTPClient client = new AuthenticatingSMTPClient();
String server = "smtp.gmail.com";
int puerto = 587;
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
kmf.init(null, null);
KeyManager km = kmf.getKeyManagers()[0];
client.connect(server, puerto);
client.setKeyManager(km);
client.ehlo(server);
if (client.execTLS()) {
if (client.auth(AuthenticatingSMTPClient.AUTH_METHOD.PLAIN,
"usuari@gmail.com", "clau")) {
SimpleSMTPHeader cabecera = new SimpleSMTPHeader(
"usuari@gmail.com", "desti@exemple.com", "Prova amb Gmail");
client.setSender("usuari@gmail.com");
client.addRecipient("desti@exemple.com");
Writer writer = client.sendMessageData();
writer.write(cabecera.toString());
writer.write("Hola.\nUsant Gmail.\nAdeu.");
writer.close();
client.completePendingCommand();
}
}
client.disconnect();Mètodes d’autenticació disponibles a AUTH_METHOD: CRAM_MD5 (contrasenya xifrada), LOGIN/PLAIN (text pla sobre canal ja xifrat
per TLS) i XOAUTH (basat en OAuth).
POP3 (Post Office Protocol) permet accedir als missatges emmagatzemats en un servidor de correu. Port 110 (o 995 amb SSL).
| Comandament | Missió |
|---|---|
USER login |
Nom d’usuari |
PASS contrasenya |
Contrasenya |
STAT |
Nombre de missatges i mida total |
LIST |
Llistat de missatges (número - mida) |
RETR n |
Recupera el missatge n |
DELE n |
Esborra el missatge n |
TOP n x |
Mostra les x primeres línies del missatge n (x=0
retorna només la capçalera) |
QUIT |
Tanca la connexió |
POP3Client: implementació del client
POP3 (RFC 1939).POP3SClient: versió amb suport SSL
(mode implícit/explícit igual que SMTPSClient).POP3MessageInfo: informació sobre un
missatge (identifier, number, size).import org.apache.commons.net.pop3.*;
POP3SClient pop3 = new POP3SClient(); // mode explícit
pop3.connect("localhost", 110);
if (pop3.login("usu1", "usu1")) {
POP3MessageInfo[] missatges = pop3.listMessages();
System.out.println("Nombre de missatges: " + missatges.length);
pop3.logout();
}
pop3.disconnect();BufferedReader reader = (BufferedReader)
pop3.retrieveMessageTop(missatges[0].number, 0); // 0 => només capçalera
String linia;
while ((linia = reader.readLine()) != null) {
System.out.println(linia);
}
reader.close();Per recuperar el missatge sencer (i que desaparegui de la bústia):
BufferedReader reader = (BufferedReader)
pop3.retrieveMessage(missatges[0].number);Per connectar amb un servidor real com Gmail
(pop.gmail.com, port 995, SSL): POP3SClient pop3 = new POP3SClient(true);
Fins ara hem creat clients de protocols ja existents. Ara construirem el nostre propi servei en xarxa: un servidor de fitxers senzill que permet als clients llistar, pujar i baixar fitxers d’un directori.
La comunicació client-servidor es farà per objectes
serialitzats sobre ObjectInputStream/ObjectOutputStream, en
comptes de text pla. En lloc de definir una classe diferent per a cada
tipus de missatge, farem servir una única classe
embolcall (MissatgeProtocol) amb un camp tipus (un enum) i una càrrega útil
(payload) genèrica; és una alternativa més compacta al
clàssic “una classe per missatge + instanceof”:
SERVIDOR --- MissatgeProtocol(ARBRE) ---> CLIENT (en connectar)
SERVIDOR <--- MissatgeProtocol(BAIXAR) --- CLIENT (vol baixar un fitxer)
SERVIDOR --- MissatgeProtocol(DADES) ---> CLIENT
SERVIDOR <--- MissatgeProtocol(PUJAR) --- CLIENT (vol pujar un fitxer)
SERVIDOR --- MissatgeProtocol(ARBRE) ---> CLIENT (arbre actualitzat)import java.io.Serializable;
public class MissatgeProtocol implements Serializable {
public enum Tipus { ARBRE, BAIXAR, DADES, PUJAR }
private final Tipus tipus;
private final Object payload;
public MissatgeProtocol(Tipus tipus, Object payload) {
this.tipus = tipus;
this.payload = payload;
}
public Tipus getTipus() { return tipus; }
public Object getPayload() { return payload; }
}DirectoriInfoAquesta classe representa, de manera recursiva, l’arbre d’un directori (nom, ruta absoluta, si és una carpeta i els seus fills):
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class DirectoriInfo implements Serializable {
private final String nom;
private final String rutaAbsoluta;
private final boolean esDirectori;
private final List<DirectoriInfo> fills = new ArrayList<>();
public DirectoriInfo(File origen) {
this.nom = origen.getName();
this.rutaAbsoluta = origen.getAbsolutePath();
this.esDirectori = origen.isDirectory();
if (esDirectori) {
File[] continguts = origen.listFiles();
if (continguts != null) {
for (File element : continguts) {
fills.add(new DirectoriInfo(element));
}
}
}
}
public String getNom() { return nom; }
public String getRutaAbsoluta() { return rutaAbsoluta; }
public boolean esDirectori() { return esDirectori; }
public List<DirectoriInfo> getFills() { return fills; }
public int quantitatFills() { return fills.size(); }
}Els altres dos tipus de càrrega útil (la petició de baixada/pujada i
les dades transferides) són encara més senzills: n’hi ha prou amb una
parella nom fitxer / directori per a les peticions i un
array de byte[] per al contingut.
El servidor:
JFileChooser).ServerSocket en un port
pactat.accept().Thread) dedicat, de manera que el
servidor pugui seguir acceptant nous clients sense bloquejar-se.import java.io.*;
import java.net.*;
import javax.swing.*;
public class ServidorFitxersCompartits {
private static final int PORT = 5050;
public static void main(String[] args) throws IOException {
JFileChooser selector = new JFileChooser();
selector.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
selector.setDialogTitle("Tria el directori que vols compartir");
if (selector.showOpenDialog(null) != JFileChooser.APPROVE_OPTION) {
System.out.println("Cal seleccionar un directori. Sortint...");
return;
}
File arrel = selector.getSelectedFile();
try (ServerSocket servidor = new ServerSocket(PORT)) {
System.out.println("Servidor escoltant al port " + PORT);
while (true) {
Socket connexio = servidor.accept();
System.out.println("Nou client: " + connexio.getInetAddress());
new GestorClient(connexio, arrel).start(); // un fil per client
}
}
}
}Cada instància de GestorClient gestiona
un client de manera independent, dins del seu propi
mètode run(). Per llegir i escriure el contingut dels
fitxers s’utilitzen els mètodes de conveniència de java.nio.file.Files en lloc d’un bucle manual byte a
byte:
import java.io.*;
import java.net.*;
import java.nio.file.*;
public class GestorClient extends Thread {
private final Socket connexio;
private final File directoriArrel;
private ObjectOutputStream sortida;
private ObjectInputStream entrada;
public GestorClient(Socket connexio, File directoriArrel) {
this.connexio = connexio;
this.directoriArrel = directoriArrel;
}
@Override
public void run() {
try {
sortida = new ObjectOutputStream(connexio.getOutputStream());
entrada = new ObjectInputStream(connexio.getInputStream());
sortida.writeObject(new MissatgeProtocol(
MissatgeProtocol.Tipus.ARBRE, new DirectoriInfo(directoriArrel)));
while (true) {
MissatgeProtocol missatge = (MissatgeProtocol) entrada.readObject();
switch (missatge.getTipus()) {
case BAIXAR -> processarBaixada((String) missatge.getPayload());
case PUJAR -> processarPujada((Object[]) missatge.getPayload());
default -> System.out.println("Missatge inesperat: " + missatge.getTipus());
}
}
} catch (IOException e) {
tancar();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private void processarBaixada(String rutaFitxer) throws IOException {
byte[] dades = Files.readAllBytes(Path.of(rutaFitxer));
sortida.writeObject(new MissatgeProtocol(MissatgeProtocol.Tipus.DADES, dades));
}
// payload = { nomFitxer, directoriDesti, dades }
private void processarPujada(Object[] payload) throws IOException {
String nomFitxer = (String) payload[0];
String directoriDesti = (String) payload[1];
byte[] dades = (byte[]) payload[2];
Path desti = Path.of(directoriDesti, nomFitxer);
Files.write(desti, dades);
// enviem l'arbre actualitzat del directori afectat
sortida.writeObject(new MissatgeProtocol(
MissatgeProtocol.Tipus.ARBRE, new DirectoriInfo(new File(directoriDesti))));
}
private void tancar() {
try {
entrada.close();
sortida.close();
connexio.close();
System.out.println("Client desconnectat");
} catch (IOException e) { e.printStackTrace(); }
}
}En aquest exemple es llegeix el fitxer sencer a memòria amb Files.readAllBytes(). Per a fitxers grans això pot provocar OutOfMemoryError. En un servei real, cal
trossejar la transferència (per exemple, en blocs de
4.000 bytes), enviant diversos objectes seguits fins a completar el
fitxer.
El client és bàsicament simètric al servidor: obre un Socket cap al host/port del
servidor, n’obté els fluxos d’objectes i:
MissatgeProtocol de tipus ARBRE inicial i
n’omple una llista visual (JList).BAIXAR i espera rebre’n un de tipus DADES.PUJAR i espera rebre l’ARBRE
actualitzat.Socket connexio = new Socket("localhost", 5050);
ObjectOutputStream sortida = new ObjectOutputStream(connexio.getOutputStream());
ObjectInputStream entrada = new ObjectInputStream(connexio.getInputStream());
// 1) rebem l'estat inicial
MissatgeProtocol inicial = (MissatgeProtocol) entrada.readObject();
DirectoriInfo arrel = (DirectoriInfo) inicial.getPayload();
// 2) demanem un fitxer
sortida.writeObject(new MissatgeProtocol(
MissatgeProtocol.Tipus.BAIXAR, "/Fotos/foto1.jpg"));
MissatgeProtocol resposta = (MissatgeProtocol) entrada.readObject();
if (resposta.getTipus() == MissatgeProtocol.Tipus.DADES) {
byte[] contingut = (byte[]) resposta.getPayload();
Files.write(Path.of("foto1.jpg"), contingut);
}El disseny de l’apartat anterior ja incorpora el mecanisme clau per atendre múltiples clients alhora: un fil per connexió.
┌──────────────┐
Client A ─────▶ │ │ ─── Thread A ─── atén el client A
Client B ─────▶ │ ServerSocket │ ─── Thread B ─── atén el client B
Client C ─────▶ │ (accept()) │ ─── Thread C ─── atén el client C
└──────────────┘Punts clau d’aquest disseny:
while(true) { accept(); ... }) només fa una cosa: acceptar
connexions i llançar fils. No fa cap operació d’E/S lenta que el pugui
bloquejar.GestorClient extends Thread)
té el seu propi Socket i els seus propis fluxos
d’entrada/sortida, de manera que els clients no interfereixen entre si
pel que fa a la connexió.synchronized sobre l’objecte o recurs
compartit.ConcurrentHashMap,
etc.) si es manté estat compartit en memòria (per exemple, una llista de
clients connectats).FileLock) si cal garantir
exclusivitat sobre un fitxer concret.REFRESCAR a l’enum Tipus de MissatgeProtocol perquè el client pugui demanar l’arbre
actualitzat sota demanda.Crear un Thread nou per cada client és senzill, però pot
ser costós si s’esperen moltes connexions simultànies.
Una alternativa més eficient és utilitzar un
ExecutorService (pool de fils) de java.util.concurrent:
ExecutorService pool = Executors.newFixedThreadPool(20); // màxim 20 fils actius
while (true) {
Socket connexio = servidor.accept();
pool.submit(new GestorClient(connexio, arrel)); // GestorClient implements Runnable
}Això limita el nombre de fils simultanis i reaprofita els fils ja creats, evitant la sobrecàrrega de crear-ne un de nou per cada connexió.
Per comprovar que el nostre servei (o qualsevol servei estàndard) està disponible i funciona correctament, disposem de diverses eines i tècniques:
telnet <host> <port>.netcat/nc: alternativa a
telnet, més flexible (nc -vz host port per comprovar si un
port està obert).Des de Java mateix podem escriure petits “health checks” que provin de connectar-se i mesurin el temps de resposta:
public static boolean serveiDisponible(String host, int port, int timeoutMs) {
try (Socket s = new Socket()) {
s.connect(new InetSocketAddress(host, port), timeoutMs);
return true;
} catch (IOException e) {
return false;
}
}Per als clients de Commons Net, comprovar sempre el codi de resposta
després de connect()
(FTPReply.isPositiveCompletion(...), SMTPReply.isPositiveCompletion(...)) és, de fet, la primera
comprovació de disponibilitat del servei remot.
addProtocolCommandListener) en els clients de Commons Net
permet registrar tota la conversa client-servidor, útil per detectar
errors intermitents.System.out/Logger) en punts clau:
connexió/desconnexió de clients, errors d’E/S, peticions rebudes. Això
facilita tant el monitoratge en producció com la
depuració durant el desenvolupament.Si el servidor tanca una connexió per inactivitat (habitual en FTP
després d’uns 900 segons), es produirà una FTPConnectionClosedException. Detectar i tractar aquestes
excepcions específiques —en comptes de només capturar IOException genèricament— permet distingir entre “el servei
no està disponible” i “la connexió ha caducat per inactivitat”, i
reaccionar-hi de manera diferent (per exemple, reconnectant
automàticament).
Per completar l’aplicació de manera professional:
finally (o amb
try-with-resources), encara que es produeixi una excepció.main() gegant.DirectoriInfo, MissatgeProtocol…) i
comentaris que expliquin el flux de comunicació client-servidor.telnet, nc o
un client de propòsit general, per assegurar-nos que la conversa de baix
nivell és correcta.FTPSClient, SMTPSClient, POP3SClient) o protocols moderns (SFTP, IMAPS).