Im Konfigurationsformular tippt der Kunde imap ein, das Skript braucht aber die 143. Eine handgepflegte Tabelle im Quelltext löst das für den Moment und ist nach zwei Wochen unvollständig, weil der nächste Kunde submission einträgt und niemand daran gedacht hat.
Die Übersetzung läuft in beide Richtungen, und genau die leistet PHP getservbyname() zusammen mit seinem Gegenstück. Die Zuordnung steht dabei nicht im PHP-Quelltext, sondern in einer Datei des Betriebssystems.
Was PHP getservbyname() aus zwei Parametern macht
Die Funktion nimmt einen Dienstnamen und ein Protokoll und gibt die Portnummer als Ganzzahl zurück. Ist der Name unbekannt oder das Protokoll falsch geschrieben, kommt false.
<?php
echo getservbyname('smtp', 'tcp'); /* 25 */
echo getservbyname('imap', 'tcp'); /* 143 */
echo getservbyname('domain', 'udp'); /* 53 */
var_dump(getservbyname('imap', 'sctp'));
/* bool(false) */
Der letzte Aufruf zeigt, dass PHP getservbyname() keinen Standardwert kennt. Ein Protokoll, das in der Dienstdatei nicht vorkommt, führt nicht etwa zu einem Treffer unter tcp, sondern zu einem glatten Fehlschlag. Die häufigsten Einträge sehen so aus:
| Dienstname | Port | Protokoll | Wofür |
ssh | 22 | tcp | Fernzugang und Dateiaustausch per scp |
smtp | 25 | tcp | Mailübergabe zwischen Servern |
domain | 53 | tcp und udp | Namensauflösung, eingetragen unter beiden Protokollen |
imap | 143 | tcp | Postfachzugriff ohne TLS |
https | 443 | tcp | Web mit TLS |
submission | 587 | tcp | Einlieferung durch das Programm des Nutzers |
Syntax: die Signatur im Original
Die englische Beschreibung ist knapp, und der zweite Parameter steht dort ohne Standardwert:
getservbyname(
string $service,
string $protocol
): int|false
/* service - the internet service name, e.g. ftp
protocol - either tcp or udp, in lower case
returns - the port number as an integer, or
false when the service is unknown */
Beide Angaben sind Pflicht, und das hat einen Grund. Ein Dienstname kann unter tcp und unter udp eingetragen sein, die Dienstdatei führt dafür zwei getrennte Zeilen. PHP getservbyname() muss also wissen, welche der beiden gemeint ist. Bei der Schreibweise des Protokolls scheiden sich übrigens die Systeme: Auf Linux und macOS wird genau verglichen, TCP trifft dort keinen Eintrag. Unter Windows ist der Vergleich dagegen unempfindlich und liefert dieselbe 25. Wer plattformübergreifend arbeitet, schickt das Protokoll deshalb durch strtolower(), bevor es in den Aufruf geht.
Der Dienst domain ist das Standardbeispiel dafür: Er steht unter tcp und unter udp, beide Male auf 53. Daraus folgt aber nicht, dass die Nummer immer dieselbe sein muss. Die Datei kann denselben Namen mit verschiedenen Nummern führen, und deshalb nimmt PHP getservbyname() das Protokoll nicht als hübsches Beiwerk entgegen, sondern als zweite Hälfte der Frage.
Woher PHP getservbyname() die Antwort nimmt
Die Funktion schlägt in der Dienstdatenbank des Systems nach. Unter Linux und macOS ist das /etc/services, unter Windows liegt die gleichnamige Datei in %SystemRoot%\System32\drivers\etc. Eine Zeile darin trägt den Namen, die Portnummer mit Protokoll und optional Aliasnamen sowie einen Kommentar.
flowchart TD
A[Dienstname und Protokoll] --> B[Dienstdatei]
B --> C{Eintrag gefunden?}
C -- ja --> D[Portnummer]
C -- nein --> E[false]
Der Aufbau einer solchen Datei ist schnell erklärt:
# Name Port/Protokoll Alias
ftp 21/tcp
ssh 22/tcp
smtp 25/tcp mail
domain 53/tcp
domain 53/udp
http 80/tcp www
imap 143/tcp imap2
https 443/tcp
Die Inhalte gehen auf die offizielle Portliste der IANA zurück. Auf dem einzelnen Rechner liegt davon aber nur eine Kopie, die das Betriebssystem mitbringt und die der Administrator erweitern kann. Ein Abruf im Netz findet nicht statt, und das ist der Grund, warum zwei Server auf dieselbe Frage verschiedene Antworten geben können.
Zwei Kleinigkeiten an dieser Datei sind im Alltag nützlich. Zeilen mit einer Raute am Anfang sind Kommentare und werden übergangen. Die dritte Spalte trägt Aliasnamen, und die zählen mit: PHP getservbyname() findet den Eintrag auch über www statt http. Eine Konfiguration muss also nicht auf den Hauptnamen getrimmt werden, bevor sie in den Aufruf geht.
Der Unterschied zwischen den Systemen ist größer, als er klingt. Die Windows-Dienstdatei kennt submission auf Port 587 nicht, das unter Linux seit Langem eingetragen ist. Und in schlanken Container-Abbildern fehlt die Datei ganz: In sechs geprüften PHP-Abbildern war /etc/services in keinem einzigen vorhanden.
getservbyport(): der Weg zurück
Das Gegenstück zu PHP getservbyname() nimmt eine Portnummer und liefert den Namen. Im Betrieb ist das sogar die häufigere Richtung, weil in Protokollen, Firewall-Regeln und Verbindungslisten Zahlen stehen.
<?php
echo getservbyport(143, 'tcp'); /* imap */
echo getservbyport(443, 'tcp'); /* https */
var_dump(getservbyport(51234, 'tcp'));
/* bool(false) */
Zu einer Portnummer können mehrere Namen eingetragen sein, etwa wenn ein Dienst unter zwei Bezeichnungen geführt wird. Geliefert wird dann der erste Treffer aus der Datei. Wer die Ausgabe einem Menschen zeigt, sollte das wissen: Der Name ist eine Auskunft aus einer Textdatei und kein Beweis dafür, welches Programm den Port tatsächlich belegt.
Praxis: Eingaben annehmen, die Name oder Zahl sein dürfen
Ein Formularfeld, in dem beides erlaubt ist, spart Rückfragen. Die Prüfung braucht drei Zweige: Ziffern gehen durch eine Bereichsprüfung, alles andere durch PHP getservbyname(), und was danach übrig bleibt, wird mit einer verständlichen Meldung abgelehnt.
<?php
function portErmitteln(string $eingabe): int
{
$eingabe = trim($eingabe);
if (ctype_digit($eingabe)) {
$port = (int) $eingabe;
if ($port < 1 || $port > 65535) {
throw new RuntimeException(
'Port außerhalb 1 bis 65535'
);
}
return $port;
}
$port = getservbyname($eingabe, 'tcp');
if ($port === false) {
throw new RuntimeException(
'Unbekannter Dienst: ' . $eingabe
);
}
return $port;
}
Der strikte Vergleich mit === ist hier kein Schmuck. Bei einem lockeren Vergleich ist false nicht von der Zahl 0 zu unterscheiden, und aus einem vertippten Dienstnamen würde stillschweigend Port 0. Die erzeugte Konfiguration wäre falsch, ohne dass irgendwo eine Meldung auftaucht.
Aus einer Dienstliste eine Portliste erzeugen
Für ein Monitoring steht oft eine Liste von Dienstnamen bereit, gebraucht werden Portnummern. Die Schleife sammelt Treffer und Fehlschläge getrennt, statt Unbekanntes zu verschlucken:
<?php
$dienste = ['http', 'https', 'ssh', 'meinapi'];
$ports = [];
$unbekannt = [];
foreach ($dienste as $name) {
$port = getservbyname($name, 'tcp');
if ($port === false) {
$unbekannt[] = $name;
continue;
}
$ports[$name] = $port;
}
file_put_contents(
'/srv/monitoring/ports.json',
json_encode($ports, JSON_PRETTY_PRINT)
);
Die zweite Liste ist das Eigentliche an diesem Muster. Sie gehört ins Protokoll oder in die Ausgabe, denn ein Eintrag wie meinapi steht in keiner Dienstdatei und muss von Hand nachgetragen werden. Zum Wegschreiben der Ergebnisdatei lohnt ein Blick auf die Feinheiten beim Schreiben, Anhängen und Sichern von Dateien mit file_put_contents().
Protokollzeilen lesbar machen
In einer Firewall-Zeile steht eine Zahl, im Kopf des Lesers aber ein Dienst. getservbyport() schreibt den Namen daneben, und der Rest ist Zeichenkettenarbeit:
<?php
$zeile = 'DROP TCP 10.0.0.5:143';
[$aktion, , $ziel] = explode(' ', $zeile);
$port = substr($ziel, strrpos($ziel, ':') + 1);
$name = getservbyport((int) $port, 'tcp');
$name = $name !== false ? $name : 'unbekannt';
printf('%s %s [%s]', $aktion, $ziel, $name);
/* DROP 10.0.0.5:143 [imap] */
Ohne den Rückfall auf unbekannt steht bei jedem hohen Quellport ein leerer String in der Ausgabe. Gerade die Ports oberhalb von 49152 vergibt das Betriebssystem dynamisch, und die stehen in keiner Dienstdatei.
Bei einem Protokoll mit zehntausend Zeilen lohnt ein kleiner Zwischenspeicher. Jeder Aufruf schlägt erneut in der Dienstdatei nach, und derselbe Zielport taucht in einem Firewall-Log hundertfach auf. Ein Array, das die bereits ermittelten Namen behält, spart diese Wege, ohne am Ergebnis etwas zu ändern. Zwei Zeilen genügen dafür: static $cache = []; am Kopf der Funktion, die eine Protokollzeile verarbeitet, und $cache[$port] ??= getservbyport($port, 'tcp'); anstelle des direkten Aufrufs.
Was PHP getservbyname() nicht kann
Diese Funktion beantwortet, welche Portnummer zu einem Dienstnamen gehört. Ob dort etwas lauscht, beantwortet sie nicht. Es fließt kein einziges Paket, es wird keine Verbindung aufgebaut, es wird eine lokale Textdatei gelesen. Ein Treffer bedeutet ausschließlich, dass ein Name eingetragen ist.
Wer wissen will, ob auf der Gegenstelle wirklich jemand antwortet, baut die Verbindung auf. Die ermittelte Portnummer wandert dafür in fsockopen() oder, mit mehr Kontrolle über Zeitlimit und TLS, in stream_socket_client() für TCP- und TLS-Verbindungen. Und wer die Adresse der Gegenstelle noch gar nicht kennt, holt sie vorher über dns_get_record() zum Abfragen von DNS-Einträgen. Host und Port ergeben zusammen die vollständige Adresse, PHP getservbyname() liefert davon genau die Hälfte.
Wenn PHP getservbyname() false liefert
Vier Ursachen kommen in Frage, und die letzte ist die unangenehmste. Der Name steht nicht in der Datei, das Protokoll ist falsch geschrieben, der Dienst existiert nur unter udp, oder die Dienstdatei fehlt ganz. Schlanke Container-Abbilder bringen /etc/services nicht immer mit, und dann liefert jeder Aufruf false, auch bei http.
Ein einzelner Aufruf mit einem sicheren Namen zeigt das sofort. Wer sein Werkzeug ausliefert und nicht weiß, auf welchem System es landet, baut einen Rückfall ein:
<?php
const PORTS = ['http' => 80, 'imap' => 143];
function portMitFallback(string $dienst): ?int
{
$port = getservbyname($dienst, 'tcp');
if ($port !== false) {
return $port;
}
return PORTS[$dienst] ?? null;
}
Das ist die Kombination aus beiden Welten: Der Systemaufruf ist aktuell und kennt auch seltene Dienste, die eigene Tabelle veraltet zwar, deckt aber genau die Fälle ab, auf die es im eigenen Werkzeug ankommt. Zur selben Familie gehören übrigens getprotobyname() und getprotobynumber(), die dasselbe für Protokollnummern tun und ihre Daten aus /etc/protocols holen.
<?php
echo getprotobyname('tcp'); /* 6 */
echo getprotobyname('udp'); /* 17 */
echo getprotobynumber(1); /* icmp */
Fazit
PHP getservbyname() übersetzt einen Dienstnamen in eine Portnummer, getservbyport() geht denselben Weg zurück. Beide Aufrufe brauchen das Protokoll als zweites Argument, beide können false liefern, und beide lesen dabei nur eine Textdatei auf dem eigenen System.
Am meisten bringt das an zwei Stellen: beim Annehmen von Eingaben, wo ein Name genauso erlaubt sein darf wie eine Zahl, und beim Lesbarmachen von Protokollen, wo aus einer 143 wieder ein imap wird. Bei der Frage, ob ein Dienst erreichbar ist, hilft PHP getservbyname() dagegen kein Stück. Dafür braucht es eine echte Verbindung.