套接字基础
套接字地址结构的传递既有用户态到内核态(如bind函数)也有内核态传递到用户态(如accept函数),对于应用程序来说,传给内核的套接字地址结构需要类型强制转换为struct sockaddr*,而接收内核传出的套接字地址结构时需要采用struct sockaddr_storage*。
对于ASCII字符串格式的地址转换建议有线采用inet_pton和inet_ntop这两个函数,因为同时兼容IPv4和IPv6。
套接字地址结构
大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。
每个协议族都定义了它自己的套接字地址结构。这些结构的名称均以sockaddr_开头,并以对应每个协议族的唯一后缀结尾。
在BSD系列的系统中,套接字地址结构中都存在一个len字段,用于避免依赖外部参数,而在Linux系统中,采用的是单独的socklen_t类型的函数参数来表示套接字长度。
IPv4
网际套接字地址结构,定义在<netinet/in.h>头文件中,如下所示:
/* Internet address. */
typedef uint32_t in_addr_t;
/* Type to represent a port. */
typedef uint16_t in_port_t;
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
struct in_addr
{
in_addr_t s_addr; /* 32 bit IPv4 address. network byte ordered */
};
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
sa_family_t sa_family; /* AF_INET */
in_port_t sin_port; /* 16 bit TCP or UDP Port number. network byte ordered*/
struct in_addr sin_addr; /* 32 bit IPv4 address. network byte ordered*/
unsigned char sin_zero[sizeof(struct sockaddr) - sizeof(unsigned short int) - sizeof(in_port_t) - sizeof(struct in_addr)];
};
POSIX规范只需要该结构中的3个字段:sin_family、sin_addr和sin_port。但几乎所有的实现都增加了sin_zero字段,所有所有的套接字地址结构大小都至少是16字节。
对于该结构中的sin_prot和sin_addr都是以网络字节序存储。
为什么sin_addr是一个结构体,而不是一个in_addr_t类型的无符号整型变量呢?这是因为早期的in_addr被定义为多种结构的union,允许访问32位IPv4地址中的所有4个字节,或者2个16位的值,用在IPv4地址被划分为A、B、C类地址时期。由于子网地址划分方式的升级和无类别域间路由的出现,各种地址类正在消失,union结构不在被需要。
套接字地址结构仅在戈丁主机上使用,虽然地址和端口口字段用在不同主机之间的通信,但是地址本身不在主机间传递。
通用地址结构
当套接字地址结构以指针形式作为参数之一传入到任何套接字函数时,支持协议族的套接字函数就必须能够处理对应协议族的套接字地址结构。例如bind函数支持AF_INET和AF_INET6两种协议族,那么该函数就需要能够处理这两种协议族对应的套接字地址结构。
当时是如何声明这个指针的数据类型的呢?因为套接字函数出现在ANSI C之后,无法使用其中提供的void类型。因此当时采用的办法是在<sys/socket.h>头文件中定义一个通用的套接字地址结构,如下所示:
/* Structure describing a generic socket address. */
struct sockaddr
{
sa_family_t sa_family; /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
于是套接字函数被定义为以指向通用套接字地址结构的一个指针作为其参数之一,例如bind函数的ANSI C函数原型所示:extern int bind(int __fd, const struct sockaddr *__addr, socklen_t __len);
因此对这些套接字函数的任何调用都必须要将指向特定于协议的套接字地址结构的指针进行类型强制转换,变成指向通用套接字地址结构的指针。
从应用程序开发的角度来看,这个通用套接字地址结构的唯一用途就是对指向特定于协议的套接字地址结构的指针执行类型强制转换。从内核的调度看,内核必须取调用者的指针,把它类型强制转换为struct sockaddr*类型,然后检查其中sa_family字段的值来去顶这个结构的真实类型。
IPv6
IPv6的套接字地址结构在<netinet/in.h>头文件中定义,如下所示:
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
/* Type to represent a port. */
typedef uint16_t in_port_t;
/* IPv6 address */
struct in6_addr
{
union
{
uint8_t u6_addr8[16];
uint16_t u6_addr16[8];
uint32_t u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
};
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址族,必须为 AF_INET6
in_port_t sin6_port; // 端口号(网络字节序)
uint32_t sin6_flowinfo; // IPv6 流信息(QoS/流标签)
struct in6_addr sin6_addr; // IPv6 地址结构
uint32_t sin6_scope_id; // 作用域 ID(如链路本地地址的接口索引)
};
#define s6_addr in6_u.u6_addr8这个宏的作用是可以使用sin6_addr.s6_addr来访问IPv6地址,和IPv4的sin_addr.s_addr保持风格一致。
sin6_flowinfo字段可以用来设置自定义流标签支持QoS功能,sin6_scope_id可以用来指定在特定接口上使用链路本地地址进行通信,如下所示:
struct sockaddr_in6 addr;
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(8080);
addr.sin6_flowinfo = htonl(0x12345); // 设置流标签
inet_pton(AF_INET6, "fe80::1", &addr.sin6_addr);
addr.sin6_scope_id = if_nametoindex("eth0"); // 指定链路本地接口
为什么会有sin6_scope_id这个字段呢?在IPv4上以169.254.x.x开头的链路本地地址在主机范围内是唯一的,指定了链路本地地址,内核就可以确定使用哪个接口进行通信。然而IPv6没有这个要求,主机范围内以FE80开头的链路本地地址可以重复(仅需要在链路上唯一),内核无法去推断使用哪个接口通信,因此需要该字段明确指出。
通用存储套接字地址结构
struct sockaddr_storage是一个通用的套接字地址结构,可以容纳所有类型的sockaddr_*结构。定义在<sys/socket.h>头文件中,所辖所示:
struct sockaddr_storage
{
sa_family_t ss_family; /* Address family, etc. */
char __ss_padding[_SS_PADSIZE];
unsigned long int __ss_align; /* Force desired alignment. */
};
struct sockaddr_storage结构主要用来解决如下几个问题:
- 套接字API(例如
accept()、recvfrom())返回的地址类型不确定,可能是IPv4(struct sockaddr_in)、IPv6(struct sockaddr_in6),甚至是其他协议族。struct sockaddr_storage提供一个统一的容器来接收这些地址。 - 传统的
struct sockaddr结构太小(只有16字节),无法存放IPv6地址,struct sockaddr_storage足够大,可以避免缓冲区溢出。 - 不同平台对结构体对齐要求不同。
struct sockaddr_storage内部通过填充和对齐字段,可以确保跨平台访问安全。 struct sockaddr_storage可以写出协议无关的代码,避免硬编码具体结构大小。
使用方法如下所示:
struct sockaddr_storage ss;
socklen_t addrlen = sizeof(ss);
int client_fd = accept(server_fd, (struct sockaddr *)&ss, &addrlen);
if (ss.ss_family == AF_INET)
{
struct sockaddr_in *addr4 = (struct sockaddr_in *)&ss;
// 处理 IPv4 地址
}
else if (ss.ss_family == AF_INET6)
{
struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)&ss;
// 处理 IPv6 地址
}
一句话概括就是在通用网络编程中提供一个安全的“万能地址缓冲区”。
字节序函数
对于多字节数据类型来说,内存中存储时有两种方法:一种是将低序字节存储在地址起始位置,称为小端字节序;另一种方法是将高序字节存储在起始地址,称为大端法。
例如对于一个4字节整型数据0x12345678,其中低序字节指的是0x78,高序字节指的是0x12,因此当内存地址的增长顺序为从左到右时,小端字节序为0x78 0x56 0x34 0x12,大端字节序为0x12 0x34 0x56 0x78。
由于网络协议中存在多字节字段的端口号、IP地址等,因此需要明确指定一个网络字节序,用于发送协议栈和接收协议栈就这些多字节字段各个字节的传送顺序达成一致。
由于大端字节序符合人的阅读习惯,因此网络协议使用大端字节序来传送多字节字段。
从理论上来说,具体实现上可以按照主机字节序存储套接字地址结构中的各个字段,等到需要在这些字段和协议首部响应字段进行移动时,再在主机字节序和网络字节序之间进行互转,避免在用户态处理转换细节。然而由于历史原因,套接字地址结构中的字段在用户态填充时就需要按照网络字节序进行维护。
因此POSIX规定了如下几个函数,用于两种字节序之间的转换:
#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,s表示short,l表示long。由于历史原因,这里分别转换的是16位和32位整型。
地址转换函数
这几个函数用于在ASCII字符串和网络字节序的二进制值之间转换网络地址。
inet_aton、inet_addr以及inet_ntoa函数在点分十进制与长度为32位的网络字节序二进制值之间转换IPv4地址。
#include <arpa/in.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
// 返回值:字符串有效则返回1,否则返回0
char *inet_ntoa(struct in_addr inaddr);
in_addr_t inet_addr(const char *strptr);
// 返回值:若字符串有效则为32位的二进制网络字节序的IPv4地址,否则位INADDR_NONE
inet_aton函数用于将参数strptr中保存的点分十进制格式的IPv4地址转换为32位的网络字节序的二进制值。
inet_addr的返回值为32位的网络字节序二进制值。但是该函数存在问题,不能转换255.255.255.255地址,因为该地址的转换结果被错误标识占用,用来指示参数出错,即INADDR_NONE。因此要尽量避免使用该函数。
inet_ntoa函数用于将32位的网络字节序的IPv4地址转换为点分十进制的IPv4地址字符串。
上面三个函数都是针对IPv4地址进行转换处理的,下面两个函数是随着IPv6出现的新函数,同时支持IPv4和IPv6。
#include <arpa/inet.h>
int inet_pton(int af, const char *restrict cp, void *restrict addrptr);
// 返回值:成功则为1,若输入不是有效的表达格式则为0,若出错则为-1
const char *inet_ntop(int af, const void *restrict addrptr, char *restrict cp, size_t len);
// 返回值:若成功则为指向结果的指针,若出错则为NULL
参数af值得是协议族,可取值为AF_INET和AF_INET6。如果传入的协议族不支持,则返回错误并设置errno为EAFNOSUPPORT。
参数addrptr用于保存转换前或转换后的网络字节序的二进制值,采用void*类型的原因是其既可以处理struct in_addr类型也可以处理struct in6_addr类型。
inet_pton函数用来转换参数cp指向的字符串为,并通过addrptr存放二进制结果。如果转换成功则返回1,如果对协议族而言不是有效的字符串格式则返回0,其他情况返回-1。
针对协议族的有效字符串格式包括几种:点分十进制的IPv4地址,IPv4映射的IPv6地址(x:x:x:x:x:x:a.b.c.d)以及IPv6地址(x:x:x:x:x:x:x:x:x)
inet_ntop函数则进行相反的转换,用于将地址从数值格式转为表达格式。len参数用于指定缓冲区的大小,定义在<netinet/in.h>中的参考值如下:
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
示例如下:
char *str = "2001:db8::1";
struct sockaddr_in6 servaddr6;
bzero(&servaddr6, sizeof(struct sockaddr_in6));
inet_pton(AF_INET6, str, &servaddr6.sin6_addr);
char buf[INET6_ADDRSTRLEN] = {0};
inet_ntop(AF_INET6, &servaddr6.sin6_addr, buf, sizeof(buf));
printf("%s\n", buf); // 2001:db8::1
常用函数封装
考虑到代码在IPv4和IPv6之间的通用性,inet_ntop函数存在一个问题:它要求调用者传入一个指向某个二进制地址的指针,而该地址通常包含在套接字地址的结构中,这就要求调用者必须知道这个结构的格式和地址族。
所以为了使用这个函数,需要分别为IPv4和IPv6编写如下代码:
struct sockaddr_in addr;
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
struct sockaddr_in6 addr6;
inet_ntop(AF_INET6, &addr6.sin6_addr, str, sizeof(str));
为了解决这种代码通用问题,下面提供一个函数sock_ntop,它以指向通用套接字地址结构作为参数,然后调用适当的函数返回该地址的表达方式:
char* sock_ntop(struct sockaddr* sa, char* buf, int len)
{
switch (sa->sa_family)
{
case AF_INET:
{
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
if (inet_ntop(AF_INET, &sin->sin_addr, buf, len) == NULL)
{
return NULL;
}
return buf;
}
case AF_INET6:
{
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
if (inet_ntop(AF_INET6, &sin6->sin6_addr, buf, len) == NULL)
{
return NULL;
}
return buf;
}
default:
return NULL;
}
return NULL;
}
int main()
{
char* str = "2001:db8::1";
struct sockaddr_in6 servaddr6;
bzero(&servaddr6, sizeof(struct sockaddr_in6));
servaddr6.sin6_family = AF_INET6;
inet_pton(AF_INET6, str, &servaddr6.sin6_addr);
char buf[INET6_ADDRSTRLEN] = {0};
printf("%s\n", sock_ntop((struct sockaddr*)&servaddr6, buf, sizeof(buf))); // 2001:db8::1
}
在TCP流套接字调用read和write读取或写入的字节数可能比请求的数量少,然而这并不代表出错,原因可能在于内核用于套接字的缓冲区已经被占满,此时则需要再次调用read或write函数再次读取或写入剩余的字节。
下面提供了两个函数readn和writen,用于一次尽可能读取或写入n个字节,函数的返回值为实际读取或写入的字节数。
ssize_t readn(int fd, void* vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char* ptr = NULL;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ((nread = read(fd, ptr, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
else
{
// 其它错误:如果已经读取部分数据,返回已读取的字节数;若尚未读取则返回 -1
return (nleft == n) ? -1 : (ssize_t)(n - nleft);
}
}
else if (nread == 0)
{
break; /* EOF */
}
nleft -= nread;
ptr += nread;
}
return (nleft == n) ? -1 : (ssize_t)(n - nleft);
}
ssize_t writen(int fd, const void* vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char* ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ((nwritten = write(fd, ptr, nleft)) <= 0)
{
if (nwritten < 0 && errno == EINTR)
{
continue;
}
else
{
// 其它错误:如果已经写入部分数据,返回已写入;若尚未写入则返回 -1
return (nleft == n) ? -1 : (ssize_t)(n - nleft);
}
}
nleft -= nwritten;
ptr += nwritten;
}
return (nleft == n) ? -1 : (ssize_t)(n - nleft);
}
- 原文作者:生如夏花
- 原文链接:https://DBL2017.github.io/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/unp/%E5%A5%97%E6%8E%A5%E5%AD%97%E5%9F%BA%E7%A1%80/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。