socket编程 -- 网络字节序、IP地址转换、sockaddr数据结构

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的 多字节数据相对于文件中的偏移地址也有大端小端之分。

网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
例如上一节的UDP 段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03, 地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。
但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#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);

h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

IP地址转换函数

早期版本

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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地址
不可重入函数
注意参数是struct in_addr

现在版本

#include <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);

新版本的支持IPv4和IPv6
可重入函数
其中inet_ptoninet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr

sockaddr 数据结构

strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

sockaddr数据结构图

socket编程 -- 网络字节序、IP地址转换、sockaddr数据结构_第1张图片

struct sockaddr 
{
    sa_family_t sa_family; /* address 族, AF_xxx */
    char sa_data[14]; /* 14位协议地址 */
};

struct sockaddr_in 
{
    __kernel_sa_family_t sin_family; /* Address 族 */
    __be16 sin_port; /* 端口号 */
    struct in_addr sin_addr; /* Internet 地址 */
    /* 填补 `struct sockaddr'. */
    unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - 
    sizeof(unsigned short int) - sizeof(struct in_addr)];
};

/* Internet 地址 */
struct in_addr 
{
    __be32 s_addr;
};

struct sockaddr_in6 
{
    unsigned short int sin6_family; /* AF_INET6 */
    __be16 sin6_port; /* 传输层端口 # */
    __be32 sin6_flowinfo; /* IPv6 流信息 */
    struct in6_addr sin6_addr; /* IPv6 地址 */
    __u32 sin6_scope_id; /* 范围id (new in RFC2553) */
};

struct in6_addr 
{
    union 
    {
    __u8 u6_addr8[16];
    __be16 u6_addr16[8];
    __be32 u6_addr32[4];
    } in6_u;

    #define s6_addr in6_u.u6_addr8
    #define s6_addr16 in6_u.u6_addr16
    #define s6_addr32 in6_u.u6_addr32
};

#define UNIX_PATH_MAX 108

struct sockaddr_un 
{
    __kernel_sa_family_t sun_family; /* AF_UNIX */
    char sun_path[UNIX_PATH_MAX]; /* 路径名*/
};

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

UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。

各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。
IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INETAF_INET6AF_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));

你可能感兴趣的:(socket,网络字节序,addr数据结构)