20180205:TP:C:TCP:SimpleServer

De wiki-prog
Aller à : navigation, rechercher


Introduction

This session is dedicated to simple server. We'll do two basic server: an echo server and a bc server.

In order to test your server, you'll need to use a basic TCP client able to send text like telnet or nc (netcat). You should prefer nc since it doesn't send, by default, the double end-line characters CR/LF (\n\r).

Submission

The folder name for this session is 20180205_tcp_server, the expected architecture is as follow:

  • 20180205_tcp_server
    • AUTHORS
    • echo_server: directory for the first exercise (source and Makefile)
    • bc_server: directory for the second exercise (source and Makefile)

As usual, you should submit your work before next monday 2pm.

Helping Code

This section provides some piece of code that can be useful:

  • setting signal handler correctly for SIGINT (with some tricks to close accepting socket.)
  • setting signal handler correctly for SIGCHLD
  • using getaddrinfo(3) in order to set-up server-side socket

SIGINT

We want to be able to close our server using SIGINT (Ctrl-C in your terminal.) For that we need to set-up correctly a signal handler that will close the file descriptor of the accepting socket.

The idea is to define a function with a static variable that store the accept socket FD, a function runned with SIGINT is received and then calling sigaction(2) correctly.

First, the functions:

// register an fd or get it
// call with -1 if you just want the fd
int fdaccept_register(int fd) {
  static int fdaccept = -1;
  if (fdaccept == -1 && fd != -1) {
    fdaccept = fd;
  }
  return fdaccept;
}
 
// signal handler for SIGINT
void sigint_handler(int sig) {
  (void)sig;
  int fd = fdaccept_register(-1);
  if (fd != -1)
    close(fd);
  _exit(0);
}

Now, in your main, before starting anything, you can add the following code:

  struct sigaction sigint;
 
  // Handle terminaison through Ctrl-C
  memset(&sigint, 0, sizeof (struct sigaction));
  sigint.sa_handler = sigint_handler;
  sigfillset(&sigint.sa_mask);
  sigint.sa_flags = SA_NODEFER;
  if ( sigaction(SIGINT, &sigint, NULL) == -1)
    err(EXIT_FAILURE, "can't change SIGINT behavior");

SIGCHLD

SIGCHLD is the signal sent when a child process dies. Normally we should handle it and perform a wait(2) in order to discard zombie process, but there exists a flag for sigaction(2) that indicate that we don't want zombie at all, so we don't need an handler function, just a correct set-up of sigaction(2):

  struct sigaction sigchld;
 
  // Avoid zombies and don't get notify about children
  memset(&sigchld, 0, sizeof (struct sigaction));
  sigchld.sa_handler = SIG_DFL;
  sigemptyset(&sigchld.sa_mask);
  sigchld.sa_flags = SA_NOCLDSTOP | SA_NOCLDWAIT;
  if ( sigaction(SIGCHLD, &sigchld, NULL) == -1 )
    err(EXIT_FAILURE, "can't change SIGCHLD behavior");

This code should be placed near the code for SIGINT in you main.

Using getaddrinfo(3) for server-side socket

Last week we use getaddrinfo(3) on client-side, now we'll see that we can also use it on server-side.

The principle is the same, somehow simpler: set some hints (mainly port number or service name) and call the function. Here is a little code sample:

  int info_err = 0;
  struct addrinfo hints, *resinfo = NULL;
 
  // setup hints and get local info
  memset(&hints, 0, sizeof (struct addrinfo));
  hints.ai_family = AF_UNSPEC;                 // IPv4 or IPv6
  hints.ai_socktype = SOCK_STREAM;             // TCP
  hints.ai_protocol = 0;
  hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; // server mode
  // let's go !
  info_err = getaddrinfo(NULL, portname, &hints, &resinfo);
 
  // Error management
  if (info_err != 0) {
    errx(EXIT_FAILURE, "Server setup fails on port %s: %s", portname,
	 gai_strerror(info_err));
  }

Once done, resinfo will points to a struct addrinfo with everything we need for socket(2) and bind(2).

What you'll use:

  • resinfo->ai_family, resinfo->ai_socktype and resinfo->ai_prototype for socket(2)
  • resinfo->ai_addr and resinfo->ai_addrlen for bind(2)

