Eine IP-Adresse sieht aus wie Text, ist aber eigentlich eine Zahl. Wer das ausnutzt, kann Adressen sortieren, Bereiche abfragen und ganze Netze in einer einzigen Rechenoperation prüfen. Genau dafür gibt es PHP ip2long: die Funktion nimmt eine IPv4-Adresse in der gewohnten Punktschreibweise entgegen und gibt eine ganze Zahl zurück.
Bevor der erste Code kommt, lohnt die Frage, warum man sich diesen Umweg überhaupt antun sollte.
Warum PHP ip2long() eine IP-Adresse zur Zahl macht
Der Grund ist der Vergleich. Als Text betrachtet steht 192.168.1.9 hinter 192.168.1.10, weil zeichenweise verglichen wird und die 9 grösser ist als die 1. Als Zahl stimmt die Reihenfolge sofort. Genauso verhält es sich mit Bereichen: die Frage, ob eine Adresse zwischen zwei anderen liegt, ist mit zwei Zahlen trivial und mit zwei Zeichenketten fast unmöglich.
Der zweite Grund ist die Datenbank. Eine Zahlenspalte lässt sich indizieren, und eine Abfrage mit BETWEEN nutzt diesen Index. Eine Suche mit LIKE über eine Textspalte kann das nicht und liest im Zweifel die ganze Tabelle. Bei einer Zugriffstabelle mit ein paar hunderttausend Zeilen ist das der Unterschied zwischen Millisekunden und Sekunden.
Wie die Umrechnung technisch funktioniert, ist schnell erzählt. Eine IPv4-Adresse besteht aus vier Zahlen zwischen 0 und 255, jede belegt also genau acht Bit. Aneinandergereiht ergeben die vier Oktette 32 Bit, und das ist die Zahl, die herauskommt. Wer sich für solche Umrechnungen zwischen Darstellungen interessiert, findet im Tutorial zu den PHP Zahlensystemen den grösseren Zusammenhang.
PHP ip2long() und long2ip() im Einsatz
PHP ip2long braucht keine Einrichtung und keine Erweiterung. Die Funktion ist seit jeher Teil der Sprache, und ihr Gegenstück long2ip() macht den Weg zurück.
<?php
$ip = '192.168.1.1';
$zahl = ip2long($ip);
echo $zahl; /* 3232235777 */
echo long2ip($zahl); /* 192.168.1.1 */
Die Zahl 3232235777 wirkt willkürlich, ergibt sich aber direkt aus den Oktetten: 192 mal 16777216, plus 168 mal 65536, plus 1 mal 256, plus 1. Jede Position ist um den Faktor 256 gewichtet, genau wie die Stellen einer Dezimalzahl um den Faktor 10.
So weit die Theorie. In der Praxis kommen die Adressen fast immer von aussen, und damit beginnt der Teil, den viele auslassen.
Erst prüfen, dann mit PHP ip2long() umwandeln
Bekommt PHP ip2long etwas, das keine gültige IPv4-Adresse ist, liefert die Funktion false. Das klingt harmlos, ist aber die häufigste Fehlerquelle überhaupt: in einem losen Vergleich verhält sich false wie 0, und die Zahl 0 steht für die Adresse 0.0.0.0. Eine Sperrliste, die das nicht abfängt, sperrt bei jeder Fehleingabe stillschweigend den falschen Eintrag.
<?php
function ipAlsZahl(string $ip): ?string
{
$geprueft = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
if ($geprueft === false) {
return null;
}
/* sprintf haelt den Wert auch auf 32-Bit-Systemen positiv.
Rueckgabe als String, damit der Wert dort nicht doch noch
am Wertebereich von int abgeschnitten wird */
return sprintf('%u', ip2long($geprueft));
}
var_dump(ipAlsZahl('8.8.8.8')); /* string(9) "134744072" */
var_dump(ipAlsZahl('256.0.0.1')); /* NULL */
var_dump(ipAlsZahl('kein text')); /* NULL */
Die Validierung mit FILTER_VALIDATE_IP erledigt zwei Dinge auf einmal: sie fängt Unsinn ab und sorgt dafür, dass Schreibweisen mit führenden Nullen gar nicht erst durchrutschen. Mehr zu dieser Filterfamilie steht im Tutorial zu filter_validate_email und _url.
Mit einer verlässlichen Zahl in der Hand wird die Speicherung zur eigentlichen Aufgabe.
IP-Adressen in der Datenbank speichern
Der richtige Spaltentyp ist INT UNSIGNED. Das UNSIGNED ist kein Schmückwerk: ein vorzeichenbehaftetes INT reicht nur bis rund 2,1 Milliarden, während IPv4-Adressen bis rund 4,29 Milliarden gehen. Alles ab 128.0.0.0 passt damit nicht mehr sauber hinein.
| Speicherform | Platz | IPv6 | Wann sinnvoll |
INT UNSIGNED | 4 Byte | nein | Reine IPv4-Umgebung, viele Bereichsabfragen |
VARBINARY(16) | bis 16 Byte | ja | Beide Protokolle, Befüllung über inet_pton() |
VARCHAR(45) | bis 45 Byte | ja | Nur Protokollierung ohne Auswertung |
Praktisch ist, dass MySQL dieselbe Umrechnung selbst beherrscht. INET_ATON() und INET_NTOA() entsprechen genau den beiden PHP-Funktionen. Ob die Umwandlung in der Anwendung oder in der Datenbank passiert, ist Geschmackssache, solange es einheitlich bleibt.
IP-Bereiche prüfen mit BETWEEN
Jetzt zahlt sich die Umwandlung mit PHP ip2long aus. Die Frage, wie viele Zugriffe aus einem bestimmten Netz kamen, wird zu einer gewöhnlichen Bereichsabfrage.
<?php
/* Spalte: ip_zahl INT UNSIGNED NOT NULL, mit Index */
$von = ip2long('192.168.1.0');
$bis = ip2long('192.168.1.255');
$stmt = $pdo->prepare(
'SELECT COUNT(*) FROM zugriffe WHERE ip_zahl BETWEEN ? AND ?'
);
$stmt->execute([$von, $bis]);
echo $stmt->fetchColumn() . ' Zugriffe aus diesem Netz';
Derselbe Gedanke trägt weiter: eine Tabelle mit Start- und Endwerten je Land wird zur Zuordnung einer Adresse zu einem Land, ohne dass dafür ein externer Dienst nötig wäre. Auch hier gilt BETWEEN auf einer indizierten Zahlenspalte, nichts weiter. Woher die Adresse des Besuchers überhaupt kommt, zeigt das Tutorial zu IP-Adresse, Host und Referer.
Noch interessanter wird es, wenn nicht ein fester Bereich gefragt ist, sondern ein Netz in CIDR-Schreibweise.
Mit Netzmasken rechnen: die CIDR-Prüfung
Eine Angabe wie 192.168.1.0/24 bedeutet: die ersten 24 Bit bestimmen das Netz, die restlichen 8 Bit den Rechner darin. Sobald beide Adressen als Zahl vorliegen, genügt eine UND-Verknüpfung, um beide auf ihren Netzanteil zu reduzieren. Sind die Ergebnisse gleich, liegen sie im selben Netz.
<?php
function imNetz(string $ip, string $cidr): bool
{
[$netz, $laenge] = explode('/', $cidr);
$ipZahl = ip2long($ip);
$netzZahl = ip2long($netz);
if ($ipZahl === false || $netzZahl === false) {
return false;
}
/* Maske aus der Praefixlaenge erzeugen: /24 setzt die
oberen 24 Bit auf 1 und den Rest auf 0 */
$maske = -1 << (32 - (int) $laenge);
return ($ipZahl & $maske) === ($netzZahl & $maske);
}
var_dump(imNetz('192.168.1.50', '192.168.1.0/24')); /* true */
var_dump(imNetz('192.168.2.50', '192.168.1.0/24')); /* false */
Die gleiche Rechnung liefert nebenbei die Eckdaten eines Netzes. Die Netzadresse ist die IP mit ausmaskiertem Rechneranteil, die Broadcast-Adresse füllt genau diesen Anteil mit Einsen auf.
<?php
$ip = ip2long('10.0.5.37');
$maske = -1 << (32 - 24); /* entspricht /24 */
$netz = $ip & $maske;
$broadcast = $netz | ~$maske;
echo long2ip($netz); /* 10.0.5.0 */
echo long2ip($broadcast); /* 10.0.5.255 */
Damit ist der praktische Teil erzählt. Es bleiben drei Punkte, an denen PHP ip2long regelmässig Fehler produziert.
Drei Stolperfallen bei PHP ip2long()
1. Negative Zahlen auf 32-Bit-Systemen Der Rückgabewert ist vorzeichenbehaftet. Auf einem 32-Bit-System kippen deshalb alle Adressen ab 128.0.0.0 ins Negative, auf einem 64-Bit-System nicht. Wer Werte zwischen Systemen austauscht oder alte Datenbestände übernimmt, bekommt zwei unvereinbare Zahlenwelten. sprintf('%u', ip2long($ip)) liefert überall denselben vorzeichenlosen Wert.
2. false verhält sich wie 0.0.0.0 Bei ungültiger Eingabe kommt false zurück. Wer mit == vergleicht oder den Wert ungeprüft speichert, legt die Adresse 0.0.0.0 ab. Immer mit === auf false prüfen oder vorher validieren.
3. Die Spalte ohne UNSIGNED Ein einfaches INT in MySQL reicht bis 2.147.483.647. Adressen darüber werden abgeschnitten oder negativ gespeichert, je nach Servereinstellung sogar stillschweigend. Die Spalte gehört als INT UNSIGNED angelegt.
Bleibt die Frage, was passiert, wenn statt IPv4 eine moderne Adresse hereinkommt.
Wann PHP ip2long() nicht reicht: IPv6
Eine IPv6-Adresse hat 128 Bit. Sie passt damit weder in eine 32-Bit-Zahl noch in den Wertebereich einer PHP-Ganzzahl. PHP ip2long liefert bei jeder IPv6-Adresse schlicht false, und daran wird sich nichts ändern. Zuständig sind stattdessen inet_pton() und inet_ntop(), die mit beiden Protokollen umgehen.
<?php
var_dump(ip2long('2001:db8::1')); /* bool(false), IPv6 geht nicht */
/* Fuer beide Protokolle: Binaerform, passt in VARBINARY(16) */
$binaer = inet_pton('2001:db8::1');
echo bin2hex($binaer); /* 20010db8000000000000000000000001 */
echo inet_ntop($binaer); /* 2001:db8::1 */
Der Preis ist, dass Bereichsabfragen mit einer Binärspalte umständlicher werden als mit einer Zahl. Solange eine Anwendung ausschliesslich IPv4 verarbeitet, bleibt der Weg über PHP ip2long deshalb die einfachere und schnellere Lösung. Sobald IPv6 dazukommt, führt kein Weg an inet_pton() vorbei.
Die folgende Übersicht fasst zusammen, welcher Weg wann der richtige ist.
flowchart TD
A[IP-Adresse als String] --> B{Gueltige IPv4?}
B -->|Nein| C[Abbrechen oder inet_pton]
B -->|Ja| D[ip2long aufrufen]
D --> E[Wert mit sprintf sichern]
E --> F[In INT UNSIGNED speichern]
F --> G{Was ist zu tun?}
G -->|Bereich| H[BETWEEN im SQL]
G -->|Netz| I[UND mit Maske]
G -->|Anzeigen| J[long2ip aufrufen]
Fazit
PHP ip2long ist eine kleine Funktion mit grosser Wirkung. Sie verwandelt eine IPv4-Adresse in eine Zahl, und erst dadurch werden Sortierung, Bereichsabfragen und Netzberechnungen einfach. Der Weg zurück führt über long2ip(), und beide zusammen genügen für Sperrlisten, Zugriffsstatistiken und Netzprüfungen.
Drei Dinge gehören in jede Umsetzung: eine Validierung mit FILTER_VALIDATE_IP vor der Umwandlung, ein sprintf('%u', ...) gegen das Vorzeichenproblem und eine Spalte vom Typ INT UNSIGNED in der Datenbank. Wer diese drei Punkte beachtet, hat mit der Funktion keine Überraschungen mehr zu erwarten. Und wenn eines Tages IPv6 dazukommt, steht mit inet_pton() der Nachfolger bereit, der beide Welten abdeckt.