Différences entre versions de « C socket »

De The Linux Craftsman
Aller à la navigation Aller à la recherche
Ligne 508 : Ligne 508 :
 
</source>
 
</source>
  
 +
On peut interroger notre serveur avec ''telnet'' (''yum -y install telnet''):
 +
<pre>
 +
# telnet 127.0.0.1 8080
 +
Trying 127.0.0.1...
 +
Connected to 127.0.0.1.
 +
Escape character is '^]'.
 +
Entrez 'exit' pour quitter
 +
coucou
 +
Vous avez dit : coucou
 +
</pre>
 
== Serveur UDP ==
 
== Serveur UDP ==
 
Si on essaye de reproduire le même type de serveur mais cette fois-ci avec une socket en UDP, cela devient beaucoup plus simple. En effet, en UDP, plus besoin de tenir un tableau avec les descripteurs des sockets des clients car... on est en mode déconnecté (datagramme) ! Entendez par la que les clients envoie un message sans attendre de réponse et sans se soucier de savoir si le message est arrivé au serveur.
 
Si on essaye de reproduire le même type de serveur mais cette fois-ci avec une socket en UDP, cela devient beaucoup plus simple. En effet, en UDP, plus besoin de tenir un tableau avec les descripteurs des sockets des clients car... on est en mode déconnecté (datagramme) ! Entendez par la que les clients envoie un message sans attendre de réponse et sans se soucier de savoir si le message est arrivé au serveur.

Version du 28 octobre 2018 à 17:22

Introduction

Les sockets permettent de connecter deux programmes qui s'exécutent sur deux machines différentes. Cette connexion se fait à travers le réseau grâce à l'utilisation d'un port (TCP ou UDP) et d'une adresse IP.

Il y a deux types de sockets, une qui est démarrée par la partie serveur en écoute et l'autre démarrée par la partie cliente qui se connecte à la première.

Ci-contre une image résumant les différentes étapes pour arriver à l'envoie de données.

Socket workflow.png

Création

Côté serveur

Tout d'abord il faut créer l'objet socket:

#include <sys/socket.h>
#include <netinet/in.h> 

int socket(int domain, int type, int protocol)
  • domain → integer, communication domain e.g., AF_INET (IPv4 protocol) , AF_INET6 (IPv6 protocol)
  • type → type de communication
    • SOCK_STREAM: TCP
    • SOCK_DGRAM: UDP
    • SOCK_RAW: socket à l'état brut (bas niveau)
  • protocol → valeur du champ protocol de l'entête de niveau 3 (généralement 0)
  • la valeur de retour est le fichier descripteur de la socket ou -1 en cas d'erreur (errno est positionné)


Une fois le descripteur de socket créé, il est possible de le configurer:

#include <sys/socket.h>
#include <netinet/in.h> 

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
  • sockfd → le fichier descripteur de la socket;
  • level → niveau d'application de l'option (SOL_SOCKET, TCP, UDP, ...);
  • optname → le nom de l'option (SO_REUSEADDR, SO_REUSEPORT, SO_KEEPALIVE, ...)
  • optval, optlen → utilisé pour accéder aux options de la socket;
  • le code retour varie entre 0 et -1 en cas d'erreur (errno est positionné)

Il existe plusieurs options de socket, les plus utilisées étant :

  • SO_REUSEADDR → permet de réutiliser l'adresse de la socket tout de suite même si cette dernière est dans l'état wait;
  • SO_REUSEPORT → permet de réutiliser le port de la socket tout de suite même si cette dernière est dans l'état wait;
  • SO_KEEPALIVE → envoie des informations périodiquement pour tester si l'extrémité du tunnel est toujours présente;


Il faut maintenant attacher la socket à un port Internet et une adresse IP:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
  • sockfd → le fichier descripteur de la socket;
  • addr → une structure symbolisant l'adresse
struct sockaddr_in {
    short            sin_family;   // e.g: AF_INET
    unsigned short   sin_port;     // e.g: htons(3490)
    struct in_addr   sin_addr;     // détaillé ci-dessous
    char             sin_zero[8];  // '0' habituellement
};
struct in_addr {
    unsigned long s_addr;  // initialiser avec inet_aton()
};
  • addrlen → la taille de l'objet addr;
  • le code retour varie entre 0 et -1 en cas d'erreur (errno est positionné)


