Publi

Cliente TCP no bloqueante en C, en el que podemos enviar y recibir en cualquier momento

Hace tiempo veíamos un artículo de un servidor que fuera capaz de aceptar múltiples conexiones, ahora, toca le toca el turno a la implementación del cliente.

En principio, vamos a implementar un cliente sencillo, en el que conectaremos a un servidor y nos permitirá enviar texto que escribamos por teclado. No podremos recibir nada del servidor, pero nos servirá como primera aproximación:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>

#define MENS_MAX_LEN 500

void usage()
{
  fprintf (stderr, "Wrong arguments. Must give two:\n");
  fprintf (stderr, "tcpclient SERVER PORT\n\n");
  fprintf (stderr, "For example:\n");
  fprintf (stderr, "  tcpclient totaki.com 80\n");
  exit(1);
}

void panic(char *msg)
{
  fprintf (stderr, "Fatal error: %s (errno %d, %s)\n", msg, errno, strerror(errno));
  exit(2);
}

int main(int argc, char *argv[])
{
  char *server;
  int port;
  int socketfd;
  int finish = 0;
  struct sockaddr_in serverAddress, clientAddress;
  struct hostent *h;
  char mens [MENS_MAX_LEN];
  size_t recvsize;

  if (argc<3)
    usage();

  server = argv[1];
  port = atoi(argv[2]);

  /* Create a TCP socket */
  socketfd = socket(AF_INET, SOCK_STREAM, 0);
  if (socketfd==-1)
    panic("Failed to create socket");

  /* Sets client address */
  clientAddress.sin_family = AF_INET;
  clientAddress.sin_addr.s_addr = htonl(INADDR_ANY);
  clientAddress.sin_port = htons(0);

  /* Bind the address to the socket */
  if (bind(socketfd, (struct sockaddr *)&clientAddress, sizeof(clientAddress))==-1)
    panic("Binding address");

  /* Get the hostname address */
  h = gethostbyname(server);

  /* Sets server address */
  serverAddress.sin_family = h->h_addrtype;
  memcpy((char*) &serverAddress.sin_addr.s_addr, h->h_addr_list[0], h->h_length);
  serverAddress.sin_port = htons(port);

  if (connect(socketfd, (struct sockaddr * )&serverAddress, sizeof(serverAddress))==-1)
    panic("Cannot connect");

  do
    {
      fgets(mens, MENS_MAX_LEN, stdin);

      if (send(socketfd, mens, strlen(mens)+1, 0)==-1)
    panic ("Cannot sent message");

    } while (!finish);      /* Never finish */

  return EXIT_SUCCESS;
}

Vamos directamente al main(), ya que usage() sólo muestra texto y panic() provoca la salida cuando se produce un error. Lo primero que hacemos es determinar el servidor y el puerto en función de los argumentos de entrada, creamos el socket y establecemos la dirección del cliente. Como el puerto es 0, el cliente cogerá un puerto libre cualquiera, es más, no nos importa cuál sea, con bind(), asignamos dicha dirección al socket.
Luego, creamos la dirección del servidor, como éste puede venir por su hostname, y tal vez debamos resolverlo utilizamos getbyhostname(), cuando creamos la variable de dirección conectamos y empezamos el intercambio de mensajes. En este caso utilizamos fgets() para pedir información al usuario por teclado y con send() enviamos la información especificando qué enviamos (mens) y cuánto ocupa (strlen(mens)+1, tenemos que incluir el terminador \0 al final de la cadena).
Nota: Es posible que fgets() envíe el retorno de carro producido al pulsar intro cuando termina la cadena. Podemos utilizar una función trim(), o filtrar con:

1
2
if (mens[strlen(mens)-1]=='\n')
   mens[strlen(mens)-1] = '\0';

Para poder recibir un mensaje del servidor, bastará con poner:

1
2
3
4
5
size_t recvsize;
recvsize = recv(socketfd, mens, MENS_MAX_LEN, 0);
if (recvsize==-1)
  panic("Cannot receive information");
mens[recvsize] = '\0';

