web-dev-qa-db-fra.com

Unix Domain Socket: Utilisation de la communication par datagramme entre un processus serveur et plusieurs processus clients

Je souhaite établir une connexion IPC entre plusieurs processus sous Linux. Je n’avais jamais utilisé de sockets UNIX auparavant et je ne sais donc pas si c’est la bonne approche de ce problème.

Un processus reçoit des données (non formatées, binaires) et les distribue via une socket AF_UNIX locale à l’aide du protocole de datagramme (c’est-à-dire similaire à UDP avec AF_INET). Les données envoyées par ce processus à un socket Unix local doivent être reçues par plusieurs clients écoutant sur le même socket. Le nombre de destinataires peut varier.

Pour ce faire, le code suivant permet de créer un socket et de lui envoyer des données (processus serveur):

struct sockaddr_un ipcFile;
memset(&ipcFile, 0, sizeof(ipcFile));
ipcFile.Sun_family = AF_UNIX;
strcpy(ipcFile.Sun_path, filename.c_str());

int socket = socket(AF_UNIX, SOCK_DGRAM, 0);
bind(socket, (struct sockaddr *) &ipcFile, sizeof(ipcFile));
...
// buf contains the data, buflen contains the number of bytes
int bytes = write(socket, buf, buflen);
...
close(socket);
unlink(ipcFile.Sun_path);

Cette écriture retourne -1 avec errno indiquant ENOTCONN ("Le noeud final de transport n'est pas connecté"). J'imagine que c'est parce qu'aucun processus de réception n'écoute actuellement ce socket local, n'est-ce pas?

Ensuite, j'ai essayé de créer un client qui se connecte à ce socket.

struct sockaddr_un ipcFile;
memset(&ipcFile, 0, sizeof(ipcFile));
ipcFile.Sun_family = AF_UNIX;
strcpy(ipcFile.Sun_path, filename.c_str());

int socket = socket(AF_UNIX, SOCK_DGRAM, 0);
bind(socket, (struct sockaddr *) &ipcFile, sizeof(ipcFile));
...
char buf[1024];
int bytes = read(socket, buf, sizeof(buf));
...
close(socket);

Ici, la liaison échoue ("Adresse déjà utilisée"). Alors, dois-je définir certaines options de socket, ou est-ce généralement une mauvaise approche?

Merci d'avance pour vos commentaires/solutions!

31
BigMick

Il y a une astuce pour utiliser des sockets datagramme Unix. Contrairement aux sockets de flux (domaine TCP ou UNIX), les sockets de datagramme ont besoin de points de terminaison définis pour le serveur ET le client. Lorsqu'on établit une connexion dans des sockets de flux, un système d'extrémité pour le client est créé implicitement par le système d'exploitation. Que cela corresponde à un port TCP/UDP éphémère ou à un inode temporaire pour le domaine Unix, le noeud final du client est créé pour vous. C'est pourquoi vous n'avez normalement pas besoin d'émettre un appel à bind () pour les sockets de flux dans le client.

La raison pour laquelle vous voyez "Adresse déjà utilisée" est parce que vous demandez au client de se lier à la même adresse que le serveur. bind() concerne l'affirmation d'une identité externe. Deux sockets ne peuvent normalement pas avoir le même nom.

Avec les sockets datagram, en particulier les sockets datagram du domaine, le client doit bind() sur son noeud final {propre}, puis connect() vers le noeud final du serveur. Voici votre code client, légèrement modifié, avec quelques autres goodies:

char * server_filename = "/tmp/socket-server";
char * client_filename = "/tmp/socket-client";

struct sockaddr_un server_addr;
struct sockaddr_un client_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.Sun_family = AF_UNIX;
strncpy(server_addr.Sun_path, server_filename, 104); // XXX: should be limited to about 104 characters, system dependent

memset(&client_addr, 0, sizeof(client_addr));
client_addr.Sun_family = AF_UNIX;
strncpy(client_addr.Sun_path, client_filename, 104);

// get socket
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);

// bind client to client_filename
bind(sockfd, (struct sockaddr *) &client_addr, sizeof(client_addr));

// connect client to server_filename
connect(sockfd, (struct sockaddr *) &server_addr, sizeof(server_addr));

