Programación con Sockets IP

De Wiki do Ciclo ASIR do IES de Rodeira

Índice

Comunicación con Sockets na Linguaxe C

Conceptos Básicos de TCP/IP.

TCP/IP é o protocolo elexido para o funcionamento da rede Internet, que ademáis é o protocolo nativo do sistema UNIX, e polo tanto tamén de Linux o que significa que o protocolo está integrado no kernel do sistema e polo tanto funciona con gran eficiencia.

Nunha rede TCP/IP, cada nodo da rede (normalmente un ordenador ou un router) está conectado con unha ou máis tarxetas de rede. Estas tarxetas están identificadas por unha dirección de rede de xeito que tada tarxeta ten unha dirección única, no caso das redes TCP/IP esta dirección recibe o nome de dirección IP. En liñas moi xerais, a comunicación mediante o protocolo IP consiste en dividir a información a enviar en pequenos paquetes de datos chamados datagramas que se envían etiquetados coa dirección fonte e destiño sin ter en conta o camiño polo que viaxarán nin o tempo que empregarán. Ademáis, IP non ten mecanismos que permitan precisar a qué proceso da máquina destiño teñen que ir os datagramas o que é un problema grave en entornos multitarefa coma Linux.

Pra solventar estes problemas, sobre o protocolo IP implementouse un novo protocolo de control da comunicación chamado TCP (Transport Control Protocol). Este protocolo encárgase de recibir e enviar a información de xeito ordeado, e é capaz de atender a varios procesos á vez mediate conexións simultáneas. Para facer esto, TCP utiliza os portos de comunicación . Unha conexión TCP faise entre dous portos de dúas máquinas, e decir, unha conexión TCP está identificada pola dirección IP das máquinas conectadas e polos dous portos utilizados polos procesos que se comunican. Antes de poder utilizar TCP un proceso ten que asociarse a un porto de comunicación TCP, unha vez feito esto pode solicitar o comenzo da comunicación.

Nos casos nos que non sexa necesario o control da comunicación proporcionado por TCP, pódese utilizar un protocolo máis sinxelo e rápido chamado UDP (User Datagram Protocol). A comunicación UDP é prácticamente igual que o uso directo de IP incorporando o uso dos portos pra identificar os procesos que interveñen na comunicación.

Un proceso pode comunicarse con outro mediante un porto TCP, UDP ou mediante varios portos TCP ou UDP simultáneamente. Se un proceso desexa comunicarse con outro a través de UDP necesita coñecer a dirección IP e o porto UDP do interlocutor. A comunicación TCP e algo máis complexa; a comunicación debe pasar por unha fase de apertura, de intercambio de datos e de peche. Pra establecer unha comunicación TCP, un proceso debe asumir o papel de activo e outro o papel de pasivo. O extremo pasivo (servidor) espera conexións de outros procesos nun porto concreto, mentras que o extremo activo (cliente) ten que abrir a conexión suministrando a IP e o porto do extremo pasivo e indicando a propia dirección IP e porto. A conexión queda entronces identificada polas dúas direccións IP e portos. Unha vez aberta a comunicación os procesos poden intercambiar bytes nas dúas direccións a través da conexión aberta. Calqueira dos dous extremos pode pechar a conexión en calquera momento.

Outro concepto importante nos protocolos TCP/IP é o de máscara de rede. As direccións IP dos equipos da rede conteñen dúas partes; unha parte que identifica a rede local á que pertence o equipo, e outra parte que identifica o equipo. A máscara de rede permite saber qué parte da dirección IP corresponde coa rede facendo un And bit a bit da dirección IP coa máscara. Aqueles bits que dean como resultado 1 indican a rede mentras que os que dean 0 pertencen o número de equipo. Por exemplo, a dirección IP 172.16.2.1 coa máscara 255.255.0.0 nos está a indicar o equipo 2.1 pertencente á rede 172.16.0.0.

Funcións de Conversión de Formato

Todo o traballo sobre TCP/IP precisa do manexo de direccións IP de de números de portos pero as redes de ordenadores poden estar formadas por equipos con arquitecturas moi diversas, nos que o formato da información transmitida pode variar. Pra evitar este problema, estableceuse un formato especial pra enviar datos pola rede que chamaremos formato de rede (network format) en contraposición co formato que empregan as máquinas pra almacenalos que chamaremos formato de equipo (host format). Todos os sistemas UNIX teñen chamadas ó sistema que permiten cambiar os números de formato de rede a formato de equipo e viceversa; estas funcións teñen a forma XtoYT(), donde X indica a orixe do dato (h (host) ou n (network)) e Y o destiño da mesma maneira, mentras que T indica o tipo (l (long) ou s (short int)), indicando por exemplo htonl a transformación de un long de formato de host a formato de rede. As funcións son as seguintes:

#include <netinet/in.h>
 
unsigned long htonl (unisigned long hostlong);
unsigned short htons (unsigned short hostshort);
unsigned long ntohl (unsigned long netlong);
unsigned long ntohs (unsigned short netshort);

