学科分类
目录
Linux编程

网络编程相关知识

(1)网络字节序

若将数据的高字节保存在内存的低地址,将数据的低字节保存在内存的高地址,这种存放方式称为大端模式;相反地,若将数据的高字节保存在内存的高地址,将数据的低字节保存在内存的低地址,这种存放方式就称为小端模式。磁盘文件中的多字节数据相对于文件中的偏移地址由大端、小端之分,内存中的多字节数据相对于内存地址也有大端、小端之分,同样地,网络数据流同样有大端、小端之分。

发送端主机在发送数据时,通常将发送缓冲区中的数据按内存地址从低到高的顺序依次发出;接收端主机从网络上接收数据时,会将从网络上接收到的数据按内存地址从低到高的顺序依次保存,因此,网络数据流的地址应这样规定:先发出的数据占据低地址,后发出的数据占据高地址。

TCP/IP协议规定,网络数据流应采用大端模式存储,即低地址存储高字节。举例说明:假设从网络端获取到了一个16位的数据0x1121,即十进制的4385,那么发送端主机在发送该数据时,会先发送低地址的数据0x21,再发送高地址的数据0x11;假设接收端为此数据分配的地址为0和1,高字节的数据0x11将被存放到低地址0所对应的空间,低字节的数据0x21将被存放到高地址1所对应的空间。

但是,若发送端主机采用小端模式存储,那么这16位的数据会被解释为0x2111,即十进制的8465,这显然是不正确的。因此,为了保证网络程序的可移植性,使同样的代码可以在大端和小端设备上都能正常编译并运行,发送端的主机在将数据填充到发送缓冲区之前需要先进行字节序转换。

Linux系统中提供了一些用于字节序转换的函数,这些函数存在于函数库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);

以上函数的函数名中h代表主机host,n代表网络network,l代表32位长整型,s表示16位短整型。其中htonl()将参数hostlong从主机字节序转换为网络字节序;htons()函数将参数netlong从网络字节序转换为主机字节序。需要注意的是,若主机使用大端模式,这些函数不做转换,将参数原样返回,也就是说,只有主机使用小端模式时,参数的字节顺序才会被修改。

(2)IP地址转换函数

常见的IP地址格式类似“192.168.10.1”,这是一个标准的IPv4格式的地址,但这种格式是为了方便用户对其进行操作,若要使计算机能够识别,需先将其转换为二进制格式。

早期Linux系统中常使用以下函数来转换IP地址:

int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);

但以上函数只能处理IPv4的ip地址,且它们都是不可重入函数,如今Linux编程中常用inet_pton()和inet_ntop()来转换IP地址,这两个函数不但能转换IPv4格式的地址in_addr,还能转换IPv6格式的地址in_addr6,它们存在于函数库arpa/inet.h中,函数定义如下:

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()会先将字符串src转换为af地址族中的网络地址结构,进而将转换后网络地址结构存储到参数dest所指的缓冲区中,其中参数af的值必须是AF_INET或AF_INET6。

函数inet_ntop()会将af地址族中的网络地址结构arc转换为字符串,再将获得的地址字符串存储到参数dest所指的缓冲区中。

以上两个函数需要转换IPv4和IPv6这两种形式的地址,因此用来传递地址的参数类型为void*。

(3)sockaddr数据结构

IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用结构体sockaddr_in表示,该结构体中包含16位的端口号和32位的IP地址;IPv6地址用结构体sockaddr_in6表示,该结构体中包含16位的端口号、128位的IP地址和一些控制字段;Unix Domain Socket的地址格式定义在sys/un.h中,用结构体sock_addr_un表示。

各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。也因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind()、accept()、connect()等函数,这些函数的参数应该设计成void *类型以便接收各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:

struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
点击此处
隐藏目录