»
S
I
D
E
B
A
R
«
Programando para la Red. Parte II
Septiembre 12th, 2009 by John Carlos Arrieta Arrieta


Simbiotica


Creative Commons License



www.simbiotica.com.co


Contáctenos…

Esta sitio y todo su contenido  está protegida por
licencia de Creative Commons

Construyendo un Sistema Cliente – Servidor básico

Primera Parte

Un cordial saludos para todos…

Bueno, primero que todo pido disculpas por tardar un poco en publicar este articulo, últimamente he tenido un montón de cosas que hacer en mi trabajo, las cuales por obvias razones no puedo ni debo descuidar, tuve muchas solicitudes de los usuarios de mi sitio web, que me animaron a terminarlo.

Como lo había prometido, en esta segunda entrega les explico la construcción básica de un de los elementos de un sistema distribuido, bajo arquitectura cliente servidor, lo he escrito en Java, ya que según el perfil que tengo de los usuarios registrados en mi sitio, la gran mayoría sabe y conoce muy bien este formidable lenguaje de programación.

Si alguien tiene algún inconveniente en comprender el código, solo tiene que hacérmelo saber en los comentarios y seguro que juntos escribiremos el mismo u otro ejemplo en el lenguaje que desee, si no lo conozco, pues ya saben que estoy habido de ganas de aprender nuevas cosa.

Manos a la obra.

Fichero ejecutable del ClienteTcpy del ServidorTcp

Con el fin de integrar y materializar los conceptos previamente leídos en del primer artículo, iniciare con la explicación de los elementos básicos de software que hacen posible la abstracción de comunicaciones  Cliente/Servidor, a través de las capas de IP y de Transporte, a estos componentes de software se les conoce como Socket (conectores o enchufe), los cuales están presentes en la gran mayoría de lenguajes de programación (C, C++, pascal, cobol, fortran, VB, Java, JavaScript, PHP, Ruby, python y .Net Framework, etc).

Sin importar cuál es el lenguaje de programación que utilicemos para escribir nuestro sistema distribuido, todos utilizan socket para establecer la conexión y el intercambio de datos entre las partes interesadas.

image002

Como se muestra en la figura toda aplicación que desee transmitir datos por una red a través del protocolo TCP/IP debe utilizar Socket, si aplicativo solo acepta conexiones y responde peticiones, se encuentra realizando el papel del un Servidor, en este caso, el aplicativo solo utiliza socket pasivos, estos socket no están conectados, solo se encuentran esperando conexiones de otros socket interesados en intercambiar datos, a los cuales se les conoce como socket activos y son utilizados por las aplicaciones clientes,  estos conectores siempre encuentran en uno de los siguientes estados:

Iniciando conexión: el conector activo utiliza la dirección y el puerto de red  y solicita aal protocolo TCP/IP que realice una búsqueda de que le permita conectarse a un Host de la red que tenga asignada la dirección y puerto de red especificados, espera un tiempo prudencial la respuesta de la red.

Conectado: Si la red encuentra un host con la IP y puerto indicado en el estado anterior, entonces el socket activo se conecta con el socket pasivo, y puede permanecer en este estado hasta que el mismo o en su defecto el socket pasivo   decida desconectarlo.

En transferencia: Este estado se presenta mientras el socket activo está conectado al socket pasivo, durante este escenario el socket activo se encuentra enviado (Output) y recibiendo (Input) flujos Stream de datos desde el cliente hacia el servidor y viceversa, los datos son enviados y recibidos por la capa de TCP del protocolo y enrutados por hacia su destino por la capa IP, la información es dividida, numerada y etiquetada con información que necesita la red para poder enviarla y recibirla, a cada fracción de información se le conoce con el nombre de Paquete, los datos propios de la información que se quiere enviar desde un punto a otro, pueden enviarse en diferentes formatos, desde el formato mas puro como lo es byte, pasando por formatos más específicos, cadenas de caracteres, numéricos, boléanos arreglos y hasta objetos.

Una vez el conector activo envía los datos desde el cliente al servidor o viceversa, debe recuperarlos y aplicarles los mimos formato en el que fueron enviados.

Para salir del estado de transferencia, el conector activo o el conector pasivo deben cerrar los flujos de envió y recepción de datos.

Desconectado: el conector activo llega a este estado cuando el proceso de comunicación entre cliente y servidor ha finalizado, para llegar a este punto es necesario que el socket activo haya salido del estado de transferencia.

