网络字节顺序和主机字节顺序的转换(htons ntohs htonl ntohl)

什么是网络字节顺序和主机字节顺序呢?

在进行网络编程时,需要进行转换以统一“格式
 

简述

网络字节顺序NBO(Network Byte Order):
按从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题。
主机字节顺序(HBO,Host Byte Order):
不同的机器HBO不相同,与CPU设计有关
 

详解

不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序

最常见的有两种

1. Little endian:将低序字节存储在起始地址

2. Big endian:将高序字节存储在起始地址

LE little-endian 最符合人的思维的字节序

地址低位存储值的低位

 

 

 

 

地址高位存储值的高位

怎么讲是最符合人的思维的字节序,是因为从人的第一观感来说

低位值小,就应该放在内存地址小的地方,也即内存地址低位

反之,高位值就应该放在内存地址大的地方,也即内存地址高位

BE big-endian

最直观的字节序

地址低位存储值的高位

地址高位存储值的低位

为什么说直观,不要考虑对应关系

只需要把内存地址从左到右按照由低到高的顺序写出

把值按照通常的高位到低位的顺序写出

两者对照,一个字节一个字节的填充进去

例子:在内存中双字0x01020304(DWORD)的存储方式

内存地址

4000 4001 4002 4003

LE 04 03 02 01

BE 01 02 03 04

例子:如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为

big-endian little-endian

0x0000 0x12 0xcd

0x0001 0x23 0xab

0x0002 0xab 0x34

0x0003 0xcd 0x12

x86系列CPU都是little-endian的字节序.

网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。      

为了进行转换 bsd socket提供了转换的函数 有下面四个

htons 把unsigned short类型从主机序转换到网络序

htonl 把unsigned long类型从主机序转换到网络序

ntohs 把unsigned short类型从网络序转换到主机序

ntohl 把unsigned long类型从网络序转换到主机序

在使用little endian的系统中 这些函数会把字节序进行转换

在使用big endian类型的系统中 这些函数会定义成空宏

同样 在网络程序开发时 或是跨平台开发时 也应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug.

函数例子解析之htonl()

简述:
    将主机的无符号长整形数转换成网络字节顺序。
    #include <winsock.h>
    u_long PASCAL FAR htonl( u_long hostlong);
    hostlong:主机字节顺序表达的32位数。
注释:
    本函数将一个32位数从主机字节顺序转换成网络字节顺序。
返回值:
    htonl()返回一个网络字节顺序的值。

inet_ntoa()

简述:
将网络地址转换成“.”点隔的字符串格式。
   #include <winsock.h>
   char FAR* PASCAL FAR inet_ntoa( struct in_addr in);
   in:一个表示Internet主机地址的结构。
注释:
   本函数将一个用in参数所表示的Internet地址结构转换成以“.” 间隔的诸如a.b.c.d”的字符串形式。请注意inet_ntoa()返回的字符串存放在WINDOWS套接口实现所分配的内存中。应用程序不应假设该 内存是如何分配的。在同一个线程的下一个WINDOWS套接口调用前,数据将保证是有效。
返回值:
若无错误发生,inet_ntoa()返回一个字符指针。否则的话,返回NVLL。其中的数据应在下一个WINDOWS套接口调用前复制出来。
 
网 络中传输的数据有的和本地字节存储顺序一致,而有的则截然不同,为了数据的一致性,就要把本地的数据转换成网络上使用的格式,然后发送出去,接收的时候也 是一样的,经过转换然后才去使用这些数据,基本的库函数中提供了这样的可以进行字节转换的函数,如和htons( ) htonl( ) ntohs( ) ntohl( ),这里n表示network,h表示host,htons( ) htonl( )用于本地字节向网络字节转换的场合,s表示short,即对2字节操作,l表示long即对4字节操作。同样ntohs( )ntohl( )用于网络字节向本地格式转换的场合。

 

附注

一、字节序定义

字节序,顾名思义字节的顺序,再多说两句就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。

其实大部分人在实际的开发中都很少会直接和字节序打交道。唯有在跨平台以及网络程序中字节序才是一个应该被考虑的问题。

在所有的介绍字节序的文章中都会提到字节序分为两类:Big-Endian和Little-Endian。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
c) 网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于 TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。比如,以太网头部中2字节的“以太网帧类型”,表示后面数据的类型。对于ARP请求或应答的以太网帧类型来说,在网络传输时,发送的顺序是0x08,0x06。在内存中的映象如下图所示:
栈底 (高地址)
---------------
0x06 -- 低位 
0x08 -- 高位
---------------
栈顶 (低地址)
该字段的值为0x0806。按照大端方式存放在内存中。

二、高/低地址与高低字节

首先我们要知道我们C程序映像中内存的空间布局情况:在《C专家编程》中或者《Unix环境高级编程》中有关于内存空间布局情况的说明,大致如下图:
----------------------- 最高内存地址 0xffffffff
 | 栈底
 .
 .              栈
 .
  栈顶
