Sockets: les communications réseaux

Guillaume Chanel

Remerciements à Jean-Luc Falcone

Sockets

Architecture Réseau

4 couches réseau

Sockets

  • Abstraction d'un canal de communication à travers le réseau
  • Descripteur de fichier:
    • Lecture avec read()
    • Ecriture avec write()
    • Pas d'accès aléatoire possible
  • Modèle client-serveur

Domaine d'adressage

  • Un socket est peut-être lié à une adresse.
  • Il existe deux domaines d'adressages:
    internet domaincommunication réseau (AF_INET)
    unix domaincommunication locale (AF_UNIX)

Types de sockets

Il existe plusieur types de sockets dont:

par flotavec connection, par exemple TCP (SOCK_STREAM)
par datagrammesans connection, par exemple UDP (SOCK_DGRAM)
brutsans protocol de transport (SOCK_RAW)

Nous couvrirons surtout le premier type.

Adresses Internet

Adressage Internet

L'adresse est composée d'une adresse IP et d'un numéro de port (16bits).

Structures IPv4 (man 7 ip)

struct sockaddr_in {
    sa_family_t     sin_family; /* famille: AF_INET */
    in_port_t       sin_port;   /*port: big-endian */
    struct in_addr  sin_addr;   /* adresse internet */
};

struct in_addr {
    uint32_t        s_addr;     /*adresse ip: big-endian
}
                        

Obtenir une adresse IP (inet_pton)

La fonction suivante permet d'obtenir une adresse IP valide:

int inet_pton(int af, const char *src, void *dst);
afFamille d'adresse soit AF_INET, soit AF_INET6
srcla représentation de l'adresse (par exemple 192.168.1.1).
dstun pointeur vers une structure in_addr ou in6_addr à initialiser.

Retourne:

1succès
0adresse non-valide
-1famille non-valide

Obtenir la représentation d'une adresse IP (inet_ntop)

La fonction suivante permet d'obtenir la représentation d'une adresse IP:

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
afFamille d'adresse soit AF_INET, soit AF_INET6
srcun pointeur vers une structure in_addr ou in6_addr initialisée.
dstun pointeur vers un buffer (pour obtenir la représentation).
sizela taille du buffer

Retourne NULL en cas d'erreur (cf errno)

Obtenir un numéro de port valide (htons)

On peut convertir un entier, en un numéro de port valide grâce à:

uint16_t htons(uint16_t hostshort); //les ports sont codés sur 16bits
uint32_t htonl(uint32_t hostlong) //pour des valeurs codées sur 32bits

Le résultat est dans le bon byte-order (Big-Endian).

Clients

Client TCP

Le fonctionement d'un client TCP est le suivant:

  1. Crée un socket (socket)
  2. Connecte un socket à un serveur (connect)
  3. Lecture/Ecriture à parir du socket (read/write)
  4. Ferme le socket (close)

Créer un socket (socket)

On utilise pour créer un socket, l'appel système:

int socket(int domain, int type, int protocol);
domainfamille d'adresse (AF_INET1, AF_INET61, AF_UNIX, …)
typetype de communication (SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, …)
protocolprotocol de transport, passer 0 pour UDP et TCP

Retourne, soit un descripteur de fichier, soit -1 (cf. errno).

1. ici PF_INET devrais être utilisé mais AF_INET est toléré et souvent utilisé à la place (voir le man)

Initier une connection (connect)

L'appel système suivant permet d'initier une connection:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfddescripteur de fichier du socket
addrun pointeur vers une adresse
addrlenla longueur de la structure

Retourne 0 en cas de succès, et -1 sinon (cf. errno).

Exemple de client TCP


struct sockaddr_in address;
memset( &address, 0, sizeof(address) );
inet_pton( AF_INET, "192.168.1.1", &(address.sin_addr) );
address.sin_family = AF_INET;
address.sin_port = htons(8080);

int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, (struct sockaddr *) &address, sizeof(address));
...
read(sock, ...)
write(sock, ...)
                    

Serveurs

Serveur TCP

Le fonctionement d'un serveur TCP est le suivant:

  1. Crée un socket serveur (socket)
  2. Attache le socket serveur à une adresse (bind)
  3. Configure le socket comme socket d'écoute (listen)
  4. Accepte une connection et obtient un socket client (accept)
  5. Lecture/Ecriture à parir du socket client (read/write)
  6. Ferme le socket client (close)
  7. Répète l'étape 4 si nécéssaire
  8. Ferme le socket serveur (close)

Créer un socket (socket)

On utilise l'appel système socket pour créer un socket serveur (cf. client).

Lier un socket à une adresse (bind)

On lie un socket à une adresse (interface locale) avec l'appel système:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfdle descripteur du socket
addrun pointeur vers l'adresse à lier.
addrlenla taille de la structure d'adresse.

Retourne 0 en cas de succès, -1 sinon (cf. errno)

Ecouter les connection (listen)

L'appel système suivant, permet de marquer un socket comme étant passif, c'est à dire un socket permettant d'accepter des connections:

int listen(int sockfd, int backlog);
sockfdle descripteur du socket
backlogtaille maximum de la queue de connections en attente.

Retourne 0 en cas de succès, -1 sinon (cf. errno)

Accepter les connections (accept)

L'appel système suivant permet d'accepter une connection:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfdDescripteur du socket (doit être passif).
addrStructure garnie avec les informations du client.
addrlenLongueur de la structure.
  • Retourne un nouveau descripteur de fichier permettant de communiquer avec le client en cas de succès et -1 sinon (cf. errno)
  • Bloque jusqu'à la prochaine connexion entrante ou en extrait une de la queue.

Exemple de serveur TCP (1)


struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(8080);

int sock = socket(AF_INET, SOCK_STREAM, 0);
bind( sock, (struct sockaddr *) &address, sizeof(address) );
listen(sock, 5);

while( 1 ) {
    struct sockaddr_in clientAddress;
    unsigned int clientLength = sizeof(clientAddress);
    int clientSock = accept(serverSock,
                            (struct sockaddr *) &clientAddress,
                            &clientLength);

    /* Lectures/Ecritures sure clientSock (read/write) */