Hay dos formas de desconectar al socket activo, una de ellas es que el socket activo se desconecte así mismo del socket pasivo, y la otra es cuando el socket pasivo decide desconectar al socket activo.

Cuando este estado ocurre, se ha eliminado todo tipo de comunicación entre el cliente y el servidor.

El socket pasivo es el complemento del socket activo, ambos conectores son necesario para poder iniciar el proceso de comunicaciones entre aplicaciones utilizando el protocolo TCP/IP, el socket pasivo también entra en un conjunto de estados secuenciales durante el proceso de comunicación, esto estado son:

Preparando el puerto: el trabajo del socket pasivo inicia cuando le solicita al sistema operativo que le permita utilizar una dirección IP y un puerto de red específico, el sistema operativo consulta en su fichero services y verifica que dicho puerto esté disponible, si es el caso, el sistema operativo asigna este puerto de red al socket pasivo, entonces este tiene potestad y control total sobre dicho puerto, existen algunos factores que pueden impedir que un conector pasivo pueda tener control sobre el puerto que se fue asignado, quizás el factor más común es la presencia de un cortafuegos en ejecutándose en el host del servidor, el cual tenga una regla establecida una regla para impedir conexiones de la red al puerto que fue asignado al socket pasivo.

Escuchado por el puerto: Una vez que el socket  pasivo posea permisos para trabajar con el puerto deseado, este conector inicia el proceso de escuchar cualquier conexión proveniente desde un socket activo ejecutándose en una Host cliente, este proceso detiene al proceso servidor hasta que esté el socket pasivo detecte una conexión, por este motivo lo más recomendable es ejecutar en un segundo plano (Hilos o Threads ver mi artículo sobre Hilos) el código que implementa el socket pasivo, de tal forma que esta operación sea ejecutada de forma concurrente y no obstruya  la ejecución del proceso (aplicación) servidor.

Aceptando conexiones: en este estado el socket pasivo acepta las conexiones de los socket activos que desean establecer comunicaciones con él, cada socket activo al cual se acepta la conexión, es enlazado o anclado al proceso servidor, este socket se utilizara como puente entre de transferencia de datos entre el cliente y el servidor y viceversa.

En transferencia: este es el mismo estado en el que se coloca el socket activo, de hecho el socket pasivo no transfiere información entre el servidor y cliente, por eso este estado es propio del socket activo, ya sea desde el lado del cliente o del lado del servidor.

Es recomendable que el proceso de transferencia sea ejecutado en segundo plano  (Hilos o Threads), de esta manera el proceso servidor puede seguir aceptando conexiones sockets activos, mientras las operaciones de comunicación con cada uno de los clientes conectados se realizan de forma paralela o concurrente, de los contrario, el servidor solo podrá aceptar una conexión de cliente al tiempo, iniciar el proceso de transferencia con dicho cliente y mantener en cola todas las conexiones que entran mientras se ejecuta la comunicación con la conexión actual, disminuyendo radicalmente su eficiencia.

Volver a aceptar conexiones: aceptada la conexión de un cliente, el socket pasivo debe volver a aceptar de forma concurrente nuevas conexiones.

Desconectar socket activos: es muy importante para el cliente y el servidor que se realice una desconexión de los sockets activos que han terminado el proceso de transferencia, puesto que esto su conexión es inutilizable y consume recursos tanto en el proceso cliente como en el proceso servidor.

Liberar el puerto de red: cuando el proceso servidor ha terminado de atender a todos los clientes, debe liberar el puerto de red solicitado y asignado por el sistema operativo, de esta manera el proceso servidor impide que se vuelvan a conectar mas socket activos de procesos clientes, además, el proceso servidor libera recursos entre ellos los flujos de entrada y de salida, el puerto de red y memoria.

image004

Socket con Java

El popular lenguaje de programación Java, al igual que muchos de los lenguajes de programación con los que contamos hoy día, permite trabajar de forma muy simple con los socket ya sea desde el protocolo TCP/IP o UDP/IP. Java contiene un API (Application Programming Interface) para escribir aplicaciones que interactúen con la redes, esta librería se llama java.net y hace parte del JSDK (Java Standar Developer Kit).

Esta librería contiene un conjunto de clases escritas solo con el objetivo de ofrecer al programador un nivel alto de abstracción sobre el trabajo con redes de computo, algunas   de estas clases son Socket y ServerSocket, DatagramSocket y InetAddressy URL, las cuales representan respectivamente al conector activo (Cliente) TCP,  al conector pasivo (Servidor) TCP, al conecto (Cliente y Servidor) UDP, a una dirección IP de host y a una dirección de recurso (de red o local).