Por outra banda as direccións IP representadas no ordenador coma números enteiros, son manipuladas normalmente polos usuarios en notación de punto que consiste en representar as direccións IP como cadeas de caracteres compostas de 4 subcadeas separadas por puntos; cada unha de elas representa un número decimal entre o 0 e o 255 (por exemplo "172.163.212.123"). Ademáis ós usuarios lles resulta moito máis fácil tratar con nomes que con direccións IP, polo que cada equipo na rede pode ter un nome. Estes nomes consisten en varias cadeas separadas por puntos (como gandalf.iesrodeira.com, www.google.com ou tocomotxo.edu.xunta.es). A primeira cadea antes do punto indica o nome da máquina, e o resto indica o dominio. Os dominios teñen unha estructura xerárquica, que vai ascendendo de dereita a esquerda, por exemplo en gandalf.iesrodeira.com, o equipo é gandalf e o dominio iesrodeira.com. Á dereita de todo está o dominio de maior nivel, ou top level domain (com). Nalgúns casos este dominio indica a actividade da organización ó que pertence o dominio (com comercial, edu educación) ou o páis (es España, fr Francia ...). Debaixo do dominio de nivel superior, nos dominios non asignados a paises, está o nome que identifica a organización. Nos asignados a países é competencia de cada ún a súa xestión (por exemplo, en España debaixo de .es vai a organización, mentras que no Reino Unido hai unha clasificación das organizacións por actividades como en www.oxford.ac.uk, pertencente a unha organización académica). Por último, a xestión dos nomes que van detrás da organización pertence á propia organización, indicando normalmente o nome da máquina ou unha división en departamentos ... etc. Por exemplo, tocomotxo.edu.xunta.es indica: españa, xunta de Galiza, educación, tocomotxo (máquina tocomotxo, pertencente o departamento de educación da xunta de Galiza no estado español), todo o que vai detrás de xunta.es é responsabilidade da xunta.

O que os usuarios podan utilizar nomes pra referirse ós equipos quere decir que ten que haber algún sistema pra traducir os nomes dos equipos á súa dirección IP e viceversa. Os métodos máis habituais son:

  • Uso dun ficheiro (nas máquinas UNIX normalmente /etc/hosts) cunha lista de nomes e as direccións IP correspondentes. As traduccións se efectúan buscando neste ficheiro. O principal problema é o mantemento deste ficheiro en todas as máquinas da rede, polo que únicamente é util pra redes locais moi pequenas.
  • Uso dun servicio especial coma o NIS (Network Information Service) de Sun Microsystems, que proporciona unha base de datos válida pra unha rede de gran tamaño pertencente a unha organización equivalente ó /etc/hosts.
  • Uso do DNS (Domain Name System), ou sistema de nomes de dominio que consiste en unha gran base de datos distribuída que permite a calquera máquina da rede obter a traduccion do nome á IP ou viceversa.

Por sorte os programadores non teñen que preocuparse polo método utilizado polo sistema pra obter os nomes e as IP's dás máquinas, xa que existen varias funcións que nos proporcionan de forma transparente estes servicios:

Funcións de transformación de direccións

#include <unistd.h>
 
int gethostname(char *name, int namelen);
  • gethostname Almacena o nome do ordenador na dirección de memoria indicada por name, que dispon dunha lonxitude namelen. No caso de que se produza algún erro devolve -1, asignándolle a errno o valor axeitado.
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
struct in_addr {
   unsigned long int s_addr;
}
 
unsigned long inet_aton(const char *cp, struct in_addr *inp);
unsigned long inet_addr(const char *cp)
unsigned long inet_network(const char *cp);
struct in_addr inet_makeaddr(const int inet, const int host);
int inet_lnaof(const struct in_addr in);
int inet_netof(const struct in_addr in);
char *inet_ntoa(const struct in_addr in);
  • inet_aton convirte a dirección IP expresada coma unha cadea de caracteres (cp) en notación de puntos a unha dirección IP numérica en formato de rede, almacenandoa en inp. Devolve 0 si a dirección non é válida.
  • inet_addr convirte a dirección IP expresada coma unha cadea de caracteres (cp) en notación de puntos a unha dirección IP numérica en formato de rede. Devolve -1 no caso de que a IP non sexa válida (ollo, esta función non se utiliza, se non que se emprega inet_aton, porque -1 agora é unha dirección válida e inet_aton proporciona unha maneira mellor de controlar os posibles erros).
  • inet_network convirte a dirección IP expresada coma unha cadea de caracteres (cp) en notación de puntos ao número de rede correspondente en formato de host. En caso de que a IP non sexa válida devolve -1.
  • inet_makeaddr combina o número de rede (inet) co número de host (host) pra formar a dirección IP en formato de rede. Tanto inet como host están expresadas en formato de host.
  • inet_lnaof devolve a dirección de rede en formato de host a partir da dirección in.
  • inet_netof devolve a dirección de host en formato de host a partir da dirección in.
  • inet_ntoa convirte unha dirección IP en formato de rede en unha cadea ASCII en notación de punto.
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
 
struct hostent {
  // Nome oficial da máquina.
  char *h_name;
  // Lista de alias.
  char **h_aliases;
  // Tipo de dirección.
  int h_addrtype;
  // Lonxitude das direccións.
  int h_length;
  // Lista de direccións en "formato de rede".  
  char **h_addr_list;
}
 
#define h_addr  h_addr_list[0];
 
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const char *addr, int len, int type);
  • gethostbyname devolve a dirección dunha estructura hostent coa información correspondente ó ordenador co nome name.
  • gethostbyaddr devolve a dirección dunha estructura hostent coa información correspondente ó ordenador con dirección addr en formato de host (o campo h_addr_list é unha táboa que contén as direccións onde se atopan as IP en formato de rede). O único tipo (type) permitido en Linux é AF_INET