After creating your socket and binding it, you'll to release resinfo using freeaddrinfo(3).

A simple echo server

An echo server is a TCP server that sends back what it receive, nothing less, nothing more.

Echo function

In order to prepare your server (and to avoid long and boring testing) you should write an echo function, which in fact is the same as a cat function: it takes an input FD and an output FD, read on input (until end of the stream) and write received data to the output.

You must be careful on error management:

  • check returns of read(2) and write(2)
  • continue reading/writing when error (errno value) is EINTR, EAGAIN or EWOULDBLOCK.
  • be sure when sending that all data have been sent and restart when needed.
  • Implement the following function:
void echo(int fdin, int fdout);

Since input and output are independent, you can test your code like a cat function, or run it with STDIN_FILENO and STDOUT_FILENO.

Server

Now we need to set-up the server. Here is a sketch of the server function:

  • define hints and get info (see previous section) using getaddrinfo(3)
  • create the socket using socket(2)
  • set the option SOL_REUSEADDR using setsockopt(2) (see below)
  • save the socket FD using our function fdaccept_register
  • bind the socket to the address information using bind(2)
  • start listening using listen(2)
  • now you can enter the accepting loop (see below.)

We want to set the option SOL_REUSEADDR in order to minimize port reuse issues. You can do that this way (assuming fdaccept is your socket):

  int reuse_err;
  int reuse = 1;
  reuse_err = setsockopt(fdaccept, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof (reuse));
 
  // Error management
  if (reuse_err == -1)
    err(EXIT_FAILURE, "Fail to set socket options");

All this code should be grouped in a server function:

void server(const char *portname);

Remarque: you need to do error management for all system calls and once the socket is created (especially after binding) you need to close it before leaving the program !

For error management, use err(3): it's clean, easy, prints readable error messages depending on errno.

Accepting loop: one by one

Our first attempt for the accepting loop will be a simple one connexion at a time variant. Here is the idea:

  • loop forever (for(;;))
    • accept incoming connexion using accept(2) store FD in fdcnx
    • call echo(fdcnx, fdcnx)
    • close fdcnx

And don't forget error management.

For accept(2) we only need the first parameter (the listening socket), remaining parameters will be NULL or 0 (check the type, please.)

This version can already be tested (see below.)

Accepting loop: using fork(2)

This loop is similar to the previous one but:

  • once accepting you'll fork
  • in the parent process, you'll close the connexion socket and loop (use continue)
  • in the child process, you'll close the accepting socket, run echo, close socket and exit.

Program and tests

We can now build our program. You need to add the correct includes (read the manual pages) and build a main that perform the following actions

  • check if there's an argument, exit with an error message if it's not the case.
  • set signal handlers as described in the first section
  • run server with the first argument (argv[1])
  • leave.

In order to test, you'll need at least 2 terminals, one for launching the server and another one for connexion.

For the server:

shell> make echo_server
gcc -fsanitize=address -Wall -Wextra -std=c99 -O0 -g    echo_server.c   -o echo_server
shell> ./echo_server 4242

In the other terminal, now you can test:

# check if server is listening
shell> netstat --inet -l | grep 4242
tcp        0      0 *:4242                  *:*                     LISTEN
# test it now
shell> nc 127.0.0.1 4242
hello
hello
echo me please !
echo me please !
shell>

In order to leave nc, just hit Ctrl-D.

You can leave your server with Ctrl-C.

A bc server

bc(1) is a shell command providing functionality of a symbolic calculator, try it, it's easy to use.

Our goal, is to write a simple server that accept connexion then redirect STDIN_FILENO and STDOUT_FILENO to the connexion socket and exec (using execvp(3)) the command bc.

You just need to take the code of the previous program and replace the echo part with a correct call to execvp(3) using redirections (with dup2(2).)

Here is a test session using the 2 terminals:

Server side:

shell> make bc_server
gcc -fsanitize=address -Wall -Wextra -std=c99 -O0 -g    bc_server.c   -o bc_server
shell> ./bc_server 4242

Client side:

shell> nc 127.0.0.1 4242
1 + 1
2
sqrt(1024)
32
27972 / 42
666
quit

Since bc outputs its error on STDERR_FILENO it can be interesting to redirect it also, but that's not mandatory.

You can also replace bc with any similar command (it just needs to read on its standard input and write it's result on standard output.)