【问题标题】:PHP: match an IP within a list of subnets (CIDR)PHP:匹配子网列表(CIDR)中的IP
【发布时间】:2018-01-18 10:10:56
【问题描述】:

我有一个这样的 CIDR 列表:

192.168.0.1/24
10.0.0.1/32
etc...

列表正在增长。
为了检查 IP 是否适合这些 CIDR 之一,我使用以下函数执行 循环

function cidr_match($ip, $range){
    list ($subnet, $bits) = explode('/', $range);
    $ip = ip2long($ip);
    $subnet = ip2long($subnet);
    $mask = -1 << (32 - $bits);
    $subnet &= $mask; // in case the supplied subnet was not correctly aligned
    return ($ip & $mask) == $subnet;
}

由于我的 CIDR 列表正在增长,我想改进该功能以避免逐行测试 CIDR 的每一行,直到它返回 true。我想摆脱上面我的函数周围的 for 循环。
有没有办法对我要检查的 IP 执行某种“预检查”,这样它就不会按顺序(从上到下)运行完整列表?
我想优化,以便我的代码会以这种方式运行:将 IP 提供给函数 -> 函数种类“排序”列表或“查找”最可能的 CIDR -> 对 IP 运行检查最可能的 CIDR->尽快返回“true”
我们将不胜感激。

