IPC con sockets - Escuela de Ingeniería Civil en Informática

Anuncio
Verisón 1.3: 8 de julio de 2013
Comunicación entre procesos mediante Sockets
Preparado por Gabriel Astudillo Muñoz
Escuela de Ingeniería Civil Informática
Universidad de Valparaíso
1
Resumen
Este documento tiene como objetivo describir algunas funciones del API de C relativas a la comunicación
entre procesos a través de sockets. Todas las funciones son relativas al kernel 2.6 de Linux. Las
herramienta de compilación son: gcc 4.4.5, make 3.81.
2
Conceptos generales de Redes de Computadores
2.1
Definiciones
•
•
•
•
2.2
Tarjeta de red: Elemento de hardware que permite que un sistema computacional se pueda
conectar a una red de datos. Se suele denominar NIC (Network Interface Card)
HOST: Es cualquier nodo conectado a una red de datos. Puede ser un computador, tablet,
teléfono, etc. Necesariamente debe tener por lo menos una tarjeta de red. Algunas veces se
utiliza la palabra “máquina”, para referirse a un host.
Número IP: Es identificador de un host en una red. En el caso de la dirección IPv4 (32 bits) se
representa como un número de 4 dígitos, cada uno de ellos de 8 bits. Por ejemplo: 200.14.68.248
Nombre de host: Es el nombre que está asociado a la IP del host. Generalmente, el servicio que
permite la traducción de IP a nombre y viceversa en el servicio DNS1.
Capas de abstracción
Los protocolos de red son desarrollados en capas, donde cada capa es responsable de una función
determinada dentro de una comunicación [STALLINGS, 2004]. El conjunto de protocolos TCP/IP es una
combinación de protocolos en varias capas. Normalmente, se considera como un sistema de 4 capas, tal
como se muestra en la Figura 1. Algunos autores, agregan la capa “Medio Físico”, para separar lo que es
el control de la transmisión que realiza la capa de Enlace de la trasmisión misma de los datos.
Aplicación
SSH, Email, Navegador Web
Transporte
TCP, UDP
Red
Enlace de
Datos
IP, ICMP, IGMP
Device driver, NIC
Figura 1. Capas del conjunto de protocolos TCP/IP
La capa de enlace de datos (data link), incluye la tarjeta de red (Network Interface Card, NIC), el driver
respectivo y todo lo relacionado con comunicación a través del medio. El direccionamiento en esta capa
es través de las direcciones de acceso al medio, conocidas como direcciones MAC.
La para de red, denominada Capa de Internet, maneja el movimiento de datos a través de la red. El
direccionamiento se realiza a través de la dirección IP. Una de las principales funciones de esta capa es el
ruteamiento. Los protocolos asociados a esta capa son IP (Internet Protocol), ICMP (Internet Control
Message Protocol) y IGMP (Internet Group Managment Protocolo).
La capa de transporte provee un flujo de datos entre dos host. Este flujo tiene asociado un número 16 bits.
Los protocolos más utilizados son TCP (Transporte Control Protocol) y UDP (User Datagram Protocol).
El primero ofrece una trasferencia confiable, divide los datos a enviar en paquetes, envía acuses de recibo,
maneja temporizadores y otros elementos que permiten una correcta entrega de información. El segundo,
provee una comunicación más simple. Sólo envía paquetes de datos (denominados Datagramas) de un
host a otro, pero no garantiza que ellos lleguen se forma correcta. Al utilizar este tipo de comunicación, es
la capa de aplicación la que debe tener la capacidad de manejar la confiabilidad del flujo de datos.
La capa de aplicación, maneja los detalles de una aplicación determinada.
1
Servicio DNS trabaja en el puerto UDP/53
1
Verisón 1.3: 8 de julio de 2013
La Figura 2 muestra cómo se realiza la comunicación entre dos host en dos redes distintas [STEVENS,
1993]. Esta situación representa el caso más general. Esta comunicación se realiza bajo el modelo
Cliente-Servidor, que será formalizado en la sección 2.7.
Host Ciente
Host Servidor
Protocolo
HTTP
Cliente Web
(Navegador)
Servidor
Web
Protocolo
TCP
TCP
TCP
Router
Protocolo
IP
IP
IP
Protocolo
Red A
Red A
driver
Red A
driver
Red B
driver
Protocolo
IP
IP
Protocolo
Red B
Red A
Red B
driver
Red B
Figura 2. Descripción del funcionamiento de los protocolos TCP/IP
2.3
Capas TCP/IP
La Figura 3 muestra un diagrama que muestra cómo las distintas capas se conectan entre sí. Además, se
detallan algunos protocolos que están asociados a cada capa.
Aplicación
Proceso de
usuario
Proceso de
usuario
Proceso de
usuario
Proceso de
usuario
Transporte
TCP
UDP
Red
ICMP
IP
ARP
Interfaz de
Hardware
IGMP
RARP
Enlace de
Datos
Medio Físico
Figura 3
ICMP utiliza IP para intercambiar mensaje de control entre distintos host. Algunos comando que utilizan
ICMP son ping y traceroute.
IGMP es ampliamente utiliza en multicasting, que es enviar datagrama UDP a múltiples host.
ARP (Address Resolution Protocol) y RARP (Reverse Address Resolution Protocol) son protocolos
especializados que se utilizan en algunas redes LAN (Ethernet, por ejemplo), para convertir las
direcciones utilizadas por la capa de red (IP) a direcciones utilizadas en la tarje de red.
2.4
Encapsulación
Una de las consecuencias del modelo por capas, es que cada una de ellas agrega datos de control
adicionales a los que ya vienen de la capa superior, que se denominan cabeceras. La Figura 4 muestra
esta situación.
Las unidades de datos de cada protocolo son llamadas PDU (Protocol Data Unit). En la capa de
transporte, el PDU de TCP es denominado Segmento y el de UDP es denominado Datagrama. En la
capa de red, el PDU se llama Paquete. El de la capa de Enlace de Datos, Frame. Finalmente, el del
medio físico, se denomina Símbolo (un símbolo pueden ser 1 o más bits).
2
Verisón 1.3: 8 de julio de 2013
Dato de usuario
Cabecera
Aplicación
Cabecera
TCP
Cabecera IP
Dato de usuario
Dato Capa Aplicación
Segmento TCP
Cabecera
Ehernet
Datagrama IP
Fin Ehernet
Frame Ethernet
PDU Aplicación
Aplicación
PDU TCP
(Segmento)
Transporte
PDU IP
(Datagrama)
Red
PDU Enlace
(Frame)
Enlace de Datos
PDU Físico
(Símboos)
Medio Físico
Figura 4
2.5
Demultiplexación
Cuando un frame Ethernet es recibido, éste es enviado a través de cada protocolo que tenga implementado
el receptor, removiendo cada cabezera respectiva. Este proceso se denomina “demultiplexación” (Figura
5).
Aplicación
Aplicación 1
Transporte
Aplicación n
TCP
Aplicación 1
UDP
Aplicación m
Demultiplexación basada
en número de puerto de
destino en el header TCP o UDP
ICMP
Red
Demultiplexación basada
en el tipo de protocolo que
está en la cabecera IP
IP
ARP
RARP
Enlace de Datos
Ethernet Driver
Medio Físico
Frame Ethernet
entrante
Demultiplexación basada
en el tipo de frame que está
en la Cabecera ethernet
Figura 5
2.6
Puertos de servicios
En el punto 2.4 se mencionó que tanto TCP como UDP tienen asociados cierta cantidad de servicios, cada
uno de ellos con un número de 16 bits. Estos números son estándar y son administrados por la ICANN2
(Internet Corporation for Assigned Names and Numbers). Algunos puertos son3:
ftp-data
20/udp
# File Transfer [Default Data]
ftp-data
20/tcp
# File Transfer [Default Data]
ftp
21/udp
# File Transfer [Control]
ftp
21/tcp
# File Transfer [Control]
ssh
22/udp
# SSH Remote Login Protocol
ssh
22/tcp
# SSH Remote Login Protocol
telnet
23/udp
# Telnet
telnet
23/tcp
# Telnet
smtp
25/udp
# Simple Mail Transfer
smtp
25/tcp
# Simple Mail Transfer
domain
53/udp
# Domain Name Server
domain
53/tcp
# Domain Name Server
http
80/udp
# World Wide Web HTTP
http
80/tcp
# World Wide Web HTTP
Normalmente, los puertos inferiores a 1024 son utilizados por el sistema operativo. Los puertos mayores
o iguales a 1024 son utilizados por las aplicaciones que necesitan comunicarse con otros hosts.
2
3
ICANN: www.icann.org
Estos servicios se encuentran en el arhcivo /etc/services, en sistemas operativos Unix.
3
Verisón 1.3: 8 de julio de 2013
2.7
Modelo Cliente Servidor
La mayor parte de las aplicaciones de red son desarrolladas asumiendo que consisten en dos partes: un
lado servidor y otro lado cliente. El propósito de la aplicación es que el servidor provee algún servicio
definido para los clientes que lo solicitan. La Figura 6 muestra el modelo de comunicación mencionado.
Request
Cliente
Servidor
Red de Datos
Response
Figura 6. Modelo cliente servidor.
Desde el punto de vista de TCP/IP, cada solicitud y respuesta son flujos TCP/UDP, que quedan
determinados por la dupla <IP, Nro Puerto>. Así, por ejempo, en la Figura 7 se muestra un modelo
cliente-servidor para una conexión HTTP, entre un navegador y un sitio web.
Request
Navegador
web
Red de Datos
Response
IP: 192.168.20.4
Puerto: 12354
Servidor
web
IP: 192.168.20.1
Puerto: 80
Figura 7. Modelo cliente servidor para una conexión HTTP.
2.7.1
Tipos de servidores Servidores Iterativos
Este tipo de servidores siguen los siguientes pasos:
1.
2.
3.
4.
Espera por el arribo de una solicitud de un cliente.
Procesa dicha solicitud
Envía la respuesta de vuelta al cliente que envío la solicitud.
Vuelve al paso 1.
El problema con este tipo de servidor es que el paso 2 puede tomar mucho tiempo. Durante este lapso, no
puede atender otros clientes.
Servidor concurrente
1.
2.
3.
4.
5.
Este tipo de servidores siguen los siguientes pasos:
Espera por el arribo de una solicitud de un cliente
Inicia un nuevo servidor que maneje la solicitud del cliente. Esto implica la creación de un nuevo
proceso. Los detalles de cómo se realiza esta tarea depende del sistema operativo.
El nuevo servidor maneja completamente la solicitud del cliente. Cuando esta lista, envía la
respuesta respectiva y termina.
Vuelve al paso 1
La ventaje de un servidor concurrente es que el servidor sólo crea otros servidores que manejan las
solicitudes que llegan. Cada cliente, en esencia, tiene su propio servidor. Asumiendo que el sistema
operativo permite multiprogramación, múltiples clientes pueden ser servidos concurrentemente
[DONAHOO e CALVERT, 2001].
4
Verisón 1.3: 8 de julio de 2013
2.8
Sockets
Un socket es una abstracción del sistema operativo mediante el cual una aplicación puede enviar y recibir
datos [DONAHOO e CALVERT, 2001].
Existen distintos tipos de sockets, cada uno de ellos corresponden a diferentes familias de protocolos de
red. En este documento sólo se tratarán los relativos a la familia de protocolos TCP/IP. Los principales
tipos de socket para esta familia son los sockets de conexión virtual y los sockets de datagramas. Los
primeros utilizan TCP como protocolo de extremo a extremo, proveyendo una comunicación confiable.
Por otro lado, los sockets de datagramas utilizan UDP, con lo que proveen una comunicación no
confiable.
Un socket que utilice TCP/IP es identificado por una dirección IP y un protocolo de extremo a extremo,
que se representa por un número de puerto. Cuando un socket es creado, es asociado a un protocolo, pero
no tiene una dirección IP o un número de puerto. Cuando un socket es asociado a un puerto, recién puede
enviar y recibir información. La Figura 8 muestra la relación lógica que existe entre las aplicación,
sockets, protocolos y números de puertos en un host determinado.
Proceso de
usuario
Socket
TCP
Puertos
TCP
Socket
TCP
1
Proceso de
usuario
Socket
TCP
65535
2
Proceso de
usuario
Socket
UDP
Socket
UDP
65535
1 2
Socket
UDP
Puertos
UDP
UDP
TCP
IP
Figura 8
2.9
Orden de almacenamiento de los Bytes
La forma en la que se almacenan los bytes en memoria varía de un sistema a otro [DONAHOO e
CALVERT, 2001]. Para evitar problemas cuando se reciban o envíen datos a través de la red, es necesario
convertirlos a un formato estándar, denominado “formato de red”. Las funciones de conversión son:
•
htonl(): Host to Network long. Convierte un long int en formato de host a formato de red
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
•
htons(): Host to Network short. Convierte un short int en formato de host a formato de
red
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
•
ntohl(): Network to host long. Convierte un long int en formato de red a formato de de
host.
#include <arpa/inet.h>
uint32_t ntohl(uint32_t netlong);
•
ntohs(): Network to host short. Convierte un short int en formato de red a formato de de
host.
#include <arpa/inet.h>
uint16_t ntohs(uint16_t netshort);
5
Verisón 1.3: 8 de julio de 2013
3
API de C para comunicación en red
3.1
Creación de socket
El socket es la abstracción usada para definir un punto de comunicación. La función socket() crea un
punto de comunicación y retorna un descriptor de él, similar a un descriptor de archivo.
#include <sys/types.h>
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
El parámetro domain especifica la familia de direcciones en que se debe interpretar la dirección del
socket. Puede ser:
•
•
•
•
AF_UNIX, AF_LOCAL
AF_INET
AF_INET6
AF_PACKET
: la comunicación va a ser local en el host.
: para comunicación que utilicen IPv4.
: para comunicación que utilicen IPv6.
: para comunicación a nivel de capa de Enlace de Datos.
El parámetro type puede ser:
•
•
•
•
SOCK_STREAM, lo cual especifica una conexión de circuito virtual. Es bidireccional y contínuo.
En la Internet, esto significa que el protocolo de transprote es TCP.
SOCK_DGRAM, lo cual especifica envío de datagramas. Esto significa que se utiliza UDP como
protocolo de transporte.
SOCK_RAW para acceso a un protocolo "crudo", por ejemplo directo a IP.
SOCK_RDM provee servicio garantizado pero sin comprometer orden.
El parámetro protocol es el número del protocolo específico a utilizar de tipo señalado. Normalmente
sólo un protocolo es soportado de un tipo dado dentro de una familia de protocolos. Lo mejor es dejarlo
en cero y el sistema resuelve el valor que corresponde a ese protocolo según el valor dado en type.
Si la operación se realiza de forma exitosa, socket() retorna un descriptor. En caso constrario, -1.
Ejemplo: Crear un socket, denominado sock, para ser utilizar para una comunicación TCP.
int sock;
sock = socket(AF_INET, SOCK_STREAM, 0);
3.2
Cierra de un socket
Para cerrar el canal de comunicación, se utiliza la función shutdown(), cuyo prototipo es el siguiente:
#include <sys/socket.h>
int shutdown(int s, int how);
Parámetros:
•
•
s: socket a cerrar.
how: cómo se va a cerrar:
o 0 : Prohibido recibir. Se puede seguir enviando.
o 1 : Prohibido enviar. Se puede seguir recibiendo.
o 2 : Prohibido recibir y enviar. Es equivalente a close().
Retorna 0 si la operación se realizó con éxito. -1 en caso contrario.
Observación: generalmente, se utiliza la función close() para cerrar el canal de comunicación.
6
Verisón 1.3: 8 de julio de 2013
3.3
Especificación de direcciones de internet
El API de socket define una estructura genérica, denominada socketaddr, para especificar las
direcciones asociadas a ellos.
struct socketaddr {
u_short sa_family; // Familias de direcciones (Ej. AF_INET)
char
sa_data[14]; // Informacion especifica
}
El primer campo de la estructura define la familiar de direcciones a utilizar. Normalmente, se utilizrá la
constante AF_INET, la que especifa la familiar de direcciones de Internet. La segunda parte es un
conjunto de bits que dependen de la familia escogida. Para evitar problemas con la eterogeneidad de los
sistemas operativos, se establece un largo de 14 Bytes para este campo.
Los socket TCP/IP utilizan la structura socketaddr_in. En la Figura 9 se muestran las diferencias
entre esta estructura y la genérica, socketaddr.
struct socketaddr_in {
u_short
sin_family; // AF_INET
u_short
sin_port;
// puerto de servicio
struct in_addr sin_addr;
// dirección asociada al puerto
}
struct in_addr {
unsigned long s_addr;
};
sa_family
sockaddr
sockaddr_in
sa_data
Familia
2 Bytes
2 Bytes
4 Bytes
8 Bytes
Familia
Puerto
Dirección de Internet
No utilizado
sin_family
sin_port
sin_addr
Figura 9
Ejemplo: Se desea rellenar la estructura sockaddr_in con los datos IP/Puerto de un servidor. Los datos de
él son: IP=192.168.56.10 y Puerto=80
struct sockaddr_in servidor;
char *servIP;
u_short servPort
servIP
= ”192.168.56.10”;
servPort = 80;
bzero(&servidor, sizeof(echoServAddr)); //Rellena con ceros
servidor.sin_family = AF_INET;
//Familia Internet
servidor.sin_addr.s_addr = inet_addr(servIP);//IP del servidor
servidor.sin_port = htons(echoServPort);
//Puerto
En la especificación del puerto, se debe utilizar la función htons() debido a que se debe priorizar el
hecho de que el código puede ser compilado en cualquier plataforma y su ejecución debe ser el mismo.
3.4
3.4.1
Funciones del lado del cliente
Conexión al servidor #include <sys/types.h>
#include <sys/socket.h>
int connect (int s, struct socketaddr *name, int addrlen);
Esta función conecta el socket s con el servidor corriendo en la máquina y puerto especificada en name.
Puede ser usada en conexiones datagramas o de circuito virtual. En el primer caso esta función deja claro
que los próximos datagramas están dirigidos al destido donde se hace la conexión. En el segundo caso, la
dirección destino debe ser especificada en cada llamado de envío del paquete de datos.
7
Verisón 1.3: 8 de julio de 2013
Ejemplo: Se desea conectar, mediante el socket sock, al servidor especificado por la estructura
servidor.
connect(sock, (struct sockaddr *)&servidor, sizeof(servidor));
Si la función connect() retorna satisfactoriamente, el socket sock está conectado y la comunicación
puede proceder a utilizar las funciones de transferencias de datos send() y recv()(Sección 3.6).
3.5
Funciones del lado del servidor
3.5.1
Asociación El servidor debe asociar el socket con una puerta y dirección de interfaz de red para que los clientes
puedan acceder a él. Esto se logra con la función bind().
#include <sys/types.h>
#include <sys/socket.h>
int bind(int s, const sockaddr* name, int addrlen);
Parámetros:
•
•
•
s: socket previamente creado.
addrlen: es el largo de la estructura name.
name es del tipo sockaddr_in.
Los host pueden tener más de una dirección Internet 4 . Por ello, se debe especificar la dirección
sin_addr. Si queremos atender requerimientos entrando por cualquier interfaz de red, se puede usar
INADDR_ANY como dirección asociada a la interfaz.
3.5.2
Esperando por conexiones Este paso es requerido sólo en el caso de conexiones TCP.
#include <sys/types.h>
#include <sys/socket.h>
int listen (int s, int queueLimit);
Esta función indica al kernel que el socket s está listo para recibir conexiones. El parámetro
queueLimit especifica el máximo número de requerimientos de conexión que pueden estar pendientes
en cada momento (conexiones encoladas). Si hay más clientes que el valor del parámetro tratando de
conectarse, el cliente recibe un mensaje “connection refused”.
Ejemplo: Prepare para recibir conexiones TCP en el socket servSock, con un máximo de encolamiento
de 10 clientes.
listen(servSock, 10);
3.5.3
Aceptando conexiones #include <sys/types.h>
#include <sys/socket.h>
int accept (int s,
struct sockaddr *clientAddress,
int *addresslength);
Esta función desencola la próxima conexión. Si está vacía, bloquea el proceso hasta que llegue una
conexión. Cuando se realiza de forma satisfactorio, esta función llena adecuadamente la estructura
sockaddr apuntada por clientAddresss, con la dirección del host en el otro extremo de la
conexión. Además, retorna un nuevo descriptor de socket que el servidor usa para comunicarse con el
cliente. addressLength especifica el tamaño máximo de la estructura clientAddress y contiene
la cantidad de bytes que actualmente se utilizan para la dirección después que la función retorna.
Ejemplo: en el siguiente ejemplo, el socket clntSock, después de la función accept(), se debe
utilizar para comunicarse con el host que se conectó al servidor.
4
Esto se logra a través de múltiples tarjetas de red o a través de interfaces virtuales. En el caso de linux, las interfaces reales ethernet
se denomina, por ejemplo, eth0. Dicha interfaz, puede tener muchas interfaces virtuales, llamados eth0:0; eth0:1, etc.
8
Verisón 1.3: 8 de julio de 2013
int clntSock; //Socket del cliente
struct sockaddr_in echoClntAddr; //Direccion cliente
clntLen = sizeof(echoClntAddr)
clntSock = accept(servSock,
(struct sockaddr *) &echoClntAddr,
&clntLen);
3.6
Funciones de transferencia de datos
#include
#include
int recv
int send
<sys/types.h>
<sys/socket.h>
(int s, char * msg, int len, int flags);
(int s, const char * msg, int len, int flags);
Son idénticas a read() and write(), sólo que ellas tienen un cuarto argumento, flags, para
especificar como enviar o recibir los datos a través del socket s. Si este parámetro es 0, el
comportamiento es el por omisión.
Cuando se usa conexión datagrama, el servidor no llama a las funciones listen() y accept(), y el
cliente generalemente no llama a la función connect(). En estos casos se usan las funciones:
int recvfrom(int s, char * buf, int len, int flags,
struct sockaddr *from, int fromlen);
int sendto(int s, const char *buf, int len, int flags,
struct sockaddr *to, int tolen);
Ejemplo: Se desea enviar el string almacenado en la variable mensaje a través del socket sock.
char* mensaje=”Hola Mundo!”;
send(sock, mensaje, strlen(mensaje), 0);
Ejemplo: Se desea recibir un dato en la variable buffer, desde el socket sock.
char buffer[1024];
recv(sock, buffer, 1024, 0);
3.7
Transformación entre nombre y dirección de máquina
#include <netdb.h>
extern int h_errno;
struct hostent * gethostbyname(const char *name);
#include <sys/socket.h>
struct hostent * gethostbyaddr(const char *address,
int addressLength,
int addressFamily);
La estructura hostent está definida en netdb.h:
struct
char
char
int
int
char
}
hostent {
*h_name;
//nombre oficial
**h_aliases; //alias
h_addrtype; //Tipo de direccion: AF_INET, AFINET6
h_length;
//Largo de la direccion, en bytes
**h_addr_list;
Las funciones retornan una estructura hostent o un NULL en caso de error. En este último caso, la
variable h_errno (definida en netdb.h) tiene el número de error, que puede ser:
•
•
•
•
HOST_NOT_FOUND
NO_ADDRESS o NO_DATA
NO_RECOVERY
TRY_AGAIN
: El host no existe.
: El nombre solicitado es válido pero no tiene una IP.
: Error en el servidor de nombres.
: Error temporal. Tratar nuevamente.
9
Verisón 1.3: 8 de julio de 2013
3.8
Funciones para manipular números IP
El API de socket de C, tiene algunas funciones que ayudan a manipular direcciones IP. En esta sección se
describirán las funciones inet_addr() y inet_ntoa(). La función inet_addr()convierte una
dirección IP en un entero largo sin signo. La función inet_ntoa()convierte una dirección IP en un
string.
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
Ejemplo: almacenar la ip “192.168.10.1” en la estructura destino.
sockaddr_in destino;
destino.sin_addr.s_addr = inet_addr(“192.168.10.1”);
Ejemplo: Determinar la IP que está almacenada en la estructura destino.
char *ip;
ip = inet_ntoa(destino.sin_addr);
printf(“La dirección es: %s\n”, ip);
10
Verisón 1.3: 8 de julio de 2013
4
Cliente Servidor TCP Iterativo
En la Figura 6, se muestra un esquema Cliente-Servidor genérico. En la Figura 10 se muestra un diagrama
de flujo en donde se visualiza la relación que hay entre un proceso cliente y otro servidor, cuando la
transmisión es orientada a la conexión (TCP), a nivel del API del lenguaje C.
Cliente
Servidor
Inicio
Inicio
sock = socket(AF_INET,
SOCK_STREAM, 0)
servSock = socket(AF_INET,
SOCK_STREAM, 0)
bind(servsock, IP_local)
solicitud de
conexión
listen(servSock,...)
connect(sock,
IP_del_servidor)
respuesta
clientSock = accept(servSock,
&echoClntAddr)
send(sock, DATOS)
recv(clientSock, BUFFER)
recv(sock, BUFFER)
send(clientSock, DATOS)
close(sock)
close(clientSock)
Fin
Figura 10
La implementación real de estos programas se muestra en la sección 7.2.
5
Cliente Servidor UDP Iterativo
En la Figura 11, se muestra un diagrama de flujo para una estructa Cliente-Servidor orientado al
datagrama. La implementación en C se muestra en la sección 7.3.
Cliente
Servidor
Inicio
Inicio
sock = socket(AF_INET,
SOCK_DGRAM, 0)
servSock = socket(AF_INET,
SOCK_DGRAM, 0)
bind(servSock, IP_local)
sendto(sock, DATOS,
&echoServerAddr)
recvfrom(servSock, BUFFER,
&echoClntAddr)
recvfrom(sock, BUFFER,
fromAddr)
sendto(servSock, DATOS,
&echoClntAddr)
Verificar si el origen
de los datos es válido
close(sock)
Fin
Figura 11
11
Verisón 1.3: 8 de julio de 2013
6
Cliente Servidor Concurrente
En la Figura 12 se muestra un diagrama de flujo de una implementación de un servidor TCP concurrente.
Se basa en utilizar la función fork(). La idea es que el cliente se conecte en forma preliminar al
proceso Servidor y luego sea atendido por un proceso hijo. La implementación en C se muestra en la
sección 7.4.
Cliente
Servidor
Inicio
Inicio
sock = socket(AF_INET,
SOCK_STREAM, 0)
servSock = socket(AF_INET,
SOCK_STREAM, 0)
bind(servsock, IP_local)
solicitud de
conexión
listen(servSock,...)
connect(sock,
IP_del_servidor)
conexión
aceptada
clientSock = accept(servSock,
&echoClntAddr)
fork()
Proceso Padre
Proceso Hijo
close(servSock)
close(clientSock)
send(sock, DATOS)
recv(clientSock, BUFFER)
Esperar que cada hijo termine
recv(sock, BUFFER)
send(clientSock, DATOS)
close(sock)
close(clientSock)
Fin
Fin
Figura 12
7
7.1
Ejemplos
Programa que obtiene la IP de un servidor cualquiera.
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<netdb.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
int main(int argc, char *argv[]){
struct hostent *servidor;
if (argc!=2) {
printf("Uso: %s <hostname>\n",argv[0]);
exit(EXIT_FAILURE);
}
12
Verisón 1.3: 8 de julio de 2013
if ((servidor=gethostbyname(argv[1]))==NULL) {
printf("error de gethostbyname(). Error %d\n", h_errno);
exit(EXIT_FAILURE);
}
printf("Nombre del host: %s\n",servidor->h_name);
printf("Dirección IP: %s\n",
inet_ntoa(*((struct in_addr *)servidor->h_addr)));
return(EXIT_SUCCESS);
}
13
Verisón 1.3: 8 de julio de 2013
7.2
7.2.1
Cliente Servidor TCP
Cliente #include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<netdb.h>
<unistd.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
#define RCVBUFSIZE 2048
int main(int argc, char *argv[]){
int sock;
struct sockaddr_in echoServAddr;
unsigned short echoServPort;
char *servIP;
char *echoString;
char echoBuffer[RCVBUFSIZE];
int bytesRcvd
// cantidad de
// Descriptor socket
// Direccion del servidor
// Puerto del servidor
// IP del servidor (A.B.C.D)
// String a enviar al servidor
//
bytes recibidos en recv()
if(argc < 3 || argc > 4){
fprintf(stderr,
"Uso: %s <Server IP:A.B.C.D> <Echo Word> [<Echo Port>]\n",
argv[0]);
exit(EXIT_FAILURE);
}
servIP
= argv[1];
echoString = argv[2] ;
if (argc == 4)
echoServPort = atoi(argv[3]); // Utilice este puerto
else
echoServPort = 7; //Puerto 7 es el estandar
//para el servicio de eco
if ((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0){
perror("Error en socket()");
exit(EXIT_FAILURE);
}
bzero(&echoServAddr, sizeof(echoServAddr));//Rellena con ceros
echoServAddr.sin_family = AF_INET;
//Familia Internet
echoServAddr.sin_addr.s_addr = inet_addr(servIP);//IP del servidor
echoServAddr.sin_port = htons(echoServPort);
//Puerto
if( connect( sock,
(struct sockaddr *) &echoServAddr,
sizeof(echoServAddr)
) < 0) {
perror("Error en Connect");
exit(EXIT_FAILURE);
}
if (send(sock, echoString, strlen(echoString), 0) != echoStringLen){
perror("Error en send()");
exit(EXIT_FAILURE);
}
14
Verisón 1.3: 8 de julio de 2013
printf("Recibidos: ");
bytesRcvd = recv(sock, echoBuffer, RCVBUFSIZE, 0);
echoBuffer[bytesRcvd] = '\0';
printf(echoBuffer);
printf("\nBytes recibidos=%d\n", bytesRcvd);
printf("\n");
close(sock);
return(EXIT_SUCCESS);
}
7.2.2
Servidor #include
#include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<netdb.h>
<unistd.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
#define MAXPENDING
#define RCVBUFSIZE
5
2048
void AtenderTCPCliente(int clntSocket);
int main(int argc, char *argv[]) {
int servSock; //Socket del servidor
int clntSock; //Socket del cliente
struct sockaddr_in echoServAddr; //Direccion Local
struct sockaddr_in echoClntAddr; //Direccion cliente
unsigned short echoServPort;
//Puerto del servidor
unsigned int clntLen;
if (argc != 2){
fprintf(stderr, "USO: %s <Server Port>\n", argv[0]) ;
exit(EXIT_FAILURE);
}
echoServPort = atoi(argv[1]);
//Crear el socket para las conexiones entrantes
if ((servSock = socket(PF_INET, SOCK_STREAM, 0)) < 0){
perror("Error en socket()");
exit(EXIT_FAILURE);
}
//Rellenar la estructura para la direccion local
bzero(&echoServAddr, sizeof(echoServAddr));
echoServAddr.sin_family = AF_INET;
echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
echoServAddr.sin_port = htons(echoServPort);
//Asociar a la direccion local
if (bind(servSock, (struct sockaddr *)&echoServAddr,
sizeof(echoServAddr)) < 0){
perror("Error en bin()");
exit(EXIT_FAILURE);
15
Verisón 1.3: 8 de julio de 2013
}
//Hacer que el socket escuche las conecciones entrantes
if (listen(servSock, MAXPENDING) < 0){
perror("Error en listen()");
exit(EXIT_FAILURE);
}
for(;;){
/* Set the size of the in-out parameter */
clntLen = sizeof(echoClntAddr);
// Esperar que un cliente se conecte
if((clntSock = accept(servSock,
(struct sockaddr *) &echoClntAddr,
&clntLen)) < 0) {
perror("Error accept()");
exit(EXIT_FAILURE);
}
printf("Atendiendo cliente %s\n",
inet_ntoa(echoClntAddr.sin_addr));
AtenderTCPCliente(clntSock);
}
return(EXIT_SUCCESS);
}
void AtenderTCPCliente(int clntSocket){
char echoBuffer[RCVBUFSIZE];
int recvMsgSize;
if((recvMsgSize = recv(clntSocket,
echoBuffer,
RCVBUFSIZE, 0)) < 0) {
perror("Error en recv");
exit(EXIT_FAILURE);
}
while (recvMsgSize > 0){ //Cero indica EOT
//Enviar el mensaje de vuelta al cliente
if(send(clntSocket, echoBuffer, recvMsgSize, 0) != recvMsgSize){
perror("Error send()");
exit(EXIT_FAILURE);
}
//Verificar si hay mas data que recibir
if((recvMsgSize = recv(clntSocket, echoBuffer, RCVBUFSIZE, 0)) < 0){
perror("Error recv()");
exit(EXIT_FAILURE);
}
}
close(clntSocket);
}
16
Verisón 1.3: 8 de julio de 2013
7.3
7.3.1
Cliente Servidor UDP
Cliente #include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<netdb.h>
<unistd.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
#define RCVBUFSIZE 2048
int main(int argc, char *argv[]){
int sock;
struct sockaddr_in echoServAddr;
struct sockaddr_in fromAddr;
unsigned short echoServPort;
unsigned int fromSize;
char *servIP;
char *echoString;
//
char echoBuffer[RCVBUFSIZE];
// Descriptor socket
// Direccion del servidor
// Direccion de origen
// Puerto del servidor
//
// IP del servidor (A.B.C.D)
String a enviar al servidor
//
int echoStringLen;
int respStringLen;
if(argc < 3 || argc > 4){
fprintf(stderr,
"Uso: %s <Server IP:A.B.C.D> <Echo Word> [<Echo Port>]\n",
argv[0]);
exit(EXIT_FAILURE);
}
servIP
= argv[1];
echoString = argv[2] ;
if(argc == 4)
echoServPort = atoi(argv[3]); // Utilice este puerto
else
echoServPort = 7; //Puerto 7 es el estandar para el servicio de eco
if((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0){
perror("Error en socket()");
exit(EXIT_FAILURE);
}
bzero(&echoServAddr, sizeof(echoServAddr)); //Rellena con ceros
echoServAddr.sin_family = AF_INET;
//Familia Internet
echoServAddr.sin_addr.s_addr = inet_addr(servIP);//IP del servidor
echoServAddr.sin_port = htons(echoServPort);
//Puerto
if(sendto(sock, echoString, strlen(echoString), 0,
(struct sockaddr *)&echoServAddr,
sizeof(echoServAddr)) < 0){
perror("Error en sendto()");
exit(EXIT_FAILURE);
}
printf("Recibidos: ");
17
Verisón 1.3: 8 de julio de 2013
fromSize = sizeof(fromAddr);
if((respStringLen = recvfrom(sock, echoBuffer, RCVBUFSIZE, 0,
(struct sockaddr*)&fromAddr,
&fromSize)) < 0){
perror("Error en recvfrom()");
exit(EXIT_FAILURE);
}
if(echoServAddr.sin_addr.s_addr != fromAddr.sin_addr.s_addr){
fprintf(stderr,
"Error: received a packet from unknown source.\n");
exit(EXIT_FAILURE);
}
echoBuffer[respStringLen] = '\0';
printf(“%s\n”;echoBuffer);
close(sock);
return(EXIT_SUCCESS);
}
7.3.2
Servidor UDP #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define RCVBUFSIZE
2048
int main(int argc, char *argv[]) {
int servSock; //Socket del servidor
struct sockaddr_in echoServAddr; //Direccion Local
struct sockaddr_in echoClntAddr; //Direccion cliente
unsigned short echoServPort;
//Puerto del servidor
unsigned int cliAddrLen;
//Largo del mensaje entrante
int recvMsgSize;
//Tamano del mensaje recibido
char echoBuffer[RCVBUFSIZE];
if(argc != 2){
fprintf(stderr, "USO: %s <Server Port>\n", argv[0]) ;
exit(EXIT_FAILURE);
}
echoServPort = atoi(argv[1]);
//Crear el socket para las conexiones entrantes
if((servSock = socket(AF_INET, SOCK_DGRAM, 0)) < 0){
perror("Error en socket()");
exit(EXIT_FAILURE);
}
//Rellenar la estructura para la direccion local
bzero(&echoServAddr, sizeof(echoServAddr));
echoServAddr.sin_family = AF_INET;
18
Verisón 1.3: 8 de julio de 2013
echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
echoServAddr.sin_port = htons(echoServPort);
//Asociar a la direccion local
if(bind(servSock, (struct sockaddr *)&echoServAddr,
sizeof(echoServAddr)) < 0){
perror("Error en bin()");
exit(EXIT_FAILURE);
}
for(;;){
cliAddrLen = sizeof(echoClntAddr);
// Esperar que un cliente se conecte
if( (recvMsgSize = recvfrom(servSock, echoBuffer,
RCVBUFSIZE , 0,
(struct sockaddr *) &echoClntAddr,
&cliAddrLen
) ) < 0 ) {
perror("Error recvfrom()");
exit(EXIT_FAILURE);
}
printf("Atendiendo cliente %s\n",
inet_ntoa(echoClntAddr.sin_addr));
printf("\tDato recibido : %s\n",echoBuffer);
printf("\tBytes recibidos: %d\n", recvMsgSize);
//Enviar el datagrama de vuelta al cliente
if( sendto(servSock, echoBuffer, recvMsgSize, 0,
(struct sockaddr *) &echoClntAddr,
sizeof(echoClntAddr)) < 0 ){
perror("Error sendto()");
exit(EXIT_FAILURE);
}
}
return(EXIT_SUCCESS);
}
19
Verisón 1.3: 8 de julio de 2013
7.4
Servidor TCP Concurrente
#include
#include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<netdb.h>
<unistd.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
#include <sys/wait.h>
/*
* Servidor TCP
*/
#define MAXPENDING
#define RCVBUFSIZE
5
2048
void AtenderTCPCliente(int clntSocket);
int main(int argc, char *argv[]) {
int servSock; //Socket del servidor
int clntSock; //Socket del cliente
struct sockaddr_in echoServAddr; //Direccion Local
struct sockaddr_in echoClntAddr; //Direccion cliente
unsigned short echoServPort;
//Puerto del servidor
unsigned int clntLen;
pid_t pid;
int
countChild;
if (argc != 2){
fprintf(stderr, "USO: %s <Server Port>\n", argv[0]) ;
exit(EXIT_FAILURE);
}
echoServPort = atoi(argv[1]);
//Crear el socket para las conexiones entrantes
if ((servSock = socket(PF_INET, SOCK_STREAM, 0)) < 0){
perror("Error en socket()");
exit(EXIT_FAILURE);
}
//Rellenar la estructura para la direccion local
bzero(&echoServAddr, sizeof(echoServAddr));
echoServAddr.sin_family = AF_INET;
echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
echoServAddr.sin_port = htons(echoServPort);
//Asociar a la direccion local
if(bind(servSock, (struct sockaddr *)&echoServAddr,
sizeof(echoServAddr)) < 0){
perror("Error en bin()");
exit(EXIT_FAILURE);
}
//Hacer que el socket escuche las conecciones entrantes
if (listen(servSock, MAXPENDING) < 0){
20
Verisón 1.3: 8 de julio de 2013
perror("Error en listen()");
exit(EXIT_FAILURE);
}
for(;;){
/* Set the size of the in-out parameter */
clntLen = sizeof(echoClntAddr);
// Esperar que un cliente se conecte
if ((clntSock = accept(servSock,
(struct sockaddr *) &echoClntAddr,
&clntLen)) < 0) {
perror("Error accept()");
exit(EXIT_FAILURE);
}
printf("Conexion aceptada del cliente %s\n",
inet_ntoa(echoClntAddr.sin_addr));
if( (pid = fork()) < 0) {
perror("Error en fork()");
exit(EXIT_FAILURE);
}
else if(pid == 0){
//Codigo del hijo
close(servSock);
printf("Atendiendo cliente %s\n",
inet_ntoa(echoClntAddr.sin_addr));
printf("con proceso %d\n", getpid());
AtenderTCPCliente(clntSock);
exit(EXIT_SUCCESS);
}
close(clntSock);
countChild++;
while (countChild) {
pid = waitpid((pid_t) -1, NULL, WNOHANG);
if(pid < 0){
perror("Error en waitpid.");
exit(EXIT_FAILURE);
}
if(pid == 0)
break;
else{
printf("Eliminando proceso %d\n", pid);
countChild--;
}
}
21
Verisón 1.3: 8 de julio de 2013
Bibliografía
STALLINGS, W. Comunicaciones y Redes de Computadores. [S.l.]: Person Educación, 2004. 896 p.
ISBN 9788420541105.
DONAHOO, M. J.; CALVERT, K. L. TCP/IP Sockets in C: Practical Guide for Programmers. [S.l.]:
Morgan Kaufmann, 2001. 130 p. ISBN 1558608265.
STEVENS, W. R. TCP/IP Illustrated Volume 1: The Protocols. [S.l.]: Addison Wesley, v. 1, 1993. 600
p. ISBN 0201633469.
22
Descargar