大端模式是指高字节数据存放在低地址处,低字节数据放在高地址处。
小端模式是指低字节数据存放在低地址处,高字节数据放在高地址处。
在网络上传输数据时,由于数据传输的两端可能对应不同的硬件平台,采用的存储字节顺序也可能不一致,因此 TCP/IP 协议规定了在网络上必须采用网络字节顺序(也就是大端模式) 。
通过对大小端的存储原理分析可发现,对于 char 型数据,由于其只占一个字节,所以不存在这个问题,这也是一般情况下把数据缓冲区定义成 char 类型 的原因之一。对于 IP 地址、端口号等非 char 型数据,必须在数据发送到网络上之前将其转换成大端模式,在接收到数据之后再将其转换成符合接收端主机的存储模式。
Linux 系统为大小端模式的转换提供了 4 个函数,输入 man byteorder 命令可得函数原型:
<EM><STRONG><SPAN>#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);</SPAN>
</STRONG>
</EM>
htons 表示 host to network short ,用于将主机 unsigned short 型数据转换成网络字节顺序;
ntohl、ntohs 的功能分别与 htonl、htons 相反。
下面介绍的这些转换函数对于这两类的无符号整型变量都可以正确的转换。
如果你想将一个短型数据从主机字节顺序转换到网络字节顺序的话,有这样一个函数htnos:
它是以"h”开头的,代表“主机”;
紧跟着它的是"to",代表“转换到”;
然后是"n",代表“网络”;
最后是"s",代表“短型数据”。
你可以使用"n", "h", "to", "s", "l"的任意组合。当然,你要在可能的情况下进行组合。比如,系统是没有stolh()函数的(Short to Long Host ?)。
下面给出套接字字节转换程序的列表:
hotns()——"Host to NetWork Short",主机字节顺序转换为网络字节顺序(对无符号短型进行操作 4bytes)
htonl()——"Host to NetWork Long",主机字节顺序转换为网络字节顺序(对无符号长型进行操作 8bytes)
ntons()——"NetWork to Host short",网络字节序转换为主机字节顺序(对无符号短型进行操作 4bytes)
ntohl()——"NetWork to Host Long",网络字节顺序转换为主机字节顺序(对无符号长型进行操作 8bytes)
例如:*.sin_addr.s_addr = htonl(innaddr_any)是什么意思?
*.sin_addr.s_addr = htonl(innaddr_any)是Socket编程中用到的。
*是任意定义的一个sockaddr_in型的结构体对象sin_addr是他的一个属性,用于定义IP地址,是strcut in_addr型的,s_addr为结构体in_addr的对象,简单说就是三个结构体嵌套包装的一个包。
inaddr_any一般为内核指定的,大多数系统取0,表示任意的IP地址。
htonl()简单说是把一个本机IP转化为网络协议中规定的格式的函数,也就是所谓的大端模式或小端模式。
htons函数是将一个u_short类型的值从主机字节顺序转换为TCP/IP的网络字节顺序,原型声明如下:
u_short htons(u_short hostshort);
htonl函数是将一个u_long的值从主机字节顺序转换为TCP/IP的网络字节顺序,原型声明如下:
u_long htonl(u_long hostlong);
字节序和网络平台有关,不同的平台,字节序不同。(字节序顾名思义——字节的排列顺序)只有多于一个字节的数据类型,才有字节序的问题,比如short或者int类型。char是没有这个问题的。字节序就是在硬件里面,一般实在内存里,如何表示存储和表示这些数据类型。如果高字节放到高地址上,就是大端(big endian),如果高字节放到低地址上,就是小端模式(little endian)。
网络通讯中,定义网络协议时,都指定用大端模式。所以,通用的办法就是,不管主机字节序是什么,往网络上发送前,都转换成网络字节序,也就是用htons或htonl;而从网络收到的数据,不管主机是什么字节序,都转换成主机字节序,也就是ntohs或者额ntohl。按照这个规则,一般来说,不会出什么问题了。
举个例子,一个int型的整数在计算机中占4个字节,那么就有两种排列方法:
整数0x01020304的两种表示方法
低地址----------------高地址
04 03 02 01---------------->方法1:小端模式(高字节放到低地址上)
01 02 03 04---------------->方法2:大端模式(高字节放到高地址上) 网络字节序
其中,方法1和方法2的区别就是高位放到高低之还是低地址。
为了使得不同的主机格式能够无歧义的和网络格式相互赋值,一般牵涉到网络的开发库都会定义一套两种格式之间的转换函数,这样直接使用转换函数就可以完成两者之间的转换。
在进行TCP通讯时,需要进行主机字节序和网络字节的转换。可如果我要发送的数据是调用ReadFile()函数从文件里读出来的,也就是读出来的数据都是保存到char[]数组里的,那我用send函数发送时还需要转换字节序吗?(http://topic.csdn.net/u/20091208/15/14925202-ce0d-4651-abfb-9e2f3cb73f1f.html)
——如果只是字节流,不需要转换。一般是ip地址,端口号码,传输一些整型数的参数,才需要做转换,字节流不需要。如果头部记录了大小的,那么这个记录了大小的整型数需要转换;
——协议解析方面的数字类型需要转换,负载字节流的不需要关心;
——需要让网络认识的数据,才需要转换,比如ip,端口号。而实际发送的数据,是没有转换要求的。从文件里读取出来的数据是你自己的数据吧,这些数据转不转换看你自己,反正发出去是什么样子,接收到就还是什么样子。
==============================================
【开发细节_大端小端的问题】
【网上很多帖子都直接把大端小端的原理说了下,然后粘贴代码就完事了,没有将代码在内存中到底如何工作的讲清,在此希望直接复制粘贴代码的朋友如果有真的懂了的,希望能得到在内存层面上的具体数据,感激不尽】
本问题是通过比较两个用UNION联合体实现的大小端判断方案所产生的
/*
* 注:提出此问题的假设
* 假设数组中的元素依次在大端、小端机器上都是从低字节向高字节依次存储7 L p; k6 h; J. x
*/: p2 T" f, P8 @/ p# G+ P$ H
//=======================================8 p1 e& i3 ]1 ~" R* v# `( }4 `
第一个方案
//---------------------------------------------------------
union: J; q% m; k4 Q3 K8 Z
{
int a;
char b;
}EndianTest;
EndianTest.a = 0x00000001;2 O& R6 S4 ^" K+ g2 B' `
2 l+ y6 o& S. [
if( EndianTest.b == 0x01 )/ u# T/ h/ X( I
//是小端
else
//是大端
这个方案能够理解,将EndianTest.a直接赋值为10 r# Y: N) {7 u9 z) `1 b) B
若是小端机器,那么存储在内存中的数据就是0x01000000
若是大端机器,那么存储在内存中的数据就是0x00000001/ \9 Q! J. Z e8 U- z1 {) q" ^2 i
这样只要取该数据在内存中低位1个字节,就能够判断出本机是小端还是大端。/ v3 g9 G, I: O" n1 F2 l9 J4 c
% F- s6 j3 b8 @' i5 S: u# W
//======================================/ =======y: n$ G% b- M. S5 ]" g! O
第二个方案(网上说是linux中的实现,也就是我苦思冥想想不通的实现)- A. a% Y& \" @: S9 l W
//----------------------------------------------------------$ b* s: q% W5 h0 p/ A' y
static union
{
char c[4];
unsigned long l;
}endian_test = { { 'l','?','?','b' } }; K/ e, R% v/ [1 j
#define ENDIANNESS ((char)endian_test.l)
& D' J0 V3 w. {$ [- M+ E- D) L; P" ~
在本问题假设条件成立的情况下,) E6 \! k6 _, W1 p5 l' X$ h) J$ D
这个实现方案把数据一个字节一个字节地依次在内存里按低字节到高字节的顺序存放) h# v# Z! c- k2 o U8 M
首先是8 G' w2 X9 l/ r- P+ m( Y
endian_test.c[0] = 'l' //即内存低字节(假设为0x00000000)存的'l'
然后
endian_test.c[1] = '?' //即内存0x00000001存的'?'
其次
endian_test.c[2] = '?' //即内存0x00000002存的'?'
最后* Y6 c% D o$ ^3 }, D R D
endian_test.c[3] = 'b' //即内存0x00000003存的'b'
: j- a- A" W2 b* i: P) [3 [
这样的话,不论大端或是小端,存进内存的16进制数都是 62 3f 3f 6c ('l''?''?''b')没有区别
那么提取该内存的第一个字节就都相同了,为0x62,也即'l',这样怎么能够7 ]9 l2 @; M. X4 j( e
判别出大小端呢?
$ z+ P) e c* i& F1 M
按我现在的理解,判断大端小端的原理就在于将数据存入内存时的顺序有异,而Linux中这个实现方案,在存入时,直接向数组存入数据,这样不就消除了存入时的顺序差异了?我确实没有弄懂他是怎么产生这种“存储时产生顺序差异”的
如 7 ^6 S1 ^. ]/ M- z$ b" ]
0x12345678
在低到高的内存方向里,按字节读出来就是1 o1 k( u7 o8 R1 q+ J; X
小尾端 0x78 0x56 0x34 0x12
大尾端 0x12 0x34 0x56 0x78
孰优孰劣问题不归我们讨论,一般理解就是小尾端在机器读写有优势,大尾端在人看汇编可能有优势。x86系列是小尾端,摩托罗拉的CPU好像是大尾端,MIPS和ARM可以通过寄存器置位进入两个尾端的工作模式,龙芯(MIPS的中国版)只实现了小尾端。
其次,对于你说的把整数打散为数组再存储的问题。由于CPU构架对程序员来说是透明的,程序员一般不关注这些东西。在存储一个整数的时候,如果你画蛇添足地改变顺序,这样会加重运算负担,因为程序的最终二进制代码是对应这个CPU结构的,你想读取一个整数,它快速地给你就行了。除非,你想做一些显示,或者在SOCKET编程里对地址进行转换(有相应的函数)。1 c6 ]- Z+ w" ~1 W% y9 N9 `
最后把你对两个方案的理解有漏洞的地方解析一下,
方案一里边的错误地方
你不该使用0x01000000和0x00000001来理解;, M5 z& c7 T: y0 ~
方案二里边
你没想到读取进去后是把数据给谁的问题
ASCII我没验证,当你的是正确的,那么假设内存里地址由低到高存储了这么一条字节数组
0x62 0x3f 0x3f 0x6c ('l''?''?''b')
那么在大尾端的机器里边,寄存器里边读到的整数是0x623f3f6c3 M- q7 j/ G$ K3 m2 g
在小尾端的机器里边,寄存器读到的数据是0x6c3f3f621 }/ [# n5 `" @& z
是两个不同的整数。4 `& B% f' n3 b" [$ L9 j0 d+ _2 K- o6 E
9 i5 N- B6 T3 \ _
如果你只是做应用程序的话根本就不用管什么狗屁大小尾端的问题,整数就是整数,字符数组就是字符数组,要显示传输出来有相应的类库。只有你进行系统移植或者不同构架机器之间的通信数据交换的时候,才考虑那么一点点。
回复 沙发 hagejid 的帖子
非常感谢斑竹的回答,只是想把自己不懂的搞清楚,恕我愚笨,还有问题慢慢请教
问题一:
斑竹说5 Z+ Y. @7 x, n3 X; P
“方案一里边的错误地方9 @8 r0 j) r" ?! y
你不该使用0x01000000和0x00000001来理解”, ~& A+ t$ d+ q u/ \- M9 Z
我没有明白你的意思,为什么不能这么用呢?这的确是4个字节丫7 o# N' C! \6 E! v# ^
问题二:# O: b) ?" }; L$ I0 o$ A
你说6 g# u/ X) T9 T) x6 Q
0x62 0x3f 0x3f 0x6c ('l''?''?''b')
那么在大尾端的机器里边,寄存器里边读到的整数是0x623f3f6c
在小尾端的机器里边,寄存器读到的数据是0x6c3f3f62;
是两个不同的整数。
6 ]2 x1 {0 u- l$ m9 E9 h
我很赞同你这一说法,我们假设用一个变量去保存内存中的这一个整数+ d9 r" Q3 L: e7 ?* O+ B
在大端机里这个变量就是 0x623f3f6c
在小端机里这个变量就是 0x6c3f3f62, H7 P! P8 q3 @, H$ \& J: U
没有异议
但问题在于,该实现方法是从内存截取低位一个字节(((char)endian_test.l); ): q! M% Z; S% j
而内存低位一个字节始终是0x62,因为并没有经过寄存器,所以并不会得到0x6c,
除非是我理解有误,((char)endian_test.l); 这句话是先将内存0x623f3f6c送入寄存器,再在寄存器中截取,这样的话就说得通了。# R& _& C. ~! Q6 e- I3 m6 d7 T+ b
不知道斑竹怎么理解的?; j' |# [; U1 e, Z
谢谢^^
0x01000000和0x00000001是两个单独的整数,在C语言描述里边0x开头是十六进制的整数,你用来描述一个ASCII码或字节队列是不符合逻辑的。
问题二
尾端不仅仅是CPU的问题,还牵涉到内存总线和其他总线的问题。牵涉面太广了,一两个贴子也难说清楚。如果你真要深入研究推荐你看《See MIPS Run Linux 2》中文版里边10.2.3 硬件和尾端,位置是PDF第271页(书页编号251页)。
下载地址:http://down.51cto.com/data/165729
回复 地板 hagejid 的帖子
对于问题一,我看内存中是这么表示的我就直接抄下来了,还望版主解释一下,为什么不符合逻辑呢?9 \- H- `: N! o9 Z7 ]6 T9 Q
/ R6 F( N/ t& w# d u2 S- B9 c+ Y& L
对于问题二,我想我还会花一段时间慢慢思考了,刚找到这么个LInux原版实现,不知道会不会有什么影响& x" b/ r/ [5 E- T- V
贴过来看看
--------------------------------------------以下部分来源网络-----------------------------------------------
在arch/arm/kernel/Setup.c中有这样一段
static union { char c[4]; unsigned long l; } endian_test __initdata = { { 'l', '?', '?', 'b' } }; #define ENDIANNESS ((char)endian_test.l)$ x7 V5 u4 l. h! u1 p |
其中__initdata指定了数据存放的section
include/linux/Init.h2 s, t9 T& G* ~4 e4 p
#define __initdata __attribute__ ((__section__ (".init.data")))3 A$ g. H7 y- k% `- q |
-------------------------------------------以上部分来源网络-----------------------------------------------------5 {' u7 p4 l. t
2 F3 ]. c+ `3 n4 t" C- b$ v: Y
不知道那个__initdata有什么影响不; d; u
参考地址:http://blog.chinaunix.net/uid-9688646-id-3149350.html
http://bbs.51cto.com/thread-812161-1.html