【问题讨论】:

    标签: php ip cidr


    【解决方案1】:

    老实说,除非您的 CIDR 范围非常大,并且您在同一进程中检查大量 IP,否则您可能不会看到性能提升的多少。但是,如果这是您正在查看的场景,那么您可以考虑通过预处理您的范围和 IP(执行一次 ip2long() 调用并存储分离的掩码/子网以进行比较来尝试压缩一些性能)。

    例如,这就是你今天的做法,我假设:

    <?php
    // Original style
    $ranges = array(
      "192.168.0.1/32",
      "192.168.0.1/26",
      "192.168.0.1/24",
      "192.168.0.1/16",
      "127.0.0.1/24",
      "10.0.0.1/32",
      "10.0.0.1/24"
    );
    
    
    // Run the check
    $start = microtime(true);
    find_cidr("10.0.0.42", $ranges);
    find_cidr("192.168.0.12", $ranges);
    find_cidr("10.0.0.1", $ranges);
    $end = microtime(true);
    echo "Ran 3 find routines in " . ($end - $start) . " seconds!\n";
    
    function find_cidr($ip, $ranges)
    {
      foreach($ranges as $range)
      {
        if(cidr_match($ip, $range))
        {
          echo "IP {$ip} found in range {$range}!\n";
          break;
        }
      }  
    }
    
    function cidr_match($ip, $range){
        list ($subnet, $bits) = explode('/', $range);
        $ip = ip2long($ip);
        $subnet = ip2long($subnet);
        $mask = -1 << (32 - $bits);
        $subnet &= $mask; // in case the supplied subnet was not correctly aligned
        return ($ip & $mask) == $subnet;
    }
    

    在我的机器上,运行时间大约为 0.0005 - 0.001 秒(针对少数范围检查 3 个 IP)。

    如果我写一些东西来预处理范围:

    <?php
    // Slightly-optimized style
    
    $ranges = array(
      "192.168.0.1/32",
      "192.168.0.1/26",
      "192.168.0.1/24",
      "192.168.0.1/16",
      "127.0.0.1/24",
      "10.0.0.1/32",
      "10.0.0.1/24"
    );
    
    $matcher = new BulkCIDRMatch($ranges);
    $start = microtime(true);
    $matcher->FindCIDR("10.0.0.42");
    $matcher->FindCIDR("192.168.0.12");
    $matcher->FindCIDR("10.0.0.1");
    $end = microtime(true);
    echo "Ran 3 find routines in " . ($end - $start) . " seconds!\n";
    
    
    class BulkCIDRMatch
    {
      private $_preparedRanges = array();
    
      public function __construct($ranges)
      {
        foreach($ranges as $range)
        {
          list ($subnet, $bits) = explode('/', $range);
          $subnet = ip2long($subnet);
          $mask = -1 << (32 - $bits);
          $subnet &= $mask; // in case the supplied subnet was not correctly aligned
    
          $this->_preparedRanges[$range] = array($mask,$subnet);
        }
      }
    
      public function FindCIDR($ip)
      {
        $result = $this->_FindCIDR(ip2long($ip));
        if($result !== null)
        {
          echo "IP {$ip} found in range {$result}!\n";
        }
        return $result;
      }
    
      private function _FindCIDR($iplong)
      {
        foreach($this->_preparedRanges as $range => $details)
        {
          if(($iplong & $details[0]) == $details[1])
          {
            return $range;
          }
        }
    
        // No match
        return null;
      }
    }
    

    ...然后我看到更快的 CHECKING 但是在初始化类并处理和存储所有范围时,开始时的开销会稍多一些。因此,如果我对少数范围仅使用 3 个 IP 对 OVERALL 运行进行计时,那么“优化”方式实际上会慢一些。但是,如果我针对 10,000 个 CIDR 运行 1,000 个 IP,则“优化”方式将比原始方式有更显着的改进(以额外的内存使用为代价来存储预处理的范围数据)。

    所以这完全取决于音量和你想要做什么。

    也就是说,如果您担心 0.001 秒的性能太慢,那么 PHP 可能不适合用于您的检查。或者至少您可能想考虑编写一个自定义扩展,以便更多的处理在 C 中完成。

    编辑:要回答有关查找要检查的“可能”范围的原始问题(在对其字符串形式进行任何类型的转换之前),尝试这可能不是一件非常可靠的事情。范围可以跨越它们的初始八位位组,因此如果您开始比较这些值(例如“我正在查看 192.168.1.0,所以我只会查看从 192 开始的范围”),您不仅会招致每个条目的字符串比较的性能开销(这会减慢整体查找速度),但您可能会错过有效范围。

    【讨论】:

    • 然后您建议顺序读取“ip2longs”列表而不是“CIDRs”列表,这与相同的问题有关。关键字确实是“可能的”。感谢您的帮助(和基准!)
    • 您可以通过它们的起始长度将范围分隔为单独的数组(例如 0-10000000、10000001-20000000 等),并首先通过较短的数组列表,然后循环遍历任何范围内的一般附近。但同样,在这样的速度下,体积必须很大才能产生很大的不同,您必须对其进行调整以找到正确的权衡。
    【解决方案2】:

    如果您真的关心性能,那么您应该将列表存储在类似于结构的东西中,并以一种并不意味着在找到匹配项之前查看每个条目的方式进行搜索。

    在这种情况下,它是一个排序列表和二进制搜索:

    class CidrList {
    
        protected $ranges = [];
    
        public function addRanges($ranges) {
            foreach($ranges as $range) {
                $this->addRange($range);
            }
            $this->sortRanges();
        }
    
        public function findRangeByIP($ip) {
            return $this->_findRangeByIP(ip2long($ip));
        }
    
        // simple binary search
        protected function _findRangeByIP($ip, $start=NULL, $end=NULL) {
            if( $end < $start || $start > $end ) { return false; }
    
            if( is_null($start) ) { $start = 0; }
            if( is_null($end)   ) { $end = count($this->ranges) -1; }
    
            $mid = (int)floor(($end + $start) / 2);
            switch( $this->inRange($ip, $this->ranges[$mid]) ) {
                case 0:
                    return $this->ranges[$mid][2];
                case -1:
                    return $this->_findRangeByIP($ip, $start, $mid-1);
                case 1:
                    return $this->_findRangeByIP($ip, $mid+1, $end);
            }
        }
    
        // add a single range, protected as the list must be sorted afterwards.
        protected function addRange($range) {
            list ($subnet, $bits) = explode('/', $range);
            $subnet = ip2long($subnet);
            $mask = -1 << (32 - $bits);
            $min = $subnet & $mask;
            $max = $subnet | ~$mask;
            $this->ranges[] = [$min, $max, $range];
        }
    
        // sort by start, then by end. aka from narrowest overlapping range to widest
        protected function sortRanges() {
            usort($this->ranges, function($a, $b) {
                $res = $a[0] - $b[0];
                switch($res) {
                    case 0:
                        return $a[1] - $b[1];
                    default:
                        return $res;
                }
            });
        }
    
        protected function inRange($ip, $range) {
            list($start, $end, $cidr) = $range;
            if( $ip < $start ) { return -1; }
            if( $ip > $end ) { return 1; }
            return 0;
        }
    }
    

    用法:

    $l = new CidrList();
    $l->addRanges(["192.168.0.1/16", "192.168.0.1/24", "127.0.0.1/24", "10.0.0.1/24"]);
    
    var_dump(
        $l->findRangeByIP('192.168.0.10'),
        $l->findRangeByIP('192.168.1.10'),
        $l->findRangeByIP('1.2.3.4')
    );
    

    输出:

    string(14) "192.168.0.1/24"
    string(14) "192.168.0.1/16"
    bool(false)
    

    此外,您应该通过缓存整个 CidrList 对象或其内部范围集来避免不断地重新处理字符串。

    【讨论】:

    • 这似乎更接近我想要实现的目标。非常感谢!您会推荐一种缓存对象的方法吗?我正在考虑好的旧文件缓存方式......(blog.graphiq.com/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-23
    • 1970-01-01
    • 1970-01-01
    • 2015-11-14
    • 1970-01-01
    • 2011-11-10
    • 2022-12-07
    相关资源
    最近更新 更多