网络字节顺序和主机字节顺序让我感到困惑

8
我对“主机字节序”和“网络字节序”感到非常困惑。我知道网络字节序是大端字节序,而我的主机字节序是小端字节序。
如果我要打印数据,为了得到正确的值,我需要将其转换为主机字节序,对吗?
我的问题是我正在尝试打印由htonl返回的数据值。以下是我的示例:
#include <stdio.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{   
    int bits = 12;
    char *ip = "132.89.39.0";
    struct in_addr addr;
    uint32_t network, netmask, last_addr;
    uint32_t total_hosts;

    inet_aton(ip, &addr);
    printf("Starting IP:\t%s\n", inet_ntoa(addr.s_addr));

    netmask = (0xFFFFFFFFUL << (32 - bits)) & 0xFFFFFFFFUL;
    netmask = htonl(netmask);
    printf("Netmask:\t%s\n", inet_ntoa(netmask));

    network = addr.s_addr & netmask;
    printf("Network:\t%s\n", inet_ntoa(network));

    printf("Total Hosts:\t%d\n", ntohl(netmask));

    return 0;

printf("Total Hosts:\t%d\n", ntohl(netmask));会打印出正确的值,但是会带有负号。如果我使用%u,则会得到错误的值。

我哪里做错了?

使用%d输出的结果为:

Starting IP:    132.89.39.0
Netmask:        255.240.0.0
Network:        132.80.0.0
Total Hosts:    -1048576

当使用%u输出时:

Starting IP:    132.89.39.0
Netmask:        255.240.0.0
Network:        132.80.0.0
Total Hosts:    4293918720

我已经被这个问题困扰了两天。看起来很简单的东西却完全让我失去方向。我不想让任何人解决这个问题,但是正确的指导将非常有帮助。

3个回答

8
如果你看到的话,htonl() 的原型是:

uint32_t htonl(uint32_t hostlong);

所以它返回一个无符号类型 uint32_t。使用 %d 打印该值是不合适的(因为 %d 需要一个 signed int 类型的参数)。
至少,你需要使用 %u 来获取该 unsigned 值。通常情况下,如果可能的话,尝试使用 PRIu32 宏来打印固定宽度(32位)的无符号整数。

如果我使用%u打印,我会得到完全不同的值。我已经编辑了我的代码,因为引起问题的那一行应该是printf("Total Hosts:\t%u\n", ntohl(netmask)); - Torra
1
@Torra,inet_ntoa() 函数不是只需要一个 struct in_addr 参数吗? - Sourav Ghosh
@Torra 同时,请在得到答案后不要更改问题。您可以随时编辑您的问题以添加更多信息或更正任何错误。 - Sourav Ghosh

4

目前有许多系统可以在系统重置或运行时在小端和大端字节顺序之间切换。作为网络程序员,我们必须处理这些字节顺序差异,因为网络协议必须指定网络字节顺序。例如,在TCP段中,有一个16位端口号和一个32位IPv4地址。发送协议栈和接收协议栈必须就这些多字节字段的字节传输顺序达成一致。Internet协议对于这些多字节整数使用大端字节顺序。

理论上,实现可以将套接字地址结构中的字段存储为主机字节顺序,然后在将字段移动到和从协议头移动字段时转换为网络字节顺序,使我们不必担心这个细节。但是,历史和POSIX规范都说套接字地址结构中的某些字段必须保持在网络字节顺序。因此,我们的关注点是在主机字节顺序和网络字节顺序之间进行转换。我们使用以下四个函数来在这两种字节顺序之间进行转换。

 #include <netinet/in.h>
    uint16_t htons(uint16_t host16bitvalue) ;
    uint32_t htonl(uint32_t host32bitvalue) ;

两者都返回:网络字节序的值

   uint16_t ntohs(uint16_t net16bitvalue) ;
    uint32_t ntohl(uint32_t net32bitvalue) ;

这两个函数都返回以主机字节顺序表示的值。

