高级C与网络编程复习(3)—— 套接字编程简介(Sockets Introducrion)(第三章)

套接字地址结构(Socket Address Structures)

  • 大多数套接字函数(socket function)都 需要一个指向套接字地址结构(socket address structure)的指针作为参数
  • 每个协议族(protocol suite)都定义它自己的套接字地址结构
  • 这些结构的名字均以 sockaddr_开头,并以对应每个协议族的唯一后缀结尾
  • IPV4 套接字地址结构

    网际(IPV4)套接字地址结构:sockaddr_in --> 定义在 <netinet/in.h> 当中

    struct in_addr{
      in_addr_t     s_addr;             //32-bit ipv4 Address, network byyte ordered
                                        //32为的ipv地址,采用网络字节序
    }struct sockaddr_in{
      uint8_t         sin_len;            //length of structure (16)
      sa_family_t     sin_family;         //AF_INET ==>本地址结构是IPV4的地址结构,他属于网际协议族
      in_port_t       sin_port;           //16-bit TCP or UDP port, 网络字节序
    
      struct in_addr  sin_addr;           //32-bit ipv4 address, 网络字节序
      char            sin_zero[8];        //unused ==> 保留位,未使用
    }
    
    • POSIX 规范要求的数据类型:
      POSIX 规范要求的数据类型
    • IPV4 地址和 TCP 或 UDP 端口号都采用网络字节序(大端序)存储
    • IPV4 地址存在两种访问方法(因为历史原因)
      • serv.sin_addr (结构体)
      • serv.sin_addr.in_addr_t (通常是一个无符号的 32 为整数)
    • sin_zero 字段未被使用,不过在填写这种结构的时候 sin_zero 通常被置为 0。(通常的做法是,在填写之前,用 bzero 将整个结构体清 0,再填写,可以保证未填写的部分都是 0
    • 套接字地址结构仅在主机上使用,虽然结构体中的某些字段(例如 IP 地址和端口号)用在不同主机之间的通信,但是结构体本身并不在主机之间传递
  • 通用套接字地址结构

    通用套接字地址结构:sockaddr --> 定义在 <sys/socket.h> 当中

    struct sockaddr{
      uint8_t       sa_len;
      sa_family_t   sa_family;      //Address family: AF_XXX value
      char          sa_data[14];    //protocol-specific address
    }
    
    • 前面提到过,大多数套接字函数都需要一个指向套接字地址结构的指针。但是套接字函数大多支持多个协议族,也就是说在调用的时候可能传入不同协议族的地址结构指针,那套接字函数在定义的时候就必须要有一个类型,可以接收各个协议族对应的地址结构指针

    • 这种需求可以用 void *** 来解决,实际上也更方便,如果用 void * 来定义,可以接收任意类型的指针,而且不用显示转换**。但是 void * 实在 ANSI C 中提出的,而套接字函数是在 ANSI C 之前定义的,所以为了解决上述需求,采用了通用套接字,下面是一个套接字函数的栗子:

      int bind(int, struct sockaddr *, socklen_t);
      
      //调用方式如下
      bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));
      
      • 通用套接字地址结构 sockaddr 和其他协议族各自的地址结构 sockaddr_XX 规定的最小 size 是一样的,都是 16 个字节
      • 套接字函数在具体处理的时候根据 sa_family 字段区分不同的地址结构,对应不同的处理
  • IPV6 套接字地址结构

    IPv6 套接字地址结构:sockaddr_in6 --> 定义在 <netinet/in.h> 当中

    struct in6_addr {
      uint8_t             s6_addr[16];         //128-bit IPV6 address
    };
    
    #define SIN6_LEN                  //require for compile-time tests
    
    struct sockaddr_in6 {
      uint8_t             sin6_len;         //length of this struct, 大小为28个字节
      sa_family_t         sin6_family;      //AF_INET6
      in_port_t           sin6_port;        //传输层端口,网络字节序
    
      uint32_t            sin6_flowinfo;    //flow information, undefined
      struct in6_addr     sin6_addr;        //IPV6 address, 网络字节序
    
      uint32_t            sin6_scope_id;    //set of interfaces for a scope
    }
    

