|
Comunicació TCP/IP de baix
nivell entre hosts: els sockets |
|
|
|
IPs, ports i sockets:
Una màquina està connectada
a la xarxa per alguna mena de dispositiu físic (una targeta
Ethernet, per exemple) que sol ser únic.
Totes les dades que entren i surten de
la màquina ho fan a través d'aquest dispositiu. Però
algunes de les dades són per a una aplicació determinada,
altres per una altra... Com es distingeixen i s'encaminen
cadascuna cap a la seva destinació?
Cada conjunt de dades que circula per la xarxa
Internet, o per la xarxa d'una
Intranet, porta annexa informació
respecte al nom de la màquina
que les ha de rebre, l'adreça IP,
que és un nombre enter de 32 bits,
és a dir, un nombre de quatre xifres escrit en base 256. Això
aconsegueix que les dades siguin "lliurades" només a
la màquina destinatària
i siguin ignorades per les altres màquines de la xarxa. Típicament,
una adreça IP té aquest
aspecte:
193.145.88.16 (màquina a la XTEC)
192.168.0.120 (màquines a intranets
petites)
Ara, la màquina que rep les dades ha de saber a quina de les aplicacions
que hi corren li van destinades. Això s'aconsegueix perquè
les dades porten annex també un nombre (enter
de 16 bits) que és el port.
Cadascuna de les aplicacions que esperen dades haurà establert
un socket (endoll) i l'haurà associat
a un port. Llavors el socket funciona
com un endoll virtual pel qual només
hi circulen les dades dirigides al port
que li està associat.
Parlant des del punt de vista del programador:
si volem que la nostra aplicació mogui
dades per la xarxa (amb el protocol TCP/IP!)
hem de construir sockets i associar-los
a ports. Les dades han de portar, annexa, informació
respecte a adreces IP i ports.
|
|
|
|
Alguns números
de port el costum els ha associat a aplicacions estàndard:
el port 21 correspon a servidors
FTP, el port 80 a servidors
HTTP, el port 23 a connexions
Telnet, encara que això és simplement un conveni avalat
per l'ús corrent. |
|
|
 |
Fem un experiment bàsic de connexió
TCP/IP amb sockets i ports.
Intenteu fer aquesta connexió via telnet
a "The National Institute for Standards
and Technology" des de la consola
de comandes del windows:
C:\WINDOWS>telnet
india.colorado.edu 13 |
Quan doneu aquesta instrucció, esteu intentant una connexió
a un dels servidors del NIST. Aquesta
màquina té en marxa un programa que té accés
a l'hora d'un rellotge atòmic. Quan nosaltres enviem un paquet
amb la sol·licitud de connexió al servidor pel port
13, el programa interpreta que ens interessa saber l'hora, genera
un paquet amb un cadena que descriu l'hora
actual (amb molta precisió!) ens l'envia i després tanca
la nostra connexió.
Programar aquest model de comunicació
amb Java és bastant senzill. Estudiem
ara com Java tracta elsl sockets.
|
|
|
|
Sockets per connectar |
|
|
|
- Un socket és,
doncs, un endoll amb una etiqueta identificativa,
el número de port, amb el qual
una aplicació surt a la xarxa. A Java,
un socket és una instància
de la classe java.net.Socket.
El mètode constructor més
elemental (el que farem servir aquí) és:
public Socket(String host, int port) |
String host és una cadena
(string) que representa, o bé,
la IP de la màquina amb la
qual volem connectar (193.145.88.16, 192.168.0.207,
etc.), o bé el seu nom (pie.xtec.net,
ftp.rediris.es, etc.), i
int port és el número
de port pel qual aquesta màquina
remota ens espera. Amb aquesta construcció, la nostra
màquina porta la iniciativa
de la connexió. La postura passiva,
és a dir, la d'esperar connexions d'altres màquines, requereix
una mica més de sofisticació i la tractem més avall.
- Una vegada establert el socket,
tant poden sortir dades cap a la màquina
remota com poden entrar-ne les que ens envia. La gestió
és aquesta:
- Construir els objectes
de flux d'entrada (classe
java.io.InputStream) i sortida (classe
java.io.OutputStream) amb els mètodes
de la classe
java.net.Socket:
public InputStream getInputStream()
public OutputStream getOutputStream() |
(Consulteu-ne la documentació
per veure la mena d'excepcions
que tiren)
- Gestionar els objectes
de flux de la manera que convingui. Als tres exemples següents,
la gestió es fa així:
- Per convertir els bytes de l'inputstream
a caràcters, construïm
un reader (classe
java.io.InputStreamReader)
per a aquest inputstream.
Després, construïm
un altre reader (classe
java.io.BufferedReader)
per a aquest inputstreamreader,
que ens permetrà llegir les dades línia per línia.
Consulteu la documentació
d'aquestes dues classes).
El mètode de la classe
java.io.BufferedReader,
ens permet llegir les dades d'entrada (suposat que són
caràcters!) línia a línia.
- Per poder escriure
sobre l'outputstream, construïm
un writer (classe
java.io.PrintWriter) per a
aquest outputstream. El mètode
per escriure és:
public void println(String x) |
i és de la classe java.io.PrintWrite
|
|
Posem tot això en funcionament: |
|
|
 |