...
char buf[1024];
int bytes = read(sockfd, buf, sizeof(buf));
...
close(sockfd);

À ce stade, votre socket doit être complètement configuré. Je pense que théoriquement, vous pouvez utiliser read()/write(), mais d'habitude j'utiliserais send()/recv() pour les sockets de datagramme.

Normalement, vous voudrez vérifier l'erreur après chacun de ces appels et émettre un perror() après. Cela vous aidera grandement lorsque les choses tournent mal. En général, utilisez un modèle comme celui-ci:

if ((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
    perror("socket failed");
}

Cela vaut pour pratiquement tous les appels système C.

La meilleure référence à cet égard est "Programmation réseau Unix" de Steven. Dans la 3ème édition, section 15.4, pages 415-419, montrent quelques exemples et énumère de nombreuses mises en garde.

À propos, en référence à

J'imagine que c'est parce qu'aucun processus de réception n'écoute actuellement ce socket local, n'est-ce pas?

Je pense que vous avez raison à propos de l'erreur ENOTCONN de write() sur le serveur. Un socket UDP ne devrait normalement pas se plaindre car il n'a aucune possibilité de savoir si le processus client est à l'écoute. Cependant, les sockets de datagramme de domaine unix sont différents. En fait, la write() bloquera si le tampon de réception du client est plein plutôt que de supprimer le paquet. Cela rend les sockets de datagramme de domaine unix bien supérieurs à UDP pour IPC car ce dernier va très certainement laisser tomber des paquets lorsqu'il est sous charge, même sur localhost. D'autre part, cela signifie que vous devez faire attention aux écrivains rapides et aux lecteurs lents.

37
adamlamar

La cause immédiate de votre erreur est que write() ne sait pas où vous voulez envoyer les données à . bind() définit le nom de votre côté de la prise - c'est-à-dire. d'où viennent les données de . Pour définir le côté de destination du socket, vous pouvez soit utiliser connect(); ou vous pouvez utiliser sendto() au lieu de write().

L'autre erreur ("Adresse déjà utilisée") est due au fait qu'un seul processus peut bind() à une adresse.

Vous devrez changer votre approche pour en tenir compte. Votre serveur devra écouter sur une adresse connue, définie avec bind(). Vos clients devront envoyer un message au serveur à cette adresse pour manifester leur intérêt pour la réception de datagrammes. Le serveur recevra les messages d'inscription des clients utilisant recvfrom() et enregistrera l'adresse utilisée par chaque client. Lorsqu'il souhaite envoyer un message, il devra parcourir tous les clients connus, en utilisant sendto() pour envoyer le message à chacun son tour.

Vous pouvez également utiliser la multidiffusion IP locale au lieu des sockets de domaine UNIX (les sockets de domaine UNIX ne prennent pas en charge la multidiffusion).

7
caf

Si la question était censée concerner la diffusion (si je comprends bien), selon unix (4) - famille de protocoles de domaine UNIX , la diffusion n'est pas disponible avec les sockets de domaine UNIX:

La famille de protocoles Unix Ns -domain ne prend pas en charge adresse de diffusion ou toute forme de "correspondance générique" sur les messages entrants. Toutes les adresses sont absolues ou noms de chemin relatifs des autres sockets Unix Ns -domain.

La multidiffusion pourrait être une option, mais j’ai le sentiment que ce n’est pas disponible avec POSIX, bien que Linux prenne en charge la multidiffusion UNIX Domain Socket .

Voir aussi: Introduction aux sockets Unix multicast .

2
Hibou57

Cela se produira à cause de le serveur ou le client meurt avant de dissocier/supprimer un fichier associé à bind (). Si l'un des clients/serveurs utilise ce chemin de liaison, essayez de réexécuter le serveur.

solutions: lorsque vous voulez lier à nouveau, vérifiez que le fichier est déjà associé puis dissociez-le Comment procéder: vérifiez tout d’abord l’accès à ce fichier par access (2); si oui unlink (2) it . met cette paix de code avant l'appel de bind (), la position est indépendante.

 if(!access(filename.c_str()))
    unlink(filename.c_str());

pour plus de référence lire unix (7)

0
Pintu Patel