Sei sulla pagina 1di 16

COMUNICACIÓN MEDIANTE SOCKETS

Enrique Ortega Edrodoso


Ingeniería Técnica de Informática de Gestión

Introducción

Los vehículos autoguiados comerciales disponibles en la actualidad tienen una serie


de inconvenientes que limitan su empleo en investigación. Entre estas cabe destacar su
elevado precio, arquitectura cerrada, falta de documentación y difícil escalabilidad,
tanto en hardware como en software.

Utilizar una arquitectura basada en PC tiene enormes ventajas: precio, potencia,


compatibilidad, escalabilidad, disponibilidad de software y hardware. El empleo de
Linux y de su extensión de tiempo real RT-Linux facilitara enormemente la realización
de proyectos.

La interfaz de sockets facilita la comunicación entre procesos, programando dicha


comunicación de manera similar a como se maneja cualquier dispositivo de entrada
salida, ya que independiza la localizacion de los procesos.

Programación usada en robótica

La existencia de los robots que realicen autónomamente tareas de modo eficiente


depende fundamentalmente de su construcción mecánica y de su programación. Una vez
construido el cuerpo mecánico del robot, conseguir que realice una tarea se convierte en
la práctica en un problema de programación. La generación de comportamiento en un
robot consiste entonces en escribir el programa que al ejecutarse en el robot causa ese
comportamiento cuando este se encuentra en cierto entorno. La autonomía y la
“inteligencia” residen en ese programa.

La programación que se emplea en robótica tiene caracteres diferentes: explicito,


en el que el operador es el responsable de las acciones de control y de las instrucciones
adecuadas que las implementan, o estar basada en la modelación del mundo exterior,
cuando se describe la tarea y el entorno el propio sistema toma las decisiones.

Los lenguajes de comunicación de alto nivel, suministran una solución general


en la comunicación hombre-robot. Los lenguajes clásicos (BASIC, FORTRAN,
PASCAL) no disponen de los comandos e instrucciones específicas que se necesitan
para la programación en la robótica. Por lo que en estos momentos no existe ningún
lenguaje universal.

Características de un lenguaje ideal para robótica

Las seis características básicas de un lenguaje ideal, expuestas por Pratt, son:

1. claridad y sencillez
2. 2. claridad de la estructura del programa
3. sencillez de la aplicación
4. facilidad de ampliación
5. facilidad de corrección y mantenimiento.
6. eficacia.

Comunicación mediante sockets tipo stream

El uso de TCP/IP independiza al robot del sistema operativo que se ejecute en


los ordenadores host. La aplicación cliente permite al usuario introducir los comandos
que controlan el robot. Estos pueden ser de tres tipos:

1. movimiento,
2. definición de estado,
3. consulta de estado.

La aplicación cliente controla la sintaxis de los comandos introducidos,


construye un mensaje apropiado para el servidor, y comunica el mensaje al
servidor mediante el empleo de Sockets.

Por su parte, la aplicación servidor está permanentemente esperando conexiones


por parte de aplicaciones cliente. Una vez establecida una conexión, el servidor hace de
intermediario intercambiando mensajes entre la aplicación cliente y el módulo de
tiempo real que ejecuta las tareas de control.

¿Qué es un socket?

• Es una interfaz de entrada-salida de datos que permite la intercomunicación


entre procesos.
• Los procesos pueden estar ejecutándose en el mismo o en distintos
sistemas, unidos mediante una red.

La analogía con los teléfonos.

Los sockets permiten la comunicación entre procesos, como los teléfonos


permiten la comunicación entre las personas.

Dominios de comunicación.

Los sockets se crean dentro de un dominio de comunicación, igual que un


archivo se crea dentro de un filesystem.

El dominio de comunicación nos dice donde se encuentran los procesos


que se van a intercomunicar.

Si los procesos están en el mismo sistema, el dominio de comunicación


será AF_UNIX, si los procesos están en distintos sistemas y estos se hallan unidos
mediante una red TCP/IP, el dominio de comunicación será AF_INET.
Cabe aclarar que existen otros dominios de comunicación.
Los sockets no se han diseñado solamente para TCP/IP. La idea original fue que
se usase la misma interfaz también para distintas familias de protocolos.
En esta introducción solo trataremos el dominio AF_INET.

Algunos dominios:

• AF_INET (unidos mediante una red TCP/IP).


• AF_UNIX (en el mismo sistema).
• Otros dominios.

Tipos de sockets en el dominio AF_INET.


• Sockets Stream.
• Sockets Datagram.
• Sockets Raw.

