Erklären Sie die 5 größten versteckten Gefahren der Socket-Netzwerkprogrammierung

1. Rückgabestatus ignorieren

Der erste Fallstrick liegt auf der Hand, ist aber der häufigste Fehler, den unerfahrene Entwickler machen. Wenn Sie den Rückgabestatus von Funktionen ignorieren, sind Sie möglicherweise verloren, wenn diese fehlschlagen oder teilweise erfolgreich sind. Dies wiederum kann dazu führen, dass sich Fehler ausbreiten und es schwierig wird, die Ursache des Problems zu lokalisieren.

Erfassen und überprüfen Sie jeden Retourenstatus, anstatt ihn zu ignorieren. Betrachten Sie das in Listing 1 gezeigte Beispiel, eine Socket-Sendefunktion.

Listing 1. Ignorieren des Rückgabestatus der API-Funktion

int status, sock, mode;
/* Create a new stream (TCP) socket */
sock = socket( AF_INET, SOCK_STREAM, 0 );
...
status = send( sock, buffer, buflen, MSG_DONTWAIT );
if (status == -1)

{

  /* send failed */
  printf( "send failed: %s\n", strerror(errno) );
}

else

{

  /* send succeeded -- or did it? */
}

Listing 1 untersucht ein Funktionsfragment, das einen Socket-Sendevorgang durchführt (Daten über einen Socket senden). Der Fehlerstatus der Funktion wird abgefangen und getestet, aber in diesem Beispiel wird eine Sendefunktion im nicht blockierenden Modus ignoriert (aktiviert durch das Flag MSG_DONTWAIT).

Die Send-API-Funktion verfügt über drei Arten möglicher Rückgabewerte:

  • Wenn die Daten erfolgreich zur Übertragung in die Warteschlange gestellt werden, wird 0 zurückgegeben.
  • Wenn die Warteschlange fehlschlägt, wird -1 zurückgegeben (der Grund für den Fehler kann mithilfe der Variablen errno ermittelt werden).
  • Wenn beim Aufruf der Funktion nicht alle Zeichen in die Warteschlange gestellt werden können, ist der endgültige Rückgabewert die Anzahl der gesendeten Zeichen.

Aufgrund der nicht blockierenden Natur der MSG_DONTWAIT-Variable von send kehrt der Funktionsaufruf zurück, nachdem alle Daten, einige Daten oder keine Daten gesendet wurden. Das Ignorieren des Retourenstatus hier führt zu einer unvollständigen Lieferung und einem anschließenden Datenverlust.

2. Peer-Socket-Schließung

Das Interessante an UNIX ist, dass Sie fast alles als Datei behandeln können. Dateien selbst, Verzeichnisse, Pipes, Geräte und Sockets werden alle als Dateien behandelt. Dies ist eine neuartige Abstraktion, die bedeutet, dass ein vollständiger Satz von APIs auf einer Vielzahl von Gerätetypen verwendet werden kann.

Betrachten Sie die API-Funktion read, die eine bestimmte Anzahl von Bytes aus einer Datei liest. Die Lesefunktion gibt die Anzahl der gelesenen Bytes zurück (bis zum von Ihnen angegebenen Maximalwert); oder -1 bei einem Fehler; oder 0, wenn das Ende der Datei erreicht wurde.

Wenn ein Lesevorgang für einen Socket abgeschlossen ist und ein Rückgabewert von 0 erhalten wird, weist dies darauf hin, dass die Peer-Schicht auf dem Remote-Socket die Close-API-Methode aufgerufen hat. Diese Anweisung ist die gleiche wie beim Lesen von Dateien – über den Deskriptor können keine zusätzlichen Daten gelesen werden (siehe Listing 2).

Liste 2. Behandeln Sie den Rückgabewert der Lese-API-Funktion ordnungsgemäß

int sock, status;
sock = socket( AF_INET, SOCK_STREAM, 0 );
...
status = read( sock, buffer, buflen );
if (status > 0)

{

  /* Data read from the socket */
}

else if (status == -1)

 {

  /* Error, check errno, take action... */
}

else if (status == 0)

 {

  /* Peer closed the socket, finish the close */
  close( sock );
  /* Further processing... */
}

Ebenso können Sie die Schreib-API-Funktion verwenden, um Peer-Socket-Schließungen zu erkennen. In diesem Fall gibt die Schreibfunktion beim Empfang des SIGPIPE-Signals oder wenn das Signal blockiert ist, -1 zurück und setzt errno auf EPIPE.

