网络通信使得数据从一个主机传递到另一个主机。然而在不同的的处理器在管理内存单元上的数据时,对需要存放在多个内存单元地址的某一数据的处理方式不尽相同,因此对数据的解析结果也不同。目前处理器数据处理类型有大端和小端两种方式。
小端(Little-endian)模式: 操作数的存放方式为高地址存放高字节。
大端(Big-endian)模式: 操作数的存放方式为高地址存放低字节。
例如,一个无符号的整数0x12345678存放在0x8000~0x8003地址上,小端模式:
大端模式:
面试中经常会考察的题目,编程判断系统是大端模式还是小端模式存储数据。在这里也顺便实现:
//法1:
int main(void)
{
int i;
union{
int dat;
unsigned char n[4];
}u;
u.dat = 11;
for (i = 3; i >= 0; --i)
printf("%x ", u.n[i]);
printf("\n");
if (u.n[0] == 0)
printf("Bit endian\n");
else
printf("Little endian\n");
return 0;
}
/*
小端:
int num = 0xb: 0x0 0 0 0 0 0 0 b
char n[4]: n[3] n[2] n[1] n[0]
打印: b 0 0 0
*/
/*
大端:
int num = 0xb: 0xb 0 0 0 0 0 0 0
char n[4]: n[3] n[2] n[1] n[0]
打印: 0000
*/
//法2
int is_small_endian()
{
int a = 1;
char b = *((char*)(&a));
return b == 1;
}
int main(void)
{
is_small_endian() ? printf("Little endian\n")
: printf("Bit endian\n");
return 0;
}
在网络上传输的数据以及各种类型的主机字节序存在差异,因此在x86平台写网络程序时需要注意大小端的转换。解决办法是: 发送端总是把要发送的数据转换为大端字节序后再发送,而接收端知道对方传来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机器需转换,大端机器不转换)。注意,即使是在同一台机器上的两个进程的通信(如两个进程分别用c语言和java编写),也要考虑字节序问题(java虚拟机采用的是大端字节序)。
#include
uint32_t htonl(uint32_t hostlong); //long, host to net
uint16_t htons(uint16_t hostshort); //short, host to net
uint32_t ntohl(uint32_t netlong); //long, net to host
uint16_t ntohs(uint16_t netshort); //short, net to host
长整型转换函数一般用来转换IP地址,短整型的用来转换端口号。当然不局限于此,任何数据通过网络传输时都应该使用这些函数来转换字节序。但是如果传输的是字符串则不需要转换,因为字符创实际上是字符数组,每个字符占据1字节,单字节的数据在网络上传输,发送/接收端没有进行大小端转换也不会影响最终的字符串值。
比较特殊的场合是在网络上传输结构体。待传输的结构体如下:
typedef _member
{
char name[32];
int age;
char gender;
char address[128];
}member;
方法1:
//发送端
member m;
ret = send(socket_fd, m.name, 32);
m.age = htonl(m.age);
ret = send(socket_fd, (void*)&m.age, sizeof(int), 0);
ret = send(socket_fd, &gender, sizeof(char), 0);
ret = send(socket_fd, address, sizeof(char) * 128, 0);
//接收端
member m;
ret = recv(socket_fd, m.name, 32);
ret = recv(socket_fd, (void* )&m.age, sizeof(int), 0);
m.age = ntohl(m.age);
ret = send(socket_fd, address, sizeof(char) * 128, 0);
方法2: 传输整个结构体,考虑不同位宽的系统问题,在定义结构体的时候强制转换为1字节对齐发送:
#pragma pack(1) //1字节对齐
typedef _member
{
char name[32];
int age;
char gender;
char address[128];
}member;
//发送端
member m;
m.age = htonl(m.age);
ret = send(socket_fd, (void* )&m, sizeof(m), 0);
//接收方
member m;
ret = recv(socket_fd, (void* )&m, sizeof(m), 0);
m.age = ntohl(m.age);
字节对齐的设置并不一定要1字节对齐,关键是双方要保持一致。假设不一致,那么sizeof(m)的大小将可能不同,这就意味着接收方接收到的数据与发送端发送的不同了。
struct sockaddr结构体类型用于表示socket网络编程接口的地址:
#include
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
sa_family_t表示地址类型,它通常与协议族类型对应,常见的协议族和对应的地址族如下表格:
由于PF_*和AF_*具有相同的值,所以二者通常混用。
/usr/include/i386-linux-gnu/bits/socket.h"
#define AF_UNIX PF_UNIX
#define AF_INET PF_INET
#define AF_INET6 PF_INET6
sa_data成员用于存放socket的地址值。不同的协议族的地址值具有不同的含义和长度:
sa_data只有14字节大小,所以无法完全容纳所有协议族的地址值。因此Linux又定义了下面新的通用结构体:
/*/usr/include/i386-linux-gnu/bits/socket.h*/
#define __ss_aligntype unsigned long int
#define _SS_SIZE 128
#define _SS_PADSIZE (_SS_SIZE - (2 * sizeof (__ss_aligntype)))
struct sockaddr_storage
{
__SOCKADDR_COMMON (ss_); /* Address family, etc. */
__ss_aligntype __ss_align; /* Force desired alignment. */
char __ss_padding[_SS_PADSIZE]; /* 内存对齐 */
};
通用的socket地址结构体并不好操作,Linux为上述各个协议提供了专门的socket结构体。
(1) UNIX本地域协议族
/* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket. */
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_);
char sun_path[108]; /* Path name. */
};
(2) IPv4版本的socket地址
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family,AF_INET */
__be16 sin_port; /* Port number,用网络字节序表示 */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
struct in_addr {
__be32 s_addr; /* IPv4地址,要用网络字节序表示 */
};
(3) IPv6版本的socket地址
#if __UAPI_DEF_SOCKADDR_IN6
struct sockaddr_in6 {
unsigned short int sin6_family; /* AF_INET6 */
__be16 sin6_port; /* 端口号,用网络字节序表示 */
__be32 sin6_flowinfo; /* 流信息,应设置为0 */
struct in6_addr sin6_addr; /* IPv6地址结构体 */
__u32 sin6_scope_id; /* scope id (new in RFC2553) */
};
#endif /* __UAPI_DEF_SOCKADDR_IN6 */
#if __UAPI_DEF_IN6_ADDR
struct in6_addr {
union {
__u8 u6_addr8[16]; //IPv6地址,要用网络字节序表示
#if __UAPI_DEF_IN6_ADDR_ALT
__be16 u6_addr16[8];
__be32 u6_addr32[4];
#endif
} in6_u;
#define s6_addr in6_u.u6_addr8
#if __UAPI_DEF_IN6_ADDR_ALT
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
#endif
};
由于所有的socket编程接口使用的地址参数类型都是通用sockaddr地址类型,所以所有专用的地址,包括sockaddr_storage类型,在使用这些接口传参时都需要强制类型转换为sockaddr。
(1) in_addr_t inet_addr(const char *cp);
将点分十进制的字符串转换为32位的网络字节序。参数为点分十进制字符串方式的IP信息,执行成功返回32位IP地址,字节序为网络字节序。
(2) in_addr_t inet_network(const char *cp);
将点分十进制的字符串转换为32位的主机字节序IP地址。
(3) char *inet_ntoa(struct in_addr in);
将网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。这是一个不可重入的函数。
(4) int inet_aton(const char *cp, struct in_addr *inp);
inet_aton()和inet_addr()具有同样的功能,但是inet_aton()将转换结果存储于参数inp中。
下面函数适用于IPv4的地址信息,从一个IP地址中获取网络ID/主机ID。
(1) in_addr_t inet_lnaof(struct in_addr in);
inet_lnaof()用于从某个32位网络字节序的IP地址中提取主机ID
(2) in_addr_t inet_netof(struct in_addr in);
inet_netof()从某个32位网络字节序的IP地址提取网络ID
(3) struct in_addr inet_makeaddr(int net, int host);
inet_makeaddr()将主机ID和网络ID组合成一个IP地址,参数1位网络ID,参数2位主机ID。执行成功返回32位网络字节序的IP地址
下面两个函数也能完成上述3个函数同样的功能,并且它们同时适用于IPv4地址和IPv6地址:
#include
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_pton()函数用于将字符串表示的IP地址src(IPv4地址是用点分十进制表示,IPv6地址是用十六进制表示)转换为网络字节序整数表示的IP地址,并将转换结果存储于dst指向的内存中。af参数指定地址族,可以是AF_INET或者AF_INET6。成功返回1失败返回0并设置errno。
inet_ntop()函数进行相反的转换,前三个参数的含义和inet_pton()的一致,参数size指定目标存储单元的大小。如下两个宏可帮助程序员指定这个大小:
/usr/include/netinet/in.h
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
inet_ntop()成功返回目标存储单元的地址,失败返回NULL并设置errno。