Figura 1-1
Sockets Stream son los más utilizados, hacen uso del protocolo TCP
(figura 1-1), el cual nos provee un flujo de datos vi direccional, secuenciado, sin
duplicación de paquetes y libre de errores.
La especificación del protocolo TCP se puede leer en la RFC-793.

Sockets Datagram hacen uso del protocolo UDP, el cual nos provee un
flujo de datos bidireccional, pero los paquetes pueden llegar fuera de secuencia,
pueden no llegar o contener errores.
Por lo tanto el proceso que recibe los datos debe realizar resecuencimiento,
eliminar duplicados y asegurar la confiabilidad.
Se llaman también sockets sin conexión, porque no hay que mantener una
conexión activa, como en el caso de sockets stream.
Son utilizados para transferencia de información paquete por paquete. Ejemplo:
dns, tftp, bootp, etc.
Entones podríamos preguntar, ¿Cómo hacen estos programas para funcionar si
pueden perder datos?
Ellos implementan un protocolo encima de UDP que realiza control de errores.
La especificación del protocolo UDP se puede leer en la RFC-768.

Sockets raw no son para el usuario más común, son provistos


principalmente para aquellos interesados en desarrollar nuevos protocolos de
comunicación o para hacer uso de facilidades ocultas de un protocolo existente.

PARA TENER EN CUETA

Byte order.
Network byte order y Host byte order son dos formas en las que el sistema
puede almacenar los datos en memoria.
Está relacionado con el orden en que se almacenan los bytes en la memoria RAM.
Si al almacenar un short int (2 bytes) o un long int (4 bytes) en RAM, en la posición
más alta se almacena el byte menos significativo, entonces está en network byte
order, caso contrario es host byte order ( figura 2-1).

Network byte order. Host byte order.

short int. short int.

byte mas byte menos byte mas byte menos


significativo significativo significativo significativo

Dirección n n+1 Dirección n+1 n

long int. long int.

byte mas byte menos byte mas byte menos


significativo significativo significativo significativo

Dirección n n+1 n+2 n+3 Dirección n+3 n+2 n+1 n


Figura 2-1.

Esto depende del microprocesador que se esté utilizando, podríamos estar


programando en un sistema host byte order o network byte order, pero cuando
enviamos los datos por la red deben ir en un orden especificado, sino enviaríamos
todos los datos al revés. Lo mismo sucede cuando recibimos datos de la red,
debemos ordenarlos al orden que utiliza nuestro sistema. Debemos cumplir las
siguientes reglas:

• Todos los bytes que se transmiten hacia la red, sean números IP o datos,
deben estar en network byte order.
• Todos los datos que se reciben de la red, deben convertirse a host byte
order.
Creación de un socket.

Analogía:

Para que una persona pueda recibir llamadas debe tener instalado un
teléfono, para poder realizar una conexión se debe crear un socket.
Los sockets se crean llamando a la función socket(), esta función retorna
un descriptor de socket, que es tipo int.
Si hubo algún error, socket() retorna -1 y la variable global errno se establece
con un valor que indica el error que se produjo

sockfd Es el descriptor de socket devuelto. Luego se utilizará para


conectarse, recibir conexiones, enviar y recibir datos, etc.

dominio Dominio donde se realiza la conexión.


Para este tutor siempre será AF_INET.

tipo Podrá ser SOCK_STREAM o SOCK_DGRAM o SOCK_RAW.

protocolo 0 (cero, selecciona el protocolo más apropiado).

Selección del protocolo:

Se puede especificar el número de protocolo o también seleccionar el


protocolo por su nombre utilizando la función getprotobyname(), que retorna una
estructura tipo protent con los siguientes datos:
char *p_name; /* Nombre oficial del protocolo */
char **p_aliases; /* lista de alias. */
int p_proto; /* número de protocolo */
Esta función lee el archivo /etc/protocols para completar la estructura.

Errores en la creación:
Los errores más comunes son :

• El protocolo especificado no está soportado dentro del dominio.


• Permiso negado para crear el socket del tipo y/o protocolo especificado.
• Etc.

Función Bind()
Analogía:
Para poder recibir llamadas, se debe tener un número telefónico, para
poder recibir conexiones, se le debe asignar un nombre al socket.

El socket se crea sin nombre, debemos asignarle uno para poder recibir
conexiones.

Bind () se utiliza para darle un nombre al socket, o sea una dirección IP y


número de puerto del host local por donde escuchará, al especificar una IP del
host local le estamos diciendo por cual interfaz física escuchará (el sistema puede
tener varias interfaces ethernet, ppp, etc).

