Programación en C/Sockets

De Wikilibros, la colección de libros de texto de contenido libre.

Concepto[editar]

Un socket, por su traducción al Castellano, es un "enchufe", es decir, una conexión con otro ordenador que nos permitirá intercambiar datos. Por así decirlo, cuando quiere conectarse a una página web, el navegador, que es un programa, crea un socket, que contendrá información acerca del servidor, información suficiente para poder realizar una asociación, y de esta manera poder ejercer una conexión. La definición y utilización de los sockets es clara: Conectar dos ordenadores para que puedan comunicarse entre ellos de forma remota o local.

¿Cómo funcionan?[editar]

Los sockets son simplemente un "puente", como hemos dicho antes, hacen la función de comunicarnos con otro ordenador. Para que ésto ocurra deben existir dos nodos: Cliente y Servidor. Para que ambos se comuniquen deben de enviarse información entre ellos para determinar con quien van a hablar. Un ejemplo práctico es el correo (no electrónico): Si quiere enviar una carta debe saber el destino donde va a enviar la carta, cuál es la ciudad donde vive el destinatario, su dirección, nombre, etc... Acto seguido, el remitente (cliente) al enviar la carta (mensaje) a una administrador de correo (servidor), éste leerá los datos y los enviará a su destino (otro cliente). Este es el caso más particular de intercambio de datos que existe. También podemos enviar datos directamente al servidor, y que el servidor nos conteste.

¿Windows o Linux?[editar]

Este tutorial de Sockets va dirigido a usuarios que programan para la plataforma Linux, ya que para esta no existe una documentación tan clara como pueda ser la Windows API Reference.

Tipos[editar]

Los sockets pueden ser de dos tipos:

-TCP: Están preparados para conexiones de tres pasos, en ingles se denomina three way handshake, ya que el cliente para conectarse, le pide permiso al servidor, el servidor acepta o declina (supongamos que acepta), y a partir de ahí comienza el envío y la recepción de datos. Se denomina así porque es un "apretón de manos" en tres pasos.

-UDP: No soportan conexión en tres pasos. Lo que hace el cliente es enviar el paquete con los datos al servidor sin ningún control. Un ejemplo de esta ineficiencia es Skype, el cual utiliza paquetes UDP, y si se pierde uno por el camino no lo podemos recuperar o no llegará al destinatario. Por ese motivo no se recomienda utilizar UDP excepto en casos concretos.

Estructuras[editar]

Los sockets se organizan en estructuras para contener la información tanto del host como de cliente. Al hablar del concepto host nos referimos a la máquina que aloja el servidor para la conexión. Las estructuras són las siguientes, la explicación está debajo de cada una:

struct sockaddr_in 
{ 
   short int sin_family;        //Protocolo para la conexión
   unsigned short int sin_port; //Puerto para la conexión
   struct in_addr sin_addr;     //Estructura a la dirección IP
   unsigned char sin_zero[8];   //Relleno
};

Esta estructura es la más importante. En ella volcaremos todos los datos para que el socket establezca una conexión fiable.

struct in_addr
{
   unsigned   long s_addr; //Contiene la dirección IP del host
};
struct sockaddr 
{ 
   unsigned short sa_family;  //Protocolo
   char sa_data[14];          //Dirección IP del host  
};

Esta estructura la podemos usar, pero no es recomendable, ya que vamos a utilizar sockets de tipo TCP, es decir, que la información va organizada, a diferencia del protocolo UDP.

struct hostent 
{   
   char *h_name;                   //Dirección IP del host
   char **h_aliases;               //Alias del host
   int h_addrtype;                 //tipo de dirección
   int h_length;                   //Longitud de la dirección IP
   char **h_addr_list;             //Lista de direcciones conectadas al servidor
   #define h_addr  h_addr_list[0]  //La primera direccion IP de la lista tiene un alias para acceder mas facilmente
};

Esta estructura contendrá todos los datos del host o servidor.

Funciones de conversión[editar]