En este segundo artículo explicare la construcción básica de un aplicativo Cliente/Servidor TCP escrito en código Java, en posteriores articulo hare lo propio con un aplicativo Cliente/Servidor UDP.

La estructura básica de un servidor TPC escrito en Java es la siguiente

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

Import java.net.*;

Import java.io.*;

public class ServidorTCP implements Runnable{

private ServerSocket  conectorPasivo;

private Thread hilo;

private static ServidorTCP objeto;

private  ServidorTCP( ){

hilo = new Thread(this);

hilo.star();

}

public static ServidorTCP getObjeto(){

if( objeto == null ){

objeto = new ServidorTCP();

}

return objeto;

}

public void run(){

try{

// Solicitar una IP y un puerto de red para aceptar conexiones

// Aceptar iterativamente conexiones de clientes

// Obtener los flujos de entrada y de salida

// Enviar y recibir datos hacia y desde el cliente

// Cerrar los flujos de entrada, salida y la conexión cliente

}

catch( IOException e ){

// Hacer algo cuando ocurra un error durante:

// Se acepta la conexión de algún cliente

// La obtención los flujos de entrada y de salida

// Se envían y reciben datos hacia y desde el cliente

// Se cierran los flujos de entrada, salida y la conexión cliente

}

}

public void deterCoenxiones(){

// cerrar el conector pasivo.

}

}

Como se pueden ver, se trata de diseñar una clase que contenga el código necesario para poder utilizar los métodos de la clase ServerSocket y Socket.

Noten que he implementado la interfaz Runnable, esta interfaz marca el código escrito dentro del método run, como código que se puede ejecutar en segundo plano, esto se hace mediante la creación de un objeto de la clase Thread.

Le método getObjeto y la variable objeto de la clase ServidorTCP, son solo una implementación sencilla del patrón de diseño Singleton, el cual nos permite restringir la creación de objetos de la clase que lo implementa (ServidorTCP), a solo un único objeto en memoria durante la ejecución de programa.

El eje de la clase se encuentra en el método run, donde podemos ver en comentario las diferentes actividades que se deben escribir para que el servidor funcione tan y como indica el estándar de socket TCP.

A continuación escribiré en código Java y explicare cada una de las actividades que se realizan dentro del método run.

Solicitar una IP y un puerto de red para aceptar conexiones

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
conectorPasivo = null;
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

try{

conectorPasivo =  new ServerSocket(7000);

}

catch(IOException e){

// Realizar algo si el Puerto no está disponible...

}

Donde ServerSocket representa la clase (a una Clase es algoritmo diseñado mediante la metodología de programación Orientación a Objetos) que tiene todo lo necesario para utilizar un puerto  de red y asignarlo a un proceso servidor, siempre y cuando este puerto se encuentre disponible. La clase ServerSocket se encuentra en la librería java.net que se ofrece junto al SDK de Java. Si el puerto no está disponible, se notifica mediante un objeto de la clase IOException de la librería java.io del JSDK.

Aceptar iterativamente conexiones de clientes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

boolean inico = true;

Socket conectorActivo = null;

while( inicio ){

try{

// Aceptar conexiones de clientes

conectorActivo = conectorPasivo.accept();

// Obtener los flujos de entrada y de salida

// Enviar y recibir datos hacia y desde el cliente

// Cerrar los flujos de entrada, salida y la conexión cliente

}

catch(IOException e){

// hacer algo cuando no se puede establecer conexión con un cliente

// es decir, no se peude anclar el cliente al servidor

}

}

Una vez el proceso servidor tiene bandera verde para poder utilizar el puerto, inicia con el proceso de escuchar las posibles conexiones que realizan los procesos clientes a través de la red, cada cliente conectado es anclado al proceso servidor y se procesa su petición.

conectorActivo = conectorPasivo.accept();realiza la aceptación de conexiones de socket Activos (Socket).

Donde conectorPasivo es el objeto ServerSocket y accept() es una de sus operaciones, responsable de escuchar y aceptar las conexiones de los proceso Clientes, conectorActivo corresponde a un objeto de la clase Socket de la librería java.net del JSDK, esta clase representa el conector que inicio y solicito establecer una conexión desde el proceso Cliente hacia el procesos Servidor.