    close( clientSock );
}
                    

Remarque

  • L'utilisation de INADDR_ANY permet de se lier à toutes les interfaces réseaux de la machine.
  • On utilise la fonction htonl pour obtenir une adresse numérique valide:
address.sin_addr.s_addr = htonl(INADDR_ANY)

Exemple: a file server

Problèmes

Dans l'exemple précédent, il faudrait:

  • Avertir le client qui demande un fichier qui n'existe pas.
  • Permettre au client d'obtenir la liste des fichiers disponibles.

  • Nécéssité de définir un protocole
Attention

L'exemple précédent contient une faille de sécurité importante.

Quasi-Universels

  • Les sockets sont implémentés au niveau de l'OS, par presque tous les OS.
  • La grande majorité des languages de programation ont une librairie permettant d'utiliser les sockets.

  • Les sockets permettent la communication entre processus écrits dans des langages différents.
  • Les sockets permettent la communication entre OS différents.

Sockets en Python


#CLIENT
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("www.mcmillan-inc.com", 80))

#SERVEUR
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((socket.gethostname(), 80))
serversocket.listen(5)
                    

Sockets en Java


try {
    serverSocket = new ServerSocket(4444);
}
catch (IOException e) {
    System.out.println("Could not listen on port: 4444");
    System.exit(-1);
}
Socket clientSocket = null;
try {
    clientSocket = serverSocket.accept();
}
catch (IOException e) {
    System.out.println("Accept failed: 4444");
    System.exit(-1);
}
                    

Sockets en Scheme


(define (id-server)
   (let ((socket (open-socket)))
    (display "Waiting on port ")
    (display (socket-port-number socket))
    (newline)
    (let loop ((next-id 0))
      (call-with-values
	(lambda ()
	  (socket-accept socket))
	 (lambda (in out)
	  (display next-id out)
	  (close-input-port in)
	  (close-output-port out)
	  (loop (+ next-id 1)))))))
                    

Autres fonctions utiles

Send/Receive

On peut utiliser les appels systèmes suivant à la place de read et de write lorsqu'on utilise des sockets:


ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
                        
  • Le paramètre flags permet de passer des paramètres supplémentaires pour contrôler finemement la transmission.
  • recv(sockfd, buf, len, 0) est équivalent à read(sockfd, buf, len)
  • send(sockfd, buf, len, 0) est équivalent à write(sockfd, buf, len)

Sendfile (non POSIX)

Sous Linux, on peut remplacer une paire read/write ou send/recv, par un appel à sendfile qui permet de rester dans l'espace du noyau:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
  • out_fd est le descripteur ouvert en écriture (devait être un socket jusqu'à Linux 2.6.33)
  • in_fd est le descripteur ouvert en lecture (ne peut pas être un socket)
  • offset représente l'offset en lecture (peut être NULL)
  • count taille à envoyer.

UDP

Différences avec TCP:

Clientpas besoin d'établir une connection.
Serveurpas besoin d'écouter et d'accepter une connection.
Lectures/Ecritures

Comme on n'établit pas de connection, on utilisera les appels systèmes suivants:

sendtopour envoyer des données vers une adresse.
recvfrompour recevoir des données depuis une adresse.

Unix Socket

  • Un socket Unix permet d'établir une communication locale entre deux processus au moyen d'un inode.
  • Il faut passer le domaine AF_UNIX à l'appel système socket.
  • L'adressage est différent, mais tout le reste est identique aux sockets internet.
Adresse Unix (man 7 unix)

#define UNIX_PATH_MAX    108
struct sockaddr_un {
    sa_family_t sun_family;        /* Famille: AF_UNIX*/
    char sun_path[UNIX_PATH_MAX];  /* Chemin sur le systeme de fichiers */
};
                        

Résolution des noms de domaine

La fonction suivante permet de résoudre le nom de domaine d'un service:


int getaddrinfo(const char *node,
                const char *service,
                const struct *hints,
                struct addrinfo **res);
Structure addrinfo

struct addrinfo {
    int              ai_flags;
    int              ai_family;
    int              ai_socktype;
    int              ai_protocol;
    socklen_t        ai_addrlen;
    struct sockaddr *ai_addr;
    char            *ai_canonname;
    struct addrinfo *ai_next; // This structure is a linked list
};