J'écris un système de file d'attente de messages point à point, qui doit pouvoir fonctionner sur UDP. Je pourrais choisir arbitrairement un côté ou l'autre comme "serveur", mais cela ne semble pas tout à fait correct, car les deux extrémités envoient et reçoivent le même type de données de l'autre.
Est-il possible de lier () et de connecter () aux deux extrémités afin qu’elles n’envoient/reçoivent que les unes par les autres? Cela semble être une façon bien symétrique de le faire.
UDP étant sans connexion, le système d'exploitation n'a donc aucun intérêt à établir une connexion.
Dans les sockets BSD, on peut faire un connect
sur un socket UDP, mais cela ne fait que définir l'adresse de destination par défaut pour send
(au lieu de donner explicitement send_to
).
La liaison sur un socket UDP indique au système d'exploitation pour quelle adresse entrante accepter réellement les paquets (tous les paquets vers d'autres adresses sont supprimés), quel que soit le type de socket.
A la réception, vous devez utiliser recvfrom
pour identifier la source du paquet. Notez que si vous souhaitez une sorte d’authentification, utiliser uniquement les adresses concernées est aussi peu sûr qu’aucun verrouillage. Les connexions TCP peuvent être détournées et le protocole UDP nu contient littéralement une usurpation d'adresse IP. Vous devez ajouter une sorte de HMAC
Voici un programme qui montre comment lier () et connecter () sur le même socket UDP à un ensemble spécifique de ports source et de destination, respectivement. Le programme peut être compilé sur n’importe quelle machine Linux et a les utilisations suivantes:
usage: ./<program_name> dst-hostname dst-udpport src-udpport
J'ai testé ce code en ouvrant deux terminaux. Vous devriez pouvoir envoyer un message au nœud de destination et recevoir des messages de celui-ci.
Au terminal 1
./<program_name> 127.0.0.1 5555 5556
Au terminal 2
./<program_name> 127.0.0.1 5556 5555
Même si je l'ai testé sur une seule machine, je pense que cela devrait également fonctionner sur deux machines différentes une fois que vous avez configuré les paramètres de pare-feu appropriés.
Voici une description du flux:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define STDIN 0
int sendall(int s, char *buf, int *len)
{
int total = 0; // how many bytes we've sent
int bytesleft = *len; // how many we have left to send
int n;
while(total < *len) {
n = send(s, buf+total, bytesleft, 0);
fprintf(stdout,"Sendall: %s\n",buf+total);
if (n == -1) { break; }
total += n;
bytesleft -= n;
}
*len = total; // return number actually sent here
return n==-1?-1:0; // return -1 on failure, 0 on success
}
int main(int argc, char *argv[])
{
int sockfd;
struct addrinfo hints, *dstinfo = NULL, *srcinfo = NULL, *p = NULL;
int rv = -1, ret = -1, len = -1, numbytes = 0;
struct timeval tv;
char buffer[256] = {0};
fd_set readfds;
// don't care about writefds and exceptfds:
// select(STDIN+1, &readfds, NULL, NULL, &tv);
if (argc != 4) {
fprintf(stderr,"usage: %s dst-hostname dst-udpport src-udpport\n");
ret = -1;
goto LBL_RET;
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; //UDP communication
/*For destination address*/
if ((rv = getaddrinfo(argv[1], argv[2], &hints, &dstinfo)) != 0) {
fprintf(stderr, "getaddrinfo for dest address: %s\n", gai_strerror(rv));
ret = 1;
goto LBL_RET;
}
// loop through all the results and make a socket
for(p = dstinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("socket");
continue;
}
/*Taking first entry from getaddrinfo*/
break;
}
/*Failed to get socket to all entries*/
if (p == NULL) {
fprintf(stderr, "%s: Failed to get socket\n");
ret = 2;
goto LBL_RET;
}
/*For source address*/
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; //UDP communication
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
/*For source address*/
if ((rv = getaddrinfo(NULL, argv[3], &hints, &srcinfo)) != 0) {
fprintf(stderr, "getaddrinfo for src address: %s\n", gai_strerror(rv));
ret = 3;
goto LBL_RET;
}
/*Bind this datagram socket to source address info */
if((rv = bind(sockfd, srcinfo->ai_addr, srcinfo->ai_addrlen)) != 0) {
fprintf(stderr, "bind: %s\n", gai_strerror(rv));
ret = 3;
goto LBL_RET;
}
/*Connect this datagram socket to destination address info */
if((rv= connect(sockfd, p->ai_addr, p->ai_addrlen)) != 0) {
fprintf(stderr, "connect: %s\n", gai_strerror(rv));
ret = 3;
goto LBL_RET;
}
while(1){
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
FD_SET(sockfd, &readfds);
/*Select timeout at 10s*/
tv.tv_sec = 10;
tv.tv_usec = 0;
select(sockfd + 1, &readfds, NULL, NULL, &tv);
/*Obey your user, take his inputs*/
if (FD_ISSET(STDIN, &readfds))
{
memset(buffer, 0, sizeof(buffer));
len = 0;
printf("A key was pressed!\n");
if(0 >= (len = read(STDIN, buffer, sizeof(buffer))))
{
perror("read STDIN");
ret = 4;
goto LBL_RET;
}
fprintf(stdout, ">>%s\n", buffer);
/*EOM\n implies user wants to exit*/
if(!strcmp(buffer,"EOM\n")){
printf("Received EOM closing\n");
break;
}
/*Sendall will use send to transfer to bound sockfd*/
if (sendall(sockfd, buffer, &len) == -1) {
perror("sendall");
fprintf(stderr,"%s: We only sent %d bytes because of the error!\n", argv[0], len);
ret = 5;
goto LBL_RET;
}
}
/*We've got something on our socket to read */
if(FD_ISSET(sockfd, &readfds))
{
memset(buffer, 0, sizeof(buffer));
printf("Received something!\n");
/*recv will use receive to connected sockfd */
numbytes = recv(sockfd, buffer, sizeof(buffer), 0);
if(0 == numbytes){
printf("Destination closed\n");
break;
}else if(-1 == numbytes){
/*Could be an ICMP error from remote end*/
perror("recv");
printf("Receive error check your firewall settings\n");
ret = 5;
goto LBL_RET;
}
fprintf(stdout, "<<Number of bytes %d Message: %s\n", numbytes, buffer);
}
/*Heartbeat*/
printf(".\n");
}
ret = 0;
LBL_RET:
if(dstinfo)
freeaddrinfo(dstinfo);
if(srcinfo)
freeaddrinfo(srcinfo);
close(sockfd);
return ret;
}
Bonjour du futur lointain de l’année 2018 à l’année 2012.
Il y a en fait une raison derrière connect()
ing un socket UDP dans la pratique (bien que POSIX béni ne l'exige en théorie pas).
Un socket UDP ordinaire ne sait rien de ses destinations futures. Il effectue une recherche d'itinéraire chaque fois que sendmsg()
est appelé .
Cependant, si connect()
est appelé au préalable avec l'adresse IP et le port d'un destinataire distant particulier, le noyau du système d'exploitation pourra écrire la référence à la route et l'affecter à la socket , ce qui accélèrera considérablement l'envoi un message si les appels ultérieurs de sendmsg()
ne spécifient pas de destinataire ( sinon le paramètre précédent serait ignoré ), en choisissant celui par défaut.
Regardez le lignes 1070
à 1171
:
if (connected)
rt = (struct rtable *)sk_dst_check(sk, 0);
if (!rt) {
[..skip..]
rt = ip_route_output_flow(net, fl4, sk);
[..skip..]
}
Jusqu'au noyau 4.18 de Linux, cette fonctionnalité était principalement limitée à la famille d'adresses IPv4. Cependant, depuis la version 4.18-rc4 (et, espérons-le, de la version 4.18 du noyau Linux également), il est également entièrement fonctionnel avec les sockets IPv6 .
Cela peut être une source de { un avantage sérieux en termes de performances }, bien que cela dépende énormément du système d'exploitation que vous utilisez. Au moins, si vous utilisez Linux et n'utilisez pas le socket pour plusieurs gestionnaires distants, essayez-le.
La clé est vraiment connect()
:
Si le socket sockfd est de type SOCK_DGRAM, addr est l'adresse à laquelle les datagrammes sont envoyés par défaut et la seule adresse à partir de laquelle les datagrammes sont reçus.
Je n'ai pas utilisé connect () sous UDP. Je pense que connect () a été conçu pour deux objectifs totalement différents sous UDP vs TCP.
La page de manuel a quelques brefs détails sur l’utilisation de connect () sous UDP:
Généralement, les sockets avec protocole basé sur la connexion (comme TCP) peuvent se connecter () avec succès une seule fois; protocole sans connexion (comme UDP), les sockets peuvent utiliser connect () plusieurs fois pour changer leur association.
Cette page contient quelques informations utiles sur les sockets connectés et non connectés: http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch08lev1sec11.html
Cette citation répond à votre question:
Normalement, c’est un client UDP qui appelle, mais il existe des applications dans lesquelles le serveur UDP communique avec un seul client pendant une longue durée (par exemple, TFTP); dans ce cas, le client et le serveur peuvent appeler connect.
Il y a un problème dans votre code:
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; //UDP communication
/*For destination address*/
if ((rv = getaddrinfo(argv[1], argv[2], &hints, &dstinfo))
En utilisant uniquement AF_UNSPEC et SOCK_DGRAM, vous obtenez une liste de toutes les adresses possibles. Ainsi, lorsque vous appelez socket, l'adresse que vous utilisez peut ne pas être celle attendue selon UDP. Tu devrais utiliser
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE;
au lieu de cela, assurez-vous que le addrinfo que vous récupérez est ce que vous vouliez.
Dans un autre mot, le socket que vous avez créé peut ne pas être un socket UDP et c'est la raison pour laquelle il ne fonctionne pas.