Al tener del lado del proceso Servidor una instancia del objeto Socket (conector del proceso Cliente) que realizo solicitud de conexión, se puede anclar objeto al servidor con el fin de mantener un enlace entre proceso Servidor y proceso Cliente.

Si se desea que cada vez se relace una nueva conexión, el proceso servidor se mantenga siempre alerta y dispuesto a atender otras posibles conexiones, se debe idear un mecanismo que permita volver a la instrucción de aceptación, en este caso utilizo un ciclo, el cual es controlado por una variable booleana que puede establecerse como verdadera (true) para que siga aceptando conexiones, o establecerse como falsa (false) para detener la aceptación de conexiones.

Obtener los flujos de entrada y de salida

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

String liena = "";

BufferedReader mensajeroInput = null;

PrintStream mensajeroOutput = null;

String mensajeEnviar = "Hola Cliente, saludos";

try{

// Obtener los flujos de entrada y de salida

// El flujo de entrada que viene del cliente

mensajeroInput = new BufferedReader(new InputStreamReader(conectorCliente.getInputStream()));

// El flujo de salida que va hacia el cliente

mensajeroOutput = new PrintStream(cliente.getOutputStream());

// Enviar y recibir datos hacia y desde el cliente

// Recibir datos

// Enviar datos

// Procesar la petición del cliente, según los datos enviados

// Cerrar los flujos de entrada, salida y la conexión cliente

// Si es necesario.

}

catch( IOException e ){

// Hacer algo cuando no se pueda obtener los flujos de entrada o de // salida desde el conectorActivo, o cuando ocurra algún problema

// al enviar o recibir datos o cerrar los flujos

// o desconectar al cliente

}

Establecida la conexión entre el proceso servidor y el proceso cliente, lo siguiente es preparar al sistema servidor para que pueda recibir y enviar información desde y hacia el proceso cliente.

El proceso Servidor utiliza flujos de salida (OutputStream) para enviar datos (bytes) al proceso Cliente, la información enviada llega al Cliente como flujos de entrada (InputSteam), este último los recupera e interpreta y realiza operaciones necesarias pertinentes.

1
2
3
4
5
6
7
8
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

mensajeroInput = new BufferedReader(

new InputStreamReader(conectorActivo.getInputStream()));

De igual forma El proceso Cliente utiliza flujos de salida (OutputStream) para enviar datos (bytes) al proceso Servidor, la información enviada llega al Servidor como flujos de entrada (InputSteam), este último los recupera e interpreta y realiza operaciones necesarias pertinentes.

1
2
3
4
5
6
7
8
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

PrintStream  mensajeroOutput = new PrintStream(

conectorActivo.getOutputStream());

El proceso de comunicación entre cliente y servidor consiste en el intercambio de flujos de información (byte) entre ambas partes, por este motivo es imprescindible poseer flujos de entrada y de salida entre cliente y servidor.

Ejemplo de obtención de los flujos de salida y de entrada del lado del proceso del servidor utilizando Java.

Enviar y recibir datos hacia y desde el cliente

En la anterior fracción de código Java podemos apreciar el uso de la instrucción conectorActivo.getInputStream();  para obtener el flujo de entrada que proviene desde el proceso Cliente, este flujo contiene todos los bytes de la información que envía el proceso Cliente, según nuestras necesidades, es recomendable convertirlos bytes a un formato mejor procesable, como por ejemplo un Buffer de caracteres leibles, operación que se realiza con la clase BufferedReader y la clase InputStreamReader, ambas clases se encuentran en el paquete java.io que vienen con el JSDK.

La operación de recepción de datos enviados desde el Cliente al Servidor, se hace utilizando alguno de los métodos que ofrece la clase BufferedReader que encapsula el flujo de entrada, como por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

// Leer datos

String línea ="";

do{

// lee todas las líneas que llegan en un envió hecho por el cliente;

línea = mensajeroInput.readLine();

}

while(mensajeroInput.ready());

Igual sucede con el flujo de escritura, la instrucción conectorActivo.getOutputStream() permite obtener el flujo de salida, útil para enviar datos al proceso cliente, estos datos se envían por defecto como un conjunto de bytes, por lo tanto es recomendable transformarlos en caracteres imprimibles, (un carácter es un dato del conjunto de códigos de la tabla ASCII, el cual puede ser numero, letra o símbolo de puntuación), esta conversión se puede realizar con la clase PrintStream que se  encuentran en el paquete java.io que vienen con el JSDK.

