Dies ist die 4. Einführung in die Socket-API zum Lernen in C-Sprache, die ich persönlich zu einer Serie gemacht habe. Dieses Mal werden wir den Mechanismus der Datenübertragung / des Datenempfangs für UDP und den Unterschied zu TCP überprüfen.
Mit den folgenden Bewegungen in Bezug auf UDP kann gesagt werden, dass es sich um ein Kommunikationsprotokoll handelt, das erneut Aufmerksamkeit erregt. Googles QUIC-Protokoll: Migrieren Sie das Web von TCP nach UDP
UDP ist eine Erweiterung von IP, einem Host-zu-Host-Protokoll, auf Anwendungsebene. UDP verwendet eine Portnummer für die Adressierung und behält im Gegensatz zu TCP die Grenzen jeder Nachricht bei.
Darüber hinaus verwirft UDP das Datagramm so, wie es ist, wenn die Daten beschädigt sind. Im Gegensatz zu TCP handelt es sich jedoch um ein Protokoll zur Bereitstellung des Datagrammdienstes vom Best-Effort-Typ und ist zuverlässig, z. B. bei der erneuten Datenübertragungsverarbeitung. Führt keine verwandte Verarbeitung durch.
Auf den ersten Blick scheint es TCP unterlegen zu sein, aber es gibt viele gute Punkte wie einen schnelleren Betrieb und einen einfacheren Empfang von Nachrichten als TCP. Der obige Artikel geht auch auf die Eigenschaften und die Güte von UDP ein, daher ist es eine gute Idee, ihn zu lesen.
Und im Gegensatz zu TCP ist UDP ein verbindungsloses Protokoll, das verwendet werden kann, ohne eine Verbindung herzustellen. Dies wird auch im folgenden Quellcode gezeigt.
Client-Software Mac OS X 10.10.5 gcc CentOs 6,8 gcc Server-Software x86_64
Ich werde sowohl Server-Software als auch Client-Software einführen, aber da Server-Software einfacher ist, werde ich sie zuerst einführen.
udps.c
#include <stdio.h> //printf(), fprintf(), perror(), getc()
#include <sys/socket.h> //socket(), bind(), sendto(), recvfrom()
#include <arpa/inet.h> // struct sockaddr_in, struct sockaddr, inet_ntoa(), inet_aton()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset(), strcmp()
#include <unistd.h> //close()
#define MSG_FAILURE -1
#define MAX_MSGSIZE 1024
#define MAX_BUFSIZE (MAX_MSGSIZE + 1)
int get_socket(const char *);
void sockaddr_init (const char *, unsigned short, struct sockaddr *);
int udp_send(int, const char *, int, struct sockaddr *);
int udp_receive(int, char *, int, struct sockaddr *);
void socket_close(int);
int main(int argc, char* argv[]) {
const char *address = "";
unsigned short port = (unsigned short)atoi(argv[1]);
struct sockaddr servSockAddr, clitSockAddr;
char recvBuffer[MAX_BUFSIZE];
int server_sock = get_socket("udp");
sockaddr_init(address, port, &servSockAddr);
if (bind(server_sock, &servSockAddr, sizeof(servSockAddr)) < 0) {
perror("bind() failed.");
exit(EXIT_FAILURE);
}
while(1) {
int recvMsgSize = udp_receive(server_sock, recvBuffer, MAX_BUFSIZE, &clitSockAddr);
if (recvMsgSize == MSG_FAILURE) continue;
printf("message received from %s.\n", inet_ntoa(((struct sockaddr_in *)&clitSockAddr)->sin_addr));
int sendMsgSize = udp_send(server_sock, recvBuffer, recvMsgSize, &clitSockAddr);
if (sendMsgSize == MSG_FAILURE) continue;
}
}
int get_socket(const char *type) {
int sock;
if (strcmp(type, "udp") == 0) {
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
} else if(strcmp(type, "tcp") == 0) {
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
}
if (sock < 0){
perror("socket() failed.");
exit(EXIT_FAILURE);
}
return sock;
}
void sockaddr_init (const char *address, unsigned short port, struct sockaddr *sockaddr) {
struct sockaddr_in sockaddr_in;
sockaddr_in.sin_family = AF_INET;
if (inet_aton(address, &sockaddr_in.sin_addr) == 0) {
if (strcmp(address, "") == 0 ) {
sockaddr_in.sin_addr.s_addr = htonl(INADDR_ANY);
} else {
fprintf(stderr, "Invalid IP Address.\n");
exit(EXIT_FAILURE);
}
}
if (port == 0) {
fprintf(stderr, "invalid port number.\n");
exit(EXIT_FAILURE);
}
sockaddr_in.sin_port = htons(port);
*sockaddr = *((struct sockaddr *)&sockaddr_in);
}
int udp_send(int sock, const char *data, int size, struct sockaddr *sockaddr) {
int sendSize;
sendSize = sendto(sock, data, size, 0, sockaddr, sizeof(*sockaddr));
if (sendSize != size) {
perror("sendto() failed.");
return MSG_FAILURE;
}
return sendSize;
}
int udp_receive(int sock, char *buffer, int size, struct sockaddr *sockaddr) {
unsigned int sockaddrLen = sizeof(*sockaddr);
int receivedSize = recvfrom(sock, buffer, MAX_BUFSIZE, 0, sockaddr, &sockaddrLen);
if (receivedSize < 0) {
perror("recvfrom() failed.");
return MSG_FAILURE;
}
return receivedSize;
}
void socket_close(int server) {
if (close(server) < 0) {
perror("close() failed.");
exit(EXIT_FAILURE);
}
}
Der Socket-Erstellungsprozess und der Adressstruktur-Initialisierungsprozess sind in Funktionen mit den Namen get_socket () bzw. sockaddr_init () unterteilt.
Sockaddr_init () trennt lediglich den vorherigen Adresseinstellungsprozess von der Hauptroutine.
In get_socket () verwenden wir den Systemaufruf socket () wie bei TCP. Der Unterschied zu TCP besteht darin, dass das zweite Argument SOCK_DGRAM zum Erstellen eines Sockets vom Typ Datagramm und das dritte Argument IPPROTO_UDP angibt, bei dem es sich um ein End-to-End-Protokoll vom Typ Datagramm handelt. Geben Sie auch hier nicht 0 an und geben Sie es an.
Wie bei TCP wird der Systemaufruf bind () verwendet, um die Adresse mit dem Socket zu verknüpfen. Im Gegensatz zu TCP werden Nachrichten jedoch so gesendet und empfangen, wie sie sind, ohne listen () oder accept () aufzurufen.
In diesem Zustand, wie im Beispiel, wenn Sie mit "netstat" prüfen, können Sie sehen, dass "udp 0 0 0.0.0.0:8080 0.0.0.0: *" angezeigt wird.
In TCP wird zum Herstellen einer Verbindung für jeden Client ein neuer Socket-Deskriptor durch den Prozess von accept () erfasst und Nachrichten werden gesendet und empfangen. Im Fall von UDP wird bind () ausgeführt, da die Verbindung nicht hergestellt wird. Senden und Empfangen von Nachrichten über den Socket.
Außerdem verwenden wir für den Client normalerweise nicht den Systemaufruf connect (). Ich benutze es normalerweise nicht, weil es benutzt werden kann.
Unter dem Namen connect () könnte man denken, dass es sich um einen reinen TCP-Systemaufruf handelt, der die Verarbeitung zum Herstellen einer Verbindung durchführt. Da connect () jedoch eine Rolle bei der Bestimmung der Adresse des Verbindungsziels beim Senden und Empfangen von Nachrichten spielt, Es kann auch für UDP verwendet werden.
Natürlich wird im Fall von TCP das Paket der SYN-Verbindungsanforderung tatsächlich an den anderen Server gesendet, so dass der Prozess zum Bestimmen der Adresse des Verbindungsziels und der Prozess zum Herstellen der Verbindung ausgeführt werden, aber im Fall von UDP Pakete für solche Verbindungsverbindungen werden nicht gesendet, und die Kommunikation über diesen Socket ist auf die Kommunikation mit dem Socket beschränkt, der der bestimmten Portnummer des Hosts mit einer bestimmten IP-Adresse zugeordnet ist.
Es ist in eine Funktion namens udp_receive () unterteilt, aber es ist der Prozess des Empfangens einer Nachricht vom Client mit dem Systemaufruf recvfrom ().
In diesem Fall blockiert es wie recv () die Programmausführung, bis das Datagramm eintrifft. Das Standardverhalten ist wie beim Systemaufruf recv () beschrieben.
Das Argument ist fast dasselbe wie der Systemaufruf recv (), aber da die Informationen des Clients, der die Quelle ist, nur beim Empfang des Datagramms abgerufen werden, wird der Zeiger auf die sockaddr-Struktur, in der die Quellinformationen gespeichert sind, als Argument im fünften Argument verwendet. Übergeben an.
Dann übergebe ich einen Zeiger, der die Länge des 6. Arguments angibt, aber beim Übergeben wird die Größe der sockaddr-Struktur gespeichert, und wenn sie vom Steuerelement von recvfrom () zurückkehrt, die tatsächlich gespeicherte Größe Beachten Sie, dass gespeichert ist.
Was den Empfangsprozess zum Benutzerprozess betrifft, werden Daten aus der Empfangswarteschlange des Socket-Moduls auf die gleiche Weise wie TCP übertragen. Da jedoch die Grenzen beibehalten und die Nachrichten einzeln gruppiert werden, erfolgt dies fälschlicherweise. Sie erhalten nicht mehrere Nachrichten.
Wenn mit recvfrom () eine Nutzlast gesendet wird, die größer als die angegebene Empfangspuffergröße ist, werden die Byte-String-Daten nach der angegebenen Größe verworfen.
Daher ist es in UDP erforderlich, die Größe des durch Recevfrom () gesicherten Empfangspuffers im Voraus entsprechend den Anwendungsanforderungen ausreichend groß zu machen. Sie benötigen mindestens eine Größe, die größer ist als die von sendto () gesendete Größe.
Übrigens beträgt die maximale Größe der Nutzdaten des UDP-Datagramms 65507 (65535 (IP-Paketlänge) -20 (IP-Header-Länge) -8 (UDP-Header-Länge)) Bytes, da IP ein niedrigeres Protokoll ist. Wenn es jedoch im Netzwerk gesendet wird, wird das IP-Datagramm aufgrund der Einschränkung der übertragbaren Nutzlastgröße der Datenverbindung (1500 Byte bei MTU * Ethernet) geteilt und fließt durch das Netzwerk. ..
Wenn beim Kopieren einer Nachricht in den Empfangspuffer kein Fehler auftritt, wird die Verarbeitung unverändert fortgesetzt.
Zeigt die Quell-IP-Adresse aus den in clitSockAddr gespeicherten Quellinformationen an.
Es ist in eine Funktion namens udp_send () unterteilt, aber der Systemaufruf sendto () sendet eine Nachricht vom Client.
Von recvfrom () hat sendto () eine ähnliche Spezifikation wie der ähnliche send () -Systemaufruf und soll zusätzlich die Adressinformationen des Zielhosts gemeinsam weitergeben. Die Adressinformationen des Zielhosts verwenden die von recvfrom () erhaltene Struktur.
Und im Gegensatz zu TCP wird es aufgrund eines Fehlers nicht erneut gesendet, sodass die zu sendenden Daten nicht im Puffer aufbewahrt werden müssen. Wenn die Steuerung von sendto () zurückgegeben wird, wurden die Daten daher bereits an das untere Modul übergeben.
Darüber hinaus wird der Maximalwert des UDP-Sendepuffers häufig vom System festgelegt. Wenn Sie also einen großen Puffer senden möchten, müssen Sie einen separaten Systemaufruf verwenden, um die Socket-Einstellungen zu ändern. Ich werde diesen Systemaufruf noch einmal einführen.
Sie müssen nicht wiederholt Daten senden und empfangen, bis die Verbindung wie in TCP geschlossen und die Datagrammnachrichten begrenzt sind. Sendto () und recvfrom () werden also beide einmal gesendet, wenn sie nicht verworfen werden. Es ist möglich, die entsprechende Nachricht zu erhalten, indem Sie sie einzeln ausführen. Die Bestellung ist jedoch nicht garantiert.
Wenn Sie connect () verwenden, können Sie übrigens die Systemaufrufe send () und recv () wie bei TCP verwenden. Natürlich unterscheidet sich das Sende- / Empfangsprinzip von TCP.
Und da UDP ein Dienst auf der besten Seite ist, ist es nicht immer möglich, Nachrichten zu empfangen. Daher fügt der folgende Clientcode einen einfachen Neuübertragungsprozess auf Anwendungsebene hinzu.
udpc.c
#include <stdio.h> //printf(), fprintf(), perror(), getc()
#include <sys/socket.h> //socket(), sendto(), recvfrom()
#include <arpa/inet.h> // struct sockaddr_in, struct sockaddr, inet_ntoa(), inet_aton()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset(), strcmp()
#include <unistd.h> //close()
#include <signal.h> //sigatcion()
#include <errno.h> //erron, EINTR
#define MSG_FAILURE -1
#define MAX_MSGSIZE 1024
#define MAX_BUFSIZE (MAX_MSGSIZE + 1)
#define MAX_TRIES 5
#define TIMEOUT_SECOND 2
int get_socket(const char *);
void sockaddr_init (const char *, unsigned short, struct sockaddr *);
int udp_send(int, const char *, int, struct sockaddr *);
int udp_receive(int, char *, int, struct sockaddr *);
void socket_close(int);
int input(char *, int);
void remove_lf(char *, int);
void sigaction_init(struct sigaction *, void (*)(int));
void catchAlarm(int);
int udp_try_receive (int, struct sockaddr *, struct sockaddr *, char *, int, char *);
int check_correct_server (struct sockaddr *, struct sockaddr *);
int intTries = 0;
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "argument count mismatch error.\n");
exit(EXIT_FAILURE);
}
const char *address = argv[1];
unsigned short port = (unsigned short)atoi(argv[2]);
struct sockaddr servSockAddr, clitSockAddr;
struct sigaction action;
int server_sock = get_socket("udp");
sockaddr_init(address, port, &servSockAddr);
sigaction_init(&action, catchAlarm);
if (sigaction(SIGALRM, &action, NULL) < 0) {
perror("sigaction() failure");
exit(EXIT_FAILURE);
}
while(1){
char sendBuffer[MAX_BUFSIZE];
char receiveBuffer[MAX_BUFSIZE];
int inputSize = input(sendBuffer, MAX_BUFSIZE);
if (strcmp(sendBuffer, "quit\n") == 0) {
socket_close(server_sock);
break;
}
int receivedSize = udp_try_receive(server_sock, &servSockAddr, &clitSockAddr, sendBuffer, inputSize, receiveBuffer);
if (check_correct_server(&servSockAddr, &clitSockAddr) == -1) {
continue;
}
remove_lf(receiveBuffer, receivedSize);
printf("server return: %s\n", receiveBuffer);
}
return EXIT_SUCCESS;
}
int get_socket(const char *type) {
int sock;
if (strcmp(type, "udp") == 0) {
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
} else if(strcmp(type, "tcp") == 0) {
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
}
if (sock < 0){
perror("socket() failed.");
exit(EXIT_FAILURE);
}
return sock;
}
void sockaddr_init (const char *address, unsigned short port, struct sockaddr *sockaddr) {
struct sockaddr_in sockaddr_in;
sockaddr_in.sin_family = AF_INET;
if (inet_aton(address, &sockaddr_in.sin_addr) == 0) {
if (strcmp(address, "") == 0 ) {
sockaddr_in.sin_addr.s_addr = htonl(INADDR_ANY);
} else {
fprintf(stderr, "Invalid IP Address.\n");
exit(EXIT_FAILURE);
}
}
if (port == 0) {
fprintf(stderr, "invalid port number.\n");
exit(EXIT_FAILURE);
}
sockaddr_in.sin_port = htons(port);
*sockaddr = *((struct sockaddr *)&sockaddr_in);
}
int udp_send(int sock, const char *data, int size, struct sockaddr *sockaddr) {
int sendSize;
sendSize = sendto(sock, data, size, 0, sockaddr, sizeof(*sockaddr));
if (sendSize != size) {
perror("sendto() failed.");
return MSG_FAILURE;
}
return sendSize;
}
int udp_receive(int sock, char *buffer, int size, struct sockaddr *sockaddr) {
unsigned int sockaddrLen = sizeof(*sockaddr);
int receivedSize = recvfrom(sock, buffer, MAX_BUFSIZE, 0, sockaddr, &sockaddrLen);
if (receivedSize < 0) {
perror("recvfrom() failed.");
return MSG_FAILURE;
}
return receivedSize;
}
void socket_close(int server) {
if (close(server) < 0) {
perror("close() failed.");
exit(EXIT_FAILURE);
}
}
int input(char *buffer, int size) {
printf("please enter the characters:");
if (fgets(buffer, size, stdin) == NULL){
fprintf(stderr, "invalid input string.\n");
exit(EXIT_FAILURE);
}
//flush the stdin buffer
if (buffer[strlen(buffer)-1] != '\n') {
int c;
while((c = getc(stdin) != '\n') && (c != EOF)){}
}
return strlen(buffer);
}
void remove_lf(char *buffer, int bufferSize) {
buffer[bufferSize-1] = '\0';
}
void catchAlarm (int ignored) {
intTries += 1;
}
void sigaction_init(struct sigaction *action, void (*handler)(int) ) {
action->sa_handler = handler;
if (sigfillset(&(action->sa_mask)) < 0) {
perror("sigfillset() failure");
exit(EXIT_FAILURE);
}
action->sa_flags = 0;
}
int udp_try_receive (int sock, struct sockaddr *servSockAddr, struct sockaddr *clitSockAddr, char *sendBuffer, int sendSize, char *receiveBuffer) {
int sendedSize = udp_send(sock, sendBuffer, sendSize, servSockAddr);
int receivedSize;
while (1) {
alarm(TIMEOUT_SECOND);
receivedSize = udp_receive(sock, receiveBuffer, MAX_BUFSIZE, clitSockAddr);
if (receivedSize == MSG_FAILURE) {
if (errno == EINTR) {
if (intTries <= MAX_TRIES) {
printf("timed out %d.\n", intTries);
sendedSize = udp_send(sock, sendBuffer, sendSize, servSockAddr);
if (sendedSize == MSG_FAILURE) break;
alarm(TIMEOUT_SECOND);
continue;
} else {
printf("total timed out %d.\n", MAX_TRIES);
exit(EXIT_FAILURE);
}
} else {
exit(EXIT_FAILURE);
}
}
break;
}
alarm(0);
return receivedSize;
}
int check_correct_server (struct sockaddr *sockaddr_1, struct sockaddr *sockaddr_2) {
if( ((struct sockaddr_in *)sockaddr_1)->sin_addr.s_addr != ((struct sockaddr_in *)sockaddr_2)->sin_addr.s_addr ) {
fprintf(stderr, "reveiceid from unknown server.\n");
} else if (ntohs(((struct sockaddr_in *)sockaddr_1)->sin_port) != ntohs(((struct sockaddr_in *)sockaddr_2)->sin_port)) {
fprintf(stderr, "reveiceid from unknown port.\n");
} else {
return EXIT_SUCCESS;
}
return MSG_FAILURE;
}
Erstellen Sie wie beim Server einen Socket und initialisieren Sie die Adressstruktur. Normalerweise müssen Sie bind () aus den gleichen Gründen nicht aufrufen wie für TCP-Clients.
Dies ist einer der Punkte dieses UDP-Programms, und wir bereiten uns darauf vor, das Zeitlimit der UDP-Übertragung durch das Signal zu erkennen. Die Verarbeitung ist in eine Funktion namens sigaction_init () unterteilt, der Verarbeitungsinhalt besteht jedoch darin, eine Sigaktionsstruktur zu erstellen.
Übergeben Sie einen Zeiger auf diese Struktur an den Systemaufruf sigaction (), um die Standardaktion zu ändern, wenn SIGALRM an den Prozess benachrichtigt wird. Wenn dieser Änderungsprozess weggelassen wird und der Standardprozess ausgeführt wird, endet das Programm bei diesem Programm in dem Moment, in dem SIGALRM dem Prozess benachrichtigt wird.
Um kurz zu erklären, was passiert, wenn SIGALRM vom Betriebssystem an einen Prozess benachrichtigt wird, wird die Funktion catchAlarm () ausgeführt und blockiert während dieser Zeit andere Signale. Während die Funktion catchAlarm () ausgeführt wird, wird der Vorgang beispielsweise durch Drücken von Strg + C (SIGINT) nicht beendet.
Obwohl Signale eng mit der Netzwerkprogrammierung verbunden sind, möchte ich einen weiteren Artikel über Signale selbst veröffentlichen.
Ich habe früher im Fall des Clients über sendto () und recvfrom () geschrieben, aber die Funktion udp_try_receive () in der 60. Zeile ist eine Reihe von Unterroutinen zum Senden und Empfangen der Verarbeitung, die die Timeout-Verarbeitung erkennen.
Verwenden Sie zunächst den Systemaufruf alarm (), damit das Betriebssystem nach 2 Sekunden SIGALRM sendet. Wenn recvfrom () die Nachricht nicht empfangen kann, wartet es standardmäßig auf die Verarbeitung des Programms. Diesmal wird SIGALRM jedoch nach ca. 2 Sekunden an den Prozess gesendet und gibt -1 zurück, was einen Fehler bedeutet. Wenn Sie die Socket-API durch Kombinieren von Signalen verwenden möchten, müssen Sie Ihr Verständnis für die Reihe von Prozessen vertiefen.
Und da EINTR als errno gesetzt ist, was einen Fehlercode bedeutet, wird die Verarbeitung basierend auf dieser Bedingung durchgeführt und die Verarbeitung der erneuten Übertragung wird durchgeführt, bis MAX_TRIES erreicht ist. Wir garantieren nicht die Zuverlässigkeit, aber die Anwendung führt so etwas wie eine TCP-Neuübertragungsverarbeitung durch.
Wenn dies nicht funktioniert, wird das Programm einmal beendet, vorausgesetzt, dass etwas nicht stimmt. Dadurch wird verhindert, dass das Programm endlos blockiert wird.
Wenn Sie die Nachricht ohne Wartezeit empfangen können, geben Sie 0 für das Argument alarm () an, um den Alarm auszuschalten.
Wir haben eine Routine hinzugefügt, um zu überprüfen, ob die Quelle der Funktion check_correct_server () korrekt ist.
Dadurch wird sichergestellt, dass der Client von dem Host und der Anwendung antwortet, die das Paket gesendet haben. Es ist sehr unwahrscheinlich, dass ein Paket von einem anderen Host stammt, aber es sollte enthalten sein, solange dies praktisch möglich ist.
Da das Ende der Nachricht ein Zeilenumbruch sein sollte, haben wir einen Prozess hinzugefügt, um den Zeilenumbruch in ein Nullzeichen umzuwandeln.
Das nächste Mal möchte ich eine Methode betrachten, die neben TCP-Sockets auch die Mehrprozessverarbeitung kombiniert. Statt der Socket-Programmierung wird das Konzept der Mehrprozessverarbeitung jedoch zu einem wichtigen Bestandteil, also Multi Werfen wir einen Blick auf den Prozess.
Recommended Posts