值 - 结果参数(Value-Result Argument)

  • 一个参数,当函数调用时,其作为一个值从函数外传入函数内,当函数返回时,该参数又存储了函数执行的部分结果,这种类型的参数称为 value-result 参数
  • value-result 参数总是以引用 / 指针的方式传递(只能用地址传递的方式,如果用值传递方式获取不到函数的返回信息)
  • 上文曾提到过,当往一个套接字函数传递地址结构的时候,该结构总是以引用的方式传递(即传递地址结构的指针)。该结构的长度也作为一个参数来传递,不过其传递的方式可能是传值,也可能是传指针,具体的传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程

  • 从进程到内核传递套接字地址结构

    • 涉及的函数有:bind、connect、sendto

    • 传递结构长度的时候传值就好了

    • 举个栗子:

      struct sockaddr_in serv;
      
      /*fill in serv{}*/
      
      connect(sockfd, (SA *) &serv, sizeof(serv));
      
    • 图示:

      从进程到内核传递套接字地址结构

  • 从内核到进程传递套接字地址结构

    • 涉及的函数有:accpet、recvfrom、getsockname、getpeername

    • 传递结构长度的时候传入一个指向 socklen_t 的指针(而不是 int,POSIX 规范建议将 socklen_t 定义为 uint32_t)

    • 举个栗子:

      struct sockaddr_un   cli;
      socklen_t len;
      
      len = sizeof(cli);
      getpeername(unixfd, (SA *) &cli, &len);
      
    • 图示:

      从内核到进程传递套接字地址结构

    • 函数被调用时,结构大小是一个值(value),它告诉内核该结构的大小,这样内核在写该结构的时候不至于越界;当函数返回时,结构大小又是一个结果(result),它告诉进程内核在该结构中究竟存储了多少信息

字节排序函数(Byte Ordering)

  • 大端和小端(big-endian and little-endian)

    16 位整数的小端字节序和大端字节序

    • 小端:低字节存储在起始地址

    • 大端:高字节存储在起始地址

    • 举个例子:

      0x0102
      
      //从左到右为内存增大方向
      
      //在大端系统中存储
      00000001 00000010
      //在小端系统中存储
      00000010 00000001
      
  • 测试主机是大端还是小端的实践 –> click_me

    ./byteorder
    
    x86_64-unknown-linux-gnu: little-endian
    
  • 网络字节序

    • 不同的机器可能采用不同的存储方式(大端 / 小端),为了统一,便为网际协议约定了一个网络字节序
    • 网际协议使用大端字节序来传送多字节整数
    • 由于不同主机的差异性,便需要有一些函数来进行网络字节序和主机字节序的转换
  • 字节转换函数

    #include <netinet/in.h>
    
    /**
    * host to network short
    * 将主机字节序的16位短整型转换为网络字节序的16位短整型
    **/
    uint16_t htons(uint16_t host16bitvalue);
    
    /**
    * host to network long
    * 将主机字节序的32位整型转换为网络字节序的32位整型
    **/
    uint32_t htonl(uint32_t host32bitvalue);
    
    /**
    * network to host short
    * 将网络字节序的16位短整型转换为主机字节序的16位短整型
    **/
    uint16_t ntohs(uint16_t net16bitvalue);
    
    /**
    * network to host long
    * 将网络字节序的32位整型转换为主机字节序的32位整型
    **/
    uint32_t ntohl(uint32_t net32bitvalue);
    
    • 事实上,在 64 为的系统中,尽管长整数占 64 位,htonl 和 ntohl 函数操作的仍然是 32 位值

字节操纵函数

字节操作函数和有两组,本书中只用到了 bzero

  • 源自 Berkeley 的函数(b 开头的字节操纵函数)

    #include <strings.h>
    
    /**
    * 将以dest为起始的目标串的前nbytes个字节置为0
    **/
    void bzero(void *dest, size_t nbytes);
    
    /**
    * 将dst中的前nbytes个字节拷贝到src串中
    **/
    void bcopy(const void *src, void *dst, size_t nbytes);
    
    /**
    * 比较ptr1和ptr2串的前n个字节
    * @return   若相等则返回0, 否则返回非0
    **/
    void bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
    
  • ANSI C 函数(mem 开头的字节操纵函数)

    #include <string.h>
    
    /**
    * 把dest串的前len个字节置为c
    **/
    void *memset(void *dest, int c, size_t len);
    
    /**
    * memcpy类似bcopy,但是两个指针的位置时候是相反的
    *
    * PS:当dest串和src串重叠时,bcopy可正常处理,memcpy的处理结果不可知,此时改用memmove函数
    **/
    void *memcpy(void *dest, const void *src, size_t nbytes);
    
    /**
    * 比较两个串的前nbytes个字节
    * @rerurn    0    相等
    *            >0   第一个不相等字节,ptr1 > ptr2
    *            <0   第一个不相等字节,ptr1 < ptr2
    **/
    void *memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
    
  • 之前的文章提到过有一个叫 bzero 宏的东西。其实是因为笔者的系统不是源自 Berkeley 的,所有没有 bzero 函数。但是 Steven 考虑的比较全面,为没有 bzero 函数的系统定义了一个 bzero 宏,间接调用 memset 函数,但是同样可以实现 bzero 的效果。