La operación de envió de datos enviados desde el Servidor hacia el Cliente, se hace utilizando alguno de los métodos que ofrece la clase PrintStream que encapsula el flujo de salida, como por ejemplo:

1
2
3
4
5
6
7
8
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

mensajeroOutput.println(mensajeEnviar); // mensajeEnviar tiene los datos</p>

mensajeroOutput.flush();

println(mensajeEnviar); envía al cliente una cadena de caracteres imprimibles, los cuales salen del servidor como parte de su flujo de salida y llegan al cliente como parte de su flujo de entrada, estos datos llegan al cliente con un carácter de nueva línea (print ln) y debes ser recuperados en el cliente con readLine();

La función flush() del objeto que encapsula el flujo de salida permite forzar que los datos salgan del buffer (memoria) del servidor para que la capa de transporte (TCP o UDP) los entregue a su cliente destino.

Cerrar los flujos de entrada, salida y la conexión cliente si es necesario

Terminadas las operaciones de envió y recepción de mensajes (bytes) entre el servidor y el cliente y viceversa, se deben cerrar los flujos de lectura (entrada – Input) y escritura (salida – Output), además se debe realizar el proceso de desconexión del proceso Cliente, para que la comunicación entre ambas partes culmine, estas operaciones se realizan con los métodos close() que pertenecen a las clases de entrada (InputStream), salida (OutputStream) y conector del Cliente (Socket).

1
2
3
4
5
6
7
8
9
10
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

mensajeroInput.close();

mensajeroOutput.close();

conectorActivo.close();

Durante el proceso obtención de los flujos de entrada y de salida puede ocurrir un error inesperado ( Exception ), entonces este error es notificado mediante un objeto (instancia) de la clase IOException, e cual se recomienda ser vigilado entro de un try{  … } y capturado por una sentencia catch(IOException error ){ ….}, c on el fin de darle procesamiento al error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

try{

// Aceptar conexiones de clientes

// Obtener los flujos de entrada y de salida

// Enviar y recibir datos hacia y desde el cliente

// Cerrar los flujos de entrada, salida y la conexión cliente

}

catch( IOException e ){

// Hacer algo cuando ocurra un error durante:

// Se acepta la conexión de algún cliente

// La obtención los flujos de entrada y de salida

// Se envían y reciben datos hacia y desde el cliente

// Se cierran los flujos de entrada, salida y la conexión cliente

}

Detener el Servidor

Si decidimos que el Servidor no siga ofreciendo el servicio deseado por sus Clientes, debemos diseñar algún mecanismo que impida que siga ejecutándose el ciclo llama a la función accept() del la clase ServerSocket (Conector Servidor) y luego debemos llamar a la función close() de la clase ServerSocket, para que libere el puerto de red que fue asignado por el S.O para que el Servidor pueda atender a sus Clientes.

Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

// Variable que define si se sigue o no ofreciendo aceptando clientes

Public void detenerConexiones(){

if( conectorPasivo != null &amp;&amp; conectorPasivo.isClosed() == false ){

try{

conectorPasivo.close();

}

catch( IOException e ){

conectorPasivo = null;

// hacer algo cuendo no se pueda cerrar el conectorPasivo

}

finally{

conectorPasivo = null;

}

}

}

Así queda daría el ejemplo del  código completo de la clase  que abstrae el trabajo básico con un proceso Servidor de TCP.

Estructura del Servidor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

import java.net.*;

import java.io.*;