On peut maintenant passer la socket en état d'écoute, elle est prête à recevoir des connexions:

int listen(int sockfd, int backlog)
  • sockfd → le fichier descripteur de la socket;
  • backlog → taille maximum de la file d'attente de la socket après laquelle le système répond avec ECONNREFUSED;
  • le code retour varie entre 0 et -1 en cas d'erreur (errno est positionné)


On peut maintenant attendre une connexion:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd → le fichier descripteur de la socket;
  • addr → l'adresse du client;
  • addrlen → la taille de la structure addr;
  • le code retour varie entre un entier positif qui correspond au descripteur de la socket cliente et -1 en cas d'erreur (errno est positionné)

Côté client

Après avoir créé la socket avec socket on peut se connecter à la partie serveur:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd → le fichier descripteur de la socket;
  • addr → une structure symbolisant l'adresse (cf. ci-dessus)
  • addrlen → la taille de l'objet addr;
  • le code retour varie entre 0 et -1 en cas d'erreur (errno est positionné)

Lecture / écriture

Fonctions génériques

Tout d'abord, on peut dire que la socket est un fichier et se manipule donc comme un fichier !

Lecture

Pour lire dans une socket on peut utiliser read:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • sockfd → le fichier descripteur de la socket;
  • buf → le tableau de caractères ou mettre le message reçu;
  • count → le nombre de caractères à recevoir;
  • le code retour correspond au nombre de caractères reçu ou -1 si une erreur survient (errno est positionné);

Écriture

Pour écrire dans une socket ou peut utiliser write:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • sockfd → le fichier descripteur de la socket;
  • buf → le tableau de caractères contenant le message à envoyer;
  • count → le nombre de caractères à envoyer;
  • le code retour correspond au nombre de caractères envoyer ou -1 si une erreur survient (errno est positionné);

Modification

Pour modifier un descripteur fichier de socket on utilise fcntl:

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* int args */ );
  • sockfd → le fichier descripteur de la socket;
  • cmd → la commande de modification (descripteur, drapeaux, ...);
  • args → les arguments correspondants à la commande

Cette commande va nous permettre de modifier le drapeau du descripteur de la socket (F_SETFL) pour le rendre non bloquant (O_NONBLOCK) !

Fonctions spécifiques

Il existe des fonctions spécifiques pour manipuler les sockets. Ces fonctions ressemble au précédentes mais acceptent des drapeaux en plus pour pouvoir modifier le descripteur à la volé.

Lecture

Pour lire dans une socket on peut utiliser recv:

#include <unistd.h>

ssize_t recv(int fd, void *buf, size_t count, int flags);
  • sockfd → le fichier descripteur de la socket;
  • buf → le tableau de caractères ou mettre le message reçu;
  • count → le nombre de caractères à recevoir;
  • flags → liste de drapeaux (eg. O_NONBLOCK);
  • le code retour correspond au nombre de caractères reçu ou -1 si une erreur survient (errno est positionné);

Écriture

Pour écrire dans une socket ou peut utiliser send:

#include <unistd.h>

ssize_t send(int fd, const void *buf, size_t count);
  • sockfd → le fichier descripteur de la socket;
  • buf → le tableau de caractères contenant le message à envoyer;
  • count → le nombre de caractères à envoyer;
  • flags → liste de drapeaux (eg. MSG_CONFIRM, MSG_DONTWAIT , ...);
  • le code retour correspond au nombre de caractères envoyer ou -1 si une erreur survient (errno est positionné);

Cas d'utilisation

Création

Pour commencer, il faut créer le descripteur de fichier:

int fdsocket;
if ((fdsocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
	printf("Echéc de la création: %s\n", strerror(errno));
	exit(EXIT_FAILURE);
}

Paramétrage des options

Il faut maintenant configurer la réutilisation de l'adresse et du port

int opt = 1;
if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,	sizeof(opt)) != 0) {
	printf("Echéc de paramètrage: %s\n", strerror(errno));
	exit(EXIT_FAILURE);
}

