Sockets en C (Parte I) – Linux

| 2013-10-12 | No hay comentarios »

Socket designa un concepto abstracto por el cual dos programas (posiblemente situados en computadoras distintas) pueden intercambiar cualquier flujo de datos, generalmente de manera fiable y ordenada.

Un socket es, como su propio nombre indica, un conector o enchufe. Con él podremos conectarnos a ordenadores remotos o permitir que éstos se conecten al nuestro a través de la red. En realidad un socket no es más que un descriptor de fichero un tanto especial. Recordemos que en UNIX todo es un fichero, así que para enviar y recibir datos por la red, sólo tendremos que escribir y leer en un fichero un poco especial.

Ya hemos visto que para crear un nuevo fichero se usan las llamadas open() o creat(), sin embargo, este nuevo tipo de ficheros se crea de una forma un poco distinta, con la función socket():

int socket(int domain, int type, int protocol);

Una vez creado un socket, se nos devuelve un descriptor de fichero, al igual que ocurría con open() o creat(), y a partir de ahí ya podríamos tratarlo, si quisiéramos, como un fichero normal. Se pueden hacer read() y write() sobre un socket, ya que es un fichero, pero no es lo habitual. Existen funciones específicamente diseñadas para el manejo de sockets, como send() o recv(), que ya iremos viendo más adelante.

Nos centraremos exclusivamente en la programación de clientes y servidores TCP/IP. Dentro de este tipo de sockets, veremos dos tipos:

  • Sockets de flujo (TCP).
  • Sockets de datagramas (UDP).

Los primeros utilizan el protocolo de transporte TCP, definiendo una comunicación bidireccional, confiable y orientada a la conexión: todo lo que se envíe por un extremo de la comunicación, llegará al otro extremo en el mismo orden y sin errores (existe corrección de errores y retransmisión).

Los sockets de datagramas, en cambio, utilizan el protocolo UDP que no está orientado a la conexión, y es no confiable: si envías un datagrama, puede que llegue en orden o puede que llegue fuera de secuencia. No precisan mantener una conexión abierta, si el destino no recibe el paquete, no ocurre nada especial.

Los puertos por debajo de 1025 están reservados y no se pueden utilizar.

Tipos de datos

La siguiente estructura es una struct derivada del tipo sockaddr, pero específica para Internet:

struct sockaddr_in {
	short int sin_family; // = AF_INET
	unsigned short int sin_port;
	struct in_addr sin_addr;
	unisgned char sin_zero[8];
}
  • sin_family: es un entero corto que indica la “familia de direcciones”, en nuestro caso siempre tendrá el valor “AF_INET”.
  • sin_port: entero corto sin signo que indica el número de puerto.
  • sin_addr: estructura de tipo in_addr que indica la dirección IP.
  • sin_zero: array de 8 bytes rellenados a cero. Simplemente tiene sentido para que el tamaño de esta estructura coincida con el de sockaddr.

Disponemos de un conjunto de funciones que traducen de el formato local (“host”) al formato de la red (“network”) y viceversa:

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

Funciones necesarias

1- Función: socket()

Es una llamada al sistema, esta función devuelve un descriptor de archivo, al igual que la función open() al crear o abrir un archivo .

Durante la llamada se reservan los recursos necesarios para el punto de comunicación, pero no se especifica nada con respecto a la dirección asociada al mismo.

Prototipo:

int socket (int family, int type, int protocol);

De los tres parámetros que recibe, sólo nos interesa fijar uno de ellos: “type”. Deberemos decidir si queremos que sea un socket de flujo (“SOCK_STREAM”)o un socket de datagramas (“SOCK_DGRAM”). El resto de parámetros se pueden fijar a “AF_INET” para el dominio de direcciones, y a “0”, para el protocolo (de esta manera se selecciona el protocolo automáticamente).

Resultado:
La llamada al sistema socket() devuelve un descriptor (número de tipo small integer) o -1 si se produjo un error.
El descriptor de archivo luego será usado para asociarlo a una conexión de red.

2- Asociar una dirección al socket: función bind()
Llamada al sistema que permite asignar una dirección a un socket existente. El sistema no atenderá a las conexiones de clientes, simplemente registra que cuando empiece a recibirlas le avisará a la aplicación.
En esta llamada se debe indicar el número de servicio sobre el que se quiere atender.

Prototipo:

 int bind (int sockfd, const struct sockaddr *myaddr, int addrlen);

 *myaddr: asigna la dirección especificada en la estructura del tipo sockaddr. En este parámetro suele utilizarse una estructura del tipo

sockaddr_in (ejemplo en transparencia 20).
 sockfd: descriptor del socket involucrado.
 addrlen: tamaño de la estructura en *myaddr, es decir sizeof(myaddr).

Resultado:
La llamada al sistema bind() devuelve un 0, si se produjo un error devuelve -1.

3- Atender conexiones: función listen()