public class ServidorTCP implements Runnable{

public ServerSocket  conectorPasivo;

private Thread hilo;

private static ServidorTCP objeto;

private boolean inicio;

public String clientesConectados = "Informacion Sobre los Cliente\n";

public short contadorClientes;

private  ServidorTCP( ){

hilo = new Thread(this);

hilo.start();

}

public static ServidorTCP getObjeto(){

if( objeto == null ){

objeto = new ServidorTCP();

}

return objeto;

}

public void run(){

// Solicitar una IP y un puerto de red para aceptar conexiones

conectorPasivo = null;

try{

conectorPasivo =  new ServerSocket(7000);

}

catch(IOException e){

// Realizar algo si el Puerto no está disponible...

return;

}

// Aceptar iterativamente conexiones de clientes

inicio = true;

Socket conectorActivo = null;

while( inicio ){

try{

// Aceptar conexiones de clientes

conectorActivo = conectorPasivo.accept();

++ contadorClientes;

String liena = "";

BufferedReader mensajeroInput = null;

PrintStream mensajeroOutput = null;

String mensajeEnviar = "Saludos Cliente No."+contadorClientes;

try{

clientesConectados += "Nombre PC: "+conectorActivo.getInetAddress().getHostName()+" IP: "+conectorActivo.getInetAddress().getHostAddress()+" Puerto: "+conectorActivo.getPort()+"\nHa enviado: ";

// Obtener los flujos de entrada y de salida

// El flujo de entrada que viene del cliente

mensajeroInput = new BufferedReader(new InputStreamReader(conectorActivo.getInputStream()));

// El flujo de salida que va hacia el cliente

mensajeroOutput = new PrintStream(conectorActivo.getOutputStream());

// Enviar y recibir datos hacia y desde el cliente

// Recibir datos

// Leer datos

String lineas ="";

do{

// lee todas las líneas que llegan en un envió hecho por el cliente;

lineas = mensajeroInput.readLine();

}

while(mensajeroInput.ready());

clientesConectados += contadorClientes+". "+ lineas+"\n";

mensajeEnviar += "\n"+lineas;

// Enviar datos

// mensajeEnviar tiene los datos a enviar

mensajeroOutput.println(mensajeEnviar);

mensajeroOutput.flush();

// Procesar la petición del cliente, según los datos enviados

// Cerrar los flujos de entrada, salida y la conexión cliente Si es necesario.

mensajeroInput.close();

mensajeroOutput.close();

conectorActivo.close();

}

catch( IOException e ){

// Hacer algo cuando no se pueda obtener los flujos de entrada o de // salida desde el conectorActivo, o cuando ocurra algún problema

// al enviar o recibir datos o cerrar los flujos

// o desconectar al cliente

}

}

catch(IOException e){

// hacer algo cuando no se puede establecer conexión con un cliente

// es decir, no se peude anclar el cliente al servidor

}

}

}

public void detenerConexiones(){

// cerrar el conector pasivo.

inicio = false;

if( conectorPasivo != null &amp;&amp; conectorPasivo.isClosed() == false ){

try{

conectorPasivo.close();

}

catch( IOException e ){

conectorPasivo = null;

// hacer algo cuendo no se pueda cerrar el conectorPasivo

}

finally{

conectorPasivo = null;

}

}

}

}

Estructura de una clase Cliente básica escrita en Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

import java.net.*;

import java.io.*;

public class ClienteTCP {

private Socket  conectorActivo;

private String  direccionIPServidor;

private int  puertoServidor;

private String datosOut;

private String datosIn;

public   ClienteTCP( ){

}

public void conectar() throws Exception{

try{

// Realizar una conexión a un socket pasivo

conectorActivo =new Socket (direccionIPServidor, puertoServidor);

// Obtener los flujos de entrada y de salida

BufferedReader mensajeroIn =

new BufferedReader(new InputStreamReader(conectorActivo.getInputStream()));

PrintStream mensajeroOut =

mensajeroOut = new PrintStream(conectorActivo.getOutputStream());

// Enviar y recibir datos hacia y desde el servidor

mensajeroOut.println(datosOut);

mensajeroOut.flush();

datosIn = mensajeroIn.readLine();

// Cerrar los flujos de entrada, salida y la conexión

mensajeroOut.close();

mensajeroIn.close();

conectorActivo.close();

}

catch( IOException e ){

// Hacer algo cuando ocurra un error durante:

// La conexión con el servidor

// La obtención los flujos de entrada y de salida

// El  envío y recepción de  datos hacia y desde el servidor

// Se cierran los flujos de entrada, salida y la conexión

e.printStackTrace();

throw new Exception("Error al conectar \n"+e.getMessage());

}

}

public void desconectar(){

// cerrar el conector activo.

if( conectorActivo != null &amp;&amp; conectorActivo.isClosed() == false){

try{

conectorActivo.close();

}

catch( IOException e ){

conectorActivo = null;

}

}

}

public Socket getConectorActivo(){

return conectorActivo;

}

public void setDireccionIPServidor(String ipServer){

direccionIPServidor = ipServer;

}

public String getDireccionIPServidor( ){

return direccionIPServidor;

}

public void setPuertoServidor(int puertoServer){

puertoServidor = puertoServer;

}

public int getPuertoServidor( ){

return puertoServidor;

}

public void setDatosOut(String datosEnv){

datosOut = datosEnv;

}

public String getDatosOut( ){

return datosOut;

}

public void setDatosIn(String datosIn){

this.datosIn = datosIn;

}

public String getDatosIn( ){

return datosIn;

}

}