Es tracta de connectar-se a una màquina de la qual sabem que ens
respondrà. Pot servir-nos qualsevol servidor FTP,
cosa que implica que el port ha de ser
el 21. La següent aplicació (projecte
Enviar, "Empty
Project") estableix un socket
a algun dels servidors i ports posats
com a variables de classe:
String
host="siscu05.infovia.xtec.net";
int port=21;
//String
host="ftp.rediris.es";
//int port=21;
//String host="pie.xtec.net";
//int port=21; |
(comenteu i descomenteu
al vostre gust).
Cada vegada que escriviu alguna cosa al teclat (stdIn)
i premeu "Retorn", s'envia
aquest text a la màquina remota
i es llegeix una línia d'allò
que la màquina remota ens està enviant.
|
|
|
|
|
|
|
|
El codi és aquest: |
|
|
|
import java.io.*;
import java.net.*;
public class Enviar {
public static void main(String[] args) throws
IOException {
//String
host="siscu05.infovia.xtec.net";
//int
port=21;
String host="ftp.rediris.es";
int port=21;
//String
host="pie.xtec.net";
//int
port=21;
Socket socket= null;
PrintWriter out=null;
BufferedReader in=null;
try
{
socket=new
Socket(host,port);
OutputStream
os=socket.getOutputStream();
out=new
PrintWriter(os,true);
InputStream
is=socket.getInputStream();
InputStreamReader
isr=new InputStreamReader(is);
in=new
BufferedReader(isr);
}
catch (UnknownHostException e) {
System.err.println("No
conec l'host: "+host);
System.exit(1);
}
catch (IOException e) {
System.err.println("No
puc establir "+
"connexió
I/O a "+host);
System.exit(1);
}
InputStreamReader
isr=new InputStreamReader(System.in);
BufferedReader stdIn=new
BufferedReader(isr);
String userInput="Escriu
comanaments per a "+host;
while
(userInput!=null) {
userInput=stdIn.readLine();
out.println(userInput);
System.out.println("R>"+in.readLine());
if
(userInput.equals("quit")) {
System.out.println("Programa
acabat...");
out.close();
in.close();
stdIn.close();
socket.close();
System.exit(0);
}
}
}
}
|
|
|
|
|
Si connecteu amb un servidor FTP
(port 21) o proveu
de fer una connexió Telnet (port
23) heu d'enviar textos que siguin comanaments,
per tal que la màquina remota, en
rebrel's, els entengui. Comandes
adequades són "user el_vostre_nom_d'usuari"
("user anonymous" si no teniu cap compte
a la màquina remota), "pass
el_vostre_password" ("pass la_meva_adreça_de_correu"
si entreu com a usuari anònim),
"pwd", etc. |
|
|
|
Sockets i threads: |
|
|
 |
A l'exemple anterior, l'esquema és
"rebo-envio-rebo-envio-rebo-...".
Però ja es veu que per a una bona comunicació, els processos
de rebre i d'enviar han de ser independents.
No ha de caldre que nosaltres enviem res per tal que veiem allò que
rebem. La solució immediata és separar el procés de
rebre en un thread apart. Això és
el que es fa en el següent exemple (projecte
EnviarAmbFils, "Empty
Project"): |
|
|
|
|
|
|
|
import java.io.*;
import java.net.*;
public class EnviarAmbFils {
//static String
host="siscu05.infovia.xtec.net";
//static int port=21;
static String host="ftp.rediris.es";
static int port=21;
//static String
host="pie.xtec.net";
//static int port=21;
RebreThread rebreThread;
BufferedReader in=null;
public static void main(String[] args) throws
IOException {
EnviarAmbFils
mainAp=new EnviarAmbFils();
Socket socket=null;
PrintWriter out=null;
try
{
socket=new
Socket(host,port);
OutputStream
os=socket.getOutputStream();
out=new
PrintWriter(os,true);
InputStream
is=socket.getInputStream();
InputStreamReader
isr=new InputStreamReader(is);
mainAp.in=new
BufferedReader(isr);
}
catch (UnknownHostException e) {
System.err.println("No
conec l'host: "+host);
System.exit(1);
}
catch (IOException e) {
System.err.println("No
puc establir "+
"connexió
I/O a "+host);
System.exit(1);
}
mainAp.rebreThread=new
RebreThread(mainAp);
mainAp.rebreThread.start();
InputStreamReader
isr=new InputStreamReader(System.in);
BufferedReader stdIn=new
BufferedReader(isr);
String userInput="
";
while
(userInput!=null) {
userInput=stdIn.readLine();
out.println(userInput);
if
(userInput.equals("quit")) {
System.out.println("Programa
acabat...");
out.close();
mainAp.in.close();
stdIn.close();
socket.close();
System.exit(0);
}
}
}
}
class RebreThread extends Thread {
EnviarAmbFils mainAp;
public RebreThread (EnviarAmbFils
ap) {
super();
mainAp=ap;
}
public void run () {
String missatge;
while
(true) {
try
{
missatge=mainAp.in.readLine();
System.out.println("R>"+missatge);
}
catch (IOException eIO) {
System.err.println("No
rebo res de "+
EnviarAmbFils.host);
System.out.println("Programa
acabat...");
System.exit(1);
}
}
}
} |
|
|
|
|
Observeu que a l'haver d'instanciar
la classe EnviarAmbFils
en un objecte mainAp,
les variables de classe, o bé es
referencien com a mainAp.variable
(variables de l'objecte), o bé
han de ser static! (variables
de la classe). |
|
|
|
Sockets per escoltar. Convertint
la nostra màquina en un servidor: |
|
|
|
Si del que es tracta és que la nostra
màquina estigui en espera
d'eventuals connexions que puguin requerir altres màquines de la
xarxa, llavors direm que la nostra màquina actua com a servidor
(server) i el procediment a seguir és
aquest:
- Construir un serversocket,
que és una instància
de la classe java.net.ServerSocket.
El mètode constructor més
elemental (el que farem servir aquí) és:
public ServerSocket(int port) |
El paràmetre
int port és el número
de port al qual s'han de dirigir les màquines
remotes per connectar-se a la nostra. Un serversocket
és, doncs, un endoll virtual
que una aplicació exposa a l'exterior.
- Per tal que la connexió s'obri efectivament,
cal crear un socket pel qual hi circularan
els streams corresponents (i que ja
hem après a gestionar abans). Cal cridar al mètode
de la classe java.net.ServerSocket:
Quan alguna màquina de la xarxa requereix
la connexió, s'executa aquest mètode
i es crea el socket i ja pot circular
la informació entre ambdues màquines!
|
|
L'exemple següent és
una variació de l'anterior per tal que la nostra màquina actui
com a servidor. El resultat és una
aplicació de xat en pantalla
negra, mitjançant la qual podem enviar i rebre en temps
real missatges de text a una altra màquina de la xarxa: |
|
|
|
import java.io.*;
import java.net.*;
public class Xat {
|
static
String host="192.168.0.150";
static int portRemot=10;
static int portLocal=10; |
RebreThread rebreThread; |
Socket
xerraSocket=null,escoltaSocket=null;
ServerSocket serverSocket=null; |
public static void main(String[] args) throws
IOException { |
if
(args.length>0) {
host=args[0];
}
if
(args.length>1) {
portRemot=Integer.parseInt(args[1]);
}
if
(args.length>2) {
portLocal=Integer.parseInt(args[2]);
} |
Xat
mainAp=new Xat();
PrintWriter out=null; |
try
{
mainAp.serverSocket=new
ServerSocket(portLocal);
}
catch (IOException e) {
System.out.println("No
puc obrir el meu port "+
portLocal);
System.exit(1);
} |
mainAp.rebreThread=new
RebreThread(mainAp);
mainAp.rebreThread.start();
InputStreamReader
isr=new InputStreamReader(System.in);
BufferedReader stdIn=new
BufferedReader(isr);
String userInput="
";
while
(userInput!=null) {
userInput=stdIn.readLine(); |
try
{
mainAp.xerraSocket=new
Socket(host,
portRemot);
OutputStream
os=
mainAp.xerraSocket.getOutputStream();
out=new
PrintWriter(os,true);
}
catch (UnknownHostException e) {
System.out.println("No
conec l'host: "+
mainAp.host);
System.exit(1);
}
catch (IOException e) {
System.out.println("No
puc connectar a "+
host);
System.exit(1);
} |
out.println(userInput); |
out.close();
mainAp.xerraSocket.close();
mainAp.xerraSocket=null; |
if
(userInput.equals("quit")) {
System.out.println("Programa
acabat...");
stdIn.close();
System.exit(0);
}
}
}
}
class RebreThread extends Thread {
Xat mainAp;
public RebreThread (Xat ap) {
super();
mainAp=ap;
}
public void run () {
String missatge;
while
(true) {
try
{ |
mainAp.escoltaSocket=
mainAp.serverSocket.accept();
InputStream
is=
mainAp.escoltaSocket.getInputStream();
InputStreamReader
isr=
new
InputStreamReader(is);
BufferedReader
in=new BufferedReader(isr);
missatge=in.readLine(); |
System.out.println("R>"+missatge); |
in.close();
mainAp.escoltaSocket.close(); |
}
catch (IOException eIO) { |
System.out.println("Lectura
fallada: "+
mainAp.portLocal);
}
mainAp.escoltaSocket=null; |
}
}
} |
|
|
|
|
Observeu:
- Per defecte, la màquina es connectarà
a la màquina 192.168.0.150 pel seu
port 10.
El port que ofereix per a connexions
és el 10. Això es pot variar
al cridar l'execució de l'aplicació si la demanem amb
els paràmetres:
<IP
de la maquina remota> <port remot> <port local> |
- Necessiteu dues màquines connectades
en xarxa per assajar l'aplicació!
- Allò que escriviu vosaltres apareixerà
a la pantalla sense més. Al prémer "Retorn",
el text, llegit del reader isr
(que llegeix de la entrada estàndard
del sistema, System.in, és a
dir, del teclat) s'escriu al writer
out i marxa cap a l'altra màquina.
- El text que ve de l'altra màquina, llegit
del reader isr
del thread (que llegeix de l'inputstream
del socket, is) s'escriu a la sortida
estàndard del sistema, és a dir, de la pantalla,
precedit de "R>".
|
|
|
|
Temps morts d'un socket: |
|
|
 |
Als programes que hem escrit, si el socket
no pot establir la connexió amb el servidor, es queda esperant.
El programa quedarà penjat tot el temps que el sistema operatiu
permeti. És raonable decidir d'antuvi el temps
mort que li permetrem, cridant el mètode
de la classe java.net.Socket, public
void setSoTimeout(int timeout).
Socket s = new
Socket(...);
s.setSoTimeout(15000); // temps
en mil·lisegons d'espera |
D'aquesta forma, quan un socket sobrepassi
el temps mort admisible, llençarà
una excepció del tipus
InterruptedIOException que el nostre programa pot interceptar i,
llavors, actuarà en consequència.
|
|
|
 |
El SDK amb el que treballem en aquest
curs, el 1.4 permet abreujar aquest procés:
Socket s = new
Socket();
s.connect(new InetSocketAddress("servidor",port),tempsmort); |
És clar que d'aquesta forma fem incompatible el nostre codi amb
SQK més antics.
|
|
|
|
Resoldre les IPs d'internet: |
|
|
 |
Com hem vist en els programes d'exemples anteriors,
una de les limitacions que tenim és que per a fer la connexió
necessitem la IP del servidor, mentres
que sovint, estem força més acostumats a memoritzar o desar
l'adreça de l'host pel seu nom.
Una altra novetat que incorpora la versió
1.4 del SDK de
Java és el de permetre la resolució
de noms de manera fàcil. Si als vostres programes us resulta
més fàcil utilitzar el nom del lloc
que la seva IP, llavors, per a resoldre
el nom, podeu fer servir la classe InetAddress
del paquet java.net.
|
|
|
 |
Escriviu aquest petit programa per tal de verificar-ne
el funcionament: |
|
|
|
import java.net.*;
class AInternet {
public static void main(String[] args) {
try
{
String
host = "www.xtec.net";
InetAddress[]
ads = InetAddress.getAllByName(host);
for
(int i=0; i < ads.length; i++) {
System.out.println(ads[i]);
}
} catch (Exception e) {
}
}
}
|
|
|
|
|
|
|
|
|
|
 |
|
|
|
|