在这些函数的名称中,h代表主机,n代表网络,s代表短整型,l代表长整型。术语“短”和“长”是4.2BSD数字VAX实现的历史遗物。我们应该将s视为16位值(例如TCP或UDP端口号),将l视为32位值(例如IPv4地址)。实际上,在64位数字Alpha上,长整数占用64位,但是htonl和ntohl函数操作32位值。 在使用这些函数时,我们不关心主机字节顺序和网络字节顺序的实际值(大端或小端)。我们必须调用适当的函数来在主机字节顺序和网络字节顺序之间转换给定的值。在那些具有与Internet协议相同字节顺序(大端)的系统上,这四个函数通常被定义为空宏。 我们将更多地讨论字节顺序问题,与网络数据包中包含的数据相对应,而不是协议头中的字段。 我们还没有定义“字节”一词。我们使用该术语表示8位数量,因为几乎所有当前计算机系统都使用8位字节。大多数Internet标准使用术语八位组来表示8位数量,而不是字节。这始于TCP / IP的早期,因为许多早期工作是在DEC-10等系统上完成的,这些系统不使用8位字节。 Internet标准中的另一个重要约定是位顺序。在许多Internet标准中,您将看到与以下类似的数据包“图片”(这是RFC 791中IPv4头的前32位):

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL|   TYPE OF SERCVICE |      TOTAL LENGTH          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 

这代表着四个字节,按照它们在电线上出现的顺序排列; 最左边的位是最重要的。然而,编号从0开始分配到最重要的位。 这是一个您应该熟悉的符号,以便更容易阅读RFC中的协议定义。 20世纪80年代,常见的网络编程错误是在Sun工作站(big-endian Motorola 68000s)上开发代码,并忘记调用这四个函数之一。 在这些工作站上,代码可以正常运行,但当移植到little-endian机器(例如VAXes)上时,代码将无法运行。


1
这里的问题不在于你在网络和主机之间转换。你的代码这部分运行得很好。
问题在于你认为子网掩码解释为整数时,匹配该掩码的主机数量就是该整数。实际上恰恰相反。
考虑你的12位子网掩码,255.240.0.0。或者说,在二进制中:
1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

根据您的代码,要使主机地址与此子网掩码匹配,两个地址需要在子网掩码中具有相同的位,其中子网掩码具有1位。在子网掩码中对应0的位可以自由选择。这样的地址数量可以通过仅考虑0位来确定。但是当然我们不能将这些位留为空;为了计算符合条件的地址数,我们需要在前面添加1。因此,在这种情况下,计数(正如您所怀疑的那样)为1,048,576:

                      1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

计算这个值的一种方法是将网络掩码取反并加1:

1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
---------------------------------------------------------------
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 (bitwise invert)
                                                              1 (+ 1)
---------------------------------------------------------------
                      1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

在二进制补码算术中,这与算术取反完全相同。因此,当您将子网掩码打印为有符号整数时,您将看到预期计数的负值。(将无符号的uint32_t作为有符号的int打印在技术上是未定义的行为,但在具有32位整数的二进制补码机器上可能会按预期工作。)
简而言之,计算子网掩码限定地址数量的方法是:
uint32_t address_count = ~ntohl(netmask) + 1;

(如果可用,大多数编译器都会将其优化为一元否定操作码。)

我尝试了反转操作,但总是得到 1048575,而且不明白为什么它总是比期望值少 1。我猜测可能是使用了 big endian 编码方式,并且把符号位丢失了一个 bit。现在我明白了原因,谢谢。我完全被搞错了,哈哈。 - Torra
1
@Torra:网络顺序不会改变2的补码的工作方式 :) 但 ntohl 和 htonl 使用无符号整数类型并非巧合。 - rici
+1 是为了实际回答问题的关键部分:实际上主要问题是误解了 netmask 包含的值 - 高位设置,低位清除。有符号与无符号解释的问题确实存在,但更加表面和偶然(将 %d 更正为 %u 并不能让有类似问题的人更接近理解为什么它显示出意外的值;而纠正对 netmask 变量内部值的理解则可以)。 - mtraceur
@Torra,既然你说这是对你最有帮助的答案,你介意把它标记为已接受的答案并点赞吗?如果我遇到类似的问题,这就是最有用的答案,而其他现有的答案只会让我更加困惑。 - mtraceur
P.S. 我认为 C 语言的语义实际上保证了你可以使用 -ntohl(netmask) 而不是 ~ntohl(netmask) + 1 来获得正确的结果。(一元取反的结果被定义为具有操作数值的取反值,将负值插入无符号类型中的结果被定义为基本上是该类型中最大值模数的值,因此换句话说,C 对无符号整数类型的操作严格定义,以便取反将可移植地对位执行正确的操作。) - mtraceur

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