Las clases para probar el sistema distribuido

He creado dos clases más, una para probar al Cliente y otra para probar al Servidor, primero debemos ejecutar el servidor, la idea de esta  clase es ofrecer interfaz que permita a usuario del servidor interactuar con su funcionalidad.

He hecho uso de la librería para javax.swing de Java, creado para construir aplicaciones de escritorio dotadas de buenas GUI (interfaces Graficas de Usuario), en este caso solo utilizo la clase JOptionPane, la cual ofrece un conjunto de funciones (operaciones o métodos) estáticas (se pueden utilizar con el nombre de la clase, sin necesidad de instanciar una variable “objeto” de dicha clase) que al ser llamadas según sea el caso muestran en pantalla ventanas de Alerta informativa showMenssageDialog, de Entrada de datos showInputDialog y de confirmación showConformDialog.

Clase PruebaServidor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

import javax.swing.*;

public class PruebaServidor{

public static void main(String[] arg){

int resp = -1;

int puerto = 7000;

do{

try{

String puertoServidor = JOptionPane.showInputDialog(null, "Digite el Puerto del Servidor: ",String.valueOf(puerto));

puerto = Integer.parseInt(puertoServidor);

resp =-1;

}

catch(Exception e){

String mensaje = "El Puerto debe ser un numero entero entre 1 a 65535";

JOptionPane.showMessageDialog(null,mensaje);

resp =  JOptionPane.showConfirmDialog(null, "¿Desea Continuar?","Ventana de Control ",JOptionPane.YES_NO_OPTION);

}

}

while(resp == JOptionPane.YES_OPTION);

if(resp == JOptionPane.NO_OPTION ){

System.exit(0);

}

ServidorTCP servidor = ServidorTCP.getObjeto();

try{

Thread.sleep(2000);

boolean inicio = true;

do{

String datosDelServidor = "Servidor escuchando por la dirección IP "+ servidor.conectorPasivo.getInetAddress().getLocalHost().getHostAddress();

datosDelServidor += " y Puerto "+ servidor.conectorPasivo.getLocalPort();

JOptionPane.showMessageDialog(null, datosDelServidor);

String mensajeCotrol = "SI, para Terminar\n"+

"NO, para Continuar\n"+

"Cancelar, para ver Clientes Conectados";

resp = JOptionPane.showConfirmDialog(null, mensajeCotrol, "Servidor" ,JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);

if(resp == JOptionPane.YES_OPTION){

servidor.detenerConexiones();

inicio = false;

}

else if( resp == JOptionPane.NO_OPTION)  {

continue;

}

else if(resp == JOptionPane.CANCEL_OPTION)  {

JOptionPane.showMessageDialog(null, servidor.clientesConectados);

}

}

while(inicio);

}

catch(Exception e){

JOptionPane.showMessageDialog(null, e.getMessage());

}

}

}

Clase PruebaCliente

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/*************************************
 *
 *   Sistemas distribuidos
 *   Ingeniero John Carlos Arrieta Arrieta
 ****************************************/

import javax.swing.*;

public class PruebaCliente{

public static void main(String[] arg){

ClienteTCP cliente = new ClienteTCP();

int resp = JOptionPane.YES_OPTION;

do{

try{

String direccionIpServidor = JOptionPane.showInputDialog(null, "Digite la direccion Ip del Servidor:","127.0.0.1"  );

String puertoServidor = JOptionPane.showInputDialog(null, "Digite el Puerto del Servidor:","7000"  );

int puerto = Integer.parseInt(puertoServidor);

cliente.setDireccionIPServidor ( direccionIpServidor);

cliente.setPuertoServidor( puerto);

String mensajeOut = JOptionPane.showInputDialog(null, "Enviale algo al Servidor:", "Hola soy John Carlos Arrieta Arrieta" );

cliente.setDatosOut ( mensajeOut);

cliente.conectar();

String datosIn = cliente.getDatosIn();

cliente.desconectar();

JOptionPane.showMessageDialog(null,"El Servidor respondio : "+datosIn);

resp =  JOptionPane.showConfirmDialog(null, "¿Desea Continuar?","Ventana de Control ",JOptionPane.YES_NO_OPTION);

}catch(Exception e){

String mensaje = "";

if( e instanceof NumberFormatException){

mensaje ="el puerto debe ser un numero entre 1 y 65535";

}

else {

mensaje = e.getMessage();

}

JOptionPane.showMessageDialog(null,mensaje);

resp =  JOptionPane.showConfirmDialog(null, "¿Desea Continuar?","Ventana de Control ",JOptionPane.YES_NO_OPTION);

}

}

while(resp == JOptionPane.YES_OPTION);

}

}

