字节序
lwip是瑞士计算机科学院的一个开源的TCP/IP协议栈实现.lwIP是TCP/IP协议栈的一个实现。lwIP协议
栈主要关注的是怎么样减少内存的使用河代码的大小,这样就可以让lwIP适用于资源有限的小型平台例如
嵌入式系统。本文我们将主要关于lwip对IP头的数据结构定义。
我们首先看看IP头的RFC描述:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
lwip对IP头的数据结构定义为:
PACK_STRUCT_BEGIN
struct ip_hdr {
/* version / header length / type of service */
PACK_STRUCT_FIELD(u16_t _v_hl_tos);
/* total length */
PACK_STRUCT_FIELD(u16_t _len);
/* identification */
PACK_STRUCT_FIELD(u16_t _id);
/* fragment offset field */
PACK_STRUCT_FIELD(u16_t _offset);
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */ #define IP_MF 0x2000 /* more fragments flag */ #define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
/* time to live / protocol*/
PACK_STRUCT_FIELD(u16_t _ttl_proto);
/* checksum */
PACK_STRUCT_FIELD(u16_t _chksum);
/* source and destination IP addresses */
PACK_STRUCT_FIELD(struct ip_addr src);
PACK_STRUCT_FIELD(struct ip_addr dest); } PACK_STRUCT_STRUCT;
PACK_STRUCT_END
看到上面的数据结构以后,大家一定觉得十分的迷惑和不解,那么我们就一一为大家解答这些疑问。我们先看看PACK_STRUCT_BEGIN和PACK_STRUCT_END定义:
#ifndef PACK_STRUCT_BEGIN
#define PACK_STRUCT_BEGIN
#endif /* PACK_STRUCT_BEGIN */
#ifndef PACK_STRUCT_END
#define PACK_STRUCT_END
#endif /* PACK_STRUCT_END */
由上面的定义可以看出,此处定义了PACK_STRUCT_BEGIN和PACK_STRUCT_END两个常量。可能大多数人看到这里都会觉得奇怪,怎么会这么使用,相信大多数人都明白#ifndef/#define/#endif的作用,主要用于防止头文件的重复引用。其实,上面两个常量的定义和使用和头文件的重复引用是一样的道理,只是这里不是防止头文件的重复引用,而是防止数据结构的重复定义。这里之所以定义其实是考虑到移植性的缘故,我们知道,不同的CPU位数可能不同,读写总线传输的字节数也可能不同,除了这些以外,还可能在其他方面存在区别;除了硬件差别外,OS也可能存在很多区别。为了便于移植,lwip采用了宏定义的方式来进行,对于不同的OS或是硬件,更改相关的宏使之适合于特定的系统即可。
上面的数据结构和常用的数据结构定义还有一些不同的地方,为此,我们先看一个常见的数据结构的定义:
typedef
struct _iphdr
{
unsigned char version:4; //版本
unsigned char ihl:4; //首部长度
unsigned char tos; //服务类型
unsigned short tot_len; //总长度
unsigned short id; //标志
unsigned short frag_off; //分片偏移
unsigned char ttl; //生存时间
unsigned char protocol; //协议
unsigned char check; //检验和
unsigned long saddr; //源IP地址
unsigned long daaddr; //目的IP地址
}iphdr;
上面的数据结构的定义和RFC的说明是完全一一对应的,而lwip的定义则和此定义相隔万里。lwip的
定义里面使用了一个宏定义PACK_STRUCT_FIELD,该宏的定义为: #ifndef PACK_STRUCT_FIELD #define PACK_STRUCT_FIELD(x) x #endif /* PACK_STRUCT_FIELD */ 这三个宏的定义和前面的宏的作用相同,都是便于移植。
在说明lwip中如何进行主机序和网络序的转换时,我们有必要先了解几个基本概念:MSB、LSB、Big Endian、Little Endian、网络序和主机序。
MSB是Most Significant Bit/Byte的首字母缩写,通常译为最重要的位或者最重要的字节;那么对于一个数字而言,什么是MSB呢,显然最高位是MSB,例如15430,1就是MSB,因为它在万位,它的变化是以10000为基数的。知道了MSB,LSB也就不难理解;LSB是Least Significant Bit/Byte的首字母缩写,通常译为最不重要的位或者最不重要的字节;对于15430而言,0显然是LSB,因为它在各位,它的变化对于整个数值的大小影响最小。
Big Endian和Little Endian是描述排列存储在计算机内存里的字节序列的术语。之所以出现两种排列次序,是由于CPU的两大对立阵营的对抗导致的,PowerPC(Moto&IBM) VS X86 = Big Vs. Little。在Big Endian机制中最重要字节(MSB)存放在最低端的地址上;而Little Endian机制中,最不重要字节(LSB)存放在最低端的地址上。
例如0x12345678在采用Big Endian的CPU(PowerPC为代表)中,其存放顺序为: 0x0000 12
0x0001 34
0x0002 56
0x0003 78
图1 0x12345678在Big Endian CPU中的存储方式
而在采用Little Endian的CPU(X86为代表)中,其存放顺序为:
0x0000 78
0x0001 56
0x0002 34
0x0003 12
图2 0x12345678在Big Endian CPU中的存储方式
关于Big Endian和Little Endian还有一点需要说明的是:软件只需要关注字节顺序就可以了,硬件除了要处理字节顺序外,还需要处理位序。如果你觉得Big Endian和Little Endian很难理解,可以这么理解,Big Endian就是最先读出最高(最大)的字节,而Little Endian最先读出最低(最小)的字节。
通常在TCP/IP协议栈所说的网络序(Network Order)就是遵循Big-Endian规则。在TCP/IP网络通信中,通信双方把消息按照如图1的方式进行编码,然后按从MSB(Bit0)到LSB的顺序在网络上传送;而通常我们说的主机序(Host Order)(X86架构CPU)就是遵循Little-Endian规则。所以当两台主机之间要通过TCP/IP协议进行通信的时候就需要调用相应的函数进行主机序(Little-Endian)和网络序(Big-Endian)的转换。
了解了这些基本概念后,我们进入正题。lwip由于考虑到移植性问题,因此它没有默认主机序为Little Endian,而是两种情况都进行了处理;而且处于灵活性考虑,还允许我们用自己定义的代码替换lwip提供的函数:
#if LWIP_PLATFORM_BYTESWAP
#define htons(x) LWIP_PLATFORM_HTONS(x)
#define ntohs(x) LWIP_PLATFORM_HTONS(x)
#define htonl(x) LWIP_PLATFORM_HTONL(x)
#define ntohl(x) LWIP_PLATFORM_HTONL(x) #else
u16_t htons(u16_t x);
u16_t ntohs(u16_t x);
u32_t htonl(u32_t x);
u32_t ntohl(u32_t x);
#endif
如果我们需要采用自己定义的函数,只需要定义LWIP_PLATFORM_BYTESWAP为1,并编写相应的函数即可:
#define LWIP_PLATFORM_BYTESWAP 1
#define LWIP_PLATFORM_HTONS(x)
#define LWIP_PLATFORM_HTONL(x)
考察了lwip实现的灵活性后,我们再来看看其移植性问题。为了便于移植,lwip引入了3个宏:BYTE_ORDER、LITTLE_ENDIAN和BIG_ENDIAN,后两个宏的定义为:
#ifndef LITTLE_ENDIAN
#define LITTLE_ENDIAN 1234
#endif
#ifndef BIG_ENDIAN
#define BIG_ENDIAN 4321
#endif
而BYTE_ORDER由我们自己根据CPU类型来定义,如果CPU采用Big Endian,就定义为4321,反之就定义为1234。
有了这三个宏以后,代码的编写就很简单了:
#if BYTE_ORDER == BIG_ENDIAN
#define htons(x) (x)
#define ntohs(x) (x)
#define htonl(x) (x)
#define ntohl(x) (x)
#else /* BYTE_ORDER != BIG_ENDIAN */ #if LWIP_PLATFORM_BYTESWAP
#define htons(x) LWIP_PLATFORM_HTONS(x) #define ntohs(x) LWIP_PLATFORM_HTONS(x) #define htonl(x) LWIP_PLATFORM_HTONL(x) #define ntohl(x) LWIP_PLATFORM_HTONL(x) #else
u16_t htons(u16_t x);
u16_t ntohs(u16_t x);
u32_t htonl(u32_t x);
u32_t ntohl(u32_t x);
#endif
当CPU类型为Big Endian时,主机序与网络序同序,不需要改动;而CPU类型为Little Endian时,主机序与网络序正好相反,此时lwip定义了相应的函数来处理,这些函数通过移位来实现,本文以u32_t
htonl(u32_t x)来说明:
u32_t
htonl(u32_t n)
{
return ((n & 0xff) << 24) |
((n & 0xff00) << 8) |
((n & 0xff0000) >> 8) |
((n & 0xff000000) >> 24); }
上面的代码其实很简单,就是将字节进行逆序排列。
写到这里,相信大家都对lwip处理网络序和字节序的机制有了一定的了解了。
lwip中有三个IP地址转换函数,分别是点分十进制字符串->数值、数值结构体<->点分十进制,比较奇怪的是lwip并未提供数值->点分十进制字符串的转换函数,不过添加这样一个函数十分简单,本文最后会给出这样一个函数。
在了解这几个函数前,我们先看看刚刚提到的结构体,定义该结构体很大程度上与BSD兼容有关: /* For compatibility with BSD code */
struct in_addr {
u32_t s_addr;
};
该结构体十分简单,就是包含一个32bit的无符号整型数值。我们下面再来看看前面提到的三个函数: u32_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *addr); char *inet_ntoa(struct in_addr addr);
我们先来看看inet_aton函数,该函数将字符串转换为in_addr结构体。在查看该函数的实现方式前,我们有必要了解下面的几个宏:
/* Here for now until needed in other places in lwIP */ #ifndef isprint
#define in_range(c, lo, up) ((u8_t)c >= lo && (u8_t)c <= up) #define isprint(c) in_range(c, 0x20, 0x7f) #define isdigit(c) in_range(c, '0', '9') #define isxdigit(c) (isdigit(c) || in_range(c, 'a', 'f') || in_range(c, 'A', 'F'))
#define islower(c) in_range(c, 'a', 'z') #define isspace(c) (c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\v') #endif
很显然,上面的几个宏主要用于判断字符c所属的范围,isprint用于判断c是否可打印;isdigit用于判断c是否是数字;isxdigit用于判断是否是十六进制数字;islower用于判断是否是小写字母;isspace判断是否是广义空格符。
为了不纠缠于代码的细节,我们来看看inet_addr函数的伪码实现:
取字符串的第一个字符;
for(;;)
{
取得的字符是否是数字,即isdigit(c)是否返回真,不是则出错退出;
针对该数字进行基数判断,即判断给定的数是十进制,还是八进制或十六进制;
for(;;)
{
判断后续字符是否是数字或A-F(十六进制时),如果不是则出错直接退出;
采用刚才获得的基数解析后面的数字,并保存;
}
判断给定的字符是否是'.',如果不是,直接退出循环;
判断数组的下标值是否大于等于3,如果是出错退出;
将刚刚计算得到的数值保存到数组中;
}
判断字符串余下的字符是否是空白符,不是则出错退出;
计算数组的下标值,由此判断字符串属于何种类型值(即32、8.24、8.8.16、8.8.8.8) 根据以上判断进行转换。
上面描述了字符串到数值的转换,看似简单,实则不然,lwip代码中有一个明显的缺陷,就是缺少对8进制数的判断,具体情形我就不详细描述了,下面给出其实现代码,感兴趣的朋友下去可以自己研究改进:
int
inet_aton(const char *cp, struct in_addr *addr)
{
u32_t val;
int base, n, c;
u32_t parts[4];
u32_t *pp = parts;
c = *cp;
for (;;) {
/*
* Collect number up to ``.''.
* Values are specified as for C:
* 0x=hex, 0=octal, 1-9=decimal.
*/
if (!isdigit(c))
return (0);
val = 0;
base = 10;
if (c == '0') {
c = *++cp;
if (c == 'x' || c == 'X') {
base = 16;
c = *++cp;
} else
base = 8;
}
for (;;) {
if (isdigit(c)) {
val = (val * base) + (int)(c - '0');
c = *++cp;
} else if (base == 16 && isxdigit(c)) {
val = (val << 4) | (int)(c + 10 - (islower(c) ? 'a' : 'A'));
c = *++cp;
} else
break;
}
if (c == '.') {
/*
* Internet format:
* a.b.c.d
* a.b.c (with c treated as 16 bits)
* a.b (with b treated as 24 bits)
*/
if (pp >= parts + 3)
return (0);
*pp++ = val;
c = *++cp;
} else
break;
}
/*
* Check for trailing characters.
*/
if (c != '#CONTENT#' && (!isprint(c) || !isspace(c)))
return (0);
/*
* Concoct the address according to
* the number of parts specified.
*/
n = pp - parts + 1;
switch (n) {
case 0:
return (0); /* initial nondigit */
case 1: /* a -- 32 bits */
break;
case 2: /* a.b -- 8.24 bits */
if (val > 0xffffff)
return (0);
val |= parts[0] << 24;
break;
case 3: /* a.b.c -- 8.8.16 bits */
if (val > 0xffff)
return (0);
val |= (parts[0] << 24) | (parts[1] << 16);
break;
case 4: /* a.b.c.d -- 8.8.8.8 bits */
if (val > 0xff)
return (0);
val |= (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8);
break;
}
if (addr)
addr->s_addr = htonl(val);
return (1);
}
看完了inet_aton函数,我们再来看看inet_ntoa函数,该函数要简单得多:
char *
inet_ntoa(struct in_addr addr) {
static char str[16];
u32_t s_addr = addr.s_addr;
char inv[3];
char *rp;
u8_t *ap;
u8_t rem;
u8_t n;
u8_t i;
rp = str;
ap = (u8_t *)&s_addr;
for(n = 0; n < 4; n++) {
i = 0;
do {
rem = *ap % (u8_t)10;
*ap /= (u8_t)10;
inv[i++] = '0' + rem;
} while(*ap);
while(i--)
*rp++ = inv[i];
*rp++ = '.';
ap++;
}
*--rp = 0;
return str;
}
代码行数也少了不少,其思想也很简单,就是求余和求商操作,因此这里不再详述。
最后我们以自定义的一个十分简单的函数结束本文:
char * inet_addrstr(const u32_t addr) {
struct in_addr val;
val.s_addr = htonl(addr);
return inet_ntoa(val);
}