无法选择ip = inet_pton($ ip)的位置

J. *_*Doe 14 php mysql pdo

我在数据库中有一个唯一的列,名为 ip

BINARY(16)使用PHP函数将IP地址转换为IP地址(不排序规则)后,将其存储在此列中

$store_ip = inet_pton($ip);
Run Code Online (Sandbox Code Playgroud)

当我尝试两次插入相同的IP时,它可以正常工作并失败,因为它是唯一的,

但是,当我尝试选择IP时,它将不起作用,并且始终返回FALSE(未找到)

<?php

try {
    $ip = inet_pton($_SERVER['REMOTE_ADDR']);
    $stmt = $db->prepare("SELECT * FROM `votes` WHERE ip=?");
    $stmt->execute([$ip]);
    $get = $stmt->fetch();

    if( ! $get){
        echo 'Not found';
    }else{
        echo 'Found';
    }

    // close connection
    $get = null;
    $stmt = null;

} catch (PDOException $e) {
    error_log($e->getMessage());
}
Run Code Online (Sandbox Code Playgroud)

我插入IP的部分:

<?php

if( ! filter_var($ip, FILTER_VALIDATE_IP)){
        return FALSE;
}

$ip = inet_pton($_SERVER['REMOTE_ADDR']);

try {
    $stmt = $db->prepare("INSERT INTO votes(ip, answer) VALUES(?,?)");
    $stmt->execute([$ip, $answer]);
    $stmt = null;
} catch (PDOException $e) {
    return FALSE;
}
Run Code Online (Sandbox Code Playgroud)

Pau*_*gel 14

首先,解决方法非常简单:如果要同时存储IPv4和IPv6地址,则应使用VARBINARY(16)而不是BINARY(16)

现在解决问题:为什么它不能按预期使用BINARY(16)

考虑我们有一个ips只有一列的表ip BINARY(16) PRIMARY KEY。我们将默认的本地IPv4地址存储在

$stmt = $db->prepare("INSERT INTO ips(ip) VALUES(?)");
$stmt->execute([inet_pton('127.0.0.1')]);
Run Code Online (Sandbox Code Playgroud)

并在数据库中找到以下值:

0x7F000001000000000000000000000000
Run Code Online (Sandbox Code Playgroud)

如您所见-这是一个4字节的二进制值(0x7F000001),右边用零填充,以适合16字节的固定长度列。

现在,当您尝试使用

$stmt = $db->prepare("SELECT * FROM ips WHERE ip = ?");
$stmt->execute([inet_pton('127.0.0.1')]);
Run Code Online (Sandbox Code Playgroud)

发生以下情况:PHP将值0x7F000001作为参数发送,然后将其与存储的值进行比较0x7F000001000000000000000000000000。但是由于两个不同长度的二进制值永远不会相等,因此WHERE条件将始终返回FALSE。你可以尝试一下

SELECT 0x00 = 0x0000
Run Code Online (Sandbox Code Playgroud)

它将返回0(FALSE)。

注意:固定长度的非二进制字符串(CHAR(N))的行为不同。

我们可以使用显式强制转换作为解决方法:

$stmt = $db->prepare("SELECT * FROM ips WHERE ip = CAST(? as BINARY(16))");
$stmt->execute([inet_pton('127.0.0.1')]);
Run Code Online (Sandbox Code Playgroud)

它将找到该行。但是如果我们看看我们得到了什么

var_dump(inet_ntop($stmt->fetch(PDO::FETCH_OBJ)->ip));
Run Code Online (Sandbox Code Playgroud)

我们会看到

string(8) "7f00:1::"
Run Code Online (Sandbox Code Playgroud)

但这不是(真的)我们尝试存储的内容。现在,当我们尝试存储时7f00:1::,我们将得到一个重复的密钥错误,尽管我们尚未存储任何IPv6地址。

因此,再次:使用VARBINARY(16),您可以使代码保持不变。如果您存储许多IPv4地址,您甚至可以节省一些存储空间。


Acc*_*t م 5

我不会回答为什么你的代码没有按预期工作,因为我不太清楚。感谢@Paul Spiegel 的精彩回答,他解释了原因。

在这个答案中我只是建议你使用 MySQL 内置函数而不是 PHP。

这就是我在应用程序中处理 IP 的方式,到目前为止,我使用此模型没有遇到任何问题。

我将 IP 存储在varbinary(16)列中,并使用 MySQL 内置函数进行转换

  1. inet6_aton用于将 IP 字符串转换为二进制

  2. inet6_ntoa用于将二进制转换为 IP 字符串

所以替换这段代码

//query 1 
$ip = inet_pton($_SERVER['REMOTE_ADDR']);
$stmt = $db->prepare("SELECT * FROM `votes` WHERE ip=?");
$stmt->execute([$ip]);
Run Code Online (Sandbox Code Playgroud)

与这个

//query 2 
$stmt = $db->prepare("SELECT * FROM `votes` WHERE ip=INET6_ATON(?)");
$stmt->execute([$_SERVER['REMOTE_ADDR']]);
Run Code Online (Sandbox Code Playgroud)

不用说,不要这样做(查询 3)-

//query 3 
$stmt = $db->prepare("SELECT * FROM `votes` WHERE INET6_NTOA(ip)= ?");
$stmt->execute([$_SERVER['REMOTE_ADDR']]);
Run Code Online (Sandbox Code Playgroud)

(因为数据库会讨厌你让它对表中的每个IP记录进行转换)

从我短暂的经验来看,我发现每当我有机会让数据库而不是应用程序层(PHP)做某事时,就让数据库立即做。让 MySQL 变胖,PHP 变瘦 =) ,就像胖模型和瘦控制器所说的那样。

当您在数据库中完成大部分工作时,这将使您的数据库可以独立于 PHP 代码更好地工作(它不需要它),这使您的数据库更加可移植。

例如,如果您想将系统从使用 、 的基于云的网络系统转变为使用语言的PHP本地系统,开发人员会喜欢您减少代码编写,因为大部分工作已经编写并完成由 MySQL 提供。.net.net

另一个例子,你的应用程序成功了,你现在有更多的客户想要你的应用程序,你只需为他们提供另一台服务器并仅在其上安装MySQL,并且由于大部分工作是由数据库完成的,因此你的应用程序扩展比安装更容易一个完整的 Web 服务器,并为 PHP 进行扩展。