Avisar al sistema operativo que comience a atender la conexión de red. El sistema registrará la conexión de cualquier cliente para transferirla a la aplicación cuando lo solicite.
Si llegan conexiones de clientes más rápido de lo que la aplicación es capaz de atender, el sistema almacena en una cola las mismas y se podrán ir obteniendo luego.

Prototipo:

int listen(int s, int backlog)

4- Función accept()
Pedir y aceptar las conexiones de clientes al sistema operativo. El sistema operativo entregará el siguiente cliente de la cola. Si no hay clientes se quedará bloqueada hasta que algún cliente se conecte.

Prototipo: 

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

5- Para poder enviar y recibir datos existen varias funciones. Ya avanzamos anteriormente que un socket es un descriptor de fichero, así que en teoría sería posible escribir con write() y leer con read(), pero hay funciones mucho más cómodas para hacer esto. Dependiendo si el socket que utilicemos es de tipo socket de flujo o socket de datagramas emplearemos unas funciones u otras:

  • Para sockets de flujo: send() y recv().
  • Para sockets de datagramas: sendto() y recvfrom().

Prototipos:

int send(int s, const void *msg, size_t len, int flags);

int  sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

int recv(int s, void *buf, size_t len, int flags);

int  recvfrom(int  s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

6- Para cerrar un socket se puede utilizar la llamada estándar close(), como con cualquier fichero. Esta función cierra el descriptor de fichero del socket, liberando el socket y denegando cualquier envío o recepción a través del mismo. Si quisiéramos tener más control sobre el cierre del socket podríamos usar la función shutdown():

int shutdown(int s, int how);

En el parámetro “how” indicamos cómo queremos cerrar el socket:

  • 0: No se permite recibir más datos.
  • 1: No se permite enviar más datos.
  • 2: No se permite enviar ni recibir más datos (lo mismo que close()).

Esta función no cierra realmente el descriptor de fichero del socket, sino que modifica sus condiciones de uso, es decir, no libera el recurso. Para liberar el socket después de usarlo, deberemos usar siempre close().

Programas: servidor y cliente

Servidor: es el programa que permanece pasivo a la espera de que alguien solicite conexión. Puede o no devolver datos.
Cliente: es el programa que solicita la conexión para enviar o solicitar datos al servidor.

Ejemplo con sockets de tipo stream

Servidor


// Ficheros de cabecera
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

//Función principal
int main(int argc, char **argv)
{

if(argc > 1)
{

//Primer paso, definir variables
int fd,fd2,longitud_cliente,puerto;
puerto=atoi(argv[1]);

//Se necesitan dos estructuras del tipo sockaddr
//La primera guarda info del server
//La segunda del cliente
 struct sockaddr_in server;
 struct sockaddr_in client;

//Configuracion del servidor
 server.sin_family= AF_INET; //Familia TCP/IP
 server.sin_port = htons(puerto); //Puerto
 server.sin_addr.s_addr = INADDR_ANY; //Cualquier cliente puede conectarse
 bzero(&(server.sin_zero),8); //Funcion que rellena con 0's

 //Paso 2, definicion de socket
 if (( fd=socket(AF_INET,SOCK_STREAM,0) )<0){
 perror("Error de apertura de socket");
 exit(-1);
 }

 //Paso 3, avisar al sistema que se creo un socket
 if(bind(fd,(struct sockaddr*)&server, sizeof(struct sockaddr))==-1) {
 printf("error en bind() \n");
 exit(-1);
 }

 //Paso 4, establecer el socket en modo escucha
if(listen(fd,5) == -1) {
 printf("error en listen()\n");
 exit(-1);
 }

 //Paso5, aceptar conexiones
 while(1) {
 longitud_cliente= sizeof(struct sockaddr_in);
 /* A continuación la llamada a accept() */
 if ((fd2 = accept(fd,(struct sockaddr *)&client,&longitud_cliente))==-1) {
 printf("error en accept()\n");
 exit(-1);
 }

send(fd2,"Bienvenido a mi servidor.\n",26,0);

close(fd2); /* cierra fd2 */
 }
close(fd);
}
else{
printf("NO se ingreso el puerto por parametro\n");
}

return 0;

}

Cliente


// Ficheros de cabecera
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
//netbd.h es necesitada por la estructura hostent

int main(int argc, char *argv[])
{

if(argc > 2)
{
 //Primer paso, definir variables
 char *ip;
 int fd, numbytes,puerto;
 char buf[100];
 puerto=atoi(argv[2]);
 ip=argv[1];

struct hostent *he;
 /* estructura que recibirá información sobre el nodo remoto */
 struct sockaddr_in server;
 /* información sobre la dirección del servidor */

if ((he=gethostbyname(ip))==NULL){
 /* llamada a gethostbyname() */
 printf("gethostbyname() error\n");
 exit(-1);
 }

//Paso 2, definicion de socket
 if ((fd=socket(AF_INET, SOCK_STREAM, 0))==-1){
 /* llamada a socket() */
 printf("socket() error\n");
 exit(-1);
 }
//Datos del servidor
 server.sin_family = AF_INET;
 server.sin_port = htons(puerto);
 server.sin_addr = *((struct in_addr *)he->h_addr);
 /*he->h_addr pasa la información de ``*he'' a "h_addr" */
 bzero(&(server.sin_zero),8);

 //Paso 3, conectarnos al servidor
 if(connect(fd, (struct sockaddr *)&server,
 sizeof(struct sockaddr))==-1){
 /* llamada a connect() */
 printf("connect() error\n");
 exit(-1);
 }

if ((numbytes=recv(fd,buf,100,0)) == -1){
 /* llamada a recv() */
 printf("Error en recv() \n");
 exit(-1);
 }

buf[numbytes]='\0';

printf("Mensaje del Servidor: %s\n",buf);
 /* muestra el mensaje de bienvenida del servidor =) */

close(fd);
}
else{
printf("No se ingreso el ip y puerto por parametro\n");
}

}

Para ver estado TCP:

netstat -pta

Ejemplos con sockets de tipo datagrama

Servidor


#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/types.h>

#define SERVER_PORT 4321
#define BUFFER_LEN 1024

int main(int argc, char *argv[])
{
 int sockfd; /* descriptor para el socket */
 struct sockaddr_in my_addr; /* direccion IP y numero de puerto local */
 struct sockaddr_in their_addr; /* direccion IP y numero de puerto del cliente */
 /* addr_len contendra el tamanio de la estructura sockadd_in y numbytes el
 * numero de bytes recibidos
 */
 int addr_len, numbytes;
 char buf[BUFFER_LEN]; /* Buffer de recepción */

 /* se crea el socket */
 if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
 perror("socket");
 exit(1);
 }

 /* Se establece la estructura my_addr para luego llamar a bind() */
 my_addr.sin_family = AF_INET; /* usa host byte order */
 my_addr.sin_port = htons(SERVER_PORT); /* usa network byte order */
 my_addr.sin_addr.s_addr = INADDR_ANY; /* escuchamos en todas las IPs */
 bzero(&(my_addr.sin_zero), 8); /* rellena con ceros el resto de la estructura */

 /* Se le da un nombre al socket (se lo asocia al puerto e IPs) */
 printf("Creando socket ....\n");
 if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
 perror("bind");
 exit(1);
 }

 /* Se reciben los datos (directamente, UDP no necesita conexión) */
 addr_len = sizeof(struct sockaddr);
 printf("Esperando datos ....\n");
 if ((numbytes=recvfrom(sockfd, buf, BUFFER_LEN, 0, (struct sockaddr *)&their_addr, (socklen_t *)&addr_len)) == -1) {
 perror("recvfrom");
 exit(1);
 }

 /* Se visualiza lo recibido */
 printf("paquete proveniente de : %s\n",inet_ntoa(their_addr.sin_addr));
 printf("longitud del paquete en bytes : %d\n",numbytes);
 buf[numbytes] = '\0';
 printf("el paquete contiene : %s\n", buf);

 /* cerramos descriptor del socket */
 close(sockfd);

 return 0;
}

