UNIX网络编程卷1(第三版)套接字编程简介

2020-08-31 10:27:39 浏览数 (1)

IPv4套接字地址结构:

  通常也被成为“网际套接字地址结构”,以sockaddr_in命名,定义在<netinet/in.h>头文件中。 

代码语言:javascript复制
struct in_addr
  {
    in_addr_t s_addr;  // 32bits的ip地址,如0xFF000001 -> 127.0.0.1
  };
代码语言:javascript复制
/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    uint8_t sin_len;       /* length of structure (16) */
    sa_family_t sin_family;   /* AF_INET */
    in_port_t sin_port;     /* Port number. 16bits */
    struct in_addr sin_addr;    /* Internet address.  */
    char sin_zero[8];           /* unused */
  };  // sin_family,sin_port,sin_addr是一定支持的3个成员

IPv4地址和TCP或UDP端口号在套接字地址结构中总是以网络字节序(区别于主机字节序)来存储。

之所以网际地址(in_addr)是一个结构体,是因为以前这个结构体中允许访问2个16位的值,用于划分A、B、C类,而现在子网划分之后,这些union结构不再需要。

sin_zero字段未曾使用,但我们总是把该字段置为0,按照惯例,我们总是在填写前把整个结构置为0。

以上是IPv4套接字地址结构,然而套接字函数是通用的,并且总是接收一个套接字地址结构的指针(eg, sockaddr_in serv; bind(sockfd, (sockaddr *) &serv, sizeof(serv)); ),可以看到第二个参数被转成了sockaddr类型,这是通用套接字地址结构。在套接字函数定义的时候,还没有通用的指针类型void *,所以必须传入一个恰当的类型,否则会报错,于是在<sys/socket.h>定义了一个通用的套接字地址结构。

通用套接字地址结构用途就是对指向特定于协议的套接字地址结构的指针执行类型强制转换。

代码语言:javascript复制
struct sockaddr{
    uint8_t      sa_len;
    sa_family_t    sa_family;    /* address family: AF_xxx value */
    char        sa_data[14];    /* protocol-specific address */
};

IPv6地址为128位长,但通常写作8组,每组为四个十六进制数的形式,如 FE80:0000:0000:0000:AAAA:0000:00C2:0002。

IPv6套接字地址结构在<netinet/in.h>头文件中定义:

代码语言:javascript复制
struct in6_addr
  {
    uint8_t s6_addr[16];
  };

#define SIN6_LEN
 
struct sockaddr_in6
  {
    uint8_t sin6_len;       /* length of this struct (28) */
    sa_family_t sin6_family;   /* AF_INET6 */
    in_port_t sin6_port;    /* Transport layer port # */
    uint32_t sin6_flowinfo; /* IPv6 flow information */
    struct in6_addr sin6_addr;  /* IPv6 address */
    uint32_t sin6_scope_id; /* IPv6 scope-id */
  };

IPV6的地址族是AF_INET6,而IPv4的地址族是AF_INET。

结构中字段的先后顺序做过编排,使得如果sockaddr_in6的结构本身是64位对齐的,那么128位的sin6_addr字段也是64位对齐的。在一些64位处理机上,如果64位数据存储在某个64位边界位置,那么对它的访问将得到优化处理。

sin6_flowinfo字段分为两个字段:低序20位是流标(flow label),高序12位保留。

对于具备范围的地址(scoped address),sin6_scope_id字段标识其范围(scope),最常见的是链路局部地址(link-local address)的接口索引(interface index)

新的通用套接字地址结构:新的结构克服了sockaddr的一些缺点,新的sockaddr_storage足以容纳系统所支持的任何套接字地址结构。

代码语言:javascript复制
struct sockaddr_storage {
  uint8_t   ss_len;   /* length of this struct (implementation dependent) */
  sa_family_t  ss_family;    /* address family: AF_xxx value */
  /* implementation-dependent elements to provide:
   * a) alignment sufficient to fulfill the alignment requirements of
   * all socket address types that the system supports.
   * b) enough storage to hold any type of socket address that the
   * system supports.
   */
};

sockaddr_storage能够满足最苛刻的对齐要求。

sockaddr_storage足够大,可以容纳系统支持的任何套接字地址结构,除了ss_family和ss_len(如果有),其他的字段可以任意放置(对用户透明),sockaddr_storage结构必须强制转换成或复制到适合于ss_family字段所给出地址类型的套接字地址结构中,才能访问其他字段。

值-结果参数(说的是传递的参数作为返回结果的引用,eg, func(&res) ):

当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式来传递,也就是说传递的是指向该结构的一个指针。该结构的长度也作为一个参数来传递,不过其传递方式取决于该结构的传递方向:是从进程的内核,还是从内核到进程。

  1)从进程到内核传递套接字地址结构的函数有3个:bind、connect、sendto。这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小。(内核需要知道到底从进程复制了多少数据进来)

  2)从内核到进程传递套接字地址结构的函数有4个:accept、recvfrom、getsockname和getpeername。这4个函数的其中两个参数是指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针(这是一个结果,所以是引用传值)。

值-结果参数返回的结果:如果套接字地址结构是固定长度(如IPv4 (16) 和IPv6 (28) ),则返回值总是固定长度;对于可变长度(unix域等),返回值可能小于该结构的最大长度。

字节排序函数

小端字节序(little-endian):低序字节存储在起始地址,如0x12345678,在内存中从小到大的地址,存储序列是 78 56 34 12

大端字节序(big-endian):高序字节存储在起始地址,如0x12345678,在内存中从小到大的地址,存储序列是 12 34 56 78

