【问题标题】:Cannot select where ip=inet_pton($ip)无法选择 ip=inet_pton($ip) 的位置
【发布时间】:2019-08-16 06:03:08
【问题描述】:

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

IP 地址在使用 PHP 函数转换后以BINARY(16)(无排序规则)形式存储在此列中

$store_ip = inet_pton($ip);

当我尝试插入相同的 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());
}

我插入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;
}

【问题讨论】:

  • @MagnusEriksson 不是。问这个问题的人的问题要严重得多。
  • @J.Doe 我也有这个确切的问题...将增加赏金以了解更多信息
  • 为什么不使用 ip2long 和 long2ip。如果您还想要一些范围搜索,它将来帮助您。然后您也可以将一些 ip 链接到位置数据库。它将帮助您对位置等进行分组。因为有时同一位置可能有多个 IP
  • MySQL 支持INET6_ATONINET6_NTOA。为什么不使用 MySQL 而不是 PHP 来管理值? sqlfiddle.com/#!9/268d96/1 你可以避免HEX/UNHEX 使用VARBINARY(16) 以及sqlfiddle.com/#!9/983762/3
  • @fyrye 谢谢,我不知道这是可能的...使用VARCHAR(250) 不好,因为我已经可以将VARCHAR(45) 用于原始IP,并尝试使用BINARY(16) 以获得更好的效果表现。但是第二个链接看起来很棒,必须尝试一下

标签: php mysql pdo


【解决方案1】:

首先修复,这很简单: 如果要同时存储 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')]);

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

0x7F000001000000000000000000000000

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

当你现在尝试用

找到它时
$stmt = $db->prepare("SELECT * FROM ips WHERE ip = ?");
$stmt->execute([inet_pton('127.0.0.1')]);

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

SELECT 0x00 = 0x0000

这将返回0 (FALSE)。

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

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

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

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

var_dump(inet_ntop($stmt->fetch(PDO::FETCH_OBJ)->ip));

我们会看到

string(8) "7f00:1::"

但这不是(真的)我们试图存储的内容。 当我们现在尝试存储7f00:1::时, 我们会得到一个重复键错误, 虽然我们还没有存储任何 IPv6 地址。

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

【讨论】:

  • 谢谢,我今晚试试,从这个人那里得到BINARY(16)的想法:stackoverflow.com/a/6427883/11210517
  • OP 不使用inet_ntop 将存储的DB 值转换为IP 地址,因此您的观点无效。不过,关于使用VARBINARY 的建议很好。
  • @Styx 我认为这不会使我的观点无效。但我同意,如果你说它没有直接解决 OP 的问题。所以我扩展了我的答案,描述了为什么查询不返回任何行。感谢您的评论。
  • 是的,现在很清楚为什么 OP 的代码不能按预期工作。谢谢。
  • 0x00 (=0) 与 0x0000 (=0) 什么时候不同了? (刚刚测试,确实不一样。这很令人惊讶。因为0 = 00 = 000
【解决方案2】:

我不会回答为什么您的代码没有按预期工作,因为我不知道具体情况。感谢@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]);

这个

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

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

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

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

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

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

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

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

【讨论】:

  • “每当我有机会让数据库做某事而不是应用程序层” - 您几乎可以使用存储过程在数据库中做任何事情,并且只使用 PHP 进行路由。但是,如果您在 PHP 或 SQL 中转换 IP 地址 - 谁在乎?唯一的区别 - 如果你用 PHP 来做,你可以节省一些流量。而且代码短了一个字节 :-)
  • 是的,让我更喜欢用 PHP 做事的唯一原因是节省数据库服务器和 Web 服务器之间的流量,感谢您注意到这一点。
【解决方案3】:

与其为逃避BINARY而苦苦挣扎,不如避免这种情况。

INSERT INTO ips (ip) VALUES(INET6_ATON(?))

SELECT INET6_NTOA(ip) FROM ips WHERE ...;

这样,您只使用人类可读的字符串。

注意事项:

  • 在 PHP 中跳过inet_pton() 的使用,因为转换现在正在 MySQL 中完成。
  • INET6... 函数在旧版本的 MySQL 中不存在。
  • 是的,请使用VARBINARY(16),并确保检查 IPv4 字符串(如“1.2.3.4”)是否有效。

【讨论】:

  • 你好 Rick,“检查 IPv4 字符串(如“1.2.3.4”)是否有效。” . IPv4 是否有可能无法以这种方式工作?
  • 澄清一下,在 MySQL 5.6+ 中可用的 INET6_ 函数支持 IPV4 和 IPV6 的符号字符串。原始的INET_ 函数仅支持 IPV4 的符号字符串。此外,还可以使用IS_IPV4()IS_IPV6() 验证IP 符号字符串的功能。当INET6_ATON 无法解析提供的值时,MySQL 将在插入期间发出警告,从而导致NULL 值。
  • 请注意INET6_NTOA 不适用于具有BINARY(16) 的IPv4 地址?您需要使用VARBINARY
  • 我还想知道 IPv4 是否有可能因为您的最后一条注释而无法以这种方式工作:请务必检查 IPv4 字符串(如“1.2.3.4”)是否存在会工作
  • @J.Doe - 试一试并报告。
猜你喜欢
  • 2012-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-28
  • 2022-01-13
  • 2016-11-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多