首页 > 代码库 > Unix网络编程 之 socket基础

Unix网络编程 之 socket基础

基本结构

(这部分的地址均为网络地址<网络字节序>)


1、struct sockaddr:通用套接字地址结构

    此结构用于存储通用套接字地址。

   数据结构定义:

typedef unsigned short  sa_family_t;
struct sockaddr {
    sa_family_t sa_family;  /* address family, AF_xxx	*/
    char sa_data[14];  /* 14 bytes of protocol address	*/
};
   sa_family:实际使用地址——根据不同的协议族,采用不同的地址类,最常用的三种:

   1)本地地址族:AF_UNIX或AF_LOCAL;

#include <sys/un.h>

struct sockaddr_un {
    sa_family_t  sun_family;         /*AF_UNIX*/
    char        sun_path[108];      /*地址数据*/
};

   2)网络地址族:AF_INET;

#include <netinet/in.h>

struct sockaddr_in {
    sa_family_t    sin_family;       /* AF_INET */
    uint16_t       sin_port;         /*端口号*/
    struct in_addr   sin_addr;        /*Internet地址*/
    unsigned char   sin_zero[8]      /*占位字节*/
};

   3)红外地址类:AF_IRDA;

#include <sys/types.h>

struct sockaddr_irda {
    sa_family_t sir_family;  /* AF_IRDA */
    u_int8_t sir_lsap_sel;     /* LSAP selector */
    u_int32_t sir_addr;         /* Device address */
    char sir_name[25];  /*:IrDA:TinyTP ,OBEX,etc.*/
};

  sa_data[]:包含远程主机的地址、端口号和套接字的数目,这些信息包含于字符串中。

   而在实际应用中,我们经常使用AF_INET。为了处理struct sockaddr,Unix建立了另外一个相似结构struct sockaddr_in。其结构如下所示。

struct sockaddr_in {
    sa_family_t    sin_family;       /* AF_INET */
    uint16_t       sin_port;         /*端口号*/
    struct in_addr   sin_addr;        /*Internet地址*/
    unsigned char   sin_zero[8]      /*占位字节*/
};
   此结构提供了简洁的方法用于访问socket address(struct sockaddr)结构中的每一个元素。sin_zero[8]是为了使两个结构在内存中具有相同的尺寸,使用sockaddr_in时要把sin_zero[]全部设置为零值(使用bzero()函数或memset()函数)。此结构可以认为是IPv4套接字地址结构。

2、struct in_addr:因特网地址结构

   该结构用于表示Internet Address(因特网地址)。

/*Internet address*/
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};
    在这里,我们由s_addr的类型定义可知,此处的因特网地址长度为32bit,也就是说此处的地址为IPv4地址。同时,当使用struct sockaddr_in类型的ina变量时,ina.sin_addr.s_addr即为32bit的IP地址。当然,此处,我们应该注意这里的IP地址是网络字节序。   

3、IPv6套接字地址结构

    在上文,我们介绍了IPv4套接字地址结构,那么,对于IPv6套接字,其地址结构又如何呢?

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

struct in6_addr{
    uint8_t s6_addr[16];   /*128bits IPv6 address network byte ordered*/
 };

struct sockaddr_in6{
    sa_family_t sin6_family;    /*AF_INET6 */
    in_port_t sin6_port;  /*transport layer port network byte ordered */
    uint32_t sin6_flowinfo;  /*flow information, undefined*/
    struct in6_addr sin6_addr; /*IPv6 address network byte ordered */
    uint32_t sin6_scope_id;  /*set of interfaces for a scope*/
};
   对于IPv6套接字地址结构,我们要注意这样几点:

   *IPv6的地址族(sa_family)是AF_INET6,而IPv4的地址族是AF_INET;

   *结构中字段的先后顺序做过编排,使得如果sockaddr_in6结构本身是64位对齐的,那么128位的sin6_addr字段也是64位对齐的。

   *sin6_flowinfo字段分为两个字段:低序20位流标(flow table) + 高序12位保留。

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

4、struct sockaddr_storage:新的通用套接字地址结构

struct sockaddr_storage{
    uint8_t    ss_len;    //length of the struct
    sa_family_t ss_family;   //address family: AF_xxx value
};
   在上文,我们可以知道struct sockaddr为通用套接字地址结构,但是由于因特网地址结构的限制,该结构仅支持IPv4地址。而新的通用套接字地址结构struct sockaddr_storage克服了现有struct sockaddr的一些缺点,足以容纳系统所支持的任何任何套接字地址结构。

   sockaddr_storage类型提供的通用套接字地址结构相比sockaddr存在以下两点差别:

(1)、如果系统支持的任何套接字地址结构有对齐需要,那么sockaddr_storage能够满足最苛刻的对齐要求;

(2)、sockaddr_storage足够大,能够容纳系统支持的任何套接字地址结构。

5、套接字地址结构的比较:

 


基本转换函数


1、值-结果参数

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

(1)、从进程到内核传递套接字地址结构的函数有3个:bind、connect和sendto。这些函数一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小。例如:

struct sockaddr_in serv;

connect(sockfd, (struct sockaddr *) &serv, sizeof(serv));
   既然指针和指针所指内容的大小都传递给了内核,内核就知道到底需要从进程复制多少数据。下图展示了这个情形。

 

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

struct sockaddr_un cli; /*Unix domain*/