Es necesario llamar a bind() cuando se está programando un servidor.


Cuando se está programando un cliente generalmente no se utiliza esta función, el
kernel le asignará al socket la dirección IP y número de puerto disponible al llamar
a ciertas funciones, por ejemplo cuando llamamos a connect() para conectarnos
con un sistema remoto.

En el servidor es necesario llamar a bind() debido a que el número de


puerto debe ser conocido para que los clientes puedan conectarse. Por ejemplo si
estamos programando un servidor de telnet debemos llamar a bind() para
asignarle al socket el puerto 23.

En la aplicación cliente se puede llamar a bind() y asignarle un número de


puerto, pero no es necesario porque nadie va a tratar ni podrá conectarse con él.

sockfd : Es el descriptor de socket devuelto por la función socket().

my_addr Es un puntero a una estructura sockaddr que contiene la IP del host


: local y el número de puerto que se va a asignar al socket.
(Más abajo se detalla).

addrlen Debe ser establecido al tamaño de la estructura sockaddr. sizeof(struct


: sockaddr).
Estructuras.
Almacenan el nombre del socket. Se utilizan con la función bind().
struct sockaddr
{
unsigned short sa_family; // AF_*
char sa_data[14]; // Direccion de protocolo.
};
struct sockaddr_in
{
short int sin_family; // AF_INET
unsigned short sin_port; // Numero de puerto.
struct in_addr sin_addr; // Dirección IP.
unsigned char sin_zero[8]; // Relleno.
};
struct in_addr
{
unsigned long s_addr; // 4 bytes.
};

La primer estructura, sockaddr, almacena la dirección de protocolo para


muchos tipos de protocolos.

Sa_family puede ser AF_INET, AF_UNIX u otros dominios, para este


trabajo solo será AF_INET.

Sa_data contiene la dirección IP y número de puerto asignado al socket.

Se creó la estructura sockaddr_in para el caso de Internet, para poder


referenciar los elementos de forma más fácil.

Los punteros a la estructura sockaddr_in deben ser precedidos con un cast


tipo *struct sockaddr antes de pasarlos como parámetros a funciones.

Notas sobre sockaddr_in:

• sin_family será AF_INET


• sin_port (número de puerto) y sin_addr (dirección IP) deberán estar en
network byte order, o sea habrá que usar htons().
• sin_family no debe convertirse a network byte order porque es solo usado
por el kernel y no es enviado por la red.
• sin_zero se utiliza para rellenar la estructura a la longitud de sockaddr, debe
estar inicializada a cero con la función bzero(). Ver la página del manual
Otras funciones utilizadas.
• inet_addr()

Convierte una dirección IP en notación números y puntos, en un unsigned long,


retorna la dirección en network byte order. Retorna -1 si hubo error.
Ejemplo:
struct sockaddr_in ina;
....
ina.sin_addr.s_addr=inet_addr("192.168.1.1");

• inet_ntoa()

Realiza la conversión inversa, convierte una dirección IP en unsigned long en


network byte order, a un string en números y puntos.
Ejemplo:
printf("%s", inet_ntoa(ina.sin_addr));

inet_ntoa() retorna un puntero a un array de caracteres, que se encuentra


almacenado estáticamente dentro de inet_ntoa(). Cada vez que se llama a
inet_ntoa() se sobrescribe la última dirección IP.
Ejemplo:
char *addr1, *addr2;
addr1=inet_ntoa(ina1.sin_addr) /* Supongamos que vale 200.41.32.127 */
addr2=inet_ntoa(ina2.sin_addr) /* Supongamos que vale 132.241.5.10 */
printf("dirección 1: \n", addr1);
printf("dirección 2: \n", addr2);
Esto imprimirá:
dirección 1: 132.241.5.10
dirección 2: 132.241.5.10

Se ve que la primer dirección IP es sobrescrita en la segunda llamada a inet_ntoa()


y se pierde su valor.
Para que esto no suceda, luego de la primera llamada a inet_ntoa() se debe usar
strcpy() para guardar la primera dirección IP y luego llamar por segunda vez a
inet_ntoa(), de esta manera no se pierde la primera dirección IP.

Asignación de valores a una variable tipo sockaddr_in