3. Fehler bei der Adressverwendung (EADDRINUSE)

Mit der Bind-API-Funktion können Sie eine Adresse (eine Schnittstelle und einen Port) an einen Socket-Endpunkt binden. Diese Funktion kann in den Servereinstellungen verwendet werden, um die Schnittstellen einzuschränken, von denen Verbindungen ausgehen können. Diese Funktion kann auch in den Client-Einstellungen verwendet werden, um die Schnittstellen einzuschränken, die für ausgehende Verbindungen verwendet werden sollen. Die häufigste Verwendung von bind besteht darin, eine Portnummer einem Server zuzuordnen und eine Platzhalteradresse (INADDR_ANY) zu verwenden, die die Verwendung jeder Schnittstelle durch eingehende Verbindungen ermöglicht.

Ein häufiges Problem beim Binden ist der Versuch, eine Bindung an einen Port herzustellen, der bereits verwendet wird. Die Falle besteht darin, dass möglicherweise kein aktiver Socket vorhanden ist, die Bindung an den Port jedoch weiterhin verboten ist (Bind gibt EADDRINUSE zurück) und durch den TCP-Socket-Status TIME_WAIT verursacht wird. Dieser Zustand bleibt nach dem Schließen der Steckdose etwa 2 bis 4 Minuten bestehen. Nach dem Verlassen im TIME_WAIT-Zustand wird der Socket gelöscht, sodass die Adresse problemlos erneut gebunden werden kann.

Das Warten auf das Ende von TIME_WAIT kann ärgerlich sein, insbesondere wenn Sie einen Socket-Server entwickeln und den Server stoppen müssen, um einige Änderungen vorzunehmen, und ihn dann neu starten müssen. Glücklicherweise gibt es Möglichkeiten, den TIME_WAIT-Status zu vermeiden. Die Socket-Option SO_REUSEADDR kann auf den Socket angewendet werden, sodass der Port sofort wiederverwendet werden kann.

Betrachten Sie das Beispiel in Listing 3. Bevor ich die Adresse binde, rufe ich setsockopt mit der Option SO_REUSEADDR auf. Um die Wiederverwendung von Adressen zu ermöglichen, habe ich den Integer-Parameter (on) auf 1 gesetzt (andernfalls könnte er auf 0 gesetzt werden, um die Wiederverwendung von Adressen zu deaktivieren).

Liste 3. Verwenden Sie die Socket-Option SO_REUSEADDR, um Fehler bei der Adressverwendung zu vermeiden

int sock, ret, on;
struct sockaddr_in servaddr;
/* Create a new stream (TCP) socket */
sock = socket( AF_INET, SOCK_STREAM, 0 ):
/* Enable address reuse */
on = 1;
ret = setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
/* Allow connections to port 8080 from any available interface */
memset( &servaddr, 0, sizeof(servaddr) );
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
servaddr.sin_port = htons( 45000 );
/* Bind to the address (interface/port) */
ret = bind( sock, (struct sockaddr *)&servaddr, sizeof(servaddr) );

Die Bind-API-Funktion ermöglicht die sofortige Wiederverwendung von Adressen nach Anwendung der Option SO_REUSEADDR.

4. Senden Sie strukturierte Daten

Sockets sind das perfekte Werkzeug zum Senden unstrukturierter binärer Byteströme oder ASCII-Datenströme (z. B. HTTP-Seiten über HTTP oder E-Mail über SMTP). Wenn Sie jedoch versuchen, Binärdaten über einen Socket zu senden, wird die Sache komplizierter.

Nehmen wir an, Sie möchten eine Ganzzahl senden: Können Sie sicher sein, dass der Empfänger die Ganzzahl auf die gleiche Weise interpretiert? Anwendungen, die auf derselben Architektur ausgeführt werden, können sich auf ihre gemeinsame Plattform verlassen, um diese Art von Daten auf die gleiche Weise zu interpretieren. Aber was passiert, wenn ein Client, der auf einem High-Endian-IBM PowerPC läuft, eine 32-Bit-Ganzzahl an einen Low-Endian-Intel x86 sendet? Die Byteausrichtung führt zu einer falschen Interpretation.

Byte-Swapping oder nicht?

Endianness bezieht sich auf die Reihenfolge, in der Bytes im Speicher angeordnet sind. Big-Endian sortiert mit dem höchstwertigen Byte zuerst, Little-Endian hingegen mit dem niedrigstwertigen Byte zuerst.