Attachement

On peut maintenant attacher la socket à un port et une adresse:

struct sockaddr_in adresse;

adresse.sin_family = AF_INET;
// Ecoute sur toutes les adresses (INADDR_ANY <=> 0.0.0.0)
adresse.sin_addr.s_addr = INADDR_ANY;
// Conversion du port en valeur réseaux (Host TO Network Short)
adresse.sin_port = htons(8080);

if (bind(fdsocket, (struct sockaddr *) adresse, sizeof(*adresse)) != 0) {
	printf("Echéc d'attachement: %s\n", strerror(errno));
	exit(EXIT_FAILURE);
}

Mise en écoute

La socket est prête à passer à l'écoute des connexions des clients:

// Taille de la file d'attente
#define BACKLOG 3

if (listen(fdsocket, BACKLOG) != 0) {
	printf("Echéc de la mise en écoute: %s\n", strerror(errno));
	exit(EXIT_FAILURE);
}

Acceptation des connexions

Il est possible d'accepter les nouvelles connexions:

int clientSocket;
// Structure contenant l'adresse du client
struct sockaddr_in clientAdresse;
int addrLen = sizeof(clientAdresse);
if ((clientSocket = accept(*serverSocket, (struct sockaddr*) &clientAdresse, (socklen_t*) &addrLen)) != -1) {
	// Convertion de l'IP en texte
	char ip[INET_ADDRSTRLEN];
	inet_ntop(AF_INET, &(clientAdresse.sin_addr), ip, INET_ADDRSTRLEN);
	printf("Connexion de %s:%i\n", ip, clientAdresse.sin_port);
}

Il est possible d'utiliser fcntl pour passer le descripteur de la socket en non bloquant, avant l'appel à la fonction accept. Si le descripteur reste bloquant, la fonction accept ne rendra pas la main tant qu'un client ne se sera connecté, ayant pour résultat le blocage complet du programme !

// Passage en mode non bloquant
fcntl(fdsocket, F_SETFL, O_NONBLOCK);

On peut remarquer, au passage, l'utilisation de inet_ntop pour convertir l'adresse du client du format binaire au format text.

Lecture / Écriture

Mode connecté (TCP)

On peut maintenant utiliser le descripteur de la socket du client pour lire et écrire.

  • avec les fonctions génériques:
#define BUFFER_LEN 200
// Descripteur de la socket du client
int clientSocket;
char buffer[BUFFER_LEN];
// Passage en mode non bloquant, sinon read attend
fcntl(clientSocket, F_SETFL, O_NONBLOCK);
int len = read(clientSocket, buffer, BUFFER_LEN);

send(clientSocket, "Coucou\n", strlen("Coucou\n"));
  • avec les fonctions spécifiques:
#define BUFFER_LEN 200
// Descripteur de la socket du client
int clientSocket;
char buffer[BUFFER_LEN];

int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT);

send(clientSocket, "Coucou\n", strlen("Coucou\n"), MSG_DONTWAIT);

Analysons le retour de la fonction read ou recv lorsque l'on utilise un descripteur non bloquant d'après la documentation :

NAME 
       recv, recvfrom, recvmsg - receive a message from a socket
[...]
MSG_DONTWAIT (since Linux 2.2)
    Enables nonblocking operation; if the operation would block, the call fails with the error EAGAIN or EWOULDBLOCK (this can also be enabled using the O_NONBLOCK flag with the F_SETFL fcntl(2)). 
[...]
RETURN VALUE
[...]
These calls return the number of bytes received, or -1 if an error
       occurred.  In the event of an error, errno is set to indicate the
       error.
When a stream socket peer has performed an orderly shutdown, the
       return value will be 0 (the traditional "end-of-file" return).
[...]

Le retour est :

  • lorsque le descripteur est vide (aucune donnée de la part du client) -1 et errno sera positionné à la valeur EAGAIN, il ne faut donc pas fermer la socket si le retour est -1 ! Il faut d'abord vérifier que errno n'a pas la valeur EAGAIN;
  • lorsque le retour est 0, cela signifie que l'extrémité est fermée, il faut donc fermer le descripteur local;
  • lorsque la valeur est supérieur à 0 elle correspond au nombre d'octets reçu.