-----------------------
 |
 |
/|/

NULL (空洞)

/|/
 |
 |
-----------------------
                堆
-----------------------
未初始化的数据
----------------(统称数据段)
初始化的数据
-----------------------
正文段(代码段)
----------------------- 最低内存地址 0x00000000

以上图为例如果我们在栈上分配一个unsigned char buf[4],那么这个数组变量在栈上是如何布局的呢[注1]?看下图:
栈底 (高地址)
----------
buf[3]
buf[2]
buf[1]
buf[0]
----------
栈顶 (低地址)

现在我们弄清了高低地址,接着来弄清高/低字节,如果我们有一个32位无符号整型0x12345678(呵呵,恰好是把上面的那4个字节buf看成一个整型),那么高位是什么,低位又是什么呢?其实很简单。在十进制中我们都说靠左边的是高位,靠右边的是低位,在其他进制也是如此。就拿 0x12345678来说,从高位到低位的字节依次是0x12、0x34、0x56和0x78。

高低地址和高低字节都弄清了。我们再来回顾一下Big-Endian和Little-Endian的定义,并用图示说明两种字节序:
以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value:
Big-Endian: 低地址存放高位,如下图:
栈底 (高地址)
---------------
buf[3] (0x78) -- 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) -- 高位
---------------
栈顶 (低地址)

Little-Endian: 低地址存放低位,如下图:
栈底 (高地址)
---------------
buf[3] (0x12) -- 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) -- 低位
---------------
栈顶 (低地址)

在现有的平台上Intel的X86采用的是Little-Endian,而像Sun的SPARC采用的就是Big-Endian。

三、例子

嵌入式系统开发者应该对Little-endian和Big-endian模式非常了解。采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节,而Big-endian模式对操作数的存放方式是从高字节到低字节。

例如,16bit宽的数0x1234在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:

内存地址  存放内容
 0x4001    0x12
 0x4000    0x34

而在Big-endian模式CPU内存中的存放方式则为:

内存地址  存放内容
 0x4001    0x34
 0x4000    0x12
 
32bit宽的数0x12345678在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:

内存地址  存放内容
 0x4003     0x12
 0x4002     0x34
 0x4001     0x56
 0x4000     0x78
 
而在Big-endian模式CPU内存中的存放方式则为:

内存地址  存放内容
 0x4003     0x78
 0x4002     0x56
 0x4001     0x34
 0x4000     0x12

四。

不同的CPU上运行不同的操作系统,字节序也是不同的,参见下表。
处理器    操作系统    字节排序
Alpha    全部     Little endian
HP-PA     NT     Little endian
HP-PA     UNIX     Big endian
Intelx86    全部     Little endian <-----x86系统是小端字节序系统
Motorola680x()    全部     Big endian
MIPS     NT     Little endian
MIPS     UNIX     Big endian
PowerPC     NT     Little endian
PowerPC    非NT     Big endian   <-----PPC系统是大端字节序系统
RS/6000     UNIX     Big endian
SPARC     UNIX     Big endian
IXP1200 ARM核心    全部     Little endian

 

=================================================================

long long 类型的网络字节顺序转换

做过socket的都知道网络字节转换的事情,网络中传输的数据有的和本地字节存储顺序一致,而有的则截然不同,为了数据的一致性,就要把本地的数据转换成网络上使用的格式,然后发送出去,接收的时候也是一样的,经过转换然后才去使用这些数据,基本的库函数中提供了这样的可以进行字节转换的函数,如和htons( ) htonl( ) ntohs( ) ntohl( ),这里n表示network,h表示host,htons( ) htonl( )用于本地字节向网络字节转换的场合,s表示short,即对2字节操作,l表示long即对4字节操作。同样ntohs( )ntohl( )用于网络字节向本地格式转换的场合。随着c99标准的推行,我们伟大的c中增加了新的类型long long int ,unsigned long long int,都是64位的,怎么办?不转肯定是不行,就得自己想办法把它转了。当然有很多方法,我这里想使用一种递归的解决方法,证明咱也看过“艺术”。

一、字节序定义
字节序,顾名思义字节的顺序,再多说两句就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。

其实大部分人在实际的开发中都很少会直接和字节序打交道。唯有在跨平台以及网络程序中字节序才是一个应该被考虑的问题。一次Sun SPARC到Intel X86的平台移植让我们的程序遭遇了“字节序问题”。

在所有的介绍字节序的文章中都会提到字节序分为两类:Big-Endian和Little-Endian。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
c) 网络字节序:TCP/IP各层协议将字节序定义为Big-Endian,因此TCP/IP协议中使用的字节序通常称之为网络字节序。

其实我在第一次看到这个定义时就很糊涂,看了几个例子后也很是朦胧。什么高/低地址端?又什么高低位?翻阅了一些资料后略有心得。