socklen_t len;
len = sizeof(cli);
getpeername(unixfd, (struct sockaddr *)&cli, &len);
   把套接字地址结构大小这一参数从一个整数改为指向某个整数变量的指针,其原因在于:

   当函数被调用时,结构大小是一个值(value),它告诉内核该结构的大小,这样内核在写该结构时不至于越界;当函数返回时,结构大小又是一个结果(result),它告诉进程内核在该结构中究竟存储了多少信息。这种类型的参数称为“值-结果(value-result)”参数。下图展示了这个情形。

 

   关于值-结果参数,在后面介绍套接字调用函数时会进一步解析。

2、字节排序函数

   IP地址的三种表示格式:

1)ASCII(点分十进制字符串)

2)网络地址(网络字节序,Network Byte Order)

3)主机地址(主机字节序,Host Byte Order)

   那么,网络字节序和主机字节序有何不同?

   内存在存储数据时有两种处理方法:一种是将低序字节存储在起始地址,此称为小端(little-endian)字节序;另一种是将高序字节存储在起始地址,此称为大端(big-endian)字节序。采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。

   对于2个字节的数据存储,大端存储与小端存储的区别如下图:

 

   在套接字地址表示方面,网络地址(网络字节序)一般采用大端字节序,而主机地址(主机字节序)主要取决于系统。

   那么主机地址和网络地址(或者说主机字节序和网络字节序)各自主要应用于什么场合?

   主机地址主要用于主机处理时,因为计算机更加擅长处理小端字节序(对采用小端存储的主机);而在网络协议中,发送协议栈和接受协议栈必须就多字节字段的各个字节的传送顺序达成一致,为了便于人类思维过程,一般采用大端字节序,即网络字节序。

   主机字节序和网络字节序之间相互转化的过程即为字节排序的过程。这两种字节序之间的转换使用以下4个函数:

#include <netinet/in.h>

/*均返回网络字节序的值*/
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);

/*均返回主机字节序的值*/
uint16_t ntonhs(uint16_t net16bitvalue);
uint32_t ntonhl(uint32_t net32bitvalue);
  在这些函数的名字中,h代表host,n代表network,s代表short,l代表long。如今,我们把s视为16位的值(例如TCP/UDP端口号),把l视为32位的值(例如IPv4地址)。

   这些函数在处理时,会根据主机系统到底是小端存储还是大端存储来相应地调整函数的处理过程,如果主机系统支持小端存储,则对应实现字节反转过程,反之,则这些函数为空宏。

   在什么时候应该使用这些函数呢?当我们存在内核与进程之间的套接字地址结构访问时就必须使用相应的字节转换函数来实现地址的正常传递。

3、字节操纵函数

   操纵多字节字段的函数有两组(Berkeley&ANSIC),它们既不对数据作解释,也不假设数据是以空字符结束的C字符串。

   名字以b(表示字节)开头的第一组函数起源于4.2BSD,现今支持套接字函数的系统仍然提供它们。名字以mem(表示内存)开头的第二组函数起源于ANSIC标准,支持ANSIC函数库的所有系统都提供它们。

Berkeley:

#include <string.h>

void bero(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值*/
/*此处使用const限定词,表示所限制的指针所指的内容不会被函数更改。换句话说,函数只是读而不修改由const指针所指的内存单元。*/

ANSIC:

#include <string.h>

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值,或大于0,或小于0*/
 4、地址转换函数

   地址转换函数实现ASCII字符串(点分十进制)与网络字节序的二进制(存放在套接字地址结构中的值)之间的网际地址的转换。

   地址转换函数有两组,分别是:

(1)、inet_aton、inet_addr和inet_ntoa函数

   完成点分十进制(如”192.168.1.1”)与它长度为32位的网络字节序二进制间转换IPv4地址。

#include <arpa/inet.h>

int inet_aton(const char *strptr, struct in_addr *addrptr);
/*
此函数将strptr所指的C字符串转换成一个32位的网络字节序二进制,并通过指针addrptr来存储。若成功,返回1,否则返回0。
 */

in_addr_t inet_addr(const char *strptr);
/*
此函数与inet_aton函数执行相同的操作,返回32位的网络字节序二进制值,否则返回INADDR_NONE。注意,此函数不能处理255.255.255.255点分十进制数串。
 */

char *inet_ntoa(struct in_addr inaddr);
/*
此函数将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。
由该函数的返回值所指向的字符串驻留在静态内存中。另外。该函数是以一个结构而不是以指向该结构的一个指针作为参数。
 */

   注:inet_addr()函数已被废弃,建议采用inet_aton()函数。

(2)、inet_pton和inet_ntop函数

    这两个函数式随IPv6出现的新函数,对于IPv4和IPv6地址均适用。函数名中的p代表表达(presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构中的二进制值。

#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr);
/*
此函数转换由strptr指针所指的字符串,并通过addrptr指针存放二进制结果。若成功,则返回值为1,否则返回0。
 */

const char *inet_ntop(int family, const void *addrptr, size_t len);
/*
此函数执行从数值格式(addrptr)到表达格式(strptr)的转换。
strptr参数不能是空指针,调用者必须为目标存储单元分配内存并指定大小。调用成功时,返回此指针。
 */
  对于这两个函数,family字段可以是AF_INET或AF_INET6。如果以不被支持的地址族作为family参数,这两个函数会返回错误,并将errno置为EAFNOSUPPORT。

   另外,在inet_ntop()函数中,len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。为了有助于指定此大小,在<netinet/in.h>头文件中有如下定义:

#define INET_ADDRSTRLEN 16 /*IPv4 dotted-decimal*/
#define INET6_ADDRSTRLEN 46 /*IPv6 hex-string*/
   如果len太小,不足以容纳表达格式结果,那么返回空指针,并置errno为ENOSPC。