Debemos asignarle valores a una variable tipo sockaddr_in antes de llamar a las
función bind().
Veamos un ejemplo:
...
..
struct sockaddr_in my_addr;
......
my_addr.sin_family = AF_INET;
my_addr.sin port = htons ( 3490 ); // Numero de puerto por donde escuchara el servidor.
my_addr.sin_addr.s_addr = inet_addr ("132.241.5.10"); // IP de la interface por donde
escuchara el servidor.
bzero ( &(my_addr.sin_zero), 8); // Relleno con ceros.
Notas :

• Si asignamos el valor cero a sin_port, el sistema nos dará automáticamente


un puerto disponible.

my_addr.sin_port=0;

• Podemos automatizar la asignación de la IP, si ponemos el valor


INADDR_ANY a s_addr, el sistema le asignará la dirección IP local.
Recordar que el programa puede ejecutarse en distintas PC's con distintas
IP's

my_addr.sin_addr.s_addr = htonl (INADDR_ANY);

• Las variables my_addr.sin_port y my_addr.sin_addr.s_addr deben estar


en network byte order, son valores que viajan por la red, pero
my_addr.sin_family no porque solo es utilizado por el kernel para saber
que tipo de dirección contiene la estructura.

Pasos para establecer la conexión.

Caso sockets stream.

En la figura 3.1 se visualiza los pasos para realizar una conexión


mediante sockets stream.

Primero haremos una descripción funcional, para luego poder realizar un


estudio más detallado de cada función utilizada.

Ambos, cliente y servidor, deben crean un socket mediante la función socket(), para
poder comunicarse.
• El servidor llama a bind() para darle un nombre al socket, para
luego poder recibir conexiones, es decir establece por cual número de
puerto escuchará. Por ejemplo si este sería un servidor de telnet,
establecería el puerto 23. Para el cliente no es necesario establecer el
número de puerto, porque no recibirá intentos de conexión, sólo intentará
conectarse con el servidor .
• El servidor habilita su socket para poder recibir conexiones,
llamando a la función listen(). El cliente no necesita realizar este paso
porque no va a recibir conexiones, solo intentará conectarse con el servidor.
• El servidor ejecuta la función accept() y queda en estado de
espera, la función accept() no retorna hasta que intenten conectarse. El
cliente usa la función connect() para realizar el intento de conexión, en ese
momento la función accept() del servidor retorna con un parámetro que es
un nuevo descriptor de socket, el cual se utiliza para realizar la
transferencia de datos por la red con el cliente.
• Una vez establecida la conexión se utilizan las funciones send()
y recv() con el descriptor de socket del paso anterior para realizar la
transferencia de datos.
• Para finalizar la conexión se utilizan las funciones close() o
shutdown().
Ejemplo cliente-servidor TCP simple

Luego del accept() el servidor queda en estado de espera hasta que el


cliente intente conectarse.

El cliente trata de conectarse con connect() y accept() retorna con un


nuevo descriptor de socket, el cual es utilizado por el server para realizar la
transferencia de datos con el cliente.

Mientras está realizando transferencia de datos con un cliente, los


intentos de conexión de otros clientes son almacenados en una cola de
conexiones, que luego serán atendidos. Los clientes se atienden de a uno por vez
en este tipo de aplicación cliente-servidor.
Código fuente de ejemplo
Cliente.
/*CLIENTE*/

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <netdb.h>
#include <time.h>

#define MAX 5
#define TAMBUFFER 512
#define PUERTO 4949

main (int argc, char *argv[])


/* argc, almacena el número de argumentos que se le pasan al programa,
argv[] almacena los argumentos*/
{

char opcion=‘0’;
char cadena[MAX];
int idsocket,tamMandado;

struct sockaddr_in infoservidor;// datosSocket;


struct hostent *he;

/*Creamos el socket*/

printf("____________________________Programa del
cliente______________________________\n");

if (argc !=2)
{
/* Nuestro programa cliente sólo necesita un
argumento, que es la IP*/
printf("Error, debe de escribrir: %s <Dirección IP>\n",argv[0]);
exit(-1);
}

/*Si argv[1] es una dirección IP, gethostbyname() simplemente copia


argv[1] en el campo he */
printf ("%s \n",argv[1]);
if ((he=gethostbyname(argv[1]))==NULL)
{
printf("Error en la IP\n");
exit(-1);
}

idsocket=socket(AF_INET,SOCK_STREAM,0);
if (idsocket==-1)
{
printf("Error: El socket no se ha abierto\n");
}
else
{

printf("Socket creado\n");
//Se prepara el nombre de la maquina remota
infoservidor.sin_family=AF_INET;
infoservidor.sin_addr/*.s_addr*/=*((struct in_addr *)he->h_addr);

inet_addr("127.0.0.1");
/* es necesario comentar esta linea cuando queremos que el cliente
y el servidor no sean la misma maquina*/

printf ("%s\n",inet_ntoa(*((struct in_addr *)he->h_addr)));


infoservidor.sin_port=htons(PUERTO);//ordenacion estandar de bytes
//Establece la conexión con el servidor
int f=connect(idsocket, (struct sockaddr*) &infoservidor,
sizeof(infoservidor));
printf ("Conectando\n");
if (f==-1)
{
printf("error de conexion\n");
}
Else //TODO VA BIEN
{
//*********************************************************************
//*************IMPLEMENTAR CODIGO AQUI***************************
}
printf("Cerrando el socket\n");
close(idsocket);
}
}
}
Servidor.
/*SERVIDOR*/

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <strings.h>
#include <stdlib.h>
#include <time.h>

