在C语言中比较包含IPv4地址的字符串

3

我有两个字符串ip1 = "192.168.145.123"ip2 = "172.167.234.120"

我可以比较这两个字符串是否相等:

strncmp(ip1,ip2) == 0

然而,我该如何找出

if (ip1 > ip2) {
    ...
}

我尝试过的方法

我可以使用sscanf:

sscanf(ip1,"%d.%d.%d.%d",&s1,&s2,&s3,&s4) 

存储数字并进行比较。 然而,在32位系统中,由于上限,我无法将数字存储为整数。

因此,我别无选择,只能将整数作为字符串进行比较。


4
你所提到的“上限”是指什么?(如果是 int 的符号性质,为什么不使用 unsigned int"%u" 进行读取?) - WhozCraig
2
一个IPv4地址可以完美地存储在一个4字节宽的无符号整数中。正如在这个答案http://stackoverflow.com/a/18291062/694576中展示给了你的问题之一。 - alk
2
@JoachimPileborg 很确定他想要 192.168.1.1 大于 10.0.0.1。将它们存储在32位的 int 中无法实现,但是使用 unsigned int 可以。 - WhozCraig
3
为什么不直接将它们作为字符串按字典顺序进行比较呢?IP地址没有实际的排序方式;172.167.234.120并不比192.168.145.123更早、更东或更快。对于管理数据结构,其中一个用途就是对其进行排序。为此,任何排序都足够了,因此最好选择您可以计算得最快的方法。只要知道没有前导零,字符串比较就可以胜任这项任务。如果这不是目的,那么为什么IP地址需要排序呢? - Eric Postpischil
2
当然IP地址有顺序,例如在路由表中按网络部分排序。您可能希望对IP地址列表进行qsort()以使用bsearch()查看其中一个是否已经在表中。不要草率下结论,Watson :-) - Jens
显示剩余7条评论
4个回答

14

是否值得提到也有 inet_aton 函数?你可以在这里找到该函数的 man 手册,下面是简短的描述和摘要。

这个解决方案适用于大多数 POSIX 系统,但我相信在 Windows API 中也有相应的等效物,甚至可能有一些抽象包装。

inet_ntoa() 在 POSIX.1-2001 中指定。inet_aton() 没有在 POSIX.1-2001 中指定,但在大多数系统上可用。


Linux 程序员手册

inet_aton() 将 IPv4 的点分十进制表示法 Internet 主机地址 cp 转换为二进制形式(网络字节顺序),并将其存储在 inp 所指向的结构中。

摘要

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

示例

下面展示了使用inet_aton()和inet_ntoa()的示例。以下是一些示例运行:

       $ ./a.out 226.000.000.037      # Last byte is in octal
       226.0.0.31
       $ ./a.out 0x7f.1               # First byte is in hex
       127.0.0.1
程序源代码
   #define _BSD_SOURCE
   #include <arpa/inet.h>
   #include <stdio.h>
   #include <stdlib.h>

   int
   main(int argc, char *argv[])
   {
       struct in_addr addr;

       if (argc != 2) {
           fprintf(stderr, "%s <dotted-address>\n", argv[0]);
           exit(EXIT_FAILURE);
       }

       if (inet_aton(argv[1], &addr) == 0) {
           fprintf(stderr, "Invalid address\n");
           exit(EXIT_FAILURE);
       }

       printf("%s\n", inet_ntoa(addr));
       exit(EXIT_SUCCESS);
   }

