1 Einführung des Problems
Der E/A-Befehlssatz des UNIX-Systems hat sich aus den Befehlen in Maltics und frühen Systemen entwickelt. Sein Modus ist Öffnen-Schreiben-Lesen-Schließen (Öffnen-Schreiben-Lesen-Schließen). ). Wenn ein Benutzerprozess eine E/A-Operation ausführt, ruft er zunächst „open“ auf, um das Recht zur Nutzung der angegebenen Datei oder des angegebenen Geräts zu erhalten, und gibt eine Ganzzahl zurück, die als Dateideskriptor bezeichnet wird und beschreibt, was der Benutzer mit der geöffneten Datei oder dem geöffneten Gerät tut . Der Prozess der E/A-Operationen. Dieser Benutzerprozess ruft dann mehrmals „Lesen/Schreiben“ auf, um Daten zu übertragen. Wenn alle Übertragungsvorgänge abgeschlossen sind, schließt der Benutzerprozess den Aufruf, um das Betriebssystem darüber zu informieren, dass er die Verwendung eines Objekts abgeschlossen hat.
Die Integration des TCP/IP-Protokolls in den UNIX-Kernel entspricht der Einführung einer neuen Art von E/A-Operationen im UNIX-System. Die Interaktion zwischen UNIX-Benutzerprozessen und Netzwerkprotokollen ist viel komplexer als die Interaktion zwischen Benutzerprozessen und herkömmlichen E/A-Geräten. Zunächst einmal befinden sich zwei Prozesse, die Netzwerkoperationen ausführen, auf demselben Computer. Wie wird die Verbindung zwischen ihnen hergestellt? Zweitens gibt es viele Netzwerkprotokolle. Wie kann ein universeller Mechanismus zur Unterstützung mehrerer Protokolle eingerichtet werden? Das sind alles Probleme, die Netzwerk-Anwendungsprogrammierschnittstellen lösen müssen.
In UNIX-Systemen gibt es zwei Arten von Netzwerk-Anwendungsprogrammierschnittstellen: Sockets von UNIX BSD und TLI von UNIX System V. Seit Sun das UNIX BSD-Betriebssystem eingeführt hat, das TCP/IP unterstützt, hat sich die Anwendung von TCP/IP weiterentwickelt. Seine Netzwerkanwendungsprogrammschnittstelle, Socket, ist in Netzwerksoftware weit verbreitet und wird bis heute verwendet Mikrocomputer-Betriebssysteme DOS- und Windows-Systeme haben sich zu einem leistungsstarken Werkzeug für die Entwicklung von Netzwerkanwendungssoftware entwickelt. In diesem Kapitel wird dieses Problem ausführlich besprochen.
2 Grundkonzepte der Socket-Programmierung
Bevor Sie mit der Verwendung der Socket-Programmierung beginnen, müssen Sie zunächst die folgenden Konzepte festlegen.
2.1 Prozesskommunikation zwischen Netzwerken
Das Konzept der Prozesskommunikation entstand ursprünglich aus eigenständigen Systemen. Da jeder Prozess innerhalb seines eigenen Adressbereichs läuft, stellt das Betriebssystem entsprechende Einrichtungen für die Prozesskommunikation zur Verfügung, um sicherzustellen, dass zwei kommunizierende Prozesse sich nicht gegenseitig stören und koordiniert arbeiten, wie z. B. Pipes in UNIX BSD ), benannt Pipe (Named Pipe) und Soft-Interrupt-Signal (Signal), UNIX-System-V-Nachricht (Nachricht), gemeinsam genutzter Speicherbereich (gemeinsam genutzter Speicher) und Semaphor (Semaphor) usw., sie sind jedoch auf die Verwendung innerhalb der lokalen Kommunikation zwischen ihnen beschränkt . Ziel der Internet-Prozesskommunikation ist es, das Problem der gegenseitigen Kommunikation zwischen verschiedenen Hostprozessen zu lösen (die Prozesskommunikation auf derselben Maschine kann als Sonderfall angesehen werden). Zu diesem Zweck muss zunächst das Problem der Prozessidentifikation zwischen Netzwerken gelöst werden. Auf demselben Host können verschiedene Prozesse durch Prozessnummern (PProzess-IDs) eindeutig identifiziert werden. In einer Netzwerkumgebung kann die von jedem Host unabhängig zugewiesene Prozessnummer den Prozess jedoch nicht eindeutig identifizieren. Beispielsweise weist Host A eine Prozessnummer 5 zu, und Prozessnummer 5 kann auch in Host B vorhanden sein. Daher ist der Satz „Prozessnummer 5“ bedeutungslos.
Zweitens unterstützt das Betriebssystem viele Netzwerkprotokolle. Verschiedene Protokolle funktionieren auf unterschiedliche Weise und haben unterschiedliche Adressformate. Daher muss die Prozesskommunikation zwischen Netzwerken auch das Problem der Identifizierung mehrerer Protokolle lösen.
Um die oben genannten Probleme zu lösen, führt das TCP/IP-Protokoll die folgenden Konzepte ein.
Port
Ein Kommunikationsport, der im Netzwerk benannt und adressiert werden kann, eine Ressource, die vom Betriebssystem zugewiesen werden kann.
Gemäß der Beschreibung des siebenschichtigen OSI-Protokolls besteht der größte funktionale Unterschied zwischen der Transportschicht und der Netzwerkschicht darin, dass die Transportschicht Prozesskommunikationsfähigkeiten bereitstellt. In diesem Sinne ist die endgültige Adresse der Netzwerkkommunikation nicht nur die Hostadresse, sondern auch eine Art Kennung, die den Prozess beschreiben kann. Zu diesem Zweck schlägt das TCP/IP-Protokoll das Konzept des Protokollports (kurz Port) vor, der zur Identifizierung des Kommunikationsprozesses dient.
Ein Port ist eine abstrakte Softwarestruktur (einschließlich einiger Datenstrukturen und E/A-Puffer). Nachdem eine Anwendung (d. h. ein Prozess) über einen Systemaufruf eine Verbindung (Bindung) mit einem bestimmten Port hergestellt hat, werden die von der Transportschicht an den Port übertragenen Daten vom entsprechenden Prozess empfangen und die vom entsprechenden Prozess an den Transport gesendeten Daten Layer wird über den Port ausgegeben. Bei der Implementierung des TCP/IP-Protokolls ähnelt die Portoperation einer allgemeinen E/A-Operation. Der Prozess erhält einen Port, was dem Abrufen einer lokalen eindeutigen E/A-Datei entspricht, auf die über allgemeine Lese- und Schreibvorgänge zugegriffen werden kann Primitive schreiben.
Ähnlich wie ein Dateideskriptor verfügt jeder Port über eine ganzzahlige Kennung, die als Portnummer bezeichnet wird und zur Unterscheidung verschiedener Ports verwendet wird. Da es sich bei den beiden Protokollen der TCP/IP-Transportschicht, TCP und UDP, um zwei völlig unabhängige Softwaremodule handelt, sind auch ihre jeweiligen Portnummern unabhängig voneinander. Beispielsweise hat TCP die Portnummer 255 und UDP kann auch eine haben Portnummer 255. Es liegt kein Konflikt vor.
Die Vergabe von Portnummern ist ein wichtiges Thema. Es gibt zwei grundlegende Zuteilungsmethoden: Die erste wird als globale Zuteilung bezeichnet. Dabei handelt es sich um eine zentralisierte Kontrollmethode, bei der eine anerkannte zentrale Organisation eine einheitliche Zuteilung entsprechend den Benutzerbedürfnissen durchführt und die Ergebnisse der Öffentlichkeit veröffentlicht. Der zweite Typ ist die lokale Zuweisung, auch bekannt als dynamische Verbindung. Das heißt, wenn der Prozess auf den Transportschichtdienst zugreifen muss, gibt das Betriebssystem eine lokale eindeutige Portnummer zurück und der Prozess stellt eine Verbindung her sich über entsprechende Systemaufrufe mit dem Port verknüpfen (binden). Die beiden oben genannten Methoden werden bei der Zuweisung von TCP/IP-Portnummern kombiniert. TCP/IP teilt die Portnummer in zwei Teile, von denen ein kleiner Teil reservierte Ports sind und dem Dienstprozess global zugewiesen werden. Daher verfügt jeder Standardserver über einen weltweit anerkannten Port (bekannter Port) und seine Portnummer ist dieselbe, auch wenn sich der Server auf demselben Computer befindet. Die restlichen Ports sind freie Ports und werden lokal zugewiesen. Sowohl TCP als auch UDP legen fest, dass nur Portnummern unter 256 reservierte Ports sein können.
Adresse
Die beiden in der Netzwerkkommunikation kommunizierenden Prozesse finden auf unterschiedlichen Maschinen statt. In einem verbundenen Netzwerk können sich zwei Maschinen in unterschiedlichen Netzwerken befinden, und diese Netzwerke sind über Netzwerkverbindungsgeräte (Gateways, Bridges, Router usw.) verbunden. Daher ist eine dreistufige Adressierung erforderlich:
1 Ein Host kann mit mehreren Netzwerken verbunden sein und muss eine bestimmte Netzwerkadresse angeben
2 eindeutige Adresse Die Adresse;
3. Jeder Prozess auf jedem Host sollte eine eindeutige Kennung auf dem Host haben.
Normalerweise besteht die Hostadresse aus einer Netzwerk-ID und einer Host-ID, die im TCP/IP-Protokoll durch einen 32-Bit-Ganzzahlwert dargestellt wird. Sowohl TCP als auch UDP verwenden 16-Bit-Portnummern zur Identifizierung des Benutzers Prozesse.
Netzwerk-Byte-Reihenfolge
Verschiedene Computer speichern Multibyte-Werte in unterschiedlicher Reihenfolge. Einige Maschinen speichern Wörter niedriger Ordnung am Anfang Adressenabschnitt (niedrige Kosten zuerst speichern), und einige speichern höherwertige Bytes (hochpreisige zuerst speichern). Um die Datengenauigkeit sicherzustellen, muss die Netzwerk-Byte-Reihenfolge im Netzwerkprotokoll angegeben werden. Das TCP/IP-Protokoll verwendet kostenintensive Speicherformate für 16-Bit-Ganzzahlen und 32-Bit-Ganzzahlen, die in der Protokoll-Header-Datei enthalten sind.
Verbindung
Die Kommunikationsverbindung zwischen zwei Prozessen wird als Verbindung bezeichnet. Verbindungen stellen sich als einige Puffer und eine Reihe von Protokollmechanismen dar und weisen äußerlich eine höhere Zuverlässigkeit auf als keine Verbindungen.
Halbbezogen
Zusammenfassend lässt sich sagen, dass ein Triplett im Netzwerk einen Prozess global eindeutig markieren kann:
(Protokoll, lokale Adresse, lokale Portnummer)
Ein solches Tripel wird als Halbassoziation bezeichnet, die jede Hälfte der Verbindung angibt.
Vollständige Korrelation
Eine vollständige Prozesskommunikation zwischen Netzwerken muss aus zwei Prozessen bestehen und kann nur dasselbe High-Level-Protokoll verwenden. Mit anderen Worten: Es ist unmöglich, dass ein Ende der Kommunikation das TCP-Protokoll und das andere Ende das UDP-Protokoll verwendet. Daher erfordert eine vollständige Kommunikation zwischen Netzwerken ein Fünf-Tupel zur Identifizierung von:
(Protokoll, lokale Adresse, lokale Portnummer, Remote-Adresse, Remote-Portnummer)
Solch ein Fünf-Tupel Eine Gruppe wird als Assoziation bezeichnet, das heißt, es können nur zwei Halbassoziationen mit demselben Protokoll zu einer geeigneten Assoziation zusammengefasst werden, oder eine Verbindung kann vollständig spezifiziert werden.
2.2 Servicemodus
In der hierarchischen Netzwerkstruktur ist jede Schicht streng einseitig abhängig, und die Arbeitsteilung und Zusammenarbeit auf jeder Ebene ist auf die Zeigerperiode konzentriert . „Dienst“ ist ein abstraktes Konzept, das die Beziehung zwischen Zeigern beschreibt, d. h. eine Reihe von Operationen, die von jeder Schicht im Netzwerk für die unmittelbar darüber liegende Schicht bereitgestellt werden. Die untere Schicht ist der Dienstanbieter und die obere Schicht ist der Benutzer, der den Dienst anfordert. Dienste werden in Grundelementen wie Systemaufrufen oder Bibliotheksfunktionen ausgedrückt. Systemaufrufe sind Dienstprimitive, die vom Betriebssystemkernel für Netzwerkanwendungen oder Protokolle auf hoher Ebene bereitgestellt werden. Die n-Schicht im Netzwerk muss der n+1-Schicht immer umfassendere Dienste bereitstellen als die n-1-Schicht, andernfalls hat die n-Schicht keinen existierenden Wert.
In der OSI-Terminologie werden die Netzwerkschicht und die darunter liegenden Schichten auch als Kommunikationssubnetze bezeichnet. Sie bieten nur Punkt-zu-Punkt-Kommunikation und haben kein Konzept für Programme oder Prozesse. Die Transportschicht implementiert die „End-to-End“-Kommunikation und führt das Konzept der Prozesskommunikation zwischen Netzwerken ein. Sie muss auch Probleme wie Fehlerkontrolle, Flusskontrolle, Datensortierung (Nachrichtensortierung), Verbindungsverwaltung usw. lösen. und stellt zu diesem Zweck verschiedene Dienstmethoden zur Verfügung:
Verbindungsorientierter (virtueller Schaltkreis) oder verbindungsloser
Verbindungsorientierter Dienst ist eine Abstraktion des Telefonsystem-Dienstmodells Bei jeder vollständigen Datenübertragung muss eine Verbindung hergestellt und mit Connection beendet werden. Bei der Datenübertragung trägt jedes Datenpaket nicht die Zieladresse, sondern eine Verbindungsnummer (Connect-ID). Im Wesentlichen ist eine Verbindung eine Pipe, und die gesendeten und empfangenen Daten sind nicht nur in derselben Reihenfolge, sondern haben auch denselben Inhalt. Das TCP-Protokoll stellt verbindungsorientierte virtuelle Verbindungen bereit.
Der verbindungslose Dienst ist eine Abstraktion des Postsystemdienstes. Jedes Paket trägt eine vollständige Zieladresse und jedes Paket wird unabhängig im System übertragen. Der verbindungslose Dienst kann die Reihenfolge der Pakete nicht garantieren, er stellt Paketfehler nicht wieder her und überträgt sie nicht erneut und garantiert nicht die Zuverlässigkeit der Übertragung. Das UDP-Protokoll bietet verbindungslose Datagrammdienste.
Die Typen und Anwendungen dieser beiden Dienste sind unten aufgeführt:
Diensttyp
Dienst
Beispiel
Verbindungsorientiert
Zuverlässiger Nachrichtenfluss
Zuverlässiger Byte-Stream
Unzuverlässige Verbindung
Dateiübertragung (FTP)
Remote-Anmeldung (Telnet)
Digitale Stimme
Keine Verbindung
Unzuverlässiges Datagramm
Bestätigtes Datagramm
Anfrage-Antwort
E-Mail
Einschreiben per E-Mail
Netzwerkdatenbankabfrage
Sequenz
Bei der Netzwerkübertragung befinden sich zwei aufeinanderfolgende Nachrichten in der End-to-End-Kommunikation. Sie können unterschiedliche Wege nehmen, sodass die Reihenfolge, in der sie am Ziel ankommen, möglicherweise anders ist als damals gesendet. „Sequenz“ bedeutet, dass die Reihenfolge des Datenempfangs mit der Reihenfolge des Datenversands übereinstimmt. Das TCP-Protokoll stellt diesen Dienst bereit.
Fehlerkontrolle
Ein Mechanismus, um sicherzustellen, dass die von der Anwendung empfangenen Daten fehlerfrei sind. Die Methode zur Fehlerprüfung ist im Allgemeinen die „Prüfsummen“-Methode. Um eine fehlerfreie Übertragung zu gewährleisten, müssen beide Parteien die Bestätigungs-Antwort-Technologie verwenden. Das TCP-Protokoll stellt diesen Dienst bereit.
Flusskontrolle
Ein Mechanismus zur Steuerung der Datenübertragungsrate während der Datenübertragung, um sicherzustellen, dass keine Daten verloren gehen. Das TCP-Protokoll stellt diesen Dienst bereit.
Byte-Stream
Die Byte-Stream-Methode bezieht sich darauf, die übertragene Nachricht nur als Byte-Sequenz zu behandeln und legt keine Grenzen für den Datenstrom fest. Das TCP-Protokoll stellt Byte-Stream-Dienste bereit.
Nachricht
Der Empfänger sollte die Nachrichtengrenze des Absenders speichern. Das UDP-Protokoll stellt Nachrichtendienste bereit.
Vollduplex/Halbduplex
End-to-End-Daten werden gleichzeitig in zwei Richtungen/eine Richtung übertragen.
Cache/Out-of-Band-Daten
Da es im Byte-Stream-Dienst keine Nachrichtengrenzen gibt, kann der Benutzerprozess eine beliebige Anzahl von Bytes zu einem bestimmten Zeitpunkt lesen oder schreiben Zeit. Zur Gewährleistung einer korrekten Übertragung oder bei Verwendung eines Protokolls mit Flusskontrolle ist Caching erforderlich. Bei einigen außergewöhnlichen Anforderungen, wie z. B. interaktiven Anwendungen, kann es jedoch erforderlich sein, diese Zwischenspeicherung abzubrechen.
Bei der Datenübertragung wird eine bestimmte Art von Informationen verwendet, von denen nicht erwartet wird, dass sie über herkömmliche Übertragungsmethoden zur rechtzeitigen Verarbeitung an den Benutzer übertragen werden, z. B. die Unterbrechungstaste (Löschen oder Strg-c) des Im UNIX-System werden Terminal-Flusskontrollzeichen (Control -s und Control-q) als Out-of-Band-Daten bezeichnet. Logischerweise scheint es, dass der Benutzerprozess einen unabhängigen Kanal zur Übertragung dieser Daten verwendet. Dieser Kanal ist jedem Paar verbundener Streams zugeordnet. Da die Implementierung von Out-of-Band-Daten in Berkeley Software Distribution nicht mit der Host-Vereinbarung gemäß RFC 1122 vereinbar ist, dürfen Anwendungsautoren zur Minimierung von Interoperabilitätsproblemen keine Out-of-Band-Daten benötigen, wenn sie mit vorhandenen Diensten zusammenarbeiten. Es ist besser, es nicht zu verwenden.
2.3 Client/Server-Modell
In TCP/IP-Netzwerkanwendungen ist der Hauptmodus der Interaktion zwischen den beiden Kommunikationsprozessen das Client/Server-Modell, das heißt, der Client sendet eine Nachricht an den Serverdienst Anfrage: Nachdem der Server die Anfrage empfangen hat, stellt er den entsprechenden Dienst bereit. Die Einrichtung des Client/Server-Modells basiert auf den folgenden zwei Punkten: Erstens besteht der Grund für die Einrichtung eines Netzwerks darin, dass die Software- und Hardwareressourcen, die Rechenleistung und die Informationen im Netzwerk ungleich sind und gemeinsam genutzt werden müssen, wodurch Hosts entstehen mit vielen Ressourcen zur Bereitstellung von Dienstleistungen und Kunden mit weniger Ressourcen zur Anforderung von Dienstleistungen. Zweitens ist die Prozesskommunikation im Internet vollständig asynchron. Es gibt weder eine Eltern-Kind-Beziehung noch einen gemeinsamen Speicherpuffer zwischen den miteinander kommunizierenden Prozessen. Daher ist ein Mechanismus erforderlich, um Verbindungen zwischen Prozessen herzustellen, die kommunizieren und Daten bereitstellen möchten Austausch zwischen den beiden, dies ist TCP/IP basierend auf dem Client/Server-Modus.
Der Schlüsselbedienungsprozess im Client/Server-Modus verwendet eine aktive Anforderungsmethode:
Zunächst muss der Server gestartet werden und entsprechende Dienste gemäß der Anforderung bereitstellen:
1. Öffnen Sie einen Kommunikationskanal und informieren Sie den lokalen Host, dass er bereit ist, Kundenanfragen an einer anerkannten Adresse zu empfangen (gemeinsamer Port, z. B. FTP ist 21); am Hafen ankommen ;
3. Empfangen Sie eine wiederholte Serviceanfrage, verarbeiten Sie die Anfrage und senden Sie ein Antwortsignal. Wenn eine gleichzeitige Serviceanfrage eingeht, muss ein neuer Prozess aktiviert werden, um die Kundenanfrage zu bearbeiten (z. B. fork und exec in UNIX-Systemen). Der neue Prozess verarbeitet diese Clientanfrage und muss nicht auf andere Anfragen antworten. Nach Abschluss des Dienstes wird die Kommunikationsverbindung zwischen diesem neuen Prozess und dem Client geschlossen und beendet.
4. Kehren Sie zu Schritt 2 zurück und warten Sie auf eine weitere Kundenanfrage.
5. Fahren Sie den Server herunter
Client-Seite:
1. Öffnen Sie einen Kommunikationskanal und stellen Sie eine Verbindung zu dem spezifischen Port des Hosts her, auf dem sich der Server befindet ;
2. Senden Sie eine Service-Anfragenachricht an den Server, warten Sie auf eine Antwort und stellen Sie weiterhin Anfragen ...
3. Schließen Sie den Kommunikationskanal und beenden.
Aus dem oben beschriebenen Prozess:
1. Die Rollen der Client- und Serverprozesse sind asymmetrisch, daher sind die Kodierungen unterschiedlich.
2. Der Serviceprozess wird im Allgemeinen zuerst als Reaktion auf eine Benutzeranfrage gestartet. Dieser Dienstprozess existiert so lange, wie das System läuft, bis er normal oder zwangsweise beendet wird.
2.4 Socket-Typ
TCP/IP-Socket bietet die folgenden drei Arten von Sockets.
Streaming-Socket (SOCK_STREAM)
Bietet einen verbindungsorientierten, zuverlässigen Datenübertragungsdienst. Daten werden ohne Fehler und Duplikate gesendet und in der Reihenfolge empfangen, in der sie gesendet wurden. Die integrierte Flusskontrolle verhindert, dass der Datenfluss den Grenzwert überschreitet; Daten werden als Bytestrom ohne Längenbeschränkung betrachtet. Das File Transfer Protocol (FTP) verwendet Streaming-Sockets.
Datagram-Socket (SOCK_DGRAM)
Bietet einen verbindungslosen Dienst. Datenpakete werden in Form unabhängiger Pakete gesendet, es wird keine Fehlerfreiheitsgarantie gegeben,
Daten können verloren gehen oder dupliziert werden und nicht in der richtigen Reihenfolge empfangen werden. Das Network File System (NFS) verwendet Datagramm-Sockets.
Raw Socket (SOCK_RAW)
Diese Schnittstelle ermöglicht den direkten Zugriff auf Protokolle niedrigerer Schichten wie IP und ICMP. Wird häufig verwendet, um neue Protokollimplementierungen zu überprüfen oder auf neue Geräte zuzugreifen, die in vorhandenen Diensten konfiguriert sind.
3 Grundlegende Socket-Systemaufrufe
Um die Prinzipien der Socket-Programmierung besser zu erklären, werden im Folgenden einige grundlegende Anweisungen für Socket-Systemaufrufe gegeben.
3.1 Erstellen Sie einen Socket──socket()
Bevor Sie einen Socket verwenden, muss das System zunächst einen Socket aufrufen, um der Anwendung die Methode zum Erstellen eines Sockets bereitzustellen , das Aufrufformat ist wie folgt:
SOCKET PASCAL FAR socket(int af, int type, int Protocol);
Dieser Aufruf muss drei Parameter empfangen: af, Typ, Protokoll. Der Parameter af gibt den Bereich an, in dem die Kommunikation stattfindet. Die von UNIX-Systemen unterstützten Adressfamilien sind: AF_UNIX, AF_INET, AF_NS usw., während DOS und WINDOWS nur AF_INET unterstützen, also den Internetbereich. Daher ist die Adressfamilie dieselbe wie die Protokollfamilie. Der Typparameter beschreibt den Typ des einzurichtenden Sockets. Der Protokollparameter gibt das vom Socket verwendete spezifische Protokoll an. Wenn der Aufrufer das verwendete Protokoll nicht angeben möchte, wird er auf 0 gesetzt und der Standardverbindungsmodus verwendet. Richten Sie einen Socket basierend auf diesen drei Parametern ein, weisen Sie ihm die entsprechenden Ressourcen zu und geben Sie eine ganzzahlige Socket-Nummer zurück. Daher gibt der Systemaufruf socket() tatsächlich das Element „Protokoll“ im entsprechenden Fünffach an.
Eine detaillierte Beschreibung von socket() finden Sie in 5.2.23.
3.2 Lokale Adresse angeben─bind()
Wenn ein Socket mit socket() erstellt wird, gibt es einen Namensraum (Adressfamilie), der jedoch nicht benannt wird. bind() ordnet die Socket-Adresse (einschließlich der lokalen Hostadresse und der lokalen Portadresse) der erstellten Socket-Nummer zu, dh sie gibt dem Socket einen Namen, um die lokale Halbrelevanz anzugeben. Das Aufrufformat ist wie folgt:
int PASCAL FAR bind(SOCKET s, const strUCt sockaddr FAR * name, int namelen);
Der Parameter s wird vom socket()-Aufruf zurückgegeben und ist nicht verbunden. Der Socket-Deskriptor (Socket-Nummer). Der Parametername ist die dem Socket s zugewiesene lokale Adresse (Name). Seine Länge ist variabel und seine Struktur variiert je nach Kommunikationsdomäne. namelen gibt die Länge des Namens an.
Wenn kein Fehler auftritt, gibt bind() 0 zurück. Andernfalls wird der Rückgabewert SOCKET_ERROR zurückgegeben.
Adressen spielen eine wichtige Rolle beim Aufbau der Socket-Kommunikation. Als Netzwerkanwendungsdesigner müssen Sie mit der Socket-Adressstruktur genau vertraut sein. Beispielsweise verfügt UNIX BSD über eine Reihe von Datenstrukturen, die Socket-Adressen beschreiben, die das TCP/IP-Protokoll verwenden:
struct sockaddr_in{
short sin_family;
u_short sin_port; /*16-Bit-Portnummer, Netzwerk-Byte-Reihenfolge*/
struct in_addr sin_addr; /*32-Bit-IP-Adresse, Netzwerk-Byte-Reihenfolge*/
char sin_zero[8]; /*reserved*/
}
Eine detaillierte Beschreibung von bind() finden Sie in 5.2.2.
3.3 Herstellen einer Socket-Verbindung──connect() und Accept()
Diese beiden Systemaufrufe werden verwendet, um einen vollständigen zugehörigen Aufbau abzuschließen, wobei connect() zum Herstellen einer Verbindung verwendet wird. Ein verbindungsloser Socket-Prozess kann auch connect() aufrufen, allerdings findet zu diesem Zeitpunkt kein tatsächlicher Nachrichtenaustausch zwischen den Prozessen statt und der Aufruf erfolgt direkt vom lokalen Betriebssystem. Dies hat den Vorteil, dass der Programmierer nicht für jedes Datenelement die Zieladresse angeben muss. Wenn ein Datagramm empfangen wird, dessen Zielport keine „Verbindung“ mit einem Socket hergestellt hat, kann festgestellt werden, dass der Port zuverlässig ist. arbeiten. Accept() wird verwendet, um den Server auf eine tatsächliche Verbindung von einem Client-Prozess warten zu lassen.
Das Aufrufformat von connect() ist wie folgt:
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
Der Parameter s soll eingerichtet werden. Der lokale Socket-Deskriptor für die Verbindung. Der Parametername zeigt auf einen Zeiger, der die Adressstruktur des Sockets der anderen Partei beschreibt. Die Länge der Socket-Adresse der anderen Partei wird durch namelen angegeben.
Wenn kein Fehler auftritt, gibt connect() 0 zurück. Andernfalls wird der Rückgabewert SOCKET_ERROR zurückgegeben. Bei einem verbindungsorientierten Protokoll führt dieser Aufruf zum eigentlichen Verbindungsaufbau zwischen dem lokalen System und dem externen System.
Da die Adressfamilie immer in den ersten beiden Bytes der Socket-Adressstruktur enthalten ist und über den Aufruf von socket () mit einer bestimmten Protokollfamilie verknüpft ist. Daher benötigen bind() und connect() kein Protokoll als Parameter.
Eine detaillierte Beschreibung von connect() finden Sie unter 5.2.4.
Das Aufrufformat von Accept() ist wie folgt:
SOCKET PASCAL FAR Accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen); 🎜>Parameter s ist ein lokaler Socket-Deskriptor, listen() sollte aufgerufen werden, bevor er als Parameter im Accept()-Aufruf verwendet wird. addr Zeiger auf die Socket-Adressstruktur des Clients, der zum Empfangen der Adresse der verbundenen Entität verwendet wird. Das genaue Format von addr wird durch die Adressfamilie bestimmt, die beim Erstellen des Sockets festgelegt wurde. addrlen ist die Länge (Anzahl der Bytes) der Socket-Adresse des Clients. Wenn kein Fehler auftritt, gibt Accept() einen Wert vom Typ SOCKET zurück, der den Deskriptor des empfangenen Sockets darstellt. Andernfalls ist der Rückgabewert INVALID_SOCKET.
accept() wird für verbindungsorientierte Server verwendet. Die Parameter addr und addrlen speichern die Adressinformationen des Clients. Vor dem Aufruf zeigt der Parameter addr auf eine Adressstruktur mit dem Anfangswert empty und der Anfangswert von addrlen ist 0. Nach dem Aufruf von Accept() wartet der Server darauf, die Client-Verbindungsanforderung vom Socket mit der Nummer s anzunehmen Die Verbindungsanforderung wird durch den connect()-Aufruf des Clients ausgegeben. Wenn eine Verbindungsanforderung eintrifft, fügt der Aufruf von Accept() die Adresse und Länge des ersten Client-Sockets in der Anforderungsverbindungswarteschlange in addr und addrlen ein und erstellt eine neue Socket-Nummer mit denselben Eigenschaften wie s. Der neue Socket kann zur Bearbeitung gleichzeitiger Anfragen an den Server verwendet werden.
Eine detaillierte Beschreibung von Accept() finden Sie unter 5.2.1.
Vier Socket-Systemaufrufe, socket(), bind(), connect() und Accept(), können die Einrichtung einer vollständigen Fünf-Elemente-Korrelation abschließen. socket () gibt das Protokollelement im Fünf-Tupel an, und seine Verwendung hat nichts damit zu tun, ob es sich um einen Client oder einen Server handelt und ob es verbindungsorientiert ist. bind() gibt die lokale Binärdatei im Fünf-Tupel an, also die lokale Hostadresse und die Portnummer. Seine Verwendung hängt davon ab, ob es verbindungsorientiert ist oder nicht: Auf der Serverseite muss bind() unabhängig davon aufgerufen werden davon, ob es verbindungsorientiert ist oder nicht; Schlüsseldisziplin Disziplin Wenn verbindungsorientiert übernommen wird, wird bind() möglicherweise nicht aufgerufen, kann aber automatisch über connect() abgeschlossen werden. Wenn verbindungslos verwendet wird, muss der Client bind() verwenden, um eine eindeutige Adresse zu erhalten.
Die obige Diskussion gilt nur für das Client/Server-Modell. Tatsächlich ist die Verwendung von Sockets sehr flexibel. Der einzige Grundsatz, der befolgt werden muss, ist, dass vor der Prozesskommunikation eine vollständige Korrelation hergestellt werden muss.
3.4 Auf Verbindungen warten──listen()
Dieser Aufruf wird verwendet, um eine Verbindung zum Server herzustellen, um anzuzeigen, dass er bereit ist, Verbindungen anzunehmen. listen() muss vor Accept() aufgerufen werden und sein Aufrufformat ist wie folgt:
int PASCAL FAR listen(SOCKET s, int backlog);
Der Parameter s identifiziert einen lokalen Verbindung, die hergestellt wurde und noch nicht verbunden wurde. Die Socket-Nummer, von der der Server bereit ist, Anfragen zu empfangen. Der Rückstand stellt die maximale Länge der Anforderungsverbindungswarteschlange dar, mit der die Anzahl der in der Warteschlange befindlichen Anforderungen begrenzt wird. Der derzeit zulässige Höchstwert beträgt 5. Wenn kein Fehler auftritt, gibt listen() 0 zurück. Andernfalls wird SOCKET_ERROR zurückgegeben.
Während des Aufrufvorgangs kann listen() die erforderlichen Verbindungen für Sockets herstellen, die bind() nicht aufgerufen haben, und eine Anforderungsverbindungswarteschlange mit einer Rückstandslänge einrichten.
Der Aufruf von listen() ist der dritte der vier Schritte, mit denen der Server eine Verbindungsanfrage empfängt. Es wird nach dem Aufruf von socket() aufgerufen, um einen Stream-Socket zuzuweisen, und nach dem Aufruf von bind(), um s einen Namen zuzuweisen, und muss vor Accept() aufgerufen werden.
Eine detaillierte Beschreibung von listen() finden Sie unter 5.2.13.
Wie in Abschnitt 2.3 erwähnt, gibt es im Client/Server-Modell zwei Arten von Diensten: wiederholte Dienste und gleichzeitige Dienste. Der Aufruf von „accept()“ bietet großen Komfort für die Implementierung gleichzeitiger Dienste, da er eine neue Socket-Nummer zurückgibt. Seine typische Struktur ist:
int initsockid, newsockid; ...)) < 0)
Fehler(„Socket kann nicht erstellt werden“);
if (bind(initsockid,....) < ; 0)
error(“bind error“);
if (listen(initsockid , 5) < 0)
error(“listen error“); (; {
newsockid = Accept(initsockid, ...) /*blocking*/
if (newsockid < 0)
error („accept error“) ;
if (fork() == 0){ /* Child Process*/
closesocket(initsockid);
do(newsockid) ; */
exit(0);
}
closesocket(newsockid); /* Übergeordneter Prozess*/
}
Das Ergebnis der Ausführung dieses Programms ist, dass die Newsock-ID mit dem Socket des Clients verknüpft ist. Nach dem Start des Unterprozesses wird die Initsock-ID des fortlaufenden Hauptservers geschlossen und die neue Newsock-ID für die Kommunikation mit dem Client verwendet. Die initsockid des Hauptservers kann weiterhin auf neue Client-Verbindungsanfragen warten. Denn in präemptiven Multitasking-Systemen wie Unix können mehrere Prozesse gleichzeitig unter Systemplanung ablaufen. Daher ermöglicht die Verwendung eines gleichzeitigen Servers, dass der Serverprozess über mehrere Unterprozesse verfügt, die gleichzeitig verschiedene Client-Programme verbinden und mit ihnen kommunizieren. Aus Sicht des Client-Programms kann der Server gleichzeitig Anforderungen von mehreren Clients verarbeiten, daher der Name Concurrent Server.
Ein verbindungsorientierter Server kann auch ein Duplikatserver sein. Seine Struktur ist wie folgt:
int initsockid, newsockid;
if ((initsockid = socket(.. ..))< ;0)
Fehler(„Socket kann nicht erstellt werden“);
if (bind(initsockid,....)<0)
Fehler(„Bind-Fehler“);
if (listen(initsockid,5)<0)
Fehler(“Listen-Fehler“); ; {
newsockid = Accept(initsockid, ...) /*blocking*/
if (newsockid < 0)
error(“accept error“);
do(newsockid); /* Verarbeiten Sie die Anfrage*/
closesocket(newsockid>}
Repeat Server kann nur eine Verbindung herstellen mit Es verarbeitet jeweils ein Client-Programm zyklisch und wird daher als Duplikatserver bezeichnet. Gleichzeitige Server und Duplikatserver haben ihre eigenen Vor- und Nachteile: Gleichzeitige Server können die Reaktionsgeschwindigkeit von Clientprogrammen verbessern es erhöht den Overhead der Systemplanung; der Server ist genau das Gegenteil. Wenn Benutzer entscheiden, ob sie einen gleichzeitigen Server oder einen doppelten Server verwenden möchten, sollten sie die Entscheidung auf der Grundlage der tatsächlichen Anwendung treffen Datenübertragung──send() und recv()
Wenn eine Verbindung hergestellt ist, können Daten übertragen werden. Zu den häufig verwendeten Systemaufrufen gehören send() und recv() Wird verwendet, um die Verbindungsnummer s anzugeben. Das Format lautet wie folgt:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
Der Parameter s ist der verbundene lokale Socket-Deskriptor, der auf den Puffer verweist, der die gesendeten Daten enthält, und seine Länge wird durch len Flags angegeben, z. B. ob gesendet werden soll Wenn Out-of-Band-Daten usw. auftreten, gibt send() die Gesamtzahl der gesendeten Bytes zurück.
Eine detaillierte Beschreibung von send() finden Sie unter 5.2.19. Aufruf wird verwendet. Eingabedaten werden auf dem durch die Nummer s angegebenen verbundenen Datagramm oder Stream-Socket empfangen. Das Format ist wie folgt:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags) ;
Der Parameter s ist der verbundene Socket-Deskriptor. buf ist ein Zeiger auf einen Puffer, der Eingabedaten empfängt, deren Länge durch len angegeben wird. Flags geben die Übertragungssteuerungsmethode an, z. B. ob Out-of-Band-Daten usw. empfangen werden sollen. Wenn keine Fehler auftreten, gibt recv() die Gesamtzahl der empfangenen Bytes zurück. Wenn die Verbindung geschlossen ist, wird 0 zurückgegeben. Andernfalls wird SOCKET_ERROR zurückgegeben.
Eine detaillierte Beschreibung von recv() finden Sie in 5.2.16.
3.6 Eingabe-/Ausgabe-Multiplexing─select()
Der Aufruf select() wird verwendet, um den Status eines oder mehrerer Sockets zu erkennen. Für jeden Socket kann dieser Aufruf Lese-, Schreib- oder Fehlerstatusinformationen anfordern. Die Menge der Sockets, die einen bestimmten Status anfordern, wird durch eine fd_set-Struktur angegeben. Bei der Rückgabe wird diese Struktur aktualisiert, um die Teilmenge der Sockets widerzuspiegeln, die die spezifischen Bedingungen erfüllen, und der Aufruf von select() gibt die Anzahl der Sockets zurück, die die Bedingungen erfüllen. Das Aufrufformat ist wie folgt:
int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * außerfds, const struct timeval FAR * timeout);
Der Parameter nfds gibt den Wert des zu prüfenden Socket-Deskriptors an Variable wird im Allgemeinen ignoriert.
Der Parameter readfds zeigt auf einen Zeiger auf den Satz von Socket-Deskriptoren, die gelesen und getestet werden sollen, und der Aufrufer hofft, Daten daraus zu lesen. Der Parameter writefds ist ein Zeiger auf den Satz von Socket-Deskriptoren, die auf Schreibzugriff getestet werden sollen. außerfds Zeiger auf den Satz von Socket-Deskriptoren zur Fehlererkennung. timeout zeigt die maximale Zeit an, die die Funktion select() wartet. Wenn sie auf NULL gesetzt ist, handelt es sich um einen blockierenden Vorgang. select() gibt die Gesamtzahl der vorbereiteten Socket-Deskriptoren zurück, die in der fd_set-Struktur enthalten sind, oder gibt SOCKET_ERROR zurück, wenn ein Fehler auftritt.
Eine detaillierte Beschreibung von select() finden Sie in 5.2.18.
3.7 Schließen Sie den Socket──closesocket()
closesocket() schließt den Socket s und gibt die dem Socket zugewiesenen Ressourcen frei; wenn s eine offene TCP-Verbindung beinhaltet, wird die Verbindung freigegeben. Das Aufrufformat von closesocket() lautet wie folgt:
BOOL PASCAL FAR closesocket(SOCKET s);
Parameter s ist der zu schließende Socket-Deskriptor. Wenn kein Fehler auftritt, gibt closesocket() 0 zurück. Andernfalls wird der Rückgabewert SOCKET_ERROR zurückgegeben.
Eine detaillierte Beschreibung von closesocket() finden Sie unter 5.2.3.
2.4 Beispiele für typische Socket-Aufrufprozesse
Wie bereits erwähnt, verwendet die Anwendung des TCP/IP-Protokolls im Allgemeinen den Client/Server-Modus. Daher müssen in tatsächlichen Anwendungen zwei Prozesse vorhanden sein, Client und Server, und der Server wird gestartet Zuerst und dann Das Zeitdiagramm für Systemaufrufe lautet wie folgt.
Der Socket-Systemaufruf eines verbindungsorientierten Protokolls (z. B. TCP) ist in Abbildung 2.1 dargestellt:
Der Server muss zuerst gestartet werden, bis er den Accept()-Aufruf abschließt und in den Wartezustand eintritt Staat, um Kundenanfragen entgegenzunehmen. Wenn der Client zuvor gestartet wurde, gibt connect() einen Fehlercode zurück und die Verbindung ist nicht erfolgreich.
Abbildung 2.1 Zeitdiagramm für verbindungsorientierte Socket-Systemaufrufe
Der Socket-Aufruf ohne Verbindungsprotokoll ist in Abbildung 2.2 dargestellt:
Abbildung 2.2 Kein Sequenzdiagramm für Socket-Aufrufe des Verbindungsprotokolls
Der verbindungslose Server muss ebenfalls zuerst gestartet werden, sonst wird die Client-Anfrage nicht an den Dienstprozess übermittelt. Verbindungslose Clients rufen connect() nicht auf. Daher wurde vor dem Senden der Daten keine vollständige Korrelation zwischen dem Client und dem Server hergestellt, sondern eine Halbkorrelation wurde über socket () und bind () hergestellt. Beim Senden von Daten muss der Absender zusätzlich zur lokalen Socket-Nummer auch die Socket-Nummer des Empfängers angeben, wodurch während des Datensende- und -empfangsprozesses dynamisch eine vollständige Korrelation hergestellt wird.
Beispiel
Dieses Beispiel verwendet das Client/Server-Modell, das sich am Verbindungsprotokoll orientiert. Der Prozess ist in Abbildung 2.3 dargestellt:
Abbildung 2.3 Verbindungsorientiertes Anwendungsflussdiagramm
Serverseitiges Programm:
/* Dateiname: streams.c */
#include
#include
#define TRUE 1
/* Dieses Programm richtet einen Socket ein und startet dann eine Endlosschleife, wenn es eine Verbindung durch die Schleife empfängt. Wenn die Verbindung getrennt wird oder eine Beendigungsnachricht empfangen wird, wird die Verbindung beendet und das Programm empfängt eine neue Verbindung. Das Format der Befehlszeile ist: streams */
main(
{
int sock, length;
struct sockaddr_in server;
struct sockaddr tcpaddr;int msgsock;
int rval, len; >/ * Socket erstellen*/
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror(“opening stream socket ");
exit(1);
}
/* Verwenden Sie einen beliebigen Port, um den Socket zu benennen*/
server.sin_family = AF_INET;
server.sin_port = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror(„binding stream socket“);
exit(1);
}
/* Suchen Sie die angegebene Portnummer und drucken Sie sie aus */
length = sizeof(server);
if (getsockname(sock, (struct sockaddr *)&server, &length) < 0) {
perror(“ Socket-Namen abrufen");
exit(1);
}
printf("Socket-Port #%d
", ntohs(server.sin_port)) ;
/* Verbindung starten*/
listen(sock, 5);
len = sizeof(struct sockaddr); do {
msgsock = Accept(sock, (struct sockaddr *)&tcpaddr, (int *)&len);
if (msgsock == -1)
perror( "accept");
else do{
memset(buf, 0, sizeof(buf));
if ((rval = recv(msgsock, buf, 1024 )) < 0)
perror(“Stream-Nachricht lesen”);
if (rval == 0)
printf(“Verbindung beenden
”) ;
else
printf(“-->%s
”, buf
}while (rval != 0); 🎜>closesocket(msgsock);
} while (TRUE); Da dieses Programm bereits eine Endlosschleife hat, wird der Socket „sock“ nie geschlossen angezeigt. Wenn der Prozess jedoch abgebrochen oder normal beendet wird, werden alle Sockets automatisch geschlossen. */
exit(0);
}
Client-Programm:
/* Dateiname: streamc.c */
#include
#include
#define DATA „half a league, half a league ...“
/* Dieses Programm richtet den Socket ein und kommuniziert dann mit The Socket-Verbindung in der Befehlszeile angegeben; wenn die Verbindung endet, senden Sie eine
-Nachricht über die Verbindung und schließen Sie dann den Socket. Das Format der Befehlszeile ist: Streamc-Hostname, Portnummer
Die Portnummer muss mit der Portnummer des Serverprogramms übereinstimmen*/
main(argc, argv)
int argc
char *argv[ ]; {
int sock; >struct hostent *hp, * gethostbyname( ;
char buf[1024];
/* Socket erstellen*/
sock = socket(AF_INET, SOCK_STREAM , 0);
if (sock < 0) {
perror(“opening stream socket“);exit(1);
}
/* Verbinden Sie den Socket mit dem in der Befehlszeile angegebenen Namen*/
server.sin_family = AF_INET; >
hp = gethostbyname(argv[1]);
if (hp == 0) {
fprintf(stderr, „%s: unbekannter Host
“, argv [1]);
exit(2);
}
memcpy((char*)&server.sin_addr, (char*)hp->h_addr, hp ->h_length);
sever.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&server, sizeof (Server)) < 0) {
perror(“connecting stream socket“);
exit(3); >if (send(sock, DATA, sizeof(DATA)) < 0)
perror(“sending on stream socket“
closesocket(sock);
exit(0);
}
2.5 Ein allgemeines Beispielprogramm
Im vorherigen Abschnitt haben wir ein einfaches Socket-Programmbeispiel vorgestellt. An diesem Beispiel können wir erkennen, dass es bei der Verwendung der Socket-Programmierung fast ein Muster gibt, das heißt, dass alle Programme fast ausnahmslos dieselben Funktionen in derselben Reihenfolge aufrufen. Daher können wir uns vorstellen, eine Zwischenschicht zu entwerfen, die nur einige einfache Funktionen bereitstellt, um die Datenübertragung im Internet zu realisieren. Programmierer müssen sich nicht allzu sehr um die Details des Socket-Programms kümmern.
In diesem Abschnitt stellen wir eine allgemeine Netzwerkprogrammschnittstelle vor, die der oberen Ebene mehrere einfache Funktionen bietet. Mithilfe dieser Funktionen können Programmierer die meisten Online-Prüfungen durchführen. Diese Funktionen isolieren die Socket-Programmierung von der oberen Schicht. Sie verwenden verbindungsorientierte Streaming-Sockets und einen nicht blockierenden Arbeitsmechanismus. Das Programm muss diese Funktionen nur aufrufen, um Netzwerknachrichten abzufragen und entsprechend zu reagieren. Zu diesen Funktionen gehören:
l InitSocketsStruct: Initialisiert die Socket-Struktur und erhält die Service-Portnummer. Wird von Client-Programmen verwendet.
l InitPassiveSock: Initialisieren Sie die Socket-Struktur, erhalten Sie die Service-Port-Nummer und richten Sie den Haupt-Socket ein. Wird von Serverprogrammen verwendet.
l CloseMainSock: Hauptsteckdose schließen. Wird von Serverprogrammen verwendet.
l CreateConnection: Verbindung herstellen. Wird von Client-Programmen verwendet.
l AcceptConnection: Verbindung akzeptieren. Wird von Serverprogrammen verwendet.
l CloseConnection: Verbindung schließen.
l QuerySocketsMsg: Socket-Nachrichten abfragen.
l SendPacket: Daten senden.
l RecvPacket: Daten empfangen.
2.5.1 Header-Datei
/* Dateiname: tcpsock.h */
/* Header-Datei enthält System-Header-Dateien, die häufig von Socket-Programmen verwendet werden (in diesem Beispiel The Die angegebenen Header-Dateien sind die Header-Dateien unter SCO Unix (die Header-Dateien anderer Unix-Versionen können geringfügig abweichen) und definieren zwei unserer eigenen Datenstrukturen und deren Instanzvariablen sowie die von uns bereitgestellten Funktionsbeschreibungen. */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct SocketsMsg{ /* socket message structure*/
int AcceptNum ; /* Gibt an, ob externe Verbindungen auf den Empfang warten*/
int ReadNum /* Die Anzahl der Verbindungen mit externen Daten, die auf das Lesen warten*/
int ReadQueue[32] ; /* Es gibt eine Verbindungswarteschlange mit externen Daten, die darauf warten, gelesen zu werden*/
int WriteNum /* Die Anzahl der Verbindungen, die Daten senden können*/
int WriteQueue[32]; /* Die Verbindungswarteschlange, die Daten senden kann */
int ExceptNum; /* Anzahl der Verbindungen mit Ausnahmen*/
int ExceptQueue[32]; /* Verbindungswarteschlange mit Ausnahmen*/
} SocketsMsg;
typedef struct Sockets { /* Socket structure*/
int DaemonSock; /* Main socket*/
int SockNum ; /* Datenanzahl der Sockets*/
int Sockets[64]; /* Daten-Socket-Array*/
fd_set readfds, writefds,exclusivefds to be erkannt Schreiben, Ausnahme-Socket-Sammlung */
int Port; /* Portnummer*/
} Sockets
Sockets Mysock;
SocketsMsg SockMsg;
int InitSocketsStruct(char * servicename);
int InitPassiveSock(char * servicename); ;
int CreateConnection(struct in_addr *sin_addr);
int CloseConnection(int Sockno); int QuerySocketsMsg();
int SendPacket(int Sockno, void *buf, int len);
int RecvPacket(int Sockno, void *buf, int size); >2.5.2 Funktionsquelldatei
/* Dateiname: tcpsock.c */
/* Diese Datei enthält den Quellcode von neun Funktionen, von denen einige chinesische Kommentare enthalten*/
#include "tcpsock.h"
int InitSocketsStruct(char * servicename )
/* Sockets-Struktur initialisieren, dann 1 zurückgeben, andernfalls Fehlercode (<0) zurückgeben */
/* Diese Funktion wird für Clients verwendet, die nur aktive Sockets benötigen wird verwendet, um Serviceinformationen zu erhalten. Die Definition des Dienstes
befindet sich in der Datei /etc/services*/
struct servon *servrec;
struct sockaddr_in serv_addr; >
if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
return(-1)
bzero ((char *)&Mysock, sizeof(Sockets));
Mysock.Port = servrec->s_port; /* Service-Port in Netzwerk-Byte-Reihenfolge */
return(1 );
}
int InitPassiveSock(char * servicename)
/* Passive Socket initialisieren, dann 1 zurückgeben, andernfalls Fehlercode zurückgeben (< 0) */
/* Diese Funktion wird für Serverprogramme verwendet, die passive Sockets benötigen. Zusätzlich zum Abrufen von Serviceinformationen wird auch ein passiver Socket eingerichtet. */
{
int mainsock, flag=1; 🎜>if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
return(-1);
bzero((char *)&Mysock, sizeof(Sockets));
Mysock.Port = servrec->s_port; /* Service-Port in Netzwerk-Byte-Reihenfolge */
if((mainsock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return(-2);
}
bzero((char *)&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* Beliebige Netzwerkschnittstelle*/
serv_addr.sin_port = servrec->s_port;
if (bind(mainsock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
close(mainsock) ;
return(-3);
}
if (listen(mainsock, 5) == -1) { /* Wird der aktive Socket zu einem passiver Socket, bereit zum Empfangen von Verbindungen*/
close(mainsock);
return(-4>}
/* Legen Sie diesen Socket als nicht blockierenden Socket fest >return(-5);
}
Mysock.DaemonSock = mainsock;
FD_SET(mainsock, &Mysock.readfds); lesbar" Hauptsocket */
FD_SET(mainsock, &Mysock.exclusfds); /* Deklariert Interesse an Ausnahmeereignissen am Hauptsocket*/
return(1);
}
void CloseMainSock()
/* Schließen Sie den Hauptsocket und löschen Sie die Deklaration der Ereignisse darüber. Es empfiehlt sich, den Hauptsocket vor dem Ende des Programms zu schließen*/ );
FD_CLR(Mysock.DaemonSock, &Mysock.exclusfds);
int CreateConnection(struct in_addr *sin_addr)
/ * Erstellen Sie eine Verbindung zum Remote-Host, dessen IP-Adresse sich in sin_addr befindet.
Parameter: sin_addr gibt die IP-Adresse in der Netzwerkbyte-Reihenfolge an.
bei Erfolg die Socket-Nummer zurückgeben, die diese Verbindung angibt,
sonst Fehlercode (<0) zurückgeben */
{
struct sockaddr_in server ; /* Serveradresse */
int tmpsock, flag= 1, i;
if ((tmpsock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return(-1);
server.sin_family = AF_INET;
server.sin_port = Mysock.Port; s_addr = sin_addr->s_addr;
/ * Diesen Socket als nicht blockierenden Socket festlegen */
if (ioctl(tmpsock, FIONBIO, &flag) == - 1) {
close(tmpsock);
return(-2);
}
/* Mit dem Server verbinden * /
if (connect(tmpsock, (struct sockaddr *) &server, sizeof(server)) < 0) {
if ((errno != EWOULDBLOCK) && (errno != EINPROGRESS )) {
/* Wenn der Fehlercode EWOULDBLOCK und EINPROGRESS lautet, muss der Socket nicht geschlossen werden, da das System später weiterhin eine Verbindung für den Socket herstellen kann kann mithilfe der Funktion select() ermittelt werden, um festzustellen, ob der Socket „beschreibbar“ ist. */
close(tmpsock);
return(-3); /* Verbindungsfehler */
}
FD_SET(tmpsock, &Mysock.readfds);
FD_SET(tmpsock, &Mysock.writefds);FD_SET(tmpsock, &Mysock.exclusivefds);
i = 0;
while (Mysock.Sockets[i] != 0) i++; /* nach einer leeren Sockets-Position suchen */
if (i >= 64) {
close(tmpsock);
return(-4); /* zu viele Verbindungen */
}
Mysock.Sockets[i] = tmpsock;
Mysock.SockNum++;
return(i);
}
int AcceptConnection(struct in_addr *IPaddr)
/* Eine Verbindung akzeptieren. Bei Erfolg wird die Daten-Socket-Nummer zurückgegeben, andernfalls wird -1 zurückgegeben. */
{
int newsock, len, flag=1, i;
struct sockaddr_in addr;
len = sizeof(addr);
bzero((char *)&addr, len);
if ((newsock = Accept(Mysock.DaemonSock, &addr, &len)) == -1)
return(-1); /* Fehler akzeptieren. */
/* Stellen Sie diesen Socket als nicht blockierenden Socket ein. */
ioctl(newsock, FIONBIO, &flag);
FD_SET(newsock, &Mysock.readfds);
FD_SET(newsock, &Mysock.writefds);
FD_SET(newsock, &Mysock.exclusfds);
/* Gibt die IP-Adresse im Parameter zurück. */
IPaddr->s_addr = addr.sin_addr.s_addr;
i = 0;
while (Mysock.Sockets[i] != 0) i++; /* nach einer leeren Sockets-Position suchen */
if (i >= 64) {
close(newsock);
return(-4); /* zu viele Verbindungen */
}
Mysock.Sockets[i] = newsock;
Mysock.SockNum++;
return(i);
}
int CloseConnection(int Sockno)
/* Eine durch Sockno angegebene Verbindung schließen. */
{
int retcode;
if ((Sockno >= 64) (Sockno < 0) (Mysock.Sockets[Sockno] == 0))
return(0);
retcode = close(Mysock.Sockets[Sockno]);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.readfds);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.writefds);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.exclusfds);
Mysock.Sockets[Sockno] = 0;
Mysock.SockNum--;
return(retcode);
}
int QuerySocketsMsg()
/* Abfrage-Sockets-Nachricht. Bei Erfolg wird die Nachrichtennummer zurückgegeben, andernfalls wird -1 zurückgegeben.
Die in der Struktur SockMsg gespeicherten Nachrichteninformationen. */
{
fd_set rfds, wfds, efds;
int retcode, i;
struct timeval TimeOut;
rfds = Mysock.readfds;
wfds = Mysock.writefds;
efds = Mysock.exclusivfds;
TimeOut.tv_sec = 0; /* 立即返回,不阻塞.*/
TimeOut.tv_usec = 0;
bzero((char *)&SockMsg, sizeof(SockMsg));
if ((retcode = select(64, &rfds, &wfds, &efds, &TimeOut)) == 0)
return(0);
if (FD_ISSET(Mysock.DaemonSock, &rfds))
SockMsg.AcceptNum = 1; /* Ein Client ruft einen Server auf. */
for (i=0; i<64; i++) /* Daten in Nachricht */
{
if ((Mysock.Sockets[ i] > 0) && (FD_ISSET(Mysock.Sockets[i], &rfds)))
SockMsg.ReadQueue[SockMsg.ReadNum++] = i;
}
for (i=0; i<64; i++) /* Data out ready message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &wfds)))
SockMsg.WriteQueue[SockMsg.WriteNum++] = i;
}
if (FD_ISSET(Mysock.DaemonSock, &efds))
SockMsg.AcceptNum = -1; /* Server-Socket-Fehler. */
for (i=0; i<64; i++) /* Fehlermeldung */
{
if ((Mysock.Sockets[i ] > 0) && (FD_ISSET(Mysock.Sockets[i], &efds)))
SockMsg.ExceptQueue[SockMsg.ExceptNum++] = i;
}
return(retcode);
}
int SendPacket(int Sockno, void *buf, int len)
/* Ein Paket senden. Bei Erfolg die Anzahl der gesendeten Daten zurückgeben, andernfalls -1 */
{
int actlen;
if ((Sockno >= 64) (Sockno < 0) (Mysock.Sockets[Sockno] == 0))
return(0);
if ((actlen = send(Mysock.Sockets[Sockno], buf, len, 0)) < 0)
return(-1);
return(actlen);
}
int RecvPacket(int Sockno, void *buf, int size)
/* Ein Paket empfangen. Bei Erfolg wird die Anzahl der Empfangsdaten zurückgegeben. Andernfalls wird 0 zurückgegeben, wenn die Verbindung
vom Peer heruntergefahren wird, andernfalls wird 0-errno */
zurückgegeben
{
int actlen;
if ((Sockno >= 64) (Sockno < 0) (Mysock.Sockets[Sockno] == 0))
return(0);
if ((actlen = recv(Mysock.Sockets[Sockno], buf, size, 0)) < 0)
return(0-errno) ;
return(actlen); /* actlen ist die empfangene Datenlänge. Wenn sie Null ist, zeigt dies an, dass die Verbindung von der anderen Partei geschlossen wurde. */
}
2.5.3 Beispiel für ein einfaches Serverprogramm
/* Dateiname: server.c */
/* Dies ist ein sehr Wiederholen Sie einfach das Serverprogramm, das den passiven Socket initialisiert und in einer Schleife auf den Empfang von Verbindungen wartet. Wenn eine Verbindung empfangen wird, zeigt es die Seriennummer des Daten-Sockets und die IP-Adresse des Clients an; wenn Daten auf dem Daten-Socket eingehen, empfängt es die Daten und zeigt die Seriennummer des Daten-Sockets der Verbindung und die empfangene Zeichenfolge an. */
#include "tcpsock.h"
main(argc, argv)
int argc;char **argv>
{
struct in_addr sin_addr;
int retcode, i;
char buf[32];
/* Bei Serverprogrammen ist es häufig in Endlosschleifenzustand, er endet nur, wenn der Benutzer den Prozess aktiv beendet oder das System heruntergefahren wird. Bei Serverprogrammen, die mit Kill zwangsweise beendet werden, kann es Auswirkungen auf nachfolgende Serverprogrammneustarts haben, da der Hauptsocket nicht geschlossen ist und Ressourcen nicht aktiv freigegeben werden. Daher ist es eine gute Angewohnheit, die Steckdose aktiv zu schließen. Die folgende Anweisung bewirkt, dass das Programm zunächst die Funktion CloseMainSock() ausführt, um den Hauptsocket zu schließen, wenn es Signale wie SIGINT, SIGQUIT und SIGTERM empfängt, und dann das Programm beendet. Wenn Sie also kill verwenden, um den Serverprozess zwangsweise zu beenden, sollten Sie zuerst kill -2 PID verwenden, um dem Serverprogramm eine Nachricht zum Schließen des Hauptsockets zu geben, und dann kill -9 PID verwenden, um den Prozess zwangsweise zu beenden. */
(void) signal(SIGINT, CloseMainSock);
(void) signal(SIGQUIT, CloseMainSock);
if ((retcode = InitPassiveSock("TestService")) < 0) {
printf("InitPassiveSock: error code = %d
", retcode);
exit(-1);
}
while (1) {
retcode = QuerySocketsMsg(); */
if (SockMsg.AcceptNum == 1) { /* Gibt es eine externe Verbindung, die auf den Empfang wartet? */
retcode = AcceptConnection(&sin_addr);
printf("retcode = %d, IP = %s
", retcode, inet_ntoa(sin_addr.s_addr)); >
}
else if (SockMsg.AcceptNum == -1) /* Primärer Socket-Fehler? */
printf("Daemon Sockets error.
");
for (i=0; i
if ((retcode = RecvPacket(SockMsg.ReadQueue[i] , buf, 32)) > 0)
printf("sockno %d Recv string = %s
", SockMsg.ReadQueue[i], buf); * Die zurückgegebene Datenlänge ist Null, was darauf hinweist, dass die Verbindung unterbrochen und der Socket geschlossen ist */
CloseConnection(SockMsg.ReadQueue[i]);
}
} /* end while */
}
2.5.4 Beispiel für ein einfaches Clientprogramm
/* Dateiname: client.c */
/* Wenn die Das Client-Programm wird ausgeführt. Initialisieren Sie zunächst die Datenstruktur und warten Sie dann, bis der Benutzer den Befehl eingibt. Es erkennt vier Befehle:
conn(ect): Stellen Sie eine Verbindung mit dem Server her >send: Daten an die angegebene Verbindung senden;
clos(e): Die angegebene Verbindung schließen
quit: Beenden Sie das Client-Programm
*/
#include "tcpsock.h" 🎜>
main(argc, argv)
int argc;char **argv; >{
char cmd_buf[16];struct in_addr sin_addr;
int sockno1, retcode;
char *buf = "Dies ist eine Zeichenfolge zum Testen ((retcode = InitSocketsStruct("TestService")) < 0) { /* Datenstruktur initialisieren*/
printf("InitSocketsStruct: error code = %d
", retcode);
exit(1)
}
while (1) {
printf(">"); 🎜>gets(cmd_buf);
if ( !strncmp(cmd_buf, "conn", 4)) {
retcode = CreateConnection(&sin_addr); /* Verbindung erstellen*/
printf("return code: %d
" , retcode);
}
else if(!strncmp(cmd_buf, "send", 4)) {
printf("Sockets Number:");
scanf("%d", &sockno1);
retcode = SendPacket(sockno1, buf, 26); /* Daten senden */
printf("return code: % d
", retcode, sizeof(buf));
}
else if (!strncmp(cmd_buf, „schließen“, 4)) {
printf("소켓 번호:");
scanf("%d", &sockno1);
retcode = CloseConnection(sockno1) /* 연결 종료*/
printf("반환 코드: %d
", retcode);
}
else if (!strncmp(cmd_buf, "quit", 4))
exit(0);
else
putchar('
위 내용은 소켓 프로그래밍 원리의 내용입니다. 더 많은 관련 글은 PHP 중국어를 참고해주세요. 홈페이지(www.php.cn)