Ejemplo de la ejecución del sistema distribuido

He colocado las ventanas que  se muestran durante la ejecución del Servidor y del Cliente en el mismo  host (de forma local), para comprobar la eficiencia del sistema, es recomendable ejecutar  el Cliente y el Servidor en host diferentes conectados a la misma red.

image005

Inicio del Servidor solicitando el Puerto necesario para conectarse a el

image007

image009

image011

Si el usuario digita un puerto con formato no valido, el sistema notifica sobre el error y solicita si desea finalizar o seguir.

image013

image016

En este punto ya ha iniciado el hilo del servidor y se está ejecutando de forma concurrente a al código que tiene el flujo principal.

Ahora inicia la ejecución del Cliente

image017

image019

Inicio del Cliente pidiendo la IP y el Puerto del Servidor, de forma similar al servidor, el aplicativo cliente realiza las validaciones correspondientes a puerto del servidor y hace las respetivas notificaciones al usuario

image009

image011

Si todo va como se desea, el cliente solicita enviar algún tipo de información al servidor

image021

Obtenidos estos datos, el cliente procede a realizar la  conexión con el proceso servidor que se encuentra esperando conexiones sobre el puerto 7000 e IP 192.168.0.10.

image024

Si la conexión es satisfactoria, el servidor envia al cliente un mensaje confirmando su conexión, diciendole el numero al que corresponde su conexión, luego de terminar la conexión, el cliente pregunta al usuario si desea continuar o terminar.

image011

Por su parte el Servidor  lleva un conteo de los clientes conectados, además de información útil como las  Direcciones IP y Puertos que utilizaron que el servidor pudiera enviar datas como respuesta a cada Cliente.

Al cerrar la ventana que notifica sobre la IP y Puerto del servidor, el sistema nos muestra una ventana solicitando una de tres operaciones de control.

image027

Si pulsamos la opción Si, el servidor finalizara con el proceso de aceptar conexiones de Clientes, liberando el puerto de red utilizado y terminado el Hilo de ejecución.

Si pulsamos la opción No, el servidor continuara aceptando conexiones de clientes.

Si pulsamos la opción Cancelar, el servidor nos mostrara una ventana con la información que ha ido recuperando de cada cliente conectado.

image029

Bueno creo que esto es todo por ahora, espero que les haya gustado la lectura, hayan comprendido el ejercicio  que diseñado para tratar de explicar de forma básica  la arquitectura Cliente / Servidor, pero sobre todos espero de corazón hablarles ayudado a comprender y a iniciar en este fabuloso campo del desarrollo de software.

Mil gracias por seguir atentos a mis artículos, les prometo que muy pronto realizare otro aplicativo mas practico, siguiendo la misma arquitectura del ejemplo que les escribí aquí en este segundo articulo de Programando para la red, ya lo he escrito en su totalidad con Java, pero tengo que esperar a que mis estudiantes de X semestre de la universidad en la que orgullosamente laboro, termine de hacerlo tal cual como se los pedí, como parte de una actividad evaluable que les he pedido que realicen, en la asignatura de Sistemas Distribuidos.

Que descansen y sigan estudiando mucho, no deseo que nadie de mis amigos y lectores les pase como a un par sujetos que conozco, que aparte de presumidos, se han ganado  una fama de charlatanes y mediocres, no hay cosa que desagrade más que una persona presumida, lambona, envidiosa y para rematar charlatán.

Bueno en la villa de nuestro señor todo poderoso hay de todo un poco y no nos queda otra que aceptarnos los unos a los otros tal y como somos.

Que Dios los siga bendiciendo mucho.

294 Visitas hoy
Comparte en tu Web:
  • Print
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • Blogplay
  • email
  • Reddit
  • RSS
  • StumbleUpon

Por favor deje sus Comentarios aquí:

Get Adobe Flash playerPlugin by wpburn.com wordpress themes
© John Carlos Arrieta Arrieta