Cliente


#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/types.h>

#define SERVER_PORT 4321
#define BUFFER_LEN 1024

int main(int argc, char *argv[])
{
 int sockfd; /* descriptor a usar con el socket */
 struct sockaddr_in their_addr; /* almacenara la direccion IP y numero de puerto del servidor */
 struct hostent *he; /* para obtener nombre del host */
 int numbytes; /* conteo de bytes a escribir */
 if (argc != 3) {
 fprintf(stderr,"uso: cliente hostname mensaje\n");
 exit(1);
 }

 /* convertimos el hostname a su direccion IP */
 if ((he=gethostbyname(argv[1])) == NULL) {
 herror("gethostbyname");
 exit(1);
 }

 /* Creamos el socket */
 if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
 perror("socket");
 exit(1);
 }

 /* a donde mandar */
 their_addr.sin_family = AF_INET; /* usa host byte order */
 their_addr.sin_port = htons(SERVER_PORT); /* usa network byte order */
 their_addr.sin_addr = *((struct in_addr *)he->h_addr);
 bzero(&(their_addr.sin_zero), 8); /* pone en cero el resto */

 /* enviamos el mensaje */
 if ((numbytes=sendto(sockfd,argv[2],strlen(argv[2]),0,(struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
 perror("sendto");
 exit(1);
 }

 printf("enviados %d bytes hacia %s\n",numbytes,inet_ntoa(their_addr.sin_addr));

 /* cierro socket */
 close(sockfd);

 return 0;
}

Para ver estado UDP:

netstat -upa

Acerca del autor: Rodrigo Paszniuk

Ingeniero Informático, amante de la tecnología, la música, el ciclismo y aprender cosas nuevas.

Posts Relacionados

  • Developers SO Sistemas Operativos preferidos por los developers
  • Instalar Tomcat 7 en CentOS 6
  • RPC (Remote Procedure Call) en C – Linux
  • Sockets en C (Parte II) – Linux