High-Endian-Architekturen (wie PowerPC®) haben Vorteile gegenüber Low-Endian-Architekturen (wie die Intel® Pentium®-Serie, deren Netzwerk-Byte-Reihenfolge Big-Endian ist). Das bedeutet, dass bei High-Endian-Maschinen die Steuerdaten innerhalb von TCP/IP natürlich geordnet sind. Die Little-Endian-Architektur erfordert Byte-Swapping – eine leichte Leistungsschwäche für Netzwerkanwendungen.

Wie wäre es mit dem Senden einer C-Struktur über einen Socket? Auch hier gerät man in Schwierigkeiten, denn nicht alle Compiler ordnen die Elemente einer Struktur gleich an. Strukturen können auch komprimiert werden, um Platzverschwendung zu minimieren, was zu einer weiteren Fehlausrichtung von Elementen innerhalb der Struktur führt.

Glücklicherweise gibt es Lösungen für dieses Problem, die eine konsistente Interpretation der Daten auf beiden Seiten gewährleisten. In der Vergangenheit stellte die Remote Procedure Call (RPC)-Toolsuite die sogenannte External Data Representation (XDR) bereit. XDR definiert eine Standarddarstellung für Daten, um die Entwicklung heterogener Netzwerkanwendungskommunikation zu unterstützen.

Jetzt gibt es zwei neue Protokolle, die ähnliche Funktionen bieten. Extensible Markup Language/Remote Procedure Call (XML/RPC) ordnet Prozeduraufrufe über HTTP im XML-Format an. Daten und Metadaten werden in XML codiert und als Zeichenfolgen übertragen, wodurch die Werte von ihrer physischen Darstellung durch das Host-Schema getrennt werden. SOAP folgt XML-RPC und erweitert seine Ideen um bessere Features und Funktionen. , um weitere Informationen zu den einzelnen Protokollen zu erhalten.

 Information Direct: Lernroute zur Linux-Kernel-Quellcode-Technologie + Video-Tutorial zum Kernel-Quellcode

Learning Express: Linux-Kernel-Quellcode, Speicheroptimierung, Dateisystem, Prozessverwaltung, Gerätetreiber/Netzwerkprotokollstapel

5. Annahmen zur Frame-Synchronisierung in TCP

TCP bietet keine Frame-Synchronisierung, was es perfekt für Byte-Stream-orientierte Protokolle macht. Dies ist ein wichtiger Unterschied zwischen TCP und UDP (User Datagram Protocol). UDP ist ein nachrichtenorientiertes Protokoll, das Nachrichtengrenzen zwischen Sender und Empfänger wahrt. TCP ist ein stromorientiertes Protokoll, das davon ausgeht, dass die übermittelten Daten unstrukturiert sind, wie in Abbildung 1 dargestellt.

Abbildung 1. Die Frame-Synchronisierungsfähigkeiten von UDP und die fehlende Frame-Synchronisierung von TCP

Der obere Teil von Abbildung 1 zeigt einen UDP-Client und -Server. Der Peer auf der linken Seite führt zwei Socket-Schreibvorgänge mit jeweils 100 Byte durch. Die UDP-Schicht des Protokollstapels verfolgt die Anzahl der Schreibvorgänge und stellt sicher, dass, wenn der Empfänger auf der rechten Seite die Daten über den Socket erhält, diese mit der gleichen Anzahl an Bytes ankommen. Mit anderen Worten: Die vom Autor bereitgestellten Nachrichtengrenzen bleiben für die Leser erhalten.

Schauen Sie sich nun den unteren Teil von Abbildung 1 an. Es zeigt die gleiche Granularität von Schreibvorgängen für die TCP-Schicht. Es werden zwei separate Schreibvorgänge (jeweils 100 Byte) in den Stream-Socket geschrieben. In diesem Fall erhält der Stream-Socket-Reader jedoch 200 Bytes. Die TCP-Schicht des Protokollstapels fasst die beiden Schreibvorgänge zusammen. Diese Aggregation kann entweder beim Sender oder beim Empfänger des TCP/IP-Protokollstapels erfolgen. Es ist wichtig zu beachten, dass möglicherweise keine Aggregation stattfindet – TCP garantiert nur, dass die Daten in der richtigen Reihenfolge gesendet werden.

Bei den meisten Entwicklern sorgt diese Falle für Verwirrung. Sie möchten die Zuverlässigkeit von TCP und die Frame-Synchronisation von UDP. Sofern kein anderes Transportprotokoll wie das Streaming Transmission Control Protocol (STCP) verwendet wird, müssen Entwickler der Anwendungsschicht Puffer- und Segmentierungsfunktionen implementieren.

