Ein Port soll geprüft werden, ein hauseigener Dienst spricht sein eigenes zeilenbasiertes Protokoll, oder ein Partnerserver verlangt eine eigene Zertifikatsdatei. Dann hilft kein fertiger HTTP-Client weiter, sondern PHP stream_socket_client. Die Funktion baut eine Leitung zu einer Adresse auf und liefert dafür etwas Vertrautes: einen ganz gewöhnlichen Stream.
Der Weg dorthin führt über vier Stationen: die Adresse, die Fehlerprüfung, die Verschlüsselung und die Zeitlimits.
Was PHP stream_socket_client macht
Die Funktion nimmt eine Zieladresse entgegen, baut die Verbindung auf und gibt bei Erfolg eine Stream-Ressource heraus. Scheitert der Aufbau, kommt false an. Mehr passiert nicht: es entsteht eine offene Leitung, und was darauf gesprochen wird, bestimmt dein Code.
Ein Stream ist dieselbe Abstraktion, die auch hinter einer geöffneten Datei steckt. Auf dem Rückgabewert gelten deshalb die vertrauten Funktionen: fwrite() schreibt, fgets() und fread() lesen, feof() meldet das Ende, fclose() schließt die Leitung wieder. Eine Socket-Bibliothek braucht es nicht, auch ext/sockets bleibt außen vor: PHP stream_socket_client steckt im Kern.
Dazu gleich die ehrliche Einordnung. Wer eine Webseite oder eine JSON-Schnittstelle abrufen will, ist hier falsch. Dafür gibt es PHP file_get_contents() und cURL, die sich um Weiterleitungen, Statuscodes und Header kümmern. PHP stream_socket_client lohnt sich bei eigenen Protokollen, bei selbst bestimmter Zertifikatsprüfung und dann, wenn die Verbindung dem eigenen Ablauf folgt.
Die Adresse: Transport, Host und Port in einem String
Der auffälligste Unterschied zu älteren Wegen steckt im ersten Parameter. PHP stream_socket_client nimmt Host und Port nicht getrennt entgegen, sondern zusammen mit dem Transport in einem String in URL-Schreibweise. Welche Transporte deine Installation mitbringt, verrät stream_get_transports().
| Transport | Wofür | Beispieladresse |
tcp:// | Klartext, der Normalfall bei eigenen Diensten | tcp://example.com:80 |
tls:// | Verschlüsselt, mit Zertifikatsprüfung | tls://example.com:443 |
udp:// | Einzelne Pakete, etwa an einen Syslog-Empfänger | udp://10.0.0.5:514 |
unix:// | Lokaler Dienst, ganz ohne Netzwerkstack | unix:///var/run/dienst.sock |
Bei unix:// fällt der Port ersatzlos weg, denn die Adresse ist ein Dateipfad. Die drei Schrägstriche sind kein Tippfehler: zwei gehören zum Schema, der dritte zum absoluten Pfad. Und ssl:// ist nicht dasselbe wie tls://: hinter ssl:// steckt die veraltete Aushandlung.
Der erste Verbindungsaufbau und die Fehlerprüfung
Der einfachste Fall ist die Frage, ob ein Dienst antwortet. PHP stream_socket_client beantwortet sie in wenigen Zeilen. Der zweite und dritte Parameter werden per Referenz gefüllt und sind im Fehlerfall die einzige Auskunftsstelle.
<?php
$verbindung = stream_socket_client(
'tcp://www.php-space.info:80',
$errno,
$errstr,
5
);
if ($verbindung === false) {
/* $errno traegt den Systemfehlercode, $errstr den Klartext dazu */
echo 'Nicht erreichbar: ' . $errstr . ' (Code ' . $errno . ')';
exit;
}
echo 'Verbindung steht';
fclose($verbindung);
Wer PHP stream_socket_client ohne diese Prüfung aufruft, steht im Störungsfall ohne jede Auskunft da. Zwei Punkte machen deutlich, warum daran nichts zu sparen ist.
Ohne die beiden Fehlerparameter bleibt jede Störung ein Rätsel Eine abgelehnte Verbindung, ein nicht auflösbarer Name und ein abgelaufenes Zeitlimit sehen im Code gleich aus, denn alle drei liefern false. Erst Fehlercode und Meldung trennen die Fälle und sagen dir, ob Firewall, DNS oder der Dienst das Problem ist.
Ein vorangestelltes @ löst gar nichts Der Klammeraffe vor dem Aufruf unterdrückt die Warnung, nicht den Fehler. Die Verbindung steht danach genauso wenig, nur ohne Spur im Protokoll.
Verschlüsselt verbinden über tls://
Der Wechsel auf eine verschlüsselte Verbindung besteht aus einem Wort in der Adresse. Alles Weitere bleibt gleich, der Rückgabewert ist wieder ein Stream. Das folgende Beispiel baut eine HTTP-Anfrage von Hand zusammen.
<?php
$host = 'www.php-space.info';
$verbindung = stream_socket_client("tls://$host:443", $errno, $errstr, 10);
if ($verbindung === false) {
exit("Kein Zugriff: $errstr ($errno)");
}
/* Eine HTTP-Anfrage ist reiner Text, jede Zeile endet mit CR und LF */
$anfrage = "GET / HTTP/1.1\r\n"
. "Host: $host\r\n"
. "Connection: close\r\n"
. "\r\n";
fwrite($verbindung, $anfrage);
while (!feof($verbindung)) {
echo fgets($verbindung, 1024);
}
fclose($verbindung);
Zwei Dinge sind daran wichtig. HTTP-Zeilen enden mit Wagenrücklauf und Zeilenvorschub, nicht mit einem einfachen Umbruch; manche Server antworten auf die falsche Variante gar nicht. Und in der Ausgabe landet der komplette Kopf samt Statuszeile. Weiterleitungen, gestückelte Übertragung und Komprimierung kämen obendrauf. Im Alltag übernehmen das PHP curl_init() und PHP curl_exec(). Das Beispiel steht hier, weil es den Stream begreifbar macht, nicht als Empfehlung.
Kontext-Optionen und die Zertifikatsprüfung
Bei der Verschlüsselung wird PHP stream_socket_client erst durch den sechsten Parameter interessant. Über stream_context_create() entsteht ein Kontext mit den TLS-Einstellungen, dazu bei Bedarf die Quelladresse per bindto. Aus einer nur verschlüsselten Verbindung wird damit eine mit einer Prüfung, die du selbst festlegst.
<?php
$kontext = stream_context_create([
'ssl' => [
/* Prueft, ob das Zertifikat auf eine bekannte CA zurueckgeht */
'verify_peer' => true,
/* Prueft zusaetzlich, ob der Name im Zertifikat zum Host passt */
'verify_peer_name' => true,
/*
Datei mit den vertrauenswuerdigen Wurzelzertifikaten.
Fuer einen internen Partnerserver steht hier dessen
eigene CA, nicht das System-Bundle.
*/
'cafile' => '/etc/ssl/certs/partner-ca.pem',
'allow_self_signed' => false,
],
]);
$verbindung = stream_socket_client(
'tls://partner.example.com:8443',
$errno,
$errstr,
10,
STREAM_CLIENT_CONNECT,
$kontext
);
if ($verbindung === false) {
exit("TLS-Verbindung fehlgeschlagen: $errstr ($errno)");
}
Die drei entscheidenden Optionen prüfen jeweils etwas anderes.
verify_peer Fragt, ob sich das Serverzertifikat auf eine Zertifizierungsstelle zurückführen lässt, der dein System vertraut. Ohne diese Prüfung ist die Leitung zwar verschlüsselt, aber niemand weiß mehr, wer am anderen Ende sitzt.
verify_peer_name Fragt zusätzlich, ob der Name im Zertifikat zum angesprochenen Host passt. Ein gültiges Zertifikat für einen ganz anderen Server fällt erst hier auf.
cafile Benennt die Datei mit den Wurzelzertifikaten, gegen die geprüft wird. Bei einem internen Partnerserver steht dort dessen eigene Zertifizierungsstelle statt des System-Bundles. Das ist die richtige Antwort auf einen Zertifikatsfehler.
Seit PHP 5.6 stehen die ersten beiden Optionen bei PHP stream_socket_client von sich aus auf true. Der Rat, sie bei einem Fehler auf false zu setzen, ist kein Fix, sondern die Abschaltung genau der Prüfung, um die es geht. Die Meldung verschwindet, das Risiko bleibt.
Die zwei Timeouts, die ständig verwechselt werden
Hier scheitert die Praxis am häufigsten. Der vierte Parameter von PHP stream_socket_client begrenzt ausschließlich den Verbindungsaufbau; steht die Leitung, ist er erledigt. Nimmt der Server sie an und schweigt danach, wartet das Skript weiter, bis das Zeitlimit für Lesen und Schreiben greift. Genau das setzt stream_set_timeout().
<?php
/* Zeitlimit 1: gilt ausschliesslich fuer den Verbindungsaufbau */
$strom = stream_socket_client('tcp://10.0.0.7:9000', $errno, $errstr, 3);
if ($strom === false) {
exit("Aufbau fehlgeschlagen: $errstr ($errno)");
}
/* Zeitlimit 2: gilt fuer jedes Lesen und Schreiben danach */
stream_set_timeout($strom, 5);
fwrite($strom, "STATUS\r\n");
$antwort = fgets($strom);
$info = stream_get_meta_data($strom);
if ($info['timed_out']) {
echo 'Der Dienst hat nicht rechtzeitig geantwortet';
} else {
echo trim((string) $antwort);
}
fclose($strom);
Der dritte Baustein ist die Auswertung: stream_get_meta_data() meldet unter timed_out, ob ein Zeitlimit zugeschlagen hat. Ohne diese Abfrage sieht ein Zeitablauf aus wie eine leere Antwort.
Bleibt der vierte Parameter weg, greift default_socket_timeout aus der php.ini mit 60 Sekunden. Für ein Wartungsskript mag das passen, für eine Webseite ist eine Minute Wartezeit auf einen Dienst nicht zumutbar.
Der folgende Ablauf fasst die Entscheidungen zusammen, die vor jedem Verbindungsaufbau anstehen.
flowchart TD
A[Verbindung noetig] --> B{Nur HTTP holen?}
B -->|Ja| C[curl oder file_get_contents]
B -->|Nein| D{Welcher Transport?}
D -->|Klartext| E[tcp://host:port]
D -->|Verschluesselt| F[tls://host:port]
D -->|Lokaler Dienst| G[unix:///pfad.sock]
F --> H[Kontext mit verify_peer]
E --> I{Timeout wofuer?}
H --> I
G --> I
I -->|Aufbau| J[4. Parameter timeout]
I -->|Lesen| K[stream_set_timeout]
J --> L[errno und errstr pruefen]
K --> L
Ein eigenes Protokoll sprechen
Viele hauseigene Dienste sprechen zeilenbasiert: ein Befehl geht raus, eine Statuszeile kommt herein. Kein HTTP-Werkzeug hilft dabei, und genau dafür ist PHP stream_socket_client gemacht. Das Muster hat drei Teile und passt auf fast jedes solche Protokoll.
<?php
function befehlSenden($strom, string $befehl): string
{
fwrite($strom, $befehl . "\r\n");
$antwort = fgets($strom);
if ($antwort === false) {
throw new RuntimeException('Keine Antwort auf ' . $befehl);
}
return trim($antwort);
}
$strom = stream_socket_client('tcp://lager.intern:7500', $errno, $errstr, 5);
if ($strom === false) {
exit("Lager nicht erreichbar: $errstr ($errno)");
}
stream_set_timeout($strom, 5);
echo befehlSenden($strom, 'LOGIN kasse01'), "\n";
echo befehlSenden($strom, 'BESTAND 4711'), "\n";
echo befehlSenden($strom, 'QUIT'), "\n";
fclose($strom);
Das Zeilenende ist auch hier die kritische Stelle. Erwartet die Gegenseite Wagenrücklauf und Zeilenvorschub, genügt ein einfacher Umbruch nicht, und der Dienst schweigt. Umgekehrt liest fgets() ohne Längenangabe bis zum Zeilenende. Kommt keines, blockiert der Aufruf, bis das Zeitlimit greift. Deshalb steht es im Beispiel mit dabei.
Viele Ziele gleichzeitig prüfen
Wer vierzig Dienste im Minutentakt abfragt, darf nicht nacheinander auf jeden Aufbau warten. Für diesen Fall kennt PHP stream_socket_client im fünften Parameter das Flag STREAM_CLIENT_ASYNC_CONNECT. Der Aufruf kehrt sofort zurück, und alle Ziele laufen nebeneinander an. Der Normalwert dieses Parameters ist übrigens STREAM_CLIENT_CONNECT, den du bisher nicht angeben musstest.
<?php
$ziele = [
'tcp://10.0.0.7:80',
'tcp://10.0.0.8:80',
'tcp://10.0.0.9:80',
];
$offen = [];
$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
foreach ($ziele as $ziel) {
$strom = stream_socket_client($ziel, $errno, $errstr, 5, $flags);
/* Hier ist noch nichts verbunden, der Aufbau laeuft erst an */
if ($strom === false) {
echo "$ziel: $errstr ($errno)\n";
continue;
}
$offen[] = $strom;
}
$lesen = null;
$fehler = null;
$schreiben = $offen;
/* Beschreibbar heisst hier: der Aufbau ist abgeschlossen */
if ($schreiben !== [] && stream_select($lesen, $schreiben, $fehler, 5) > 0) {
foreach ($schreiben as $strom) {
echo 'Erreichbar: ', stream_socket_get_name($strom, true), "\n";
}
}
foreach ($offen as $strom) {
fclose($strom);
}
Ein Detail entscheidet hier über richtig und falsch: ein Rückgabewert ungleich false bedeutet bei PHP stream_socket_client im asynchronen Betrieb noch keinen Erfolg, sondern nur, dass der Versuch angelaufen ist. Ob die Verbindung steht, zeigt erst stream_select(), indem der Stream in der Schreibliste auftaucht. Wer das weglässt, meldet unerreichbare Server als erreichbar. Geht es nur um den Statuscode einer URL, ist PHP get_headers() der kürzere Weg.
Unix-Domain-Sockets und persistente Verbindungen
Auf derselben Maschine braucht es keinen Netzwerkstack. Lokale Dienste bieten ihre Schnittstelle deshalb oft als Socket-Datei an, und PHP stream_socket_client erreicht sie über einen Pfad ohne Port. Ein trotzdem angehängter Port führt zu einem Aufbau, der nie gelingt.
<?php
/* Kein Port: die Adresse ist der Pfad zur Socket-Datei */
$strom = stream_socket_client(
'unix:///var/run/dienst.sock',
$errno,
$errstr,
5,
STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT
);
if ($strom === false) {
exit("Lokaler Dienst nicht erreichbar: $errstr ($errno)");
}
fwrite($strom, "PING\r\n");
echo fgets($strom);
/*
Bei einer persistenten Verbindung beendet fclose() die Leitung
nicht wirklich. Sie bleibt im Prozess erhalten, und der naechste
Aufruf uebernimmt sie samt allem, was vorher darauf passiert ist.
*/
fclose($strom);
Das zweite Flag spart bei jedem Folgeaufruf den Aufbau, weil die Leitung im Prozess bestehen bleibt. Der Preis wird gern übersehen: mitübernommen wird auch der Zustand. Steckt das Gegenüber noch in einer angemeldeten Sitzung, erbt der nächste Aufruf das mit. Bei einem zustandslosen Dienst ist das ein Gewinn, bei einem zustandsbehafteten eine Fehlerquelle.
Das Gegenstück auf der Serverseite heißt stream_socket_server() und arbeitet nach demselben Adressschema. Was daraus entstehen kann, zeigt das Tutorial zu WebSocket mit PHP.
Wann PHP stream_socket_client und wann cURL?
Diese Frage gehört vor die erste Zeile Code. Vier Wege stehen zur Auswahl, und sie unterscheiden sich weniger im Tempo als in der Arbeit, die sie dir abnehmen.
| Weg | Wofür geeignet | TLS-Einstellungen | Aufwand |
file_get_contents() | Eine URL abrufen, sonst nichts | Über denselben Kontext wie hier | Eine Zeile |
| cURL | HTTP in jeder Form, auch POST und Upload | Eigene Optionen, System-Bundle als Vorgabe | Gering |
fsockopen() | Einfache Verbindungen in gewachsenem Code | Kein eigener Kontext | Mittel |
stream_socket_client() | Eigene Protokolle, Unix-Sockets, asynchroner Aufbau | Voll steuerbar über den Kontext | Höher |
Wer mit fsockopen() bereits arbeitet, findet den Einstieg in Daten von einer externen Webseite auslesen. PHP stream_socket_client setzt dort an, wo jene Funktion nicht mehr weiterhilft: bei Verschlüsselung mit eigener Zertifikatsprüfung, bei Unix-Domain-Sockets und bei Verbindungen, die nicht auf den Aufbau warten sollen. Drei Unterschiede sind es im Kern: die einheitliche Adresse mit Transport, der Kontext für TLS und Socket-Optionen, und die beiden Flags. Wer keinen davon braucht, darf bei der alten Funktion bleiben.
Die Gegenrichtung gehört genauso deutlich gesagt: eine handgeschriebene HTTP-Anfrage über einen Socket ist fast immer die schlechtere Lösung, weil Statuscodes, Weiterleitungen und Komprimierung dann alle in deinem Code landen. Der Vorteil von PHP stream_socket_client liegt in der Kontrolle, nicht in der Geschwindigkeit.
Fazit zu PHP stream_socket_client
Vier Dinge tragen den größten Teil der Praxis. Die Adresse enthält Transport, Host und Port in einem String, bei einem lokalen Dienst einen Dateipfad ohne Port. Der Fehlerfall wird über die beiden Referenzparameter ausgewertet, nie stillgestellt. Zeitlimits gibt es zwei, eines für den Aufbau und eines fürs Lesen. Ein Zertifikatsfehler wird über eine passende Zertifikatsdatei gelöst, nicht durch Abschalten der Prüfung.
Bleibt die Frage nach dem Aufwand. Bei eigenen Protokollen, bei selbst bestimmter Prüfung, bei Unix-Domain-Sockets und beim gleichzeitigen Anlaufen vieler Ziele gibt es keine bequemere Alternative. Für alles, was am Ende doch nur eine HTTP-Anfrage ist, greifst du besser zu cURL. Diese Unterscheidung macht PHP stream_socket_client zu einem Werkzeug, das man selten braucht und dann durch nichts ersetzen kann.