20180205:TP:C:TCP:SimpleServer
Sommaire
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.)