二、高/低地址与高低字节
首先我们要知道我们C程序映像中内存的空间布局情况:在《C专家编程》中或者《Unix环境高级编程》中有关于内存空间布局情况的说明,大致如下图:
----------------------- 最高内存地址 0xffffffff
 | 栈底
 .
 .              栈
 .
  栈顶
-----------------------
 |
 |
/|/

NULL (空洞) 

/|/

|
 |
-----------------------
                堆
-----------------------
未初始化的数据
----------------(统称数据段)
初始化的数据
-----------------------
正文段(代码段)
----------------------- 最低内存地址 0x00000000

以上图为例如果我们在栈上分配一个unsigned char buf[4],那么这个数组变量在栈上是如何布局的呢[注1]?看下图:
栈底 (高地址)
----------
buf[3] 
buf[2]
buf[1]
buf[0]
----------
栈顶 (低地址)

现在我们弄清了高低地址,接着我来弄清高/低字节,如果我们有一个32位无符号整型0x12345678(呵呵,恰好是把上面的那4个字节buf看成一个整型),那么高位是什么,低位又是什么呢?其实很简单。在十进制中我们都说靠左边的是高位,靠右边的是低位,在其他进制也是如此。就拿0x12345678来说,从高位到低位的字节依次是0x12、0x34、0x56和0x78。

高低地址和高低字节都弄清了。我们再来回顾一下Big-Endian和Little-Endian的定义,并用图示说明两种字节序:
以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value:
Big-Endian: 低地址存放高位,如下图:
栈底 (高地址)
---------------
buf[3] (0x78) -- 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) -- 高位
---------------
栈顶 (低地址)

Little-Endian: 低地址存放低位,如下图:
栈底 (高地址)
---------------
buf[3] (0x12) -- 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) -- 低位
---------------
栈顶 (低地址)

在现有的平台上Intel的X86采用的是Little-Endian,而像Sun的SPARC采用的就是Big-Endian。

      三、网络字节序的转换

       首先考虑网络字节转换的结果与原来有什么不同(我这里举的例子都是使用little endian的IA-32架构上面的Linux),如 int a = 0x12345678,b = htnl(a),那么就应该是0x78563412。如果是 short c = 0x1234,short d = 0x5678,e = htons(c),f = htons(d),这样e=0x3412,f=0x7856,如果能把e和f调换一下组合放在一起,不就是一个整型a(a=0x12345678)转换之后的值么。实验的代码如下:

#include <stdio.h>

struct ST{

    short val1;

    short val2;

};

union U{

    int val;

    struct ST st;

};

 

int main(void)

{

    int a = 0;

    union U u1, u2;

 

    a = 0x12345678;

    u1.val = a;

    printf("u1.val is 0x%x/n", u1.val);

    printf("val1 is 0x%x/n", u1.st.val1);

    printf("val2 is 0x%x/n", u1.st.val2);

    printf("after first convert is: 0x%x/n", htonl(u1.val));

    u2.st.val2 = htons(u1.st.val1);

    u2.st.val1 = htons(u1.st.val2);

    printf("after second convert is: 0x%x/n", u2.val);

    return 0;

}

输出结果:

u1.val is 0x12345678

val1 is 0x5678

val2 is 0x1234

after first convert is: 0x78563412

after second convert is: 0x78563412

按照这种想法我们实现long long int(64bit) 类型,把它分割成两个int(32bit) ,然后分别使用htonl(),分别转换,然后再从新组合数据。

代码如下:

#include <stdio.h>

struct ST{

    int val1;

    int val2;

};

union test {

    long long int val;

    struct ST st;

};

 

int main(void)

{

    long long int a;

    union test u1, u2;

 

    a = 0x7654321087654321LL;

    u1.val = a;

    u2.st.val2 = htonl(u1.st.val1);

    u2.st.val1 = htonl(u1.st.val2);

    printf("val1 is 0x%x/n", u2.st.val1);

    printf("val2 is 0x%x/n", u2.st.val2);

    printf("u1.val     is    : 0x%llx/n", u1.val);

    printf("after convert is : 0x%llx/n", u2.val);

   

    return 0;

}

执行结果:

val1 is 0x10325476

val2 is 0x21436587

u1.val     is    : 0x7654321087654321

after convert is  : 0x2143658710325476

              另外注意long long int 最大值是0x7fffffffffffffff,即7后面15个f(2的63次方减1) unsigned long long int 最大值是0xffffffffffffffff,16个f(2的64次方减1)。程序中long long int 可以简写为 long long,但是记住这是简写,就像long是long int的简写。

想看数据在内存中如何存储的,就用gdb吧!使用gdb中 x命令,如 x /xb &a表示要察看存储在变量a中的前一个字节(byte)中的数据(16进制)。x /xw &a 就是要察看变量a中前4个字节(word)数据(16进制)。x /xg &a 察看a开始8个字节的数据。自己去看看吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值