以上两种格式都有系统使用!我们把某个给定系统所用的字节序成为主机字节序(host byte order)

代码语言:javascript复制
#include    "../unpv13e/unp.h"
#include    "../unpv13e/apueerror.h"
// 以上路径是我自己的配置
// page.64 确定主机字节序的程序(小端对齐还是大端对齐)
 
int
main(int argc, char **argv)
{
    union {
        short  s;
        char   c[sizeof(short)];
    } un;
 
    un.s = 0x0102;
    printf("%s: ", CPU_VENDOR_OS);  // 输出CPU类型、厂家和操作系统版本。
    if (sizeof(short) == 2) {
        if (un.c[0] == 1 && un.c[1] == 2)
            printf("big-endiann");
        else if (un.c[0] == 2 && un.c[1] == 1)
            printf("little-endiann");
        else
            printf("unknownn");
    } else
        printf("sizeof(short) = %dn", (int)sizeof(short));
 
    exit(0);
}

问题1:网络字节序和主机字节序的区别?

答:网际协议使用大端字节序来传送这些多字节整数,而系统使用的主机字节序可能是大端也可能是小端。

问题2:具体实现方法是怎样?

答:套接字地址结构的字段按照网络字节序(大端)进行维护,所以要通过函数进行转换。(以下h:host,n:network,s:short->16bits port,l:long->32bits ipv4)

htons 返回网络字节序的端口

htonl 返回网络字节序的ip

ntohs 返回主机字节序的端口

ntohl 返回主机字节序的ip

注意:事实上在64位系统中,长整数虽然占用64位,to long的函数操作的仍然是32位的值。

在大端字节序的系统中,这4个函数被定义为空宏。

字节操纵函数

处理字符串的函数被放在string.h中,然而像套接字地址结构这种多字节字段,需要全部清0,则需要用到字节操纵函数(有2组):

代码语言:javascript复制
#include <strings.h>
// strings.h是从BSD系UNIX系统继承而来,里面定义了一些字符串函数,参考自 http://blog.csdn.net/xin_yu_xin/article/details/38672137
void bzero(void *dest, size_t nbytes);
void bcopy(const void *src, void *dest, size_t nbytes);
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);    // 0为相等,非0为不相等
代码语言:javascript复制
#include <string.h>
// ANSI C标准
void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src, size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);    // 0为相同,非0为不相同

地址转换函数

作用:从点分十进制数串(如:206.168.112.96)转成网络字节序二进制值

两组函数:

  (1) inet_aton , inet_addr , inet_ntoa (仅适用于IPv4)   

代码语言:javascript复制
#include <arpa/inet.h>
 
int inet_aton(const char *strptr, struct in_addr *addrptr);    
// 若字符串有效则返回1,否则为0,。如果addrptr指针为空,那么该函数仍然对输入的字符串执行有效性检查,但是不存储任何结果
 
in_addr_t inet_addr(const char *strptr);   
// 字符串有效则返回32位二进制网络字节序的IPv4地址,否则返回INADDR_NONE(通常是255.255.255.255,这意味着这个有限广播地址不能由该函数来处理,还有一个问题是一些编译器编译的程序将返回-1的结果,而不是INADDR_NONE,所以这个函数现在已经被废弃,要用inet_aton或inet_pton来代替)
 
char *inet_ntoa(struct in_addr inaddr);
// 注意,参数是一个结构而不是一个结构指针(这是非常罕见的..),返回值是指向一个点分十进制数串的指针,该函数的返回值指向的字符串是驻留在静态内存中的,以为着该函数是不可重入的(后面的概念)

(2) inet_pton , inet_ntop (对IPv4和IPv6都适用)   p for presentation(表达) n for numeric(数值)

代码语言:javascript复制
#include <arpa/inet.h>
 
int inet_pton(int family, const char *strptr, void *addrptr);
// 成功则返回1,输入不是有效的表达格式则返回0,出错返回-1
 
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
// 成功则返回指向结果的指针,出错返回NULL,len是目标存储单元的大小,用于防止缓冲区溢出,为了有助于指定这个大小,在<netinet/in.h>头文件中定义了
// #define     INET_ADDRSTRLEN     16
// #define     INET6_ADDRSTRLEN    46
// 如果len太小,不足以容纳表达格式的结果(包括结尾的空字符),则返回一个空指针,置errno为ENOSPC,strptr参数不可以是一个空指针,必须先分配大小,调用成功时,这个指针就是该函数的返回值。
只支持IPv4的inet_pton和inet_ntop函数的简单定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
inet_pton(int family, const char *strptr, void *addrptr)
{
    if (family == AF_INET) {
        struct in_addr  in_val;
 
        if (inet_aton(strptr, &in_val)) {
            memcpy(addrptr, &in_val, sizeof(struct in_addr));
            return (1);
        }
        return(0);
    }
    errno = EAFNOSUPPORT;
    return (-1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const char *
inet_ntop(int family, const void *addrptr, char *strptr, size_t len)
{
    const u_char *p = (const u_char *) addrptr;
 
    if (family == AF_INET) {
        char    temp[INET_ADDRSTRLEN];
 
        snprintf(temp, sizeof(temp), "%d.%d.%d.%d",
                 p[0], p[1], p[2], p[3]);
        if (strlen(temp) >= len) {
            errno = ENOSPC;
            return (NULL);
        }
        strcpy(strptr, temp);
        return (strptr);
    }
    errno = EAFNOSUPPORT;
    return (NULL);
}

0 人点赞