Introducción |
Tipos de Internet Sockets |
Estructuras Especiales |
Llamadas fundamentales |
DNS |
Programació en ambiente Cliente Servidor |
Algunas funciones útiles |
Rutinas útiles |
Principios de Encapsulamiento |
Ahora bien, qué es un file descriptor. Como todos sabemos, en Unix todo es tratado como si fuera un archivo: la entrada estándar (stdin), la salida estándar (stdout), etc., y el uso de sockets no es la excepción. Por ejemplo, para trabajar con un archivo en C, nosotros tenemos que crear el archivo (fopen) leer o escribir (fread, fwrite) y por último, una vez que hemos terminado de trabajar con el archivo, cerrarlo (fclose). Con la programación de sockets tenemos que hacer algo parecido. Por lo tanto, un file descriptor no es más que un número entero asociado con un archivo abierto. Así pues, cuando nosotros queremos comunicarnos con algún otro programa a través de Internet, lo haremos utilizando un file descriptor.
En lo que a la programación de sockets se refiere, para que podamos obtener un file descriptor, tenemos que hacer la llamada a la función socket(), la cual nos regresará un socket descriptor, y podremos comunicarnos a través de él por medio de la llamada a las funciones send() y recv().
Existen varios tipos de internet sockets. En este curso sólamente nos vamos a ocupar de dos de ellos: "Stream Sockets" y "Datagram Sockets" ", los cuales se definen como SOCK_STREAM y SOCK_DGRAM, respectivamente.
Los Datagram Sockets generalmente son llamados como connectionless sockets, esto se debe a que al utilizar sockets de este tipo no es necesario mantener abierta una conexión, como se tiene que hacer al utilizar los Stream Sockets. Al utilizar este tipo se sockets, nosotros sólamente construimos un paquete con la información que queremos mandar, le agregamos una dirección a la cual queremos mandar dicha información y lo mandamos, no necesitamos, como ya se dijo, mantener abierta una conexión. Ahora bien, se podrá pensar, qué es lo que pasa si un paquete que utiliza este tipo de sockets se pierde. Lo que utilizan este tipo de sockets es un protocolo llamado UDP, el cual funciona como sigue: por cada paquete que recibe, el destinatario de dicho paquete manda un mensaje de recibido (acknowledgment - ACK) al emisor de dicho paquete. Si, digamos, en 10 segundos el programa que mandó el paquete de información recibe ningún mensaje de ACK, éste retransmitirá el paquete hasta que reciba el mensaje de confirmación (ACK). Este proceso de confirmación es muy importante cuando se implementan aplicaciones que utilicen SOCK_DGRAM.
La siguiente estructura guarda la información general de muchos tipos de sockets y está definida en <sys/socket.h>:
struct sockaddr {
unsigned short sa_family; //address family AF_XXX
char sa_data[14]; //14 bytes or protocol address
}
sa_family puede tener muchos valores, para nuestro caso, utilizaremos siempre AF_INET, que es la familia para los protocolos de internet. sa_data contiene la dirección destino junto con el puerto al que se conectará el programa para iniciar el intercambio de información.
Para poder interactuar con la estructura sockaddr, se definieron en <netinet/in.h> dos estructuras paralelas: struct sockaddr_in ("in" de internet) y struct in_addr:
struct sockaddr_in {
short int sin_family; // address family
unsigned short int sin_port; // Puerto
struct in_addr sin_addr; // Internet address
unsigned char sin_zero[8]; //Del mismo tamaño que struct sockaddr
}
struct in_addr {
unsigned long s_addr; // 4 bytes
}
La estructura anterior hace más fácil referirnos a los elementos de struct sockaddr. Nótese que sin_zero debe ser inicializada a ceros con la función memset(). También hay que hacer notar que un apuntador a struct sockaddr_in puede ser convertido (por medio de un cast) a un apuntador a struct sockaddr y viceversa. sin_family corresponde a sa_family de struct sockaddr y, por lo tanto, debe inicializarse en "AF_INET". Finalmente, sin_port y sin_addr deben estar en Network Byte Order.
htons() - "Host to Network Short"
Convierte una dirección IP en notación xxx.yyy.zzz.aaa a un número de tipo long.
Ejemplo:
struct sockaddr_in ina;
ina.sin_addr.s_addr = inet_addr("192.168.17.19");La llamada anterior nos regresa el número en Network Byte Order y en caso de que haya algún error, nos regresará un -1.
Convierte una dirección en formato de un número tipo long a su correspondiente dirección en el formato xxx.yyy.zzz.aaa
Ejemplo:
printf("%s", inet_ntoa(ina.sin_addr);
Llamada socket
Para poder mandar y recibir datos a través de una red, lo primero que debemos hacer es llamar a la función socket, especificando el tipo de protocolo de comunicación deseado (Internet TCP, Internet UDP, XNS SPP, etc).
#include <sys/types.h>
#include <sys/socket.h>
int socket (int family, int type, int protocol);
debe ser alguno de los siguientes valores:
- family
AF_UNIX Unix internal protocols AF_INET Internet protocols AF_NS Xerox NS protocols AF_IMPLINK IMP link layer AF significa "address family". Existe otros términos que también se utilizan, empezando por el prefijo PF_, que significa "protocol family": PF_UNIX, PF_INET, PF_NS y PF_IMPLINK. Cualquiera de los dos términos pueden ser utilizados, ya que son equivalentes.
debe tener alguno de los siguientes valores:
- type
SOCK_STREAM stream socket SOCK_DGRAM datagram socket SOCK_RAW raw socket SOCK_SEQPACKET sequenced packet socket SOCK_RDM reliably delivered message socket El parámetro protocol puede ser fijado a "0", para dejar que la llamada a la función socket escoja el protocolo adecuado, dependiendo del tipo definido.
Como se dijo anteriormente, en nuestro caso, utilizaremos siempre el parámetro family como AF_INET y el parámetro type como SOCK_STREAM o SOCK_DGRAM según sea el caso.Llamada bind
Una vez que hemos creado el socket, tenemos que asociarlo con algún puerto en la máquina en donde estamos corriendo el programa. (Esto se hace sobre todo cuando vamos a escuchar a conexiones entrantes en dicho puerto en nuestra computadora). El número de puerto es utilizado por el kernel para asociar un paquete de información entrante con un socket descriptor de un proceso determinado.
#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd, struct sockaddr *my_addr, int addrlen);
es el socket descriptor regresado por la llamada a la función
- sockfd
socket().my_addr es un apuntador a struct sockaddr que contiene información acerca de la dirección de la computadora en donde se está corriendo el programa, el puerto y la dirección IP. addrlen puede ser fijada como sizeof(struct sockaddr).
Ejemplo:
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MYPORT 7890
int main(void)
{
int sockfd;
struct sockaddr_in my_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = inet_addr("192.168.17.19");
memset(&(my_addr.sin_zero), '\0', 8);
bind(sockfd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr));
.
.
.
}
Con respecto al ejemplo anterior, existen dos aspectos que pueden servir para simplificar el proceso:
my_addr.sin_port = 0; /* selecciona un puerto libre
aleatoriamente */
my_addr.sin_addr.s_addr = INADDR_ANY /* utiliza nuestra
dirección IP */si nosotros inicializamos my_addr.sin_port a cero, le estamos diciendo a la función bind que escoja el puerto por nosotros; de la misma manera, si inicializamos my_addr.sin_addr.s_addr a INADDR_ANY, automáticamente utilizaremos la dirección IP de la computadora en la cual estamos corriendo el programa.
La llamada a la función bind nos regresará un -1 en caso de que haya ocurrido algún error.
Es importante hacer una aclaración con respecto a los puertos en una computadora. Los puertos entre 1 y 1023 están reservados para procesos en particular de una computadora (80 - http, 79 - finger, 25 - SMTP, etc.). Cualquier puerto entre 1024 y 65535 está libre para ser usado, siempre y cuando no haya sido ocupado ya por otro proceso.En ocasiones, al ejecutar la llamada a la función bind podemos obtener un mensaje de "Address already in use". Esto sucede porque en el kernel del sistema, dicho puerto está todavía registrado como asociado a algún proceso. Nosotros podemos esperarnos algún tiempo para que dicho registro desaparezca o forzar al sistema a utilizar dicho puerto con el siguiente código:
int yes = 1;
//char yes = '1'; //esta linea se utilizara en solaris
if(setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,
&yes,sizeof(int)) == -1) {
perror("setsockotp");
exit(1);
}Llamada connect
Un proceso cliente se conecta a un socket descriptor después de haber hecho la llamada a la función socket para establecer una conexión con un servidor.
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);
es el socket descriptor regresado por la llamada a la función
- sockfd
socket().El segundo y tercer agrumento son iguales a los de la llamada a la función bind().
Ejemplo:
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define DEST_PORT 7890
#define DEST_IP "192.168.17.2"
int main(void)
{
int sockfd;
struct sockaddr_in dest_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(DEST_PORT);
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
memset(&(dest_addr.sin_zero), '\0', 8);
connect(sockfd, (struct sockaddr *)&dest_addr,
sizeof(struct sockaddr));
.
.
.
}Llamada listen
La llamada a esta función se utiliza cuando nosotros queremos desarrollar un programa que va a correr en un servidor y que va a esperar a conexiones entrantes para manejarlas de cierta manera. Para este caso, primero vamos a escuchar (listen) y después aceptaremos las conexiones entrantes (accept).
int listen (int sockfd, int backlog);
es el socket file descriptor regresado por la llamada a la función socket().
- sockfd
backlog es el número de conexiones permitidas para nuestro programa. Este argumento generalmente es inicializado a 5.
Si nosotros quisiéramos hacer un programa que escuche conexiones entrantes, la secuencia en la que vamos a hacer las llamadas a las funciones es la siguiente:
- socket();
- bind();
- listen();
- accept();
Llamada accept
Una vez que un programa ejecuta la llamada listen, una conexión de algún cliente es esperada y una vez que ésta llega, se ejecutará la llamada a la función accept.
#include <sys/types.h>
#include <sys/socket.h>
int accept (int sockfd, struct sockaddr *peer, int *addrlen);
es el socket file descriptor que siempre se ha estado utilizando.
- sockfd
peer es un apuntador a struct sockaddr addrlen es un número entero que se debe inicializar a sizeof(struct sockaddr_in) antes de que su dirección sea pasada a la función accept().
Esta función nos regresa un nuevo socket descriptor, que es el que se utilizará para enviar y recibir información entre los programas, o un -1 en el caso de un error.
Ejemplo:
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MYPORT 7890
#define BACKLOG 5
int main(void)
{
int sockfd, newfd;
struct sockaddr_in my_addr;
struct sockaddr_in client_addr;
int sin_size;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = inet_addr("192.168.17.19");
memset(&(my_addr.sin_zero), '\0', 8);
bind(sockfd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr));
listen(sockfd, BACKLOG);
sin_size = sizeof(struct sockaddr_in);
new_fd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size);
.
.
.
}Cabe aclarar que la variable new_fd se utilizará para enviar información a las conexiones subsecuentes que lleguen a nuestro programa. En el caso de que nuestro programa sólamente quiera recibir una conexión, se deberá cerrar
(close()) el new_fd para prevenir más conexiones entrantes.Llamadas send y recv
Estas dos funciones se utilizan para mandar y recibir información una vez que se han conectado dos procesos, siempre y cuando se estén utilizando Stream Sockets.
La llamada a la función send es la siguiente:
#include <sys/types.h>
#include <sys/socket.h>
int send (int sockfd, char *buff, int nbytes, int flags);
es el socket descriptor al que se quiere mandar información. Puede ser el socket descriptor regresado por la llamada a la función socket() o el que nos regresó la función accept()
- sockfd
buff es un apuntador a la información que se quiere mandarnbytes es la longitud de la información que se va a enviar. Para nuestro caso, podemos fijar el valor de flags a 0
Ejemplo:
char *msg = "Este es el mensaje que vamos a enviar";
int len, bytes_sent;
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
.Esta función nos regresa el número de bytes que fueron enviados satisfactoriamente y -1 si ocurrió algún error. Cabe aclarar que no necesariamente siempre se va a enviar toda la información que nosotros queremos. Por lo tanto, cuando el valor que nos regresa la función send es menor al valor de len, es responsabilidad del programador enviar el resto de la información en algún momento.
La llamada a la función recv es la siguiente:
#include <sys/types.h>
#include <sys/socket.h>
int recv (int sockfd, char *buff, int nbytes, int flags);
es el socket descriptor del que se quiere recibir información.
- sockfd
buff es un apuntador a la variable en la que se va a almacenar la información que se va a leer. nbytes es la máxima longitud del buffer de lectura. Para nuestro caso, podemos fijar el valor de flags a 0.
Esta función nos regresa el número de bytes de la información leída, o un -1 en caso de un error. Cabe aclarar que en el caso de que la función nos regrese un 0, lo que esto significa es que la conexión establecida ha terminado. De esta manera sabremos si el servidor ha terminado la conexión con nosotros.Llamadas sendto y recvfrom
Estas dos funciones se utilizan para mandar y recibir información cuando estamos utilizando Datagram Sockets. Como ya se dijo anteriormente, al manejar este tipo de sockets no es necesario establecer una conexión, únicamente debemos crear el paquete con la información que queremos transmitir y mandar dicha información. Por lo tanto, un parámetro que necesita esta función es la dirección de la computadora que va a recibir la información.La llamada a la función sendto es la siguiente:
#include <sys/types.h>
#include <sys/socket.h>
int sendto (int sockfd, char *buff, int nbytes, int flags,
struct sockaddr *to, int addrlen);Los parámetros de esta función son exactamente los mismos que los de la función send, con la diferencia de los dos últimos.
- to es un apuntador a struct sockaddr (para el cual muy probablemente vamos a necesitar hacer un cast a struct sockaddr_in), el cual contiene la dirección IP y el puerto al que vamos a mandar la información.
- addrlen puede ser inicializado a sizeof(struct sockaddr).
Esta función nos regresa el número de bytes que fueron enviados satisfactoriamente, ó -1 en caso de error.
La llamada a la función recvfrom es la siguiente:
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom (int sockfd,char *buff,int nbytes,int flags,
struct sockaddr *from, int *addrlen);Los parámetros de esta función son similares a los de la anterior.
- from es un apuntador a struct sockaddr, el cual tiene la dirección IP y el puerto de la computadora que nos está mandando la información.
- addrlen es un apuntador a un dato de tipo entero que debe ser inicializado a sizeof(struct sockaddr). Cuando la función regresa, addrlen tendrá la longitud de la dirección que está guardada en from.
Esta función regresa el número de bytes recibidos, ó -1 en caso de error.
Llamadas close y shutdown
Una vez que hemos terminado de enviar y recibir información a través de la red, y que nuestro proceso ha terminado, utilizaremos la función close() para cerrar la conexión:
close(sockfd);
Una vez que hemos llamado a esta función, cualquier intento de leer o escribir utilizando nuestro socket descriptor mandará un mensaje de error.
En el caso de que queramos tener mayor control en la manera en que el socket va a terminar la conexión, podemos utilizar la función shutdown(). Esta función nos permite cerrar la comunicación en cierta dirección o en ambas:
int shutdown(int sockfd, int how);
es el socket descriptor que queremos cerrar
- sockfd
how puede tener alguno de los siguientes valores:
- 0 - Ya no se permite recibir información
- 1 - Ya no se permite mandar información
- 2 - Ya no se permite ni mandar ni recibir información (igual que un close()).
Esta función nos regresa un 0 en caso de éxito y un -1 en caso de que haya habido algún error.
Nota: la llamada a la función shutdown() no va a cerrar el file descriptor, únicamente va a cambiar su estado. Para cerrarlo definitivamente es necesario llamar a la función close().Llamada getpeername
Esta función sirve para saber quién es la computadora que se está conectando a nuestro programa. La función es la siguiente:
#include <sys/socket.h>
int getpeername (int sockfd, struct sockaddr *addr, int *addrlen);
es el file descriptor de la computadora que se está conectando a nosotros.
- sockfd
addr es un apuntador a struct sockaddr que contiene la información acerca de la computadora que se está conectando a nosotros addrlen es un apuntador a un entero que debe ser inicializado a
sizeof(struct sockaddr).La función regresa un -1 en caso de error.
Una vez que tenemos la dirección, podemos utilizar las funciones inet_ntoa() o gethostbyaddr() para imprimir mayor información.
Llamada gethostname
Esta función nos regresa el nombre de la computadora en la cual estamos corriendo el programa:
#include <sys/socket.h>
int gethostname (char *hostname, size_t size);
es un apuntador a un arreglo de caracteres que contendrá el nombre de la computadora.
- hostname
size es la longitud en bytes del arreglo hostname. La función regresa un 0 en caso de éxito y un -1 en caso de error.
- Nombre oficial de la computadora.
gethostbyname() regresa un apuntador a la estructura hostent, o NULL si ocurre un error.
En nuestro caso, el tipo de sockets utilizados para la conexión podría ser un SOCK_STREAM o un SOCK_DGRAM. Como ejemplo de este tipo de aplicaciones podemos mencionar a los programas telnet y ftp.
Blocking
En un proceso servidor, cuando se está "escuchando" y esperando a que llegue información de un cliente al programa, se podría decir que el programa se "bloquea" hasta que le llegue la información que está esperando. De esta manera, existen varias funciones que "bloquean" el programa hasta que obtenga la información que espera ( accept(), recv(), etc.). Si nosotros no queremos que esto suceda tenemos la opción de hacer una especie de "poleo", recorriendo todas las funciones que esperan recibir alguna información, una por una, hasta que alguna de ellas obtenga la información esperada.
Lo anterior se puede hacer con la función fcntl():
#include <unistd.h>
#include <fcntl.h>
.
.
sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
.
.
Este tipo de "poleo" es, generalmente, una mala idea, ya ocasiona que el programa esté "ocupado" esperando por nueva información en el socket, lo que ocasiona mucha pérdida de tiempo con el procesador. Una opción más recomendable es el uso de la función select().
Llamada select
Esta llamada puede ser algo complicada, sin embargo resulta ser muy útil para determinadas situaciones. Supongamos el siguiente escenario: tenemos un servidor en el que queremos escuchar conexiones entrantes, y también queremos seguir atendiendo las conexiones que ya están activas. La función select() nos permite monitorear muchos sockets al mismo tiempo y nos dirá qué sockets están listos para leer, cuáles están listos para escribir y en cuáles ha ocurrido algún error, si es que nos interesa.
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);Lo que hace esta función es monitorear el conjunto de file descriptors (fd_set) que especificamos para lectura, escritura o error (readfds, writefds y exceptfds, respectivamente). Si se quiere saber si se puede leer de la entrada estándar y de algún socket descriptor, lo que se tiene que hacer es agregar el file descriptor 0 (que es de la entrada estándar) y el socket al conjunto de file descriptors readfds. El parámetro numfds debe ser inicializado con el valor del más alto file descriptor más uno.
Para poder manipular el conjunto de file descriptors existen los siguientes macros:
- Inicializa el file descriptor en cero.
- FD_ZERO(fd_set *set)
FD_SET(int fd, fd_set *set) - agrega fd al conjunto de file descriptors. FD_CLR(int fd, fd_set *set) - elimina fd del conjunto de file descriptors. FD_ISSET(int fd, fd_set *set) - verifica si fd está listo.
Finalmente, el último parámetro de la función:
struct timeval {
int tv_sec; /* segundos */
int tv_usec; /* microsegundos */
};Esta estructura nos permite especificar el tiempo máximo que podemos esperar a que llegue la información. únicamente hay que inicializar tv_sec al número de segundos a esperar y tv_usec al número de microsegundos a esperar.
El siguiente ejemplo espera 2.5 segundos a que algo sea entrado desde la entrada estándar:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define STDIN 0 /* file descriptor para la entrada estándar */
int main(void
{
struct timeval tv;
fd_set readfds;
tv.tv_sec = 2;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
/* no nos importan writefds y exceptfds: */
select(STDIN+1, &readfds, NULL, NULL, &tv);
if (FD_ISSET(STDIN, &readfds))
printf("A key was pressed!\n");
else
printf("Timed out.\n");
}øQué es lo que pasa si un socket que está incluido en el conjunto de sockets para lectura (readfds) termina la conexión?. En ese caso, la función select() regresará dicho socket como "listo para lectura". En el momento en que ejecutemos la función recv() con dicho socket, obtendremos como respuesta un 0. De esta manera sabremos que algún cliente ha cerrado la conexión.
Por último, si tenemos un socket que está escuchando (listen()), podemos revisar si existe una nueva conexión poniendo el file descriptor de dicho socket en el conjunto readfds.
Cuando se vio la función send() se comentó que en las aplicaciones de red no siempre se va a enviar toda la información que nosotros queremos. Esto no necesariamente es un error. La razón por lo que sucede esto es que el límite del buffer pudo haber sido alcanzado por el socket en el kernel, y lo que se debe hacer es volver a llamar la función send() para terminar de transmitir la información que quedó pendiente. Esta condición sucede tanto para mandar datos como para recibirlos. A continuación se presentan algunas funciones útiles para leer y escribir datos por medio de sockets.
/*
* Lee "n" bytes de un descriptor.
* Usar en lugar de read() cuando fd es un stream socket.
*/
int readn(int fd, char *ptr, int nbytes)
{
int nleft, nread;
nleft = nbytes;
while (nleft > 0) {
nread = read(fd, ptr, nleft);
if (nread < 0)
return(nread); /* error, regresa < 0 */
else if (nread == 0)
break; /* EOF */
nleft -= nread;
ptr += nread;
} return(nbytes - nleft); /* regresa >= 0 */
}
/*
* Escribe "n" bytes a un descriptor.
* Usar en lugar de write() cuando fd sea un stream socket.
*/
int writen(int fd, char *ptr, int nbytes)
{
int nleft, nwritten;
nleft = nbytes;
while (nleft > 0) {
nwritten = write(fd, ptr, nleft);
if (nwritten <= 0)
return(nwritten); /* error */
nleft -= nwritten;
ptr += nwritten;
}
return(nbytes - nleft);
}
/*
* Lee una línea de un descriptor. Lee la línea un byte a la vez
* buscando al caracter de nueva línea. Se gurada el carácter de nueva línea
* en el buffer, seguido de un null.
* Se regresa el número de caracteres de la línea, sin incluir
* el null.
*/
int readline(int fd, char *ptr, int maxlen)
{
int n, rc;
char c;
for (n = 1; n < maxlen; n++) {
if ( (rc = read(fd, &c, 1)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return(0); /* EOF, no hay datos */
else
break; /* EOF, se leyó información */
} else
return(-1); /* error */
}
*ptr = 0;
return(n);
}
int sendall(int s, char *buf, int *len)
{
int total = 0; // El número de bytes que enviamos
int bytesleft = *len; // El número de bytes que no se han enviado
int n;
while(total < *len){
n = send(s, buf + total, bytesleft, 0);
if(n == -1)
break;
total += n;
bytesleft -= n;
}
*len = total; //regresa el número de bytes enviados
return n == -1 ? -1; 0; //regresa -1 en caso de error, 0 en éxito
}
En este ejemplo, s es el socket al que queremos enviar información, buff es el buffer que contiene la información que se quiere enviar y len es un apuntador a int que contiene el número de bytes en el buffer.
La función regresa un -1 en caso de error. De igual manera, el número de bytes que fueron enviados es regresado en la variable len. éste será el mismo número de bytes que en un principio se quiso enviar, a menos que ocurra un error.
char buff[10] = "Mensaje de prueba";
int len;
len = strlen(buff);
if(sendall(s, buff, &len) == -1){
perror("sendall");
printf("Solo se enviaron %d bytes debido a un error\n", len);
}
Por ejemplo, supongamos que queremos desarrollar un programa que sirva para un multi-chat, el cual utilizará SOCK_STREAMS. Cuando alguno de los usuarios del chat escriba algo, se necesitarán mandar básicamente dos datos al servidor: Lo que se dijo y quién lo dijo.
Supongamos que el nombre de usuario será de máximo 8 caracteres, seguido del caracter '\0'. También supongamos que la longitud máxima de la información a enviar será de 128 caracteres. Por lo tanto, una estructura que se basa en la información anterior sería la siguiente:
- La longitud total del paquete, incluyendo los 8 caracteres del nombre de usuario.
0A | 74 | 6F | 6D | 00 | 00 | 00 | 00 | 00 | 48 | 69 | |
(length) | T | o | m | (pa | dd | i | ng | ) | H | i |
14 | 42 | 65 | 6E | 6A | 61 | 6D | 69 | 6E | 48 | 65 | 79 | 20 | 67 | 75 | 79 | 73 | ... | |
(length) | B | e | n | j | a | m | i | n | H | e | y | g | u | y | s | ... |
Cuando se van a mandar muchos paquetes, deberá usarse una función similar a la función sendall(), de manera que aseguremos que toda la información se ha enviado.
De la misma manera, cuando vamos a recibir la información tendremos que mandar llamar a la función recv() hasta que estemos seguros que hemos recibido toda la información que se mandó. Esto se puede hacer ya que en el encabezado se especifica el número total de bytes que componen al paquete. También sabemos el tamaño total del paquete, que es de 1 + 8 + 128, o sea 137 bytes (ya que así se especificó anteriormente).
Otro aspecto que al que se debe poner atención es que en algunas ocasiones se podrá recibir en un solo recv(), todo un paquete y parte de un segundo paquete. Es por eso que se debe trabajar con un arreglo en el que quepan dos paquetes.
Debido a que conocemos la longitud del primer paquete y que se puede obtener el número de bytes recibidos, se podrá calcular fácilmente cuántos bytes del buffer pertenecen al segundo paquete (que estará incompleto). Cuando se termine de trabajar con el primer paquete, se podrá eliminar del buffer y mover el segundo paquete al principio del buffer, de manera que podamos estar listos para el siguiente recv().