Sòcols
- TCP i UDP
- Protocol exemple: ECHO
- Protocol exemple: SMTP
- Comunicació TCP
- Comunicació UDP
- Timeouts
- Tancament
- Comunicació asíncrona
Un sòcol és un enllaç de doble sentit que permet comunicar dos programaris que són a la xarxa.
Aquests dos programaris fan dues funcions: la del client i la del servidor. El servidor proveeix algun servei des d'un lloc conegut (adreça IP + port), i el client accedeix a aquest servei. Aquest servei ha d'implementar un protocol ben definit, sigui un estàndard o un de dissenyat a mida.
Els números de ports són:
- El rang 0 a 1023 són els ports coneguts (well-known) o de sistema. En Linux, cal ser administrador per tenir un servei en aquests ports.
- El rang 1024-49151 són els ports registrats, assignats per IANA.
- El rang 49152–65535 són ports dinàmics o privats, o de vida curta.
Com que els dos programaris treballen en l'àmbit del protocol, al codi dels dos programaris no hi ha dependències mútues. Però és habitual que qui implementa el protocol proveeixi d'una llibreria de client per poder accedir al servei. Això permet reduir el codi que un client ha d'escriure, i assegura que utilitzarà correctament el protocol. A Java, la llibreria de client es materialitza mitjançant un arxiu jar i una documentació d'ús.
TCP i UDP
Es poden utilitzar els protocols TCP o UDP. TCP està orientat a connexió, i UDP no. Això vol dir que TCP requereix un pas previ de connexió entre el client i el servidor per tal de comunicar-se. Un cop establerta la connexió, TCP garanteix que les dades arribin a l’altre extrem o indicarà que s’ha produït un error.
En general, els paquets que han de passar en l'ordre correcte, sense pèrdues, utilitzen TCP, mentre que els serveis en temps real on els paquets posteriors són més importants que els paquets més antics utilitzen UDP. Per exemple, la transferència d’arxius requereix una precisió màxima, de manera que normalment es fa mitjançant TCP, i la conferència d’àudio es fa freqüentment a través d’UDP, en què pot ser que no es notin les interrupcions momentànies.
TCP necessita uns paquets de control per a establir la connexió en tres fases: SYN, SYN + ACK i ACK. Cada paquet enviat es contesta amb un ACK. I finalment, es produeix una desconnexió des de les dues bandes amb FIN + ACK i ACK.
UDP, en canvi, només transmet els paquets de petició / resposta, sense cap control sobre la transmissió.
Protocol exemple: ECHO
A continuació es pot veure la visualització del protocol ECHO amb Wireshark, tant per a la implementació TCP com la UDP.
Captura TCP (petició i resposta)
Captura UDP (petició i resposta)
Protocol exemple: SMTP
SMTP és un protocol que funciona al port 25, sobre TCP. El client envia comandes, i el servidor respon amb un codi d'estat.
A continuació, veiem una conversa (C: client / S: servidor). Tota aquesta conversa es manté sobre una connexió oberta.
C: <client connects to service port 25>
C: HELO snark.thyrsus.com la máquina que envia s'identifica
S: 250 OK Hello snark, glad to meet you el receptor accepta
C: MAIL FROM: <esr@thyrsus.com> identificació de l'usuari que envia
S: 250 <esr@thyrsus.com>... Sender ok el receptor accepta
C: RCPT TO: cor@cpmy.com identificació del destí
S: 250 root... Recipient ok el receptor accepta
C: DATA
S: 354 Enter mail, end with "." on a line by itself
C: Scratch called. He wants to share
C: a room with us at Balticon.
C: . final de l'enviament multi-línia
S: 250 WAA01865 Message accepted for delivery
C: QUIT l'emissor s'acomiada
S: 221 cpmy.com closing connection el receptor es desconnecta
C: <client hangs up>
Comunicació TCP
Java té dues classes del paquet java.net que ho permeten:
Socket
: implementació del sòcol client, que permeten comunicar dos programaris a la xarxa.ServerSocket
: implementació del sòcol servidor, que permet escoltar peticions rebudes des de la xarxa.
El funcionament es reflecteix en les següents dues imatges: primer el client demana una connexió, i després el servidor l'accepta, i s'estableix.
Servidor a majúscules
Aquest servidor escolta línies de text i retorna la versió en majúscules.
La comunicació comença amb la petició de connexió del client, i l'acceptació del servidor. Aquestes dues accions creen un sòcol compartit del tipus Socket, sobre el qual, tan el client com el servidor, poden utilitzar els mètodes:
getInputStream()
getOutputStream()
El client pot enviar cadenes de text, que el servidor convertirà a majúscules.
El protocol que ens hem inventat estableix que la comunicació s'acaba quan el client envia una línia buida. a la qual el servidor contesta amb un comiat.
Aquest podria ser un codi del servidor que implementa el protocol descrit utilitzant TCP.
ServerSocket serverSocket = new ServerSocket(PORT);
Socket clientSocket = serverSocket.accept();
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
out.println("hola!");
String text;
while ((text = in.readLine()).length() > 0)
out.println(text.toUpperCase());
out.println("adeu!");
clientSocket.close();
serverSocket.close();
Pots provar-ho mitjançant la comanda netcat (nc).
Com seria el protocol d'aquest servei? En pseudocodi:
- Quan et connectes al servidor, envia una línia amb una salutació.
- Per cada línia que envies, et retorna la mateixa línia en majúscules.
- Quan envies una línia en blanc, et contesta amb el comiat, i es desconnecta.
A continuació, es pot veure un client que accedeix a aquest servei, implementant aquest protocol.
Socket clientSocket = new Socket(HOST, PORT);
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String salutacio = in.readLine();
System.out.println("salutacio: " + salutacio);
for (String text: new String[]{"u", "dos", "tres"}) {
out.println(text);
String resposta = in.readLine();
System.out.println(text + " => " + resposta);
}
out.println();
String comiat = in.readLine();
System.out.println("comiat: " + comiat);
in.close();
out.close();
clientSocket.close();
Comunicació basada en text
Als exemples que hem vist, PrintWriter
i BufferedReader
són els objectes que permeten utilitzar cadenes de text sobre l'OutputStream
i l'InputStream
respectivament, que són mitjans de comunicació binària.
El més correcte en aquests casos seria indicar quin és el Charset amb que codifiquem els Strings. Si volguèssim utilitzar UTF-8, seria així:
Charset UTF8 = StandardCharsets.UTF_8;
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), UTF8), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF8));
Relacionat amb el charset, tenim la conversió des de i cap a String a partir de dades binàries, que podem realitzar a UDP. Aquestes són les dues operacions:
byte[] strBytes = "Hola, món!".getBytes(UTF8);
String str = new String(strBytes, UTF8);
Comunicació UDP
Amb UDP no hi ha connexió: simplement s'envien paquets (Datagrames) amb destinació un servidor UDP. Si volem respondre, cal conèixer l'adreça i port destí, que pot obtenir-se del paquet rebut.
Un servidor es pot crear amb:
DatagramSocket socket = new DatagramSocket(PORT)
El client funciona exactament igual, però el socol es crea amb:
DatagramSocket socket = new DatagramSocket();
Per rebre un paquet de mida màxima MIDA:
byte[] buf = new byte[MIDA];
DatagramPacket paquet = new DatagramPacket(buf, MIDA);
socket.receive(paquet);
// dades a paquet.getData() i origen a paquet.getAddress() i paquet.getPort()
Per enviar un paquet al servidor:
InetAddress address = InetAddress.getByName(HOST);
paquet = new DatagramPacket(buf, MIDA, address, PORT);
socket.send(paquet);
Per enviar un paquet de resposta a un client, hem d'utilitzar el port que hi ha al paquet que ens ha enviat prèviament:
InetAddress address = paquetRebut.getAddress();
int port = paquetRebut.getPort();
DatagramPacket paquetResposta = new DatagramPacket(buf, buf.length, address, port);
socket.send(paquet);
Concurrència
Com podem fer que els servidors acceptin peticions concurrents de diversos clients?
Ho vam veure a la UF "Processos i fils". Caldria atendre cada petició a un fil diferent. Per exemple, per al cas de TCP:
Executor executor = Executors.newFixedThreadPool(NFILS);
ServerSocket serverSocket = new ServerSocket(PORT);
while (true) {
final Socket clientSocket = serverSocket.accept();
Runnable tasca = new Runnable() {
public void run() {
atendrePeticio(clientSocket);
}
};
executor.execute(tasca);
}
D'aquesta manera, no fem esperar nous clients quan atenem un.
Timeouts
Quan utilitzem sòcols TCP, les operacions de connexió i enviament de dades requereixen la confirmació de l'altra part. Per defecte, l'API que hem vist no té temporitzadors (timeouts), i es bloqueja indefinidament.
També hi ha l'operació UDP de llegir un datagrama, que per defecte es bloqueja esperant la resposta.
Aquestes són les operacions de connexió TCP:
new Socket(host, port...)
: creació d'un sòcol i connexió sense timeout.new Socket()
: creació d'un sòcol no connectat (no hi ha bloqueig).Socket.connect(SocketAddress, timeout)
: connexió d'un sòcol no connectat amb un timeout.
Un cop creat un sòcol TCP, sigui de client (Socket
) o de servidor (ServerSocket
), podem canviar la temporització utilitzant Socket.setSoTimeout(int timeout)
. Quan es cumpleix el temporitzador, es llença una excepció de tipus SocketTimeoutException
. Aquest timeout (en mil·lisegons) afecta les següents operacions TCP/UDP:
ServerSocket.accept()
: acceptació d'una connexió d'un client al servidor TCP.SocketInputStream.read()
: lectura de dades a un sòcol TCP.DatagramSocket.receive()
: lectura d'un datagrama UDP.
Tancament
Aquests són alguns aspectes associats al tancament d'un sòcol TCP:
- Podem comprovar si una connexió TCP està tancada amb
Socket.isClosed()
iServerSocket.isClosed()
. - Si tanquem la connexió d'un sòcol amb
Socket.close()
, es tanquen automàticament els seus streamsInputStream
iOutputStream
. - Si tanquem la connexió de qualsevol dels seus streams, es tanca automàticament la del sòcol associat.
- Si fem un
ServerSocket.close()
i s'està esperant amb unServerSocket.accept()
, s'interromprà i hi haurà una SocketException. - Si un sòcol està esperant amb un
SocketInputStream.read()
i el sòcol de l'altra banda es tanca, elread()
s'interromprà i hi haurà una SocketException. - Si un sòcol està esperant en un canal de text amb un
BufferedReader.read()
oBufferedReader.readLine()
i el sòcol de l'altra banda es tanca, es retornarà -1 i null, respectivament.
Comunicació asíncrona
Basada en les llibreries NIO. Veure aquest post.