On peut donc en déduire le morceau de code suivant pour gérer le retour de read ou recv:

int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT);

if (len == -1 && errno != EAGAIN) {
	// Une erreur est survenue
} else if (len == 0) {
	// Le client s'est déconnecté (extrémité de la socket fermée)			
} else if (len > 0) {
	// Le client à envoyé des données	
}

Mode datagramme (UDP)

Lecture

Pour lire dans une socket on peut utiliser recvfrom:

#include <unistd.h>

ssize_t recv(int fd, void *buf, size_t count, int flags, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd → le fichier descripteur de la socket;
  • buf → le tableau de caractères ou mettre le message reçu;
  • count → le nombre de caractères à recevoir;
  • flags → liste de drapeaux (eg. O_NONBLOCK);
  • addr → une structure symbolisant l'adresse du client (cf. ci-dessus)
  • addrlen → la taille de l'objet addr;
  • le code retour correspond au nombre de caractères reçu ou -1 si une erreur survient (errno est positionné);

Écriture

Pour écrire dans une socket ou peut utiliser sendto:

#include <unistd.h>

ssize_t send(int fd, const void *buf, size_t count, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd → le fichier descripteur de la socket;
  • buf → le tableau de caractères contenant le message à envoyer;
  • count → le nombre de caractères à envoyer;
  • flags → liste de drapeaux (eg. MSG_DONTWAIT , ...);
  • addr → une structure symbolisant l'adresse du client (cf. ci-dessus)
  • addrlen → la taille de l'objet addr;
  • le code retour correspond au nombre de caractères envoyer ou -1 si une erreur survient (errno est positionné);

Exemples

Serveur TCP

Voici un exemple de serveur echo qui renvoie le message au client. Les connexions utilisent TCP et un simple client comme telnet suffit pour l'utiliser.

#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <fcntl.h>

// Port d'écoute de la socket
#define PORT 8080
// Adresse d'écoute (toutes les adresses)
#define IP INADDR_ANY
// Taille de la file d'attente
#define BACKLOG 3
// Nombre de connexions clients
#define NB_CLIENTS 2
// Taille du tampon de lecture des messages
#define BUFFER_LEN 200
// Commande pour arrêter le serveur
#define EXIT_WORD "exit"

void initAdresse(struct sockaddr_in * adresse);
int initSocket(struct sockaddr_in * adresse);
int waitForClient(int * serverSocket);
void addClientToTab(int clientSocket, int clients[]);
void manageClient(int clients[]);

int main(void) {
	// Création et initialisation du tableau contenant les descripteurs des sockets clients
	int clients[NB_CLIENTS];
	for (int i = 0; i < NB_CLIENTS; i++) {
		clients[i] = -1;
	}
	// Structure contenant l'adresse
	struct sockaddr_in adresse;
	initAdresse(&adresse);
	// Descripteur de la socket du serveur
	int serverSocket = initSocket(&adresse);
	int clientSocket;
	while (1) {
		// Descripteur de la socket du client, on attend une connexion
		if ((clientSocket = waitForClient(&serverSocket)) != -1) {
			// On ajoute le nouveau client au tableau des descripteurs
			addClientToTab(clientSocket, clients);
		}
		manageClient(clients);
	}
	return EXIT_SUCCESS;
}
// Initialisation de la structure sockaddr_in
void initAdresse(struct sockaddr_in * adresse) {
	(*adresse).sin_family = AF_INET;
	(*adresse).sin_addr.s_addr = IP;
	(*adresse).sin_port = htons( PORT);
}
// Démarrage de la socket serveur
int initSocket(struct sockaddr_in * adresse) {
	// Descripteur de socket
	int fdsocket;
	// Nombre d'options
	int opt = 1;
	// Création de la socket en TCP
	if ((fdsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
		printf("Echéc de la création: %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	}
	printf("Création de la socket\n");
	// Paramètrage de la socket
	if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
			sizeof(opt)) != 0) {
		printf("Echéc de paramètrage: %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	}
	printf("Paramètrage de la socket\n");
	// Attachement de la socket sur le port et l'adresse IP
	if (bind(fdsocket, (struct sockaddr *) adresse, sizeof(*adresse)) != 0) {
		printf("Echéc d'attachement: %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	}
	printf("Attachement de la socket sur le port %i\n", PORT);
	// Passage en écoute de la socket
	if (listen(fdsocket, BACKLOG) != 0) {
		printf("Echéc de la mise en écoute: %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	}
	// Passage en mode non bloquant
	fcntl(fdsocket, F_SETFL, O_NONBLOCK);
	printf("Mise en écoute de la socket\n");
	return fdsocket;
}
// Attente de connexion d'un client
int waitForClient(int * serverSocket) {
	int clientSocket;
	// Structure contenant l'adresse du client
	struct sockaddr_in clientAdresse;
	int addrLen = sizeof(clientAdresse);
	if ((clientSocket = accept(*serverSocket, (struct sockaddr*) &clientAdresse,
			(socklen_t*) &addrLen)) != -1) {
		// Convertion de l'IP en texte
		char ip[INET_ADDRSTRLEN];
		inet_ntop(AF_INET, &(clientAdresse.sin_addr), ip, INET_ADDRSTRLEN);
		printf("Connexion de %s:%i\n", ip, clientAdresse.sin_port);
		// Passage en mode non bloquant
		int opt = 1;
		setsockopt(clientSocket, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(1));
	}
	return clientSocket;
}
// Ajoute les clients au tableau
void addClientToTab(int clientSocket, int clients[]) {
	// On vérifie si on à de la place de libre
	int found = -1;
	for (int i = 0; i < NB_CLIENTS; i++) {
		// On cherche de la place
		if (clients[i] == -1) {
			// On ajoute le client
			printf("Ajout du client à l'index %i\n", i);
			clients[i] = clientSocket;
			// Nouvelle connexion, envoie du message de bienvenu
			send(clientSocket, "Entrez 'exit' pour quitter\n", 	strlen("Entrez 'exit' pour quitter\n"),
					MSG_DONTWAIT);
			found = 0;
			break;
		}
	}
	if (found == -1) {
		// On a plus de place de libre
		puts("Plus de place, désolé");
		close(clientSocket);
	}
}
// On traite l'input des clients
void manageClient(int clients[]) {
	// Création d'un tampon pour stocker les messages des clients dans la heap
	static char buffer[BUFFER_LEN + 1];
	for (int i = 0; i < NB_CLIENTS; i++) {
		// On vérifie les messages
		int clientSocket = clients[i];
		if (clientSocket == -1) {
			continue;
		}
		int len = recv(clientSocket, buffer, BUFFER_LEN, MSG_DONTWAIT);
		// Booléen pour suivre l'état de la socket
		int isClosed = 0;
		if (len == -1 && errno != EAGAIN) {
			// Une erreur est survenue
			printf("Errno [%i] : %s\n", errno, strerror(errno));
			isClosed = 1;
		} else if (len == 0) {
			// Le client s'est déconnecté (extrémité de la socket fermée)
			isClosed = 1;
		} else if (len > 0) {
			// Ajout du terminateur de chaîne
			buffer[len] = '\0';
			if (strncmp(buffer, EXIT_WORD, 4) == 0) {
				// Le client veut se déconnecter
				send(clientSocket, "Bye\n", strlen("Bye\n"), 0);
				isClosed = 1;
			} else {
				// On renvoie le texte au client dans un buffer assez grand
				int len = strlen("Vous avez dit : ") + strlen(buffer) + 1;
				char response[len];
				strcpy(response, "Vous avez dit : ");
				strcat(response, buffer);
				// Un seul envoie permet de ne pas surcharger le réseau
				send(clientSocket, response, strlen(response), 0);
			}
		}
		if (isClosed == 1) {
			// La socket est fermé ou le client veut quitter le serveur !
			printf("Fermeture de la connexion avec le client\n");
			// Fermeture de la socket
			close(clientSocket);
			// On fait de la place dans le tableau
			clients[i] = -1;
		}
	}
}

On peut interroger notre serveur avec telnet (yum -y install telnet):

# telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Entrez 'exit' pour quitter
coucou
Vous avez dit : coucou

Serveur UDP

Si on essaye de reproduire le même type de serveur mais cette fois-ci avec une socket en UDP, cela devient beaucoup plus simple. En effet, en UDP, plus besoin de tenir un tableau avec les descripteurs des sockets des clients car... on est en mode déconnecté (datagramme) ! Entendez par la que les clients envoie un message sans attendre de réponse et sans se soucier de savoir si le message est arrivé au serveur.

#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <fcntl.h>

// Port d'écoute de la socket
#define PORT 8080
// Adresse d'écoute (toutes les adresses)
#define IP INADDR_ANY
// Taille de la file d'attente
#define BACKLOG 3
// Taille du tampon de lecture des messages
#define BUFFER_LEN 200

void initAdresse(struct sockaddr_in * adresse);
int initSocket(struct sockaddr_in * adresse);
void manageClient(int serverSocket);

int main(void) {
	// Structure contenant l'adresse
	struct sockaddr_in adresse;
	initAdresse(&adresse);
	// Descripteur de la socket du serveur
	int serverSocket = initSocket(&adresse);
	while (1) {
		// Descripteur de la socket du client, on attend une connexion
		manageClient(serverSocket);
	}
	return EXIT_SUCCESS;
}
// Initialisation de la structure sockaddr_in
void initAdresse(struct sockaddr_in * adresse) {
	(*adresse).sin_family = AF_INET;
	(*adresse).sin_addr.s_addr = IP;
	(*adresse).sin_port = htons( PORT);
}
// Démarrage de la socket serveur
int initSocket(struct sockaddr_in * adresse) {
	// Descripteur de socket
	int fdsocket;
	// Nombre d'options
	int opt = 1;
	// Création de la socket en UDP
	if ((fdsocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) {
		printf("Echéc de la création: %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	}
	printf("Création de la socket\n");
	// Paramètrage de la socket
	if (setsockopt(fdsocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
			sizeof(opt)) != 0) {
		printf("Echéc de paramètrage: %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	}
	printf("Paramètrage de la socket\n");
	// Attachement de la socket sur le port et l'adresse IP
	if (bind(fdsocket, (struct sockaddr *) adresse, sizeof(*adresse)) != 0) {
		printf("Echéc d'attachement: %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	}
	printf("Attachement de la socket sur le port %i\n", PORT);
	return fdsocket;
}
// On traite l'input des clients
void manageClient(int serverSocket) {
	// Création d'un tampon pour stocker les messages des clients dans la heap
	static char buffer[BUFFER_LEN + 1];
	// Structure contenant l'adresse du client
	struct sockaddr_in clientAdresse;
	unsigned int addrLen = sizeof(clientAdresse);
	// On vérifie si on a reçu un message
	int len = recvfrom(serverSocket, buffer, BUFFER_LEN, MSG_DONTWAIT,
			(struct sockaddr*) &clientAdresse, &addrLen);
	if (len == -1 && errno != EAGAIN) {
		printf("Problème de socket %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	} else if (len > 0) {
		printf("Message reçu de %s:%i\n", inet_ntoa(clientAdresse.sin_addr),
				ntohs(clientAdresse.sin_port));
		// Ajout du terminateur de chaîne
		buffer[len] = '\0';
		// On renvoie le texte au client dans un buffer assez grand
		len = strlen("Vous avez dit : ") + strlen(buffer) + 1;
		char response[len];
		strcpy(response, "Vous avez dit : ");
		strcat(response, buffer);
		// Un seul envoie permet de ne pas surcharger le réseau
		sendto(serverSocket, response, strlen(response), MSG_DONTWAIT,
				(struct sockaddr*) &clientAdresse, addrLen);
	}
}

On peut interroger notre serveur avec netcat (yum -y install nc):

# nc -u 127.0.0.1 8080
coucou
Vous avez dit : coucou