La recepción es muy parecida al envío, sólo que esta vez no sabemos cuánto ocupa el mensaje, por lo que damos un tamaño máximo (si se sobrepasa este tamaño, el resto del mensaje se recibirá en sucesivas llamadas a recv().
Como este mensaje puede no tener un terminador (debe haberse enviado el terminador expresamente), como último carácter en el buffer imponemos este terminador.

Hasta aquí todo es sencillo, el gran problema viene cuando tenemos que establecer turnos fijos para envío y recepción, es más, el cliente podrá estar en modo recepción o modo envío. Pero si estamos acostumbrados a utilizar telnet, o incluso cualquier programa de envío de mensajes o chat, podemos ver que los mensajes pueden salir o entrar en cualquier momento, no tenemos por qué esperar a que alguien escriba un mensaje para recibir todo lo que tenemos pendiente.
Ahora completaremos este ejemplo con una llamada a select(), esta función detectará cuándo hay datos en alguno de los descriptores monitorizados y podremos elegir si leer de la entrada estándar o del socket, pero tiene una funcionalidad más que nos resultará muy interesante, dispone de timeout, por lo que si transcurrido un tiempo no ha venido nada por ninguno de los descriptores, la ejecución puede continuar, no estaremos bloqueando el programa de forma indefinida hasta que venga un dato.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>

#define MENS_MAX_LEN 500

void usage()
{
  fprintf (stderr, "Wrong arguments. Must give two:\n");
  fprintf (stderr, "tcpclient SERVER PORT\n\n");
  fprintf (stderr, "For example:\n");
  fprintf (stderr, "  tcpclient totaki.com 80\n");
  exit(1);
}

void panic(char *msg)
{
  fprintf (stderr, "Fatal error: %s (errno %d, %s)\n", msg, errno, strerror(errno));
  exit(2);
}

int main(int argc, char *argv[])
{
  char *server;
  int port;
  int socketfd;
  int finish = 0;
  struct sockaddr_in serverAddress, clientAddress;
  struct hostent *h;
  char mens [MENS_MAX_LEN];
  fd_set readmask;
  struct timeval timeout;
  size_t recvsize;

  if (argc<3)
    usage();

  server = argv[1];
  port = atoi(argv[2]);

  /* Create a TCP socket */
  socketfd = socket(AF_INET, SOCK_STREAM, 0);
  if (socketfd==-1)
    panic("Failed to create socket");

  /* Sets client address */
  clientAddress.sin_family = AF_INET;
  clientAddress.sin_addr.s_addr = htonl(INADDR_ANY);
  clientAddress.sin_port = htons(0);

  /* Bind the address to the socket */
  if (bind(socketfd, (struct sockaddr *)&clientAddress, sizeof(clientAddress))==-1)
    panic("Binding address");

  /* Get the hostname address */
  h = gethostbyname(server);

  /* Sets server address */
  serverAddress.sin_family = h->h_addrtype;
  memcpy((char*) &serverAddress.sin_addr.s_addr, h->h_addr_list[0], h->h_length);
  serverAddress.sin_port = htons(port);

  if (connect(socketfd, (struct sockaddr * )&serverAddress, sizeof(serverAddress))==-1)
    panic("Cannot connect");

  do
    {
      /* We must set all this information on each select we do */
      FD_ZERO(&readmask);   /* empty readmask */
      /* Then we put all the descriptors we want to wait for in a */
      /* mask = readmask */
      FD_SET(socketfd, &readmask);
      FD_SET(STDIN_FILENO, &readmask); /* STDIN_FILENO = 0 (standard input) */
      /* Timeout, we will stop waiting for information */
      timeout.tv_sec=0;
      timeout.tv_usec=100000;

      /* The first parameter is the biggest descriptor+1. The first one
       was 0, so every other descriptor will be bigger.*/

      /* readfds = &readmask */
      /* writefds = we are not waiting for writefds */
      /* exceptfds = we are not waiting for exception fds */
      if (select(socketfd+1, &readmask, NULL, NULL, &timeout)==-1)
    panic("Error on SELECT");

      /* If something was received */
      if (FD_ISSET(socketfd, &readmask))
    {
      recvsize = recv(socketfd, mens, MENS_MAX_LEN, 0);
      if (recvsize==-1)
        panic("Cannot receive information");
      mens[recvsize] = '\0';
      printf (">> %s\n", mens);
    }

      /* If something was written by the user */
      if (FD_ISSET(STDIN_FILENO, &readmask))
    {
      fgets(mens, MENS_MAX_LEN, stdin);

      if (send(socketfd, mens, strlen(mens)+1, 0)==-1)
        panic ("Cannot sent message");
    }
    } while (!finish);

  return EXIT_SUCCESS;
}

Foto: Julien Gong Min (Flickr) CC-by

También podría interesarte....

There are 3 comments left Ir a comentario

  1. Pingback: BlogESfera.com /

  2. basketball stars /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Your posts are very helpful; I hope you’ll keep them coming.

  3. lily jane /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    This is a really good website article. Not many people actually do that. the way you simply did. I am truly amazed that such a large amount of data has been revealed on this topic and that you have put in so much effort. gacha life

Leave a Reply