tuto pratique
Directory
Programmation de sockets
ressources:
simple-client.c (base)
video explicative
tuto sockets
second tuto sockets
addrinfo
manPage addrinfo
fameux Beej’s guide to network programming
Le même mais traduit en français
tutorialsPoint
best
Qu’est-ce qu’un socket?
Un socket autorise la communication entre deux process différents.
- Sur la même machine
- Entre des machines distantes
Sur Unix, toutes les actions I/O sont faites en écrivant ou en lisant un descripteur de fichier.
L’entrée standard STDIN, la sortie standard STDOUT et l’erreur standard STDERR sont les trois descripteurs de fichier POSIX standard pour tout processus qui n’est pas un daemon.
wikipedia: descripteur de fichiers
Un descripteur de fichier est un entier int
associé avec un fichier ouvert. Ce fichier peut être:
- Une connexion réseau
- Un fichier texte
- Un terminal
- …
D’un point de vue programmatique, un socket ressemble et se comporte comme un descripteur de fichier. On utilise sur lui des fonctions comme read()
et write()
qui fonctionnent de la même façon avec un socket que sur des fichiers ou sur des pipes.
2. Contexte d’utilisation
Un socket Unix est utilisé dans un contexte client-serveur. Un Serveur est un process qui effectue des actions (fonctions) à la demande d’un client. La plupart des protocoles de la couche application (FTP, SMTP, POP3…) utilisent des sockets pour:
- établir une connexion entre client et serveur
- échager des donnnées
3. Types de sockets
types de sockets sont disponible pour les utilisateurs.
Stream Sockets (TCP)
- fiables
- orienté connexion
- pas de limite de taille
- message d’erreur en cas d’échec de l’envoi
Datagram Sockets (UDP)
- non-fiable
- non-orienté connexion (pas de phase connexion comme en TCP, construction d’un paquet avec les coordonnées de la destination et puis envoi)
Raw Sockets
- utilisés dans le developpement de protocoles de communication …
- … et le hacking
Les process sont théoriquement censés communiquer uniquement entre sockets de même type mais il n’y a aucune restriction qui empêche la communication entre sockets de type différent.
4. Modèle “Client Server”
La majorité des applications d’Internet utilisent ce Modèle il se réfère à: Deux process ou deux applications qui communiquent en échangeant des informations. L’un des deux process agit comme un client et l’autre comme un serveur.
Processus Client
Typiquement celui qui fait la requête d’informations. Après avoir reçu la résponse, ce process soit se termine soit continue à faire des traitements.
exemple: Un navigateur internet est une application client qui envoie une requête à un serveur Web pour recevoir une page HTML.
Processus Serveur
C’est le process qui reçoit les requêtes de la part des clients. Après avoir reçu une requête d’un client, ce process va effectuer le traitement demandé (rassembler des informations, et les renvoyer au client). Une fois ces taches éffectuées le process est disponible pour servir un autre client. Les process serveurs sont toujours prêts à servir les requêtes qui leurs parviennent.
exemple: Un serveur web est toujours entrain d’attendre des requêtes de navigateurs internet. Aussi tôt qu’un client (navigateur) lui fait parvenir une requête, il lui renvoie la page demandée.
Un client a besoin de connaitre l’adresse d’un serveur mais pas l’inverse.
Types de serveurs
-
Serveurs itératifs: forme la plus simple. Le serveur gère un client à la fois et les clients attendent pour s’y connecter.
-
Serveurs concurents (Concurrent(en)): gènre plusieurs processus en parallèle et permet de servire plusieurs requêtes à la fois. La forme la plus simple pour implémenter un serveur concurent sous Unix est de FORKER un processus enfant pour prendre en charge chaque client.
Interaction entre client et serveur
Ce schéma décrit l’intéraction complète entre un client et un serveur.
5. Client HTTP
a. Création de socket
La première chose à faire est de créer un socket. C’est le rôle de la fonction socket
.
#include<stdio.h>
#include<sys/socket.h>
int main(int argc, char **argv)
{
// variables
int socket_desc;
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc == -1)
{
printf("Could not create socket");
}
return 0;
}
La fonction socket()
crée un socket et retourne un descripteur qui peut être utilisé dans d’autres fonctions. Le code précédent crée un socket avec les propriétés suivantes:
Address Family - AF_INET (ipv4)
Type - SOCK_STREAM (protocol TCP)
Protocol - 0 [ou IPPROTO_IP c'est le protocol IP]
- type
SOCK_STREAM
désigne TCP- type
SOCK_DGRAM
désigne UDP
b. Connecter un socket à un serveur
Pour se connecter à un serveur nous avons donc besoins de deux choses:
- adresse IP
- numéro de port
Pour se connecter à un serveur distant il nous faut également d’autres choses.
La première est de créer une structure sockaddr_in
avec les bonnes valeures:
Voici à quoi ressemble cette structure:
// IPv4 AF_INET sockets:
struct sockaddr_in {
short sin_family; // AF_INET pour ipv4 et AF_INET6 pour ipv6
unsigned short sin_port; // port
struct in_addr sin_addr; // voir in_addr plus bas
char sin_zero[8]; // optionnel
};
struct in_addr {
unsigned long s_addr; // load with inet_pton()
};
struct sockaddr {
unsigned short sa_family; // address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
sockaddr_in
possède un membre appelé sin_addr
de type in_addr
qui a un membre s_adr
qui n’est rien d’autre qu’un long
.
La fonction inet_addr
est très pratique dans le sens où elle converti une adresse IP en un type long. Voici comment faire:
server.sin_addr.s_addr = inet_addr("74.125.235.20"); // ip de google
Il est donc nécéssaire de connaitre l’adress IP du serveur distant qu’on veut connecter.
La dernière chose dont on a besoin c’est de la fonction connect
qui prend en paramètre un socket et la structure sockaddr
.
#include<stdio.h>
#include<sys/socket.h>
#include<arpa/inet.h> // inet_addr
int main(int argc, char **argv)
{
// variables
int socket_desc;
// creation d'un objet de type sockaddr_in
struct sockaddr_in server;
// creation du socket -> socket_desc
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc == -1)
{
printf("Could not create socket");
}
// initialisation des membres de l'objet server
server.sin_addr.s_addr = inet_addr("172.217.22.78");
server.sin_family = AF_INET;
server.sin_port = htons(80);
// connexion a un serveur distant
if (connect(socket_desc, (struct sockaddr * )&server, sizeof(server)) < 0)
{
puts("connect error");
return 1;
}
puts("Connected");
return 0;
}
Les connexions ne sont nécéssaires uniquement dans le cas du l’utilisation de TCP
Le concepte de connexion ne s’applique que dans le cas de l’utilisation de SOCK_STREAM (socket TCP
). Une connexion veut dire “reliable stream of data” de telle façon qu’on puisse avoir plusieurs de ces “stream” qui effectue chacun une communication.
think of this as a pipe which is not interfered by other data.
D’autres sockets comme UDP, ICMP ou ARP par exemple n’ont pas besoin de se connecter. Se sont des protocoles non-orienté connexion.
c. Envoyer des données
La fonction send
nous permet d’envoyer des données. Elle a besoin en paramètre du descripteur de socket, la donnée à envoyer et sa taille.
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h> // inet_addr
int main(int argc, char **argv)
{
// variables
int socket_desc;
char* message;
// creation d'un objet de type sockaddr_in
struct sockaddr_in server;
// creation du socket -> socket_desc
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc == -1)
{
printf("Could not create socket");
}
// initialisation des membres de l'objet server
server.sin_addr.s_addr = inet_addr("172.217.22.78");
server.sin_family = AF_INET;
server.sin_port = htons(80);
// connexion a un serveur distant
if (connect(socket_desc, (struct sockaddr * )&server, sizeof(server)) < 0)
{
puts("connect error\n");
return 1;
}
puts("Connected\n");
// envoie de donnees
message = "GET / HTTP/1.1\r\n\r\n";
if (send(socket_desc, message, strlen(message), 0) < 0)
{
puts("Send failed\n");
return 1;
}
puts("Data Send\n");
return 0;
}
Dans cette exemple, dans un premier temps nous nous connectons à l’adresse IP et ensuite envoyons notre chaîne de caractères. Ce message est une commande HTTP pour fetch la mainpage d’un site web.
Maintenant que nous avons envoyé des données, il faut faire en sorte de recevoir une réponse du serveur.
Quand on envoie des données à un socket, de façon similaire à écrire dans un fichier, nous écrivons dans le socket, par exemple nous pouvons utiliser la fonction
write
pour envoyer des données à un socket.
d. Recevoir des données sur un socket.
La fonction recv
est utilisée pour recevoir des données sur un socket. Dans le prochain exemple, nous allons envoyer le même message quand dans le précédent exemple et recevoir une réponse du serveur.
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h> // inet_addr
#include<unistd.h> // close
int main(int argc, char **argv)
{
// variables
int socket_desc;
char* message;
char server_reply[2000];
// creation d'un objet de type sockaddr_in
struct sockaddr_in server;
// creation du socket -> socket_desc
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc == -1)
{
printf("Could not create socket");
}
// initialisation des membres de l'objet server
server.sin_addr.s_addr = inet_addr("172.217.22.78");
server.sin_family = AF_INET;
server.sin_port = htons(80);
// connexion a un serveur distant
if (connect(socket_desc, (struct sockaddr * )&server, sizeof(server)) < 0)
{
puts("connect error\n");
return 1;
}
puts("Connected\n");
// envoie de donnees
message = "GET / HTTP/1.1\r\n\r\n";
if (send(socket_desc, message, strlen(message), 0) < 0)
{
puts("Send failed\n");
return 1;
}
// recevoir une reponse du serveur
if (recv(socket_desc, server_reply, 2000, 0) < 0)
{
puts("recv failed");
}
puts("Reply received\n");
puts(server_reply);
puts("Data Send\n");
close(socket_desc)
return 0;
}
output:
Connected
Reply received
HTTP/1.1 302 Found
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Referrer-Policy: no-referrer
Location: http://www.google.ch/?gfe_rd=cr&ei=9oMsWbfPHJOT8QfFgK2IBg
Content-Length: 258
Date: Mon, 29 May 2017 20:26:30 GMT
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.ch/?gfe_rd=cr&ei=9oMsWbfPHJOT8QfFgK2IBg">here</A>.
</BODY></HTML>
Data Send
Nous voyons ici la réponse de google qui nous envoie le contenu de la page demandé.
Quand nous recevons des données sur un socket, nous lisons les données dans le socket comme nous le ferions pour un simple fichier. Nous pourions utiliser la fonction
read
pour lire des données sur un socket.
read(socket_desc, server_reply, 2000);
Maintenant que nous avons reçu notre réponse, nous pouvons fermer le socket (fichier).
e. Fermeture de socket
Je ne l’ai pas dit dans le précédent exemple mais j’ai également ajouter la fonction close
qui vit dans le header <unistd.h>
a la fin de notre main.
close(socket_desc)
Voici une capture wireshark de l’échange:
f. Récapitulatif
- Création d’un socket
- Connexion à un serveur distant
- Envoie de données
- Réception de données
- Fermeture du socket
C’est exactement ce que fait notre navigateur quand nous ouvrons la page www.google.com Ce genre d’utilisation d’un socket est appelé socket client.
Un client est une application qui se connecte sur un serveur pour y fetcher des données.
L’autre genre d’application socket sont donc naturellement les sockets serveurs. Un serveur est un système qui utilise des sockets pour recevoir des connexions et fournir des données par ce biais.
Dans notre exemple www.google.com est un serveur HTTP et notre navigateur est client HTTP.
Avant de faire notre propre serveur, nous allons passer par quelques explications d’ordre général sur la programmation de sockets.
6. Trouver l’ip d’un domaine
Quand on se connecte à un hôte distant, il est nécéssaire d’avoir son adresse IP. C’est là que la fonction gethostbyname
nous est utile. Elle prend un nom de domaine comme paramètre et retourne une structure de type hostent
et contient l’IP que l’on cherche. gethostbynam
vit dans le header <netdb.h>
.
Voici cette structure:
struct hostent
{
char *h_name; // Official name of host.
char **h_aliases; // Alias list.
int h_addrtype; // Host address type.
int h_length; // Length of address.
char **h_addr_list; // List of addresses from name server.
};
h_addr_list contient les adresses pour joindre notre hôte.
#include<stdio.h> //printf
#include<string.h> //strcpy
#include<sys/socket.h>
#include<netdb.h> //hostent
#include<arpa/inet.h>
int main(int argc, char **argv)
{
char *hostname = "www.google.com";
char ip[16];
struct hostent *he;
struct in_addr **addr_list;
if ((he = gethostbyname(hostname)) == NULL)
{
herror("gethostbyname");
return 1;
}
addr_list = (struct in_addr ** )he->h_addr_list;
strcpy(ip, inet_ntoa(*addr_list[0]));
printf("%s resolved to : %s\n", hostname, ip);
return 0;
}
output:
www.google.com resolved to : 216.58.205.164
Cette fonction très pratique nous permet de résoudre un nom de domaine en adresse IP et ensuite nous pouvons utiliser l’adresse IP pour créér une connexion à l’aide d’un Socket.
La fonction inet_ntoa
converti une adresse IP stoquée dans un long (type) en notation à point.
Récapitulatif des structures
Nous avons vu quelques structure importantes qui reviennent tout le temps dans le domaine de la progrmtion de sockets:
sockaddr_in
: information de connexion principalement utilisées par les fonctionsconnect
,send
,recv
in_addr
: adresse IP au format long (type)- sockaddr : informations sur le socket
hostent
: l’IP de l’hôte utilisé par la fonctiongethostbyname
7. Serveur HTTP
a. cycle de vie
Passons à un serveur. Les sockets serveur fonctionne de la façon suivante:
- création d’un socket
- bind (lie) du socket à une adresse(et un port)
- écoute du port en attente d’une connexion
- acceptation de la connexion
- lecture/envoi
b. implémentation
Nous savons déja comment créer un socket donc nous allons directement passer à la seconde étape.
La fonction bind
sert à lier un socket à un couple adresse port spécifique. Tout comme la fonction connect
elle a besoin d’une structure sockaddr_in
.
#include<stdio.h>
#include<sys/socket.h>
#include<arpa/inet.h> // inet_addr
int main(int argc, char **argv)
{
// variables
int socket_desc, new_socket, c;
struct sockaddr_in server, client;
// creation de socket
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc == -1)
{
printf("\nCould not create socket\n");
}
// initialisation des membres de l'objet server
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons (8888);
// bind
if (bind(socket_desc, (struct sockaddr * )&server, sizeof(server)) < 0)
{
puts("\nbind failded\n");
}
puts("\nbind done\n");
// ecoute
listen(socket_desc, 3);
// Accept incoming connection
puts("Waiting for incoming connections...\n");
c = sizeof(struct sockaddr_in);
new_socket = accept(socket_desc, (struct sockaddr * )&client, (socklen_t*)&c);
if (new_socket < 0)
{
perror("accept failed\n");
}
puts("Connection accepted\n");
return 0;
}
On lance le programme dans un premier terminal:
sol@debian:~/code/sockets/serveurHTTP$ ./serveur
bind done
Waiting for incoming connections...
dans un second terminal on lancer un telnet localhost 888
:
sol@debian:~/LABOS/labo5/03Trace$ telnet localhost 8888
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Connection closed by foreign host.
Dans le premier terminal un nouveau message apparait avant de nous rendre le prompt:
sol@debian:~/code/sockets/serveurHTTP$ ./serveur
bind done
Waiting for incoming connections...
Connection accepted
sol@debian:~/code/sockets/serveurHTTP$
c. IP et port du client qui se connecte
Pour avoir l’IP du client et le port de connexion en utilisant les informations de la structure sockaddr_in
qu’on a passé en argument de la fonction accept
:
char *client_ip = inet_ntoa(client.sin_addr);
int client_port = ntohs(client.sin_port);
d. rendre tout ça plus productif
Dans cet exemple nous acceptons une connexion mais la refermons directement. Ce n’est pas très interessant.
Répondons donc quelque chose au client qui se connecte à nous dire faire genre on est pas totalement innutile.
On peut utiliser la fonction write
pour écire qqch dans le socket qui se connecte à nous et le client devrait le voir.
Il suffit d’ajouter ce bout de code à la fin de notre main en prenant soins d’ajouter les header <string.h>
et <unistd.h>
// repond au client
char *message = "Salut client, je te sens bien en moi mais k thx bye!\n.";
write(new_socket, message, strlen(message));
Sur son terminal, le client verra:
sol@debian:~/LABOS/labo5/03Trace$ telnet localhost 8888
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Salut client, je te sens bien en moi mais k thx bye!
.Connection closed by foreign host.
4. Serveur live (multiprocessus)
Notre serveur coupe la connexion directement après avoir envoyé sa réponse. Un serveur comme google.com est toujours à l’écoute de nouvelles connexions entrantes.
Celà veut dire que ce serveur est sensé tourner et accepter des connexions non-stop. Pour y arriver, la façon la plus facile est de mettre la fonction accept
dans une boucle.
#include<stdio.h>
#include<string.h> //strlen
#include<sys/socket.h>
#include<arpa/inet.h> //inet_addr
#include<unistd.h> //write
int main(int argc, char **argv)
{
// variables
int socket_desc, new_socket, c;
struct sockaddr_in server, client;
char* message;
// creation de socket
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc == -1)
{
printf("\nCould not create socket\n");
}
// initialisation des membres de l'objet server
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons (8888);
// bind
if (bind(socket_desc, (struct sockaddr * )&server, sizeof(server)) < 0)
{
puts("\nbind failded\n");
}
puts("\nbind done\n");
// ecoute
listen(socket_desc, 3);
// Accepte les connexions
puts("Waiting for incoming connections...\n");
c = sizeof(struct sockaddr_in);
while ( (new_socket = accept(socket_desc, (struct sockaddr * )&client, (socklen_t * )&c)) )
{
// affiche les infos du client
printf("Connection from %s:%d accepted\n",inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// repond au client
message = "Salut client, je te sens bien en moi mais k thx bye!\n.";
write(new_socket, message, strlen(message));
}
if (new_socket < 0)
{
perror("accept failed\n");
}
return 0;
}
En décortiquant le screenshot, nous pouvons voir que le Serveur tourne en permanance et accepte toutes les connexions telnet qu’on lui propose. Une fois qu’on coupe le serveur, les 3 connexions cessent automatiquement (logique).
5. Serveur TCP de base avec fork() des process enfants
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h> // strlen
#include<arpa/inet.h> // inet_addr
#include<unistd.h> // write
void childProcess(int sock);
int main (int argc, char **argv) {
int socket_desc;
int new_socket;
int n, pid;
char buffer[256];
struct sockaddr_in server;
struct sockaddr_in client;
// creation du socket
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc == -1) {
perror("ERROR opening socket");
exit(1);
}
// initialisation des membres de l'objet server
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(8888);
// bind
if (bind(socket_desc, (struct sockaddr * )&server, sizeof(server)) < 0) {
perror("ERROR on binding");
exit(1);
}
printf("\nbind done\n");
// sleep mode en attendant un client
listen(socket_desc, 3);
printf("\nwaiting for incoming connections...\n");
n = sizeof(struct sockaddr_in);
// boucle infinie qui accepte et fork les clients distant
while(1) {
new_socket = accept(socket_desc, (struct sockaddr *) &client, (socklen_t *) &n);
if (new_socket < 0) {
perror("ERROR on accept");
exit(1);
}
// affiche les infos du client
printf("Connection from %s:%d accepted\n",
inet_ntoa(client.sin_addr), // conversion adr en un format Inet Std dot Ntation (str)
ntohs(client.sin_port)); // conversion Network Byte Order vers Host Byte Order (endianess)
// creation process enfant
pid = fork();
if (pid < 0) {
perror("ERROR on fork");
exit(1);
}
if (pid == 0) {
// processus du client
close(socket_desc);
childProcess(new_socket);
exit(0);
}
else {
close(new_socket);
}
}
return 0;
}
void childProcess(int sock) {
int n;
char buffer[256];
bzero(buffer,256);
// lecture du message du client
n = read(sock, buffer, 255);
if (n < 0) {
perror("ERROR reading from socket\n");
exit(1);
}
// affichage message client
printf("Message from client: %s\n", buffer);
// ecrit une reponse au client
n = write(sock, "ok", 18);
if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}
}
6. client TCP basique
#include<stdio.h>
#include<stdlib.h>
#include<netdb.h>
#include<unistd.h>
#include<string.h>
int main (int argc, char **argv) {
int socket_desc;
int portno;
int n;
struct sockaddr_in server_addr;
struct hostent *server;
char buffer[256];
if (argc < 3) {
fprintf(stderr, "usage %s hostname port\n", argv[0]);
exit(0);
}
portno = atoi(argv[2]);
// creation socket
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (socket_desc < 0) {
perror("ERROR opening socket");
exit(1);
}
server = gethostbyname(argv[1]);
if (server == NULL) {
fprintf(stderr, "ERROR, no such host\n");
exit(0);
}
bzero(( char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
bcopy(( char *)server->h_addr, (char *) &server_addr.sin_addr.s_addr, server->h_length);
server_addr.sin_port = htons(portno);
// connexion au serveur
if (connect(socket_desc, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
perror("ERROR connecting");
exit(1);
}
// demande a l'utilisateur d'ecrire un message qui sera affiche chez le serveur
printf("Please enter a message: ");
bzero(buffer,256);
fgets(buffer,255,stdin);
// envoie du message
n = write(socket_desc, buffer, 255);
if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}
printf("%s\n", buffer);
return 0;
}