Introducción á Comunicación con Sockets.

A maior parte das aplicacións que se realizan sobre unha rede TCP/IP están estructuradas seguindo un modelo cliente/servidor. Esto quere decir que unha aplicación constará de dúas partes:

  • O servidor é un programa que normalmente está en execución permanentemente (en Linux tamén reciben o nome de daemons ou demos) e que está especializado en realizar unha tarefa concreta (un servidor Web, un servidor de FTP, de correo electrónico...).
  • O cliente é un programa que executa un usuario cando o precisa, e que se pon en contacto cun servidor pra solicitarlle un servicio concreto (un navegador Web, un cliente de correo, un cliente FTP ...).

Cando falamos de TCP/IP temos dúas posibilidades a hora de comunicar dous procesos: TCP e UDP. Na comunicación TCP normalmente o cliente (por exemplo un cliente Web) solicita unha conexión co servidor, si o servidor acepta a petición de conexión será posible enviarlle información (peticións) e obter a resposta pola canle de comunicación establecida. Pola súa parte os servidores (por exemplo un servidor Web) están a espera de peticións de conexión nun porto determiñado. Cando reciben unha petición a aceptan (si están en condicións) creando unha conexión para facer a comunicación e unha nova tarefa ou thread que se encargará de xestionala, mentras que o thread ou proceso principal continúa a espera de novas peticións. O esquema xeral podería ser o seguinte

Cliente Servidor
Crea o socket Crea o Socket
Fai unha solicitude de conexión Nomea-lo socket: Asocia o Socket cunha dirección de rede (IP e Porto)
Si a solicitude é aceptada envía peticións e recolle as respostas Establece unha cola pra almacenar as peticións mentras son aceptadas
pecha o socket, finalizando a conexión Mentras (sempre)
a) acepta a conexión, creando un novo socket pra comunicarse
b) crea un novo proceso ou lanza un thread que se encargará de xestionar a comunicación, pechando logo o socket de comunicación e finalizando.

Na comunicación UDP, en cambio, non é necesario solicitar (cliente) nin aceptar (servidor) conexións, se non que se envía a información directamente ó socket co que desexamos comunicarnos mediante a función sendto.