#define MAXIMOCONEXIONES 1
//numero maximo de conexiones en espera
#define PUERTO 4949
#define TAMBUFFER 512
main ()
{

int idsocket,s_aux;
int asignaDir,tamdir,tamRecibido;
struct sockaddr_in datosSocket,infocliente;
char buf[TAMBUFFER];

/*Creamos el socket*/
printf("____________________________Programa del
servidor______________________________\n");
idsocket=socket(AF_INET,SOCK_STREAM,0);
if (idsocket==-1)
{
printf("Error: El socket no se ha abierto\n");
}
else
{
datosSocket.sin_family=AF_INET;
datosSocket.sin_addr.s_addr=INADDR_ANY;
//INADDR_ANY coloca la del cliente IP automaticamente.

inet_addr("127.0.0.1");//comentar cuando se utilicen varios equipos.

datosSocket.sin_port=htons(PUERTO);
bzero(&(datosSocket.sin_zero),8);/*rellena a 0 este capo se la
estructura*/
printf("Socket creado\n");
//Asigar un nombre local al socket
if ((bind(idsocket,(struct
sockaddr*)&datosSocket,sizeof(datosSocket))<0) ||
(listen(idsocket,MAXIMOCONEXIONES)<0))
{
printf("Error al nombrar el socket o servidor no a la escucha\n");
}
else
{

printf("Servidor activo escuchando \n");


while (1)
{
s_aux=accept(idsocket,(struct sockaddr*) &infocliente,
&tamdir);
printf("Se obtuvo una conexión desde
%s\n",inet_ntoa(infocliente.sin_addr) );
tamRecibido= recv(s_aux, buf,1, 0);
buf[tamRecibido]='\0';
if (tamRecibido==-1)
{
printf("error al recibir\n");

}
else//TODO VA BIEN
{

//***************************************************************
//********************IMPLEMENTAR CODIGO AQUI*********************
}
}
}

printf("____________________________Programa del
servidor______________________________\n");
}//else

}//main

Con este codigo de ejemplo se puede establecer una comunicacio cliente


servidor entre el robot (cliente) y el servidor (ordenador, portátil o similar).
Conclusiones

La arquitectura basada en PC trae una serie de ventajas como el bajo precio,


facililidad de ampliación, tanto en hardware como de software, versatilidad por la gran
cantidad de software disponible y potencia en cuanto a capacidad de cálculo. Además,
el hardware nunca se queda obsoleto, puesto que su substitución por otro de la siguiente
generación es inmediata, guardando la compatibilidad con el software anterior.

La elección del Linux junto con su extensión de tiempo real RT-Linux. Nos
proveen de herramientas de desarrollo muy potentes como el entorno gnu wpe
(Windows Programming Environment), o el compilador de c gnu, todo ello sin coste.

Todo ello nos da robustez en la práctica, el sistema aprovecha los recursos


de la máquina al máximo, de manera que se podra utilizar como computador de a bordo
una máquina 486 o similares, sin prejuicio en tiempo de ejecución ni en tiempo de
desarrollo (varios usuarios en paralelo sobre esta máquina).

La totalidad del software utilizado es de dominio público, con lo que las


fuentes han sido accesibles en todo momento. Esto ha permite una comprensión en
profundidad del sistema de tiempo real y abre nuevas posibilidades de ampliación
futuras (uso de otros planificadores, desarrollo de drivers en la parte de tiempo real,
etc.), impensables en vehículos comerciales.

Bibliografía

Memoria personal sobre una practica desarrollada en la carrera de Ingeniería


Técnica de Informática de Gestión.

Fragmentos de un tutorial extraído de :http://www.starlinux.net, así como de


diversos manuales de la red

Potrebbero piacerti anche