Linux网络编程基础API--socket地址API

1. 主机字节序和网络字节序

1.1 大小端原理

  网络通信使得数据从一个主机传递到另一个主机。然而在不同的的处理器在管理内存单元上的数据时,对需要存放在多个内存单元地址的某一数据的处理方式不尽相同,因此对数据的解析结果也不同。目前处理器数据处理类型有大端小端两种方式。

小端(Little-endian)模式: 操作数的存放方式为高地址存放高字节。
大端(Big-endian)模式: 操作数的存放方式为高地址存放低字节。

  例如,一个无符号的整数0x12345678存放在0x8000~0x8003地址上,小端模式:
Linux网络编程基础API--socket地址API_第1张图片
  大端模式:
Linux网络编程基础API--socket地址API_第2张图片

  面试中经常会考察的题目,编程判断系统是大端模式还是小端模式存储数据。在这里也顺便实现:

//法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;
}

1.2 字节序转换函数

  在网络上传输的数据以及各种类型的主机字节序存在差异,因此在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)的大小将可能不同,这就意味着接收方接收到的数据与发送端发送的不同了。

2. socket地址

2.1 通用socket地址

  struct sockaddr结构体类型用于表示socket网络编程接口的地址:

#include 
struct sockaddr {
   sa_family_t sa_family;
   char        sa_data[14];
}

  sa_family_t表示地址类型,它通常与协议族类型对应,常见的协议族和对应的地址族如下表格:
Linux网络编程基础API--socket地址API_第3张图片

  由于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的地址值。不同的协议族的地址值具有不同的含义和长度:
Linux网络编程基础API--socket地址API_第4张图片

  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]; /* 内存对齐 */
};

2.2 专用socket地址

  通用的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

3. IP地址转换函数

3.1 IPv4的地址转换

(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中。

3.2 通过IP地址获取网络ID和主机ID

  下面函数适用于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.3 IPv4和IPv6通用的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。

你可能感兴趣的:(Linux系统/网络编程,Linux编程)