更多信息

  • 字节顺序 (@Jonathan Leffler)

    inet_ntoa()函数将以网络字节序表示的Internet主机地址in转换为IPv4点分十进制表示的字符串。 inet_aton()将IPv4数字和点符号表示法中的Internet主机地址cp转换为二进制形式(采用网络字节序),并将其存储在inp指向的结构中。

  • in_addr结构 (@POW)

    in_addr结构在inet_ntoa()inet_makeaddr()inet_lnaof()inet_netof()中使用,其定义如下:

   typedef uint32_t in_addr_t;

   struct in_addr {
       in_addr_t s_addr;
   };
  • 独立于计算机字节序,比较地址 in_addr 中的地址是以网络字节序(大端序)存储的,因此如 @glglgl 所指出的,您需要使用 ntohl 函数进行转换,其手册页面可在此处查看.

    ntohl() 函数将无符号整数 netlong 从网络字节序转换为主机字节序。

  • uint32_t ntohl(uint32_t netlong);
    

    1
    这个在大端和小端机器上都能正确排序吗?网络排序不一定是本地排序,所以我怀疑它可能会混淆。如果它能正确工作,那么这就是一个好答案。 - Jonathan Leffler
    @JonathanLeffler,使用inet_aton()函数会得到一个网络字节序的“内存转储”。然后使用ntohl()函数可以得到正确的顺序。 - glglgl
    这是一个很好的回答,但不确定是否回答了“如何找出if (ip1 > ip2) { ...}”的问题。 - P0W
    @JonathanLeffler,我在我的帖子中添加了一句话,这应该可以回答你的问题。 - Geoffroy
    @POW,我使用“struct in_addr”结构进行编辑,这样您就可以获得数字值 :) - Geoffroy
    1
    谢谢更新。我的担忧是inet_aton()将字符串转换为网络顺序地址,但网络顺序(大端)在Intel(小端)机器上无法准确比较。我认为@glglgl或多或少同意我的看法。为了使结果在机器上可比较,您必须使用例如ntohl()将网络顺序值转换为主机顺序。然后您可以使用本机值进行比较。 - Jonathan Leffler

    4

    你可以尝试一种高端的方式,将所有值存储在一个无符号整数中并进行比较。

      const char* ip1 = "192.168.145.123";
      const char* ip2 = "172.167.234.120";
    
      unsigned char s1, s2, s3, s4;
      unsigned int uip1, uip2;
    
      sscanf(ip1,"%hhu.%hhu.%hhu.%hhu",&s1,&s2,&s3,&s4);
      uip1 = (s1<<24) | (s2<<16) | (s3<<8) | s4; //store all values in 32bits unsigned int
    
      sscanf(ip2,"%hhu.%hhu.%hhu.%hhu",&s1,&s2,&s3,&s4);
      uip2 = (s1<<24) | (s2<<16) | (s3<<8) | s4;
    
      if (uip1 > uip2)
      {
        printf("ip1 greater !");   
      }
      else
      {
        printf("ip2 greater or equal !");     
      }
    

    1
    调用sscanf()的格式符不应该使用%hhu吗? - alk
    3
    请勿使用 sscanf() 函数,改用 strtoul() 函数。 - user529758
    4
    为什么不使用sscanf()?相比于sscanf(),使用strtoul()解决了什么问题?测试strtoul()的结果是一个非常微妙的问题,但由于这些数据的值应该在0..255范围内,因此验证应该比一般的“任何值都有效”的情况简单得多。 - Jonathan Leffler
    4
    为什么?使用sscanf是完全可以的(但应该测试返回值)。与使用strtok或其他查找点号的方法相比,用sscanf解析四个数字要容易得多。 - Jens
    5
    因此,这是教育正确使用 sscanf() 的原因;而不是要求使用 strtoul()。使用 strtoul() 必须非常小心地处理所有错误情况(比 sscanf() 更加小心),以便正确检测所有错误条件。诚然,strtoul() 将检测到 sscanf() 无法检测到的错误(如溢出)- 但是一概而论的“不要使用”是过于笼统的,需要更加细致入微的回应。 - Jonathan Leffler
    显示剩余7条评论

    1
    这个怎么样:
    #include<stdio.h>
    #include<conio.h>
    
    unsigned int convIP(const char ip[]) {
        unsigned char s1, s2, s3, s4;
    
        if (sscanf(ip, "%hhu.%hhu.%hhu.%hhu", &s1, &s2, &s3, &s4) != 4)
            return 0;
    
        /* Create a 32 bit Integer using left shift & bitwise OR
                MSB                                            LSB
                +-----8----+-----8------+-----8-----+----8-----+
                |    s1    |     s2     |    s3     |    s4    |   
                +----------+------------+-----------+----------+
         */
        return  (s1 << 24) | (s2 << 16) | (s3 << 8) | (s4 << 0);
    
    }
    
    int ipComp(const char ip1[], const char ip2[]) {
        unsigned int ip_addr1 = convIP(ip1);
        unsigned int ip_addr2 = convIP(ip2);
    
        return (ip_addr1 >= ip_addr2);
    
    }
    
    
    int main()
    {
    
        printf("%d\n",ipComp("192.168.145.123","172.167.234.120") ); //1
    
        printf("%d\n", ipComp("10.0.0.1","192.168.1.1") );  //0
    
        printf("%d\n",ipComp("192.168.145.123","192.168.145.123")); //1
    }
    

    编辑: 如H2CO3所建议:

    通常应避免使用sscanf,而应该使用strtol(),例如:

    unsigned long ip2int(const char *ip)
    {
        const char *end = ip + strlen(ip);
        unsigned long n = 0;
        while (ip < end) {
            n <<= 8;
            n |= strtoul(ip, (char **)&ip, 10);
            ip++;
        }
    
        return n;
    }
    

    1
    @alk 同意,我正在使用MINGW,而%hhu在加上-Wall后会产生警告。 与这个相同。 不过我已经更新了。 - P0W
    1
    @DeepakTivari:关于 <<:请了解“左移”运算符;关于 | 请了解“按位或”运算符。 - alk
    1
    @DeepakTivari - 这个操作是将四个独立的整数打包成一个无符号整数。其中三个值必须移位到高阶位,这就是位移运算符(<<)所做的。 - Carey Gregory
    2
    对于s1int类型而不是unsigned char类型,所以sscanf(ip, "%hhu.%hhu.%hhu.%hhu"是错误的。建议改用sscanf(ip, "%hhu.%hhu.%hhu.%hhu"并将s1定义为unsigned char类型,或者使用sscanf(ip,"%u.%u.%u.%u"并将s1定义为unsigned类型。 - chux - Reinstate Monica
    1
    @chux 您可以使用 sscanf(ip, "%3u.%3u.%3u.%3u%c", &c1, &c2, &c3, &c4, &dummy) 并检查结果是否为5(则为错误)或4(则为正常)。 - glglgl
    显示剩余13条评论

    1
    一个拘泥于细节的“在被接受答案之后”的回答。采用错误检查
    #include <inttypes.h>
    int IPstringToUns32(const char *IPString, uint32_t *IPNumber) {
      uint8_t c[4];  // LSByte in c[0]
      char ch;
      const char * format = "%" SCNu8 ".%" SCNu8 ".%" SCNu8 ".%" SCNu8 "%c";
      if (4 != sscanf(IPString, format, &c[3], &c[2], &c[1], &c[0], &ch)) {
        return 1; // parse error
      }
      *IPNumber = (((uint32_t) c[3]) << 24) | (((uint32_t) c[2]) << 16)
          | (((uint32_t) c[1]) << 8) | ((uint32_t) c[0]);
      return 0;
    }
    

    假设可以使用 uint_fast32_t 替代。这个解决方案允许在数字前使用前导空格。

    [编辑] 在格式的末尾添加了经典的 %c。感谢 @glglgl。


    在这里使用数组没有任何意义。当然,它是可以的,但与例如 uint8_t c1、c2、c3、c4 相比没有任何好处。 - glglgl
    glglgl 扫描计数!= 4 不显示。索引数组允许循环处理未设置的值。从1开始变量ID而不是惯用的0很有趣。我发现在C语言中从0开始比从1开始更一致。我认为将 "((uint32_t) c [3]) << 24" 视为 "((uint32_t) c [3]) << 8 * 3" 是减少魔术数字。似乎更多是样式问题。如果您觉得这很有吸引力,建议您将其作为SO问题提出。 - chux - Reinstate Monica
    只是一个建议而已。最后,我认为这是个人风格的问题。 - glglgl
    @glglgl 对于风格的看法赞同。顺便说一下:每当断言“使用……没有意义”存在确定性时,我总是被咬得太频繁了。;-) - chux - Reinstate Monica

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接