Ich schreibe ein Point-to-Point-Message-Queue-System, das über UDP arbeiten kann. Ich könnte die eine oder andere Seite willkürlich als "Server" auswählen, aber es scheint nicht ganz richtig zu sein, da beide Enden den gleichen Datentyp von der anderen Seite senden und empfangen.
Ist es möglich, beide Enden zu binden () und zu verbinden (), so dass sie nur voneinander senden/empfangen? Das scheint eine sympathische Art zu sein.
UDP ist verbindungslos. Daher ist es für das Betriebssystem wenig sinnvoll, eine Verbindung herzustellen.
In BSD-Sockets kann eine connect
für einen UDP-Socket ausgeführt werden. Dies setzt jedoch im Wesentlichen nur die Standardzieladresse für send
(stattdessen explizit send_to
anzugeben).
Die Bindung an einen UDP-Socket teilt dem Betriebssystem mit, für welche eingehende Adresse tatsächlich Pakete angenommen werden sollen (alle Pakete werden an andere Adressen verworfen), unabhängig von der Art des Sockets.
Nach Erhalt müssen Sie recvfrom
verwenden, um zu ermitteln, von welcher Quelle das Paket stammt. Wenn Sie eine Art Authentifizierung wünschen, ist die Verwendung nur der betroffenen Adressen so unsicher wie überhaupt keine Sperre. TCP Verbindungen können gekapert werden und nackte UDP hat IP-Spoofing buchstäblich über den Kopf geschrieben. Sie müssen eine Art HMAC hinzufügen
Hier ist ein Programm, das demonstriert, wie bind () und connect () auf demselben UDP-Socket an einen bestimmten Satz von Quell- bzw. Zielports angeschlossen werden. Das Programm kann auf jeder Linux-Maschine kompiliert werden und hat folgende Verwendung:
usage: ./<program_name> dst-hostname dst-udpport src-udpport
Ich habe diesen Code beim Öffnen zweier Terminals getestet. Sie sollten in der Lage sein, eine Nachricht an den Zielknoten zu senden und Nachrichten von diesem zu empfangen.
Im Terminal 1-Lauf
./<program_name> 127.0.0.1 5555 5556
In Terminal 2 ausführen
./<program_name> 127.0.0.1 5556 5555
Obwohl ich es auf einem einzelnen Computer getestet habe, sollte es nach der Einrichtung der korrekten Firewall-Einstellungen auch auf zwei verschiedenen Computern funktionieren
Hier ist eine Beschreibung des Flusses:
#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;
}
Hallo aus der fernen Zukunft, die das Jahr 2018 ist, bis zum Jahr 2012.
Es gibt tatsächlich einen Grund für connect()
ing eines UDP-Sockets in der Praxis (obwohl der gesegnete POSIX in der Theorie dies nicht erfordert).
Ein gewöhnlicher UDP-Socket weiß nichts über seine zukünftigen Ziele, so dass jedes Mal, wenn sendmsg()
aufgerufen wird, eine Routensuche durchführt .
Wenn jedoch connect()
zuvor mit der IP und dem Port eines bestimmten Remote-Empfängers aufgerufen wird, kann der Betriebssystem-Kernel den Verweis auf die Route aufschreiben und dem Socket zuordnen , wodurch er erstellt wird wesentlich schneller zum Senden einer Nachricht, wenn nachfolgende sendmsg()
-Aufrufe keinen Empfänger angeben ( sonst würde die vorherige Einstellung ignoriert ), stattdessen die Standardeinstellung.
Schauen Sie sich die Zeilen 1070
bis 1171
an:
if (connected)
rt = (struct rtable *)sk_dst_check(sk, 0);
if (!rt) {
[..skip..]
rt = ip_route_output_flow(net, fl4, sk);
[..skip..]
}
Bis zum Linux-Kernel 4.18 war diese Funktion hauptsächlich auf die IPv4-Adressfamilie beschränkt. Allerdings ist seit Version 4.18-rc4 (und hoffentlich auch der Linux-Kernel Version 4.18) auch mit IPv6-Sockets voll funktionsfähig .
Dies kann eine Quelle für ein ernsthafter Leistungsvorteil sein, wird jedoch stark vom verwendeten Betriebssystem abhängen. Wenn Sie Linux verwenden und den Socket nicht für mehrere Remote-Handler verwenden, sollten Sie es zumindest versuchen.
Der Schlüssel ist wirklich connect()
:
Wenn der Socket sockfd vom Typ SOCK_DGRAM ist, dann ist addr die Adresse, an die Datagramme standardmäßig gesendet werden, und die einzige Adresse, von der Datagramme empfangen werden.
Ich habe connect () nicht unter UDP verwendet. Ich glaube, connect () wurde für zwei völlig unterschiedliche Zwecke unter UDP vs. TCP entwickelt.
Die Manpage enthält einige kurze Details zur Verwendung von connect () unter UDP:
Im Allgemeinen können verbindungsbasierte Protokolle (wie TCP) Sockets nur einmal erfolgreich verbinden (); Verbindungslose Protokollsockets (wie UDP) können connect () mehrmals verwenden, um ihre Zuordnung zu ändern.
Diese Seite enthält einige nützliche Informationen zu verbundenen und nicht verbundenen Sockets: http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch08lev1sec11.html
Dieses Zitat beantwortet Ihre Frage:
Normalerweise ist es ein UDP-Client, der eine Verbindung aufruft, es gibt jedoch Anwendungen, in denen der UDP-Server für einen langen Zeitraum mit einem einzelnen Client kommuniziert (z. B. TFTP). In diesem Fall können sowohl der Client als auch der Server eine Verbindung herstellen.
Es gibt ein Problem in Ihrem 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))
Wenn Sie nur AF_UNSPEC und SOCK_DGRAM verwenden, erhalten Sie eine Liste aller möglichen Zusätze. Wenn Sie also Socket anrufen, ist die von Ihnen verwendete Adresse möglicherweise nicht Ihre erwartete UDP-Adresse. Du solltest benutzen
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE;
stellen Sie stattdessen sicher, dass die abgerufene Addrinfo Ihren Wünschen entspricht.
In einem anderen Word ist der von Ihnen erstellte Socket möglicherweise kein UDP-Socket. Deshalb funktioniert er nicht.
Ja, du kannst. Ich mache es auch.
In Ihrem Anwendungsfall ist dies nützlich: Beide Seiten fungieren als Client und Server, und auf beiden Seiten gibt es nur einen Prozess.