地址转换函数

  • 地址转换函数在 ASCII 字符串和网络字节序的二进制值之间转换网际地址
  • inet_aton、inet_addr、inet_ntoa 在点分十进制数串(例如 “192.168.1.1”)与它长度为 32 位的网络字节序二进制值之间转换 IPv4 地址。
  • 两个比较新的函数,inet_ptoninet_ntop 对于 IPv4 和 IPv6 都适用
  • inet_aton、inet_addr 和 inet_ntoa 函数

    #include<arpa/inet.h>
    
    /**
    * 将strptr所指c字符串转换成一个32位的网络字节序二进制值,并通过addrptr指针来存储
    **/
    int inet_aton(const char *strptr, struct in_addr *addrptr);
    
    /**
    * 将strptr所指c字符串转换成一个32位的网络字节序二进制值, 并返回
    **/
    in_addr_t inet_addr(const char *strptr);
    
    /**
    * 将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。
    **/
    char *inet_ntoa(struct in_addr inaddr);
    
    • inet_addr 被废弃
      • inet_addr 函数出错时返回 INADDR_NOE(通常是一个 32 位均为 1 的值)
      • 这意味着该函数不能处理 “255.255.255.255”,因为它的二进制值和 INADDR_NONE 的值是一样的,被用来指示函数执行失败
    • inet_ntoa 函数返回值所指向的串驻留在静态内存当中,着意味着该函数是不可重入的
    • 上面这些函数至支持 IPv4,如果要支持 IPv6,使用下面介绍的两个函数
  • inet_pton 和 inet_ntop 函数

    #include <arpa/inet.h>
    
    /**
    * 尝试转换由strptr所指的字符串,并通过addrptr指针存放二进制结果
    *
    * @return   1 ==> 转换成功
    *           0 ==> 输入的不是有效表达式
    *          -1 ==> 转换出错
    **/
    int inet_pton(int family, const char*strptr, void *addrptr);
    
    /**
    * 与inet_pton进行相反的转换,从数值格式(addrptr)转换到表达式格式(strptr)。
    * @param len     指定目标存储单元的大小,以免该函数溢出调用者的缓冲区
    * @param strptr  用来存储目标串,如果执行成功,返回值即为这个指针
    *               (不能传递空指针,调用这必须为目标存储单元分配内存,并指定其大小)
    * @return  NULL   ==> 失败
    *          strptr ==> 成功
    **/
    const char *inet_ntop(int family, void *addrptr, char *strptr, size_t len);
    
    • 两个函数的 family 参数可以是 AF_INET 或 AF_INET6。如果以不被支持的地址族作为 family 参数。这两个函数都返回一个错误,并将 errno 置为 EAFNOSUPPORT

sock_ntop 和相关函数

  • 在使用 inet_ntop 的时候,对于 IPv4 和 IPv6 的调用方法不同(这使得我们的代码和协议相关了):

    //IPv4
    struct sockaddr_in addr;
    inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
    
    //IPv6
    struct sockaddr_in6 addr6;
    inet_ntop(AF_INET6, &addr6.sin6_addr, str, sizeof(str));
    
  • 所以 Steven 针对上述问题写了下面这么个封装函数

    #include "unp.h"
    
    /**
    * 同时支持IPv4和IPv6版本的inet_ntop
    *
    * @return 成功  ==> 非空指针
    *         出错  ==> NULL
    **/
    char *sock_ntop(const struct *sockaddr, socklen_t addrlen);
    
  • sock_ntop 的实现和其它相关函数此处不做详细讨论,有兴趣可以查看 Steven 的《Unix 网络编程 卷 1》

readn, writen 和 readline 函数

#include "unp.h"

/**
* 从一个描述符中读n字节
**/
ssize_t readn(int filedes, void *buff, size_t nbytes);

/**
* 往一个描述符里写n个字节
**/
ssize_t writen(int filedes, const void *buff, size_t nbytes);

/**
* 从一个描述符中读文本行,以字节为单位
**/
ssize_t readline(int filedes, void *buff, size_t maxlen);
  • 上面三个函数对 read 和 write 操作可能发生的 EINTR 错误做了处理,是比较安全的读写函数
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值