Tools zum Debuggen von Socket-Anwendungen

GNU/Linux bietet mehrere Tools, die Ihnen dabei helfen können, einige Probleme in Ihren Socket-Anwendungen zu identifizieren. Darüber hinaus ist die Verwendung dieser Tools lehrreich und kann dabei helfen, das Verhalten von Anwendungen und des TCP/IP-Stacks zu erklären. Hier sehen Sie eine Übersicht über mehrere Tools. Weitere Informationen finden Sie weiter unten.

Details zum Netzwerksubsystem anzeigen

Das Netstat-Tool bietet die Möglichkeit, das GNU/Linux-Netzwerksubsystem anzuzeigen. Mit netstat können Sie aktuell aktive Verbindungen (nach einzelnen Protokollen), Verbindungen in einem bestimmten Zustand (z. B. Server-Sockets im Überwachungszustand) und viele andere Informationen anzeigen. Listing 4 zeigt einige der von netstat bereitgestellten Optionen und die Funktionen, die sie ermöglichen.

Liste 4. Nutzungsmuster für das Netstat-Dienstprogramm

View all TCP sockets currently active
$ netstat --tcp
View all UDP sockets
$ netstat --udp
View all TCP sockets in the listening state
$ netstat --listening
View the multicast group membership information
$ netstat --groups
Display the list of masqueraded connections
$ netstat --masquerade
View statistics for each protocol
$ netstat --statistics

Obwohl es viele andere Dienstprogramme gibt, ist netstat umfassend und deckt die Funktionalität von route, ifconfig und anderen Standard-GNU/Linux-Tools ab.

Überwachen Sie den Verkehr

Mehrere Tools für GNU/Linux können verwendet werden, um den Low-Level-Verkehr im Netzwerk zu untersuchen. Das Tool tcpdump ist ein älteres Tool, das Netzwerkpakete aus dem Internet „schnüffelt“ und sie auf stdout ausgibt oder in einer Datei aufzeichnet. Diese Funktion ermöglicht die Anzeige des von Anwendungen generierten Datenverkehrs und der von TCP generierten Low-Level-Flusskontrollmechanismen. Ein neues Tool namens tcpflow ergänzt tcpdump durch die Bereitstellung einer Protokollflussanalyse und einer Möglichkeit, den Datenfluss unabhängig von der Paketreihenfolge oder Neuübertragung angemessen zu rekonstruieren. Listing 5 zeigt zwei Nutzungsmuster für tcpdump.

Auflistung 5. Nutzungsmuster des Tools tcpdump

Display all traffic on the eth0 interface for the local host
$ tcpdump -l -i eth0
Show all traffic on the network coming from or going to host plato
$ tcpdump host plato
Show all HTTP traffic for host camus
$ tcpdump host camus and (port http)
View traffic coming from or going to TCP port 45000 on the local host
$ tcpdump tcp port 45000

Die Tools tcpdump und tcpflow verfügen über zahlreiche Optionen, einschließlich der Möglichkeit, komplexe Filterausdrücke zu erstellen. Weitere Informationen zu diesen Tools finden Sie weiter unten.

Sowohl tcpdump als auch tcpflow sind textbasierte Befehlszeilentools. Wenn Sie eine grafische Benutzeroberfläche (GUI) bevorzugen, gibt es ein Open-Source-Tool namens Ethereal, das möglicherweise Ihren Anforderungen entspricht. Ethereal ist eine professionelle Protokollanalysesoftware, die beim Debuggen von Protokollen auf Anwendungsebene helfen kann. Seine Plug-in-Architektur kann Protokolle wie HTTP und nahezu jedes erdenkliche Protokoll aufschlüsseln (insgesamt 637 zum Zeitpunkt des Schreibens).

Zusammenfassen

Die Programmierung von Sockets ist einfach und macht Spaß. Sie möchten jedoch vermeiden, dass Fehler auftreten oder diese zumindest leicht auffindbar machen, indem Sie die fünf in diesem Artikel beschriebenen häufigen Fallstricke berücksichtigen und standardmäßige fehlersichere Programmierpraktiken anwenden. GNU/Linux-Tools und -Dienstprogramme können auch dabei helfen, kleinere Probleme in einigen Programmen zu finden.

Originalautor: Zusammen eingebettet lernen

Supongo que te gusta

Origin blog.csdn.net/youzhangjing_/article/details/132853335
Recomendado
Clasificación