La conversión la utilizamos para hacer que el programa funcione correctamente, y que cada tipo concuerde con lo requerido en cada momento. Lo hemos visto a lo largo de la guía con funciones como atoi(), itoa(), atof(), etc... Definidas en stdlib, algunas de ellas. (http://www.cplusplus.com/reference/cstdlib/). En este caso, podremos convertir un entero (int) en una dirección de nodo de la siguiente manera, para que el socket entienda donde debe llevar la información:

...
int puerto = 5555;
cliente.sin_port = htons(puerto); //tambien podríamos poner htons(5555);
...

O también, si queremos saber de qué puerto proviene una conexión podemos determinarlo con la función ntohs():

...
printf("El cliente se conecta desde el puerto %d\n", ntohs(cliente.sin_port));
...

Bibliotecas[editar]

Es esta sección utilizaremos las bibliotecas usuales: <stdio.h> <stdlib.h> <string.h> <unistd.h> Y bibliotecas más específicas para los sockets como són: <sys/socket.h> <arpa/inet.h> <netinet/in.h> <netdb.h> Todas ellas son importantes para el desarrollo de estructuras y tipos durante el programa.

Funciones[editar]

"Divide y vencerás"' dijo Julio César, y los desarrolladores no paramos de dividir para crear nuestros programas. En programación dividir quiere decir usar funciones, por ello, vamos a plantear las funciones a usar en cada caso.

Cliente[editar]

En este caso, lo primero que usaremos será la función socket(); para crear el socket, y por ello la conexión de red. Su uso se resume en:

...
int conexion;
if((conexion = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
   fprintf(stderr, "Error al crear el socket\n");
   return 1;
}
...

socket();[editar]

En este caso explicaremos que AF_INET es el dominio. Existen muchos más dominios, definidos en el enlace del final de la explicación. Con SOCK_STREAM especificaremos que la conexión será TCP. La función socket devuelve un descriptor de fichero con el que se accede al socket. Si fracasa devuelve -1. Por lo tanto, si se cumple < 0 devolveremos error. Más información en: http://www.linuxhowtos.org/manpages/2/socket.htm

gethostbyname();[editar]

A veces podríamos usar la función gethostbyname();, yo la recomiendo más que ninguna para resolver el dominio. Ejemplo:

...
struct hostent *servidor;
servidor = gethostbyname("google.com");
if(servidor == NULL)
{
   printf("La dirección IP del host es errónea\n");
   return 1;
}

connect();[editar]

Una vez esté todo correcto nos tendremos que conectar con el host con la función connect();. Ejemplo:

...
if(connect(conexion,(struct sockaddr *)&cliente,sizeof(cliente)) < 0)
{
   printf("Imposible conectar con el host\n");
   return 1;
}

send(); o write();[editar]

Tanto la función send() definida en <sys/socket.h> como la función write(); definida en <unistd.h> sirven para enviar sockets. La única diferencia es la utilidad última de cada función. send() envía bytes por el socket específico permitiendo al programador controlar de mejor manera cómo se envía la información, y write() sólo escribe bytes en un descriptor.

...
char buffer[200];
write(conexion,buffer,200);
...
send(conexion,buffer,200,0);
...

read(); o recv();[editar]

Tanto la función recv() definida en <sys/socket.h> como la función read(); definida en <unistd.h> sirven para recibir sockets.

...
char buffer[200];
read(conexion,buffer,200);   //conexion es el socket
...
recv(conexion,buffer,200,0);
...

Servidor[editar]

socket();[editar]

Utilizaremos la función socket, anteriormente definida.

bind();[editar]

Sirve para asignar un socket a un puerto. Ejemplo:

...
bind(conexion_servidor,(struct sockaddr *)&servidor,sizeof(servidor));
...

Debemos tener en cuenta siempre que vamos a trabajar con la estructura sockaddr_in, y que por ello, en algunas funciones deberemos convertirla a struct sockaddr ó incluso struct in_addr.

listen();[editar]

Sirve para poner a la escucha un socket. Ejemplo:

...
listen(conexion_servidor,3);
...

De esta manera estamos poniendo a la escucha en socket de conexión, que va a permitir 3 conexiones, es decir, 3 clientes distintos.

accept();[editar]

Por último la función accept que nos servirá para estar a la escucha y permitir que algún cliente se conecte tras utilizar la función connect();. Ejemplo:

...
int clilong=sizeof(cliente);
conexion_cliente=accept(conexion_socket,(struct sockaddr *)&cliente,&clilong);
...

Y por último ya podríamos aplicar la función recv(); o, en caso de querer escribir, send();.

Ahora vamos con los ejemplos cliente-servidor.

Cliente[editar]

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
int main(int argc, char **argv){
  if(argc<2)
  {
    printf("<host> <puerto>\n");
    return 1;
  }
  struct sockaddr_in cliente; //Declaración de la estructura con información para la conexión
  struct hostent *servidor; //Declaración de la estructura con información del host
  servidor = gethostbyname(argv[1]); //Asignacion
  if(servidor == NULL)
  { //Comprobación
    printf("Host erróneo\n");
    return 1;
  }
  int puerto, conexion;
  char buffer[100];
  conexion = socket(AF_INET, SOCK_STREAM, 0); //Asignación del socket
  puerto=(atoi(argv[2])); //conversion del argumento
  bzero((char *)&cliente, sizeof((char *)&cliente)); //Rellena toda la estructura de 0's
        //La función bzero() es como memset() pero inicializando a 0 todas la variables
  cliente.sin_family = AF_INET; //asignacion del protocolo
  cliente.sin_port = htons(puerto); //asignacion del puerto
  bcopy((char *)servidor->h_addr, (char *)&cliente.sin_addr.s_addr, sizeof(servidor->h_length));
  //bcopy(); copia los datos del primer elemendo en el segundo con el tamaño máximo 
  //del tercer argumento.


  //cliente.sin_addr = *((struct in_addr *)servidor->h_addr); //<--para empezar prefiero que se usen
  //inet_aton(argv[1],&cliente.sin_addr); //<--alguna de estas dos funciones
  if(connect(conexion,(struct sockaddr *)&cliente, sizeof(cliente)) < 0)
  { //conectando con el host
    printf("Error conectando con el host\n");
    close(conexion);
    return 1;
  }
  printf("Conectado con %s:%d\n",inet_ntoa(cliente.sin_addr),htons(cliente.sin_port));
  //inet_ntoa(); está definida en <arpa/inet.h>
  printf("Escribe un mensaje: ");
  fgets(buffer, 100, stdin);
  send(conexion, buffer, 100, 0); //envio
  bzero(buffer, 100);
  recv(conexion, buffer, 100, 0); //recepción
  printf("%s", buffer);
return 0;
}

Servidor[editar]

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<netdb.h>
int main(int argc, char **argv){
  if(argc<2)
  { //Especifica los argumentos
    printf("%s <puerto>\n",argv[0]);
    return 1;
  }
  int conexion_servidor, conexion_cliente, puerto; //declaramos las variables
  socklen_t longc; //Debemos declarar una variable que contendrá la longitud de la estructura
  struct sockaddr_in servidor, cliente;
  char buffer[100]; //Declaramos una variable que contendrá los mensajes que recibamos
  puerto = atoi(argv[1]);
  conexion_servidor = socket(AF_INET, SOCK_STREAM, 0); //creamos el socket
  bzero((char *)&servidor, sizeof(servidor)); //llenamos la estructura de 0's
  servidor.sin_family = AF_INET; //asignamos a la estructura
  servidor.sin_port = htons(puerto);
  servidor.sin_addr.s_addr = INADDR_ANY; //esta macro especifica nuestra dirección
  if(bind(conexion_servidor, (struct sockaddr *)&servidor, sizeof(servidor)) < 0)
  { //asignamos un puerto al socket
    printf("Error al asociar el puerto a la conexion\n");
    close(conexion_servidor);
    return 1;
  }
  listen(conexion_servidor, 3); //Estamos a la escucha
  printf("A la escucha en el puerto %d\n", ntohs(servidor.sin_port));
  longc = sizeof(cliente); //Asignamos el tamaño de la estructura a esta variable
  conexion_cliente = accept(conexion_servidor, (struct sockaddr *)&cliente, &longc); //Esperamos una conexion
  if(conexion_cliente<0)
  {
    printf("Error al aceptar trafico\n");
    close(conexion_servidor);
    return 1;
  }
  printf("Conectando con %s:%d\n", inet_ntoa(cliente.sin_addr),htons(cliente.sin_port));
  if(recv(conexion_cliente, buffer, 100, 0) < 0)
  { //Comenzamos a recibir datos del cliente
    //Si recv() recibe 0 el cliente ha cerrado la conexion. Si es menor que 0 ha habido algún error.
    printf("Error al recibir los datos\n");
    close(conexion_servidor);
    return 1;
  }
  else
  {
    printf("%s\n", buffer);
    bzero((char *)&buffer, sizeof(buffer));
    send(conexion_cliente, "Recibido\n", 13, 0);
  }
  close(conexion_servidor);
  return 0;
}