En xeral, pra realizar calquera comunicación entre procesos mediante sockets cada proceso ten que ter un socket ligado a unha dirección (IP+porto). Un proceso poderá enviar información a outro a través do seu socket si coñece a dirección do socket remoto. A comunicación sempre se produce entre dous sockets. Cando falamos de TCP/IP temos dúas familias de protocolos (PF_UNIX pra comunicar procesos dentro da mesma máquina e PF_INET pra comunicar procesos de máquinas distintas), e dúas familias de direccións que corresponden cos protocolos (AF_UNIX na que unha dirección é un nome de ficheiro e AF_INET na que as direccións consisten en unha dirección IP e un número de porto. Asociado cos protocolos utilizados temos tamén dous estilos de comunicación: SOCK_STREAM orientado a conexión (TCP) e SOCK_DGRAM sin conexións (UDP), baseado en datagramas.

As principais funcións pra realizar comunicación mediante sockets son as seguintes :

Creación da Socket de Comunicación
#include <sys/types.h>
#include <sys/socket.h>
 
int socket(int dominio, int tipo, int protocolo);

Crea un punto de comunicación e devolve un descritor que o identifica. No caso de erro devolve -1.

  • O parámetro dominio especifica o dominio en que se fará a comunicación recoñecendo actualmente o linux moitas familias de protocolos distintas como PF_NETLINK, ou PF_IPX (de novell).

As familias coas que traballaremos serán PF_UNIX (entre procesos que residen na mesma máquina) e PF_INET (Internet v4, pra comunicar procesos que poden estar en máquinas distintas).

  • tipo indica como se vai a realizar a comunicación entre as posibilidades ofrecidas pola familia de protocolos empregada, sendo de particular interés SOCK_STREAM que ofrece unha comunicación orientada a conexión (TCP), ou SOCK_DGRAM que ofrece unha comunicación sen conexión non confiable basada en datagramas (UDP).
  • En canto a protocolo é o protocolo empregado dentro da familia de protocolos escollida, normalmente a familia de protocolo e o tipo de comunicación teñen un único protocolo posible, pode poñerse 0 para utilizar o protocolo por defecto.
Asociación da Socket cunha Dirección de Rede
#include <sys/types.h>
#include <sys/socket.h>
 
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

Asigna unha dirección ó socket sockfd creado coa chamada á fución socket. O formato da dirección varía dependendo da familia de protocolos e do tipo de socket empregado, sendo my_addr dun tipo xenérico que se adapta a todas as direccións, para cada formato de direccións se emprega no programa unha estructura de datos específica, no caso de TCP/IP:

  • sockaddr_in pra familia PF_INET
  • sockaddr_un pra familia PF_UNIX
struct sockaddr_in {
  sa_family_t    sin_family; /* address family: AF_INET */
  u_int16_t      sin_port;   /* port in network byte order */
  struct in_addr  sin_addr;  /* internet address */
};
 
/* Internet address. */
struct in_addr {
  u_int32_t s_addr;     /* address in network byte order */
};
 
#define UNIX_PATH_MAX    108
 
struct sockaddr_un {
  sa_family_t  sun_family;              /* AF_UNIX */
  char         sun_path[UNIX_PATH_MAX]; /* pathname */
};

addrlen é a lonxitude da dirección que lle pasamos a bind. Devolve 0 si funciona correctamente.

Facer Petición de Conexión
#include <sys/types.h>
#include <sys/socket.h>
 
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

Realiza unha petición de conexión mendiate o socket sockfd ó servidor indicado en serv_addr. addrlen contén o tamaño da dirección especificada en serv_addr. Devolve 0 si se acepta a conexión ou -1 si se produce un erro.

Establecer Cola de Espera
#include <sys/types.h>
#include <sys/socket.h>
 
int listen(int sockfd, int backlog);

Crea unha cola de tamaño backlog donde se van almacenando as peticións de conexións feitas sobre o socket sockfd mentras a súa petición non poda ser atendida. Devolve 0 en caso de conseguir crear a cola, si se produce algún erro -1.

Aceptar Petición de Conexión
#include <sys/types.h>
#include <sys/socket.h>
 
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

Acepta unha petición de conexión sobre o socket sockfd devolvendo un novo socket sobre o que se vai a realizar a comunicación ou -1 si se produxo algún erro. Si se especifica unha dirección en addr e o tamaño dispoñible en esa dirección en addrlen se almacenará dentro de addr a dirección do cliente que solicita a conexión.

Envío e Recepción de Datos
#include <sys/types.h>
#include <sys/socket.h>
 
int sendto(int s, const void *bf, size_t ln, int flg, 
           const struct sockaddr *to,socklen_t lnto);

Envía a información especificada na dirección bf que ten unha lonxitude ln ó socket que ten a dirección indicada en to de lonxitude lnto a través do socket s. Os flags flg conteñen opcións sobre o xeito de realizar a comunicación, por exemplo MSG_DONTWAIT fai que non se espere a poder enviar a información, se non que se devolve un erro no caso de que en ese instante a comunicación non sexa posible. En caso de funcionar ben a función devolve o número de bytes enviados, en caso de erro devolve -1.

#include <sys/types.h>
#include <sys/socket.h>
 
int recvfrom(int s, void *bf, size_t ln, int flg, 
             struct sockaddr *from, socklen_t *lnfrom);

Recibe información a través do socket s almacenándoa na dirección especificada en bf que ten un espacio de ln bytes. Opcionalmente (si se especifica unha dirección distinta de NULL) si o socket non é conectado (SOCK_DGRAM) almacenará a dirección do proceso que envía a información en from e a súa lonxitude en lnfrom. flg indica os flags que normalmente serán 0. Devolve o número de bytes recibidos ou -1 si se produxo algún erro.

#include <sys/types.h>
#include <sys/socket.h>
 
int send(int sockfd, void *buffer, size_t lon, int flags);

Utilízase con sockets conectados (SOCK_STREAM) e envía a información almacenada na dirección buffer que mide lon bytes polo socket sockfd. Os flags son os mesmos que a función sendto. Devolve o número de bytes enviados ou -1 si se produxo algún erro.

#include <sys/types.h>
#include <sys/socket.h>
 
int recv(int sockfd, void *buffer, size_t lon, int flags);

So se pode utilizar con sockets conectados (SOCK_STREAM), recibe información a través do socket s almacenándoa na dirección especificada en bf que ten un espacio de ln bytes. flg indica os flags que normalmente serán 0. Devolve o número de bytes recibidos ou -1 si se produxo algún erro.

A comunicación con sockets conectados (SOCK_STREAM) tamén pode realizarse mediante as funcións estándar de lectura e escritura de ficheiros:

#include <unistd.h>
 
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

Onde fd sería o descritor do socket aberto e conectado.

Pechar o Socket

Un Socket aberto non é mais que un descritor de ficheiro, co que se pechará coa función de peche de ficheiros de baixo nivel:

#include <unistd.h>
 
int close(int fd);

Onde fd é o socket aberto.

Obter Información dun Socket

#include <sys/types.h>
#include <sys/socket.h>
 
int getsockname(int sockfd, struct sockaddr *name, socklen_t *lonname);

Devolve a dirección que ten asignado o socket sockfd na dirección apuntada polo parámetro name e deixando a súa lonxitude en lonname. En caso de éxito devolve 0.


Como se pode observar nas funcións de sockets vistas anteriormente, as direccións das sockets veñen especificadas como de tipo struct sockaddr. Este tipo de datos é capaz de almacenar direccións de diferentes tipos de sockets (PF_UNIX, PF_INET, PF_IPX, PF_X25, PF_APPLETALK, PF_INET6 ..), polo tanto nunca o utilizaremos directamente, se non que cada familia de protocolos ten definida unha estructura pra almacenar as direccións correspondentes, sendo struct sockaddr_in pra os protocolos PF_INET e struct sockaddr_un pra os protocolos PF_UNIX.

Outras Operacións con Sockets

É posible "conectar" dúas sockets de datagramas (SOCK_DGRAM) mediante a instrucción connect. Si facemos esto, non se realiza realmente unha conexión entre os dous sockets, se non que o que ocurre é que se establece un destiño por defecto de xeito que poderemos empregar recv, send, read e write na comunicación. Tamén é posible convertir o descriptor de tipo int a FILE * pra poder utilizar as funcións de ficheiros de alto nivel sobre a socket mediante a función:

#include <stdio.h>
 
FILE *fdopen(int fildes, const char *type);

O primeiro argumento (fildes) é o descritor que queremos asociar co FILE * que devolve a función, e o segundo é o modo de apertura habitual na función fopen ("rb", "wb", "rwb", ... etc). Devolve NULL en caso de erro. Cando utilicemos esta función con sockets é convinte que eliminemos o buffer do FILE * mediante a función:

#include <stdio.h>
 
void setbuf(FILE *fp, char *type);

Pasándolle NULL como segundo argumento. Outra posibilidade que podemos utilizar cos sockets é manipular o seu comportamento mediante a función:

#include <unistd.h>
#include <fcntl.h>
 
void fcntl(int fd, int cmd, long arg);

Por exemplo a seguinte secuencia de instruccións sobre o socket sk, o convirte en non bloqueante :

int fflags;
 
fflags=fcntl(sk,F_GETFL,0);
fnctl(sk, F_SETFL, fflags | FNDELAY);

Despois de esto as funcións recv, read, write, accept, connect e send devolverán -1 nos casos en que normalmente se quedarían esperando, e poñen errno ó valor EWOULDBLOCK.

Exemplos

Modelo de Cliente

Os clientes terían a forma seguinte:

#include <stdlib.h>
#include <sys/types.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
 
void main(int argc,char *arg[])
{
   int sk;
   struct hostent *he;
   struct sockaddr_in addr;
 
   // Comprobo que o número de parámetros pasados na liña de comandos é correcto
   if (argc!=3) {
      printf("USO: cliente máquina porto\n");
      exit(0);
   }
   // Creo o Socket, Protocolo Internet, orientado a conexión (TCP)
   sk=socket(PF_INET,SOCK_STREAM,0);
   if (sk!=-1) {
      // Obteño a IP do servidor a partir do nome suministrado na liña de comandos
      he=gethostbyname(arg[1]);
 
      if (he!=NULL) {
        // Poño na variable addr a dirección do servido co que quero conectar
        //   -- Familia de direccións de Internet
        //   -- Porto indicado na liña de comandos
        //   -- IP obtida con gethostbyname
        // Pasamos o porto a número (na liña de comandos ven como cadea ASCII)
        addr.sin_family=AF_INET;
        addr.sin_port=htons(atoi(arg[2]));
        memcpy(&addr.sin_addr,he->h_addr,sizeof(addr.sin_addr));
 
        // Realizo a conexión
        if (connect(sk,(struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=-1)
        {
           // AQUI FARíAMOS O TRABALLO :
           //      * Realizar peticións ó Servidor
           //      * Leer as respostas do Servidor
        }
        else printf("Erro conectando co servidor!!!\n");
      }
      else printf("Host descoñecido!!!\n");
 
      // Pecho o Socket, e polo tanto a comunicación co servidor.
      close(sk);
   }
   else printf("Erro creando socket!!!\n");
}
Modelo de Servidor con Procesos (fork)

Os servidores, con procesos, terían a seguinte forma:

#include <stdlib.h>
#include <sys/types.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
// Esta función atenderá a conexión co cliente.
// Neste caso se limita a visualizar no terminal
// donde se está a executar o servidor a IP e o Porto
// da máquina cliente que se conectou, pero noutros
// casos recibiría a petición do cliente, faría as operacións
// necesarias pra satisfacer a petición e enviaría a resposta
// ó cliente.
 
void atende(int skd,struct sockaddr_in *addr)
{
   printf("Aceptada conexión de %s no porto %d\n",inet_ntoa(addr->sin_addr),
                                                  ntohs(addr->sin_port));
   close(skd);
   exit(0);
}
 
//  ======    M A I N    ======
void main(int argc,char *arg[])
{
   int sk;
   struct hostent *he;
   struct sockaddr_in addr;
   int skd;
   int kk;
 
   // Comprobo que o número de parámetros é correcto
   if (argc!=2) {
      printf("USO: servidor porto\n");
      exit(0);
   }
 
   // Creo o socket pra aceptar conexións.
   // Protocolo Internet e TCP.
   sk=socket(PF_INET,SOCK_STREAM,0);
   if (sk!=-1) {
      // Asigno dirección ó socket con bind para eso teño
      // que poñer a dirección nunha variable sockaddr_in
      // no caso das direccións Internet. Nas direccións Unix
      // sería nunha variables sockaddr_un.
      // O porto o collo da liña de comandos, e como é unha cadea
      // de caracteres a teño que pasar a número, e logo a número
      // en formato de rede
      // A IP vai ser calquera das IP que teña a máquina donde se
      // vai a executar o servidor (INADDR_ANY).
      addr.sin_family=AF_INET;
      addr.sin_port=htons(atoi(arg[1]));
      addr.sin_addr.s_addr=htonl(INADDR_ANY);
      if (bind(sk,(struct sockaddr *)&addr,sizeof(addr))!=-1) {
         // Establezo o tamaño da cola pra os procesos que están
         // esperando a que a súa petición de conexión sexa aceptada
         // a 5
         if (listen(sk,5)!=-1) {
            // Neste bucle infinito vanse aceptando (accept) conexións e
            // creando fillos pra atendelas. O pai despois de crear
            // o fillo que vai a atender a conexión continuará aceptando
            // novas conexións. Cando se acepta unha conexión obtense a información
            // do cliente e se obtén un socket novo pra realizar a comunicación.
            while(1) {
               kk=sizeof(addr);
               skd=accept(sk,(struct sockaddr *)&addr,&kk);
               if (skd!=-1) {
                  switch(fork()) {
                     case 0: // FILLO: Pecho o socket que non preciso
                             // e atendo a conexión. Pásolle o proceso
                             // o socket de comunicación e a información
                             // do cliente.
                             close(sk);
                             atende(skd,&addr);
                             break;
                     case -1:  printf("Non se creou o proceso\n!!!");
                     default:  // PAI: Pecho o socket que non preciso
                               // e continúo aceptando conexións.
                               close(skd);
                               break;
                  }
               }
               else printf("Conexión rexeitada!!!\n");
            }
         }
         else printf("Erro creando cola de peticións!!!\n");
      }
      else printf("Erro nomeando socket!!!\n");
      // Pecho o socket de aceptar conexións
      close(sk);
   }
   else printf("Erro creando socket!!!\n");
}
Modelo de Servidor con Threads
#include <stdlib.h>
#include <sys/types.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
 
// Tipo de datos pra almacenar o parámetro que se lle pasará ó thread.
// Pasarémoslle: A dirección do Cliente, e o socket de diálogo.
 
typedef struct tagSOCKINFO {
   int skd;
   struct sockaddr_in addr;
} SOCKINFO;
 
// Esta función atenderá a conexión co cliente.
// Neste caso se limita a visualizar no terminal
// donde se está a executar o servidor a IP e o Porto
// da máquina cliente que se conectou, pero noutros
// casos recibiría a petición do cliente, faría as operacións
// necesarias pra satisfacer a petición e enviaría a resposta
// ó cliente.
// As funcións que se lancen como thread deben ter o seguinte prototipo:
// void *funcion (void *parametro)
 
void *atende(void *arg)
{
   SOCKINFO datos;
 
   //  Fago unha copia do parámetro, xa que o utilizan outros Threads
   //  Logo poño arg->skd a 0 pra indicar que a copia xa está feita
   memcpy(&datos,arg,sizeof(SOCKINFO));
   arg->skd=0;
   printf("Aceptada conexión de %s no porto %d\n",inet_ntoa(datos.addr.sin_addr),
                                                  ntohs(datos.addr.sin_port));
   close(datos.skd);
   return NULL;
}
 
//  ======    M A I N    ======
void main(int argc,char *arg[])
{
   int sk;
   pthread_t thr;
   struct hostent *he;
   SOCKINFO datos;
   int kk;
 
   // Comprobo que o número de parámetros é correcto
   if (argc!=2) {
      printf("USO: servidor porto\n");
      exit(0);
   }
 
   // Creo o socket pra aceptar conexións.
   // Protocolo Internet e TCP.
   sk=socket(PF_INET,SOCK_STREAM,0);
   if (sk!=-1) {
      // Asigno dirección ó socket con bind para eso teño
      // que poñer a dirección nunha variable sockaddr_in
      // no caso das direccións Internet. Nas direccións Unix
      // sería nunha variables sockaddr_un.
      // O porto o collo da liña de comandos, e como é unha cadea
      // de caracteres a teño que pasar a número, e logo a número
      // en formato de rede
      // A IP vai ser calquera das IP que teña a máquina donde se
      // vai a executar o servidor (INADDR_ANY).
      datos.addr.sin_family=AF_INET;
      datos.addr.sin_port=htons(atoi(arg[1]));
      datos.addr.sin_addr.s_addr=htonl(INADDR_ANY);
      if (bind(sk,(struct sockaddr *)&(datos.addr),sizeof(datos.addr))!=-1) {
         // Establezo o tamaño da cola pra os procesos que están
         // esperando a que a súa petición de conexión sexa aceptada
         // a 5
         if (listen(sk,5)!=-1) {
            // Neste bucle infinito vanse aceptando (accept) conexións e
            // creando fíos de execución pra atendelas. O pai despois de crear
            // o fío que vai a atender a conexión continuará aceptando
            // conexións. Cando se acepta unha conexión obtense a información
            // do cliente e se obtén un socket novo pra realizar a comunicación.
            while(1) {
               kk=sizeof(addr);
               datos.skd=accept(sk,(struct sockaddr *)&addr,&kk);
               if (datos.skd!=-1) {
                  // Lanzo o thread pra atender a conexión. O pai continúa aceptando conexións
                  if (pthread_create(&thr,0,atende,&datos)) printf("Fallou a creación do Thread\n");
                  // Espero a que o thread faga a copia do parámetro
                  while(datos.skd!=0);
               }
               else printf("Conexión rexeitada!!!\n");
            }
         }
         else printf("Erro creando cola de peticións!!!\n");
      }
      else printf("Erro nomeando socket!!!\n");
      // Pecho o socket de aceptar conexións
      close(sk);
   }
   else printf("Erro creando socket!!!\n");
}

Comunicación No Dominio Unix.

A comunicación no dominio UNIX so é posible entre procesos que se están a executar na mesma máquina, sendo polo tanto as direccións diferentes ás do dominio INET. Pra almacenar direccións de procesos no dominio UNIX emprégase a seguinte estructura:

struct sockaddr_un {
  short sun_family;
  char sun_path[108];
}

O campo sun_family terá sempre o valor AF_UNIX, que indica que estamos a tratar cunha dirección da familia UNIX, en canto a sun_path é o nome do socket co que estamos a tratar. Este nome ten que ser un nome de ficheiro accesible por todos os procesos que se estén a comunicar.

A comunicación no dominio UNIX ten as seguintes posibilidades:

Comunicación con Pares de Sockets

A comunicación con pares de sockets é similar ás tuberías (pipes), pero permiten a comunicación en ambos sentidos. Únicamente permiten comunicar procesos dentro da mesma máquina que teñan un ancestro común (pai e fillo ou irmáns). Os pares de sockets créanse mediante a seguinte función:

#include <sys/types.h>
#include <sys/socket.h>
 
int socketpair(int domain, int type, int protocol, int sv[2]);

sv é unha parella de descritores que almacenarán os descritores de dous sockets conectados entre sí, de xeito que se pode escribir (enviar información o outro proceso) e ler (recibir información). Como se traballa normalmete con sockets conectados, o type debería ser SOCK_STREAM e o dominio PF_UNIX, xa que comunicamos procesos dentro da mesma máquina. O valor devolto é 0 si a función se leva a cabo correctamente, e se non -1.

Comunicación UDP no Dominio UNIX.

A comunicación UDP non é orientada a conexión polo tanto non se establece conexión co programa remoto, se non que se lle envía información directamente mediante a chamada á función sendto. Para que un programa poda actuar como receptor da información o seu socket ten que ter unha dirección de tipo sockaddr_un asignada coa función bind. Cando os procesos finalizan a comunicación será necesario que pechen os sockets con close e o servidor debe eliminar o ficheiro creado no momento de asignar a dirección o socket chamado á función:

#include <unistd.h>
 
int unlink(const char *pathname);

Comunicación TCP no Dominio UNIX.

A comunicación TCP necesita establecer unha conexión entre o cliente e o servidor antes de poder efectuar a comunicación. Pra facer esto, o servidor ten que asignarlle unha dirección AF_UNIX ó socket (é decir que a dirección é de tipo sockaddr_un) mediante bind, establecer unha cola de espera (listen) e aceptar as conexións. O cliente únicamente ten que establecer a conexión (connect) e enviar ou recibir os datos (send, write, recv, recvfrom ou read). Cando o servidor finalice debe borrar o ficheiro asociado co socket mediante unlink.

Comunicación no Dominio Internet.

A comunicación no dominio INET é posible tanto entre procesos que se están a executar na mesma máquina como entre procesos que están executándose en máquinas distintas, sendo polo tanto as direccións diferentes ás do dominio UNIX. Pra almacenar direccións de procesos no dominio INET emprégase a seguinte estructura:

struct sockaddr_in {
  short sin_family;
  unsigned short sin_port;
  struct in_addr sin_addr;
}

O campo sin_family terá sempre o valor AF_INET, que indica que estamos a tratar cunha dirección da familia INET; sin_port almacenará o número de porto do proceso en formato de rede, mentras que a struct in_addr almacenará a dirección IP da máquina na que reside o proceso en formato de rede .

A comunicación no dominio INTERNET ten as seguintes posibilidades:

Comunicación UDP no Dominio Internet

A comunicación UDP non é orientada a conexión polo tanto non se establece conexión co programa remoto, se non que se lle envía información directamente mediante a chamada á función sendto. Para que un programa poda actuar como receptor da información o seu socket ten que ter unha dirección de tipo sockaddr_in asignada coa función bind. Cando os procesos finalizan a comunicación será necesario que pechen os sockets con close .

Comunicación TCP no Dominio Internet

A comunicación TCP necesita establecer unha conexión entre o cliente e o servidor antes de poder efectuar a comunicación. Pra facer esto, o servidor ten que asignarlle unha dirección AF_INET ó socket (é decir que a dirección é de tipo sockaddr_in) mediante bind, establecer unha cola de espera (listen) e aceptar as conexións. O cliente únicamente ten que establecer a conexión (connect) e enviar ou recibir os datos (send, write, recv, recvfrom ou read).

Comunicación Multidestiño (Multicast)

A comunicación multidestiño permite que un datagrama enviado por un emisor chegue a tódolos compoñentes dun grupo multidestiño creado con anterioridade. A creación de grupos multidestiño e a pertenencia ós mesmos é dinámica, pudendo cada compoñente si unirse ou deixar o grupo. So é posible baixo UDP.

A transmisión multidestiño pode simularse mediante varias conexións punto a punto, pero si a rede e de tipo Ethernet na que o medio está baseado en difusión de tramas é moito máis eficiente aproveitar que a información emitida por unha estación pode ser recibida por todas as demás. Deste xeito, si queremos enviar a mesma información dende unha estación a outras cinco utilizando comunicación punto a punto será necesario enviar cada trama cinco veces, mentras que si unimos os equipos a un grupo multicast únicamente se terá que enviar a información unha vez.

Si a rede está constituída por varias subredes interconectadas será preciso o uso de routers multicast para que os datagramas multidestiño pasen dunha subrede á outra. Os grupos multidestiño veñen determiñados por unha dirección IP multicast, que é unha das comprendidas entre 224.0.0.0 e 239.255.255.255. As direccións comprendidas entre 224.0.0.0 e 224.0.0.255 son tratadas de xeito especial polos routers multicast impedíndolles o paso, utilizándose normalmente pra uso de protocolos de manteñemento e encamiñamento.

Un parámetro importante cando enviamos un datagrama multidestiño e o seu TTL (tempo de vida), que define o alcance do datagrama indicando cantos enlaces entre redes poderá atravesar antes de ser eliminado. Típicamente os valores de 1 indican alcance dentro da rede local, menores de 32 para comunicación dentro de unha organización, entre 32 e 63 permiten a saída a unha "rexión", entre 65 e 127 teñen alcance "continental" e os que están por enriba de 128 alcance "mundial".

Para enviar un datagrama multidestiño é necesario un socket AF_INET do tipo SOCK_DGRAM, sendo suficiente que a dirección de envío sexa multicast pra conseguir que a reciban todos os membros do grupo. O TTL por omisión é 1, sendo posible cambialo manipulando os atributos do socket coa función setsockopt. Un socket pode pertencer a máis de un grupo multidestiño, e para engadilo ó grupo ou retiralo do mesmo se utilizan tamén os atributos do socket mediante a función setsockopt.

As funcións e operacións básicas a realizar cando queremos establecer unha comunicación multidestiño son as seguintes:

#include <sys/types.h>
#include <sys/socket.h>
#include <net/netinet.h>
 
struct ip_mreqn {
  // Dirección IP do grupo multidestiño.
  struct in_addr imr_multiaddr;
  // Dirección IP da interfaz afectada.
  struct in_addr imr_address;
  // Indice da interfaz de rede. 0 indica todas.
  int imr_ifindex;
};
 
int setsockopt(int s, int nivel, int nomopc, const  void  *valopc,  socklen_t lonopc);

setsockopt permite manipular os atributos asociados con un socket s, devolvendo 0 en caso de éxito ou -1 si se produciu un erro. O nivel pode ser SOL_SOCKET pra manipular opcións a nivel de socket. Cando estamos manipulando sockets baixo o protocolo IP (como é o caso) utilizaremos SOL_IP ou IPPROTO_IP.

nomopc é a opción que imos a manipular (a lista completa de opcións pode obterse escribindo man 7 ip), no que respecta á comunicación multidestiño a nós nos interesan:

  • IP_MULTICAST_TTL que establece o tempo de vida dos paquetes multidestiño que saian polo socket ó valor especificado en valopc.
  • IP_ADD_MEMBERSHIP que permite engadir o socket a un grupo multidestiño pasado en valopc mediante unha estructura ip_mreqn e IP_DROP_MEMBERSHIP que permite abandoar o grupo multidestiño especificado en valopc mediante unha estructura ip_mreqn.

A estructura ip_mreqn sirve pra almacenar os datos multidestiño do grupo ó que nos queremos engadir ou deixar. imr_multiaddr será a IP do grupo ó que nos queremos unir ou abandoar, imr_address é a IP da interface que uniremos ó grupo, si especificamos INADDR_ANY o sistema se encarga de escoller o máis axeitado, e imr_ifindex é o índice da interfaz de rede que se vai a unir ou deixar o grupo, un valor de 0 indica calquera interfaz.

A continuación se indica cun exemplo como realizar estas operacións (supoñemos que o socket xa está creado e cunha dirección asignada mediante bind...):

Establecendo o tempo de vida dos Datagramas
// Tempo de vida que queremos pra os datagramas que enviaremos polo socket
int ttl=7;
 
setsockopt(socket,SOL_IP, IP_MULTICAST_TTL,(char *)&ttl,sizeof(ttl));
Engadirse a un grupo multidestiño

Por exemplo ó grupo 225.100.100.100:

// Definición da variable pra almacenar os datos do grupo multicast
struct ip_mreqn  grupo;
 
// Poñemos os datos do grupo multicast
// Poño a IP multidestiño
grupo.imr_multiaddr.s_addr=inet_addr("225.100.100.100" );
// O sistema elixe o interfaz de rede axeitado
grupo.imr_address.s_addr=htonl(INADDR_ANY);
// Calquera interfaz
grupo.imr_ifindex=0;
 
setsockopt(socket,SOL_IP,IP_ADD_MEMBERSHIP,(char *)&grupo,sizeof(grupo));
Abandoar un grupo multidestiño
// Definición da variable pra almacenar os datos do grupo multicast
struct ip_mreqn  grupo;
 
// Poñemos os datos do grupo multicast
// Poño a IP multidestiño
grupo.imr_multiaddr.s_addr=inet_addr("225.100.100.100" );
// O sistema elixe o interfaz de rede axeitado
grupo.imr_address.s_addr=htonl(INADDR_ANY);
// Calquera interfaz
grupo.imr_ifindex=0;
 
setsockopt(socket,SOL_IP,IP_DROP_MEMBERSHIP,(char *)&grupo,sizeof(grupo));

Exemplos

  • Resolución Directa: de Nome de Host a IP
  • Resolución Inversa: de IP a Nome de Host
  • Comunicación con Pares de Sockets
  • Comunicación UDP no Dominio UNIX
  • Comunicación TCP no Dominio UNIX
  • Comunicación UDP no Dominio Internet
  • Comunicación Multidestiño