Internet domain socket
Internet domain 流socket是基于TCP之上的,它们提供了可靠的双向字节流通信信道。
Internet domain数据报socket是预计UDP之上的。 UDP socket与之在Unix domin中的对应实体类似,但要注意如下差别
- Unix domin数据报socket是可靠的,而UDP socket是不可靠的
- 在一个Unix domain数据报socket上发送数据会在接受socket的数据队列为满时阻塞。与之不同的是,使用UDP时如果进入的数据报会使接受者的队列溢出,那么数据报被静默丢弃
网络字节序
IP地址和端口号是整数值。在将这些值在网络中传递时碰到的一个问题是不同的硬件结构会以不同的顺序来存储一个多字节整数的字节。存储整数时先存储(即在最小内存地址处)最高有效位的被称为大端,那些先存储最低有效位的被称为小端。一些硬件结构可以在这两种格式之间切换。在特定主机上使用的字节序被称为主机字节序
由于端口号和IP地址必须在网络中的所有字节之间传递并且需要被它们所理解,因此必须要使用一个标准的字节序,这种字节序被称为网络字节序(它是大端的)
有时候可能会直接使用 IP 地址和端口号的整数常量形式,如可能会选择将端口号硬编码进程序中,或者将端口号作为一个命令行参数传递给程序,或者在指定一个 IPv4 地址时使用诸如INADDR_ANY 和 INADDR_LOOPBACK 之类的常量。这些值在 C 中是按照主机的规则来表示的,因此它们是主机字节序的,在将它们存储进socket 地址结构中之前需要将这些值转换成网络字节序。
htons()、htonl()、ntohs()以及 ntohl()函数被定义(通常为宏)用来在主机和网络字节序之间转换整数。
#include <netinet/in.h>
// 返回一个32位的网络字节序,h表示主机
uint32_t htonl (uint32_t __hostlong)
// 返回一个16位的网络字节序,h表示主机
uint16_t htons (uint16_t __hostshort)
// 返回一个32位的主机字节序,n表示网络
uint32_t ntohl (uint32_t __netlong)
// 返回一个16位的主机字节序,n表示网络
uint16_t ntohs (uint16_t __netshort)
总结
网络中用的是字节而不是字符串,我们常说的字节序指的是二进制值
主机有不同的存储字节方式(主机字节序):大端模式和小端模式;在不同的主机通信时,为了避免转换和错误,我们引入了网络字节序。这样我们就只需要关注主机字节序和网络字节序的转换了.
其他用于处理字节的函数:
#include <string.h>
/*
* 功能:把s中指定数组的字节置0
*/
void bzero (void *__s, size_t __n)
/*
* 功能:src复制到_dest
*/
void bcopy (const void *__src, void *__dest, size_t __n)
/*
* 功能:比较字节s1和s2。相等返回0,否则非0
*/
int bcmp (const void *__s1, const void *__s2, size_t __n)
数据表示
在编写网络程序时需要清除不同的计算机架构中会使用不同的规则表示各种数据类型。比如整数类型可以以大端或小端的形式存储。此外,还存在其他的差别,如 C long 数据类型在一些系统中可能是 32 位的,但在其他系统上可能是 64 位的。当考虑结构中,问题就更加复杂了,因为不同的实现采用了不同的规则来将一个结构中的字段对齐到主机系统的地址边界,从而使得字段之间的填充字节数量是不同的。
由于在数据表现上存在这些差异,因此在网络的异构系统之间交换数据的应用程序必须要采用一些公共编码来编码数据。发送者必须要根据这些规则来对数据进行编码,而接收者则必须要遵循同样的规则对数据进行解码。将数据变成一个标准格式以便在网络上传输的过程被称为信号编集
然而,一种比信号编集更简单的方法通常会被采用:将所有传输的数据编码成文本形式,其中数据项之间使用特定的字符来分隔开,这个特定的字符通常是换行符。
如果将在一个流 socket 上传输的数据编码成使用换行符分隔的文本,那么定义一个诸如readLine()之类的函数将是比较便捷的。
readLine()函数从文件描述符参数 fd 引用的文件中读取字节直到碰到换行符为止。输入字节序列将会返回在 buffer 指向的位置处,其中 buffer 指向的内存区域至少为 n 字节。返回的字符串总是以 null 结尾,因此实际上至多有(n–1)个字节会返回。在成功时,readLine()会返回放入 buffer 的数据的字节数,结尾的 null 字节不会计算在内。
ssize_t readLine(int fd, void *buffer, size_t n)
{
ssize_t numRead; /* # of bytes fetched by last read() */
size_t totRead; /* Total bytes read so far */
char *buf;
char ch;
if (n <= 0 || buffer == NULL) {
errno = EINVAL;
return -1;
}
buf = buffer; /* No pointer arithmetic on "void *" */
totRead = 0;
for (;;) {
numRead = read(fd, &ch, 1);
if (numRead == -1) {
if (errno == EINTR) /* Interrupted --> restart read() */
continue;
else
return -1; /* Some other error */
} else if (numRead == 0) { /* EOF */
if (totRead == 0) /* No bytes read; return 0 */
return 0;
else /* Some bytes read; add '\0' */
break;
} else { /* 'numRead' must be 1 if we get here */
if (totRead < n - 1) { /* Discard > (n - 1) bytes */
totRead++;
*buf++ = ch;
}
if (ch == '\n')
break;
}
}
*buf = '\0';
return totRead;
}
如果在遇到换行符之前读取的字节数大于或等于(n–1),那么 readLine()函数会丢弃多余的字节(包括换行符)。如果在前面的(n–1)字节中读取了换行符,那么在返回的字符串中就会包含这个换行符。(因此可以通过检查在返回的 buffer 中结尾 null 字节前是否是一个换行符来确定是否有字节被丢弃了。)
套接字地址结构
Internet domain socket地址由两种:IPv4和IPv6
IPv4 socket 地址:struct sockaddr_in
一个 IPv4 socket 地址会被存储在一个 sockaddr_in 结构中,该结构在<netinet/in.h>中进行定义,具体如下:
#include <netinet/in.h>
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
struct in_addr /* IPv4 4-byte address */
{
in_addr_t s_addr; /* unsigned 32-bit integer */
};
struct sockaddr_in /* IPv4 socket address */
{
sa_family sin_family; /* Address family(AF_INET) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
unsigned char sin_zero[x]; /* Pad to size of `struct sockaddr'. */
};
- sin_family 字段用来标识socket domain,其值总是为AF_INET
- sin_port 和 sin_addr 字段是端口号和 IP 地址,它们都是网络字节序的
- in_port_t 和in_addr_t t 数据类型是无符号整型,其长度分别为 16 位和 32 位。
- sin_sero在绑定一个非通配的IPv4地址时,必须为0
- 套接字地址结构仅在给定主机上使用:虽然结构中的IP地址、端口号等可以用在不同主机之间的通信中,但是结构本身并不在主机之间传递
IPv6 socket 地址:struct sockaddr_in6
与 IPv4 地址一样,一个 IPv6 socket 地址包含一个 IP 地址和一个端口号,它们之间的差别在于 IPv6 地址是 128 位而不是 32 位的。一个 IPv6 socket 地址会被存储在一个 sockaddr_in6结构中,该结构在<netinet/in.h>中进行定义,具体如下。
struct in6_addr // IPv6 address structure
{
union
{
uint8_t __u6_addr8[16]; // 16 bytes == 128bits
#if defined __USE_MISC || defined __USE_GNU
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
#endif
} __in6_u;
};
struct sockaddr_in6 /* IPv6 socket address */
{
sa_family sin_family; /* Address family(AF_INET) */
in_port_t sin6_port; /* Port number. */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
- sockaddr_in6 结构中的所有字段都是以网络字节序存储的
- sin_family 字段会被设置成 AF_INET6
- sin6_port 和 sin6_addr 字段分别是端口号和 IP地址。
IPv6 和 IPv4 一样也有通配和回环地址,但它们的用法要更加复杂一些,因为 IPv6 地址是存储在数组中的(并没有使用标量类型),下面将会使用 IPv6 通配地址(0::0)来说明这一点。系统定义了常量 IN6ADDR_ANY_INIT 来表示这个地址,具体如下
#define IN6ADDR_ANY_INIT { { { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 } } }
在变量声明的初始化器中可以使用IN6ADDR_ANY_INIT ,但无法在一个赋值语句中的右边使用这个常量。因为C语法不允许在赋值语句中使用一个结构化的常量。取而代之的做法是必须要使用一个预先定义的变量in6addr_any,C库会按照下面的方式对该变量进行初始化:
const struct in6_addr in6addr_any = IN6ADDR_ANY_INIT;
因此可以向如下这样使用通配地址来初始化一个IPv6 socket地址:
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(struct sockaddr_in6));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons(SOME_PORT_NUM);
IPv6 环回地址(::1)的对应常量和变量是 IN6ADDR_LOOPBACK_INIT 和 in6addr _loopback
如果 IPv4 和 IPv6 共存于一台主机上,那么它们将共享同一个端口号空间。这意味着如果一个应用程序将一个 IPv6 socket 绑定到了 TCP 端口 2000 上(使用 IPv6 通配地址),那么 IPv4 TCP socket 将无法绑定到同一个端口上。(TCP/IP 实现确保位于其他主机上的socket 能够与这个 socket 进行通信,不管那些主机运行的是 IPv4 还是 IPv6。
sockaddr_storage 结构(新的通用套接字)
在IPv6 socket API中新引入了一个通用的sockaddr_storage结构,这个结构的空间足以存储任意类型的socket地址(即可以将任意类型的socket地址结构强制转换并存储在这个结构中)。特别地,这个结构允许透明地存储 IPv4 或 IPv6 socket 地址,从而删除了代码中的 IP 版本依赖性。sockaddr_storage 结构在 Linux 上的定义如下所示。
struct sockaddr_storage
{
__SOCKADDR_COMMON (ss_); /* Address family, etc. */
char __ss_padding[_SS_PADSIZE];
__ss_aligntype __ss_align; /* Force desired alignment. */
};
sockaddr结构(老的通用套接字、很少用)
#include <sys/socket.h>
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
从应用开发的角度来看,这个结构的唯一用途就是对指向特定于协议的套接字地址结构指针指向类型转换
用于将主机名(如 www.kernel.org)和服务名(如 http)转换成对应的数字形式的函数。这些函数一般会返回用网络字节序表示的整数,并且可以直接将这些整数复制进一个 socket 地址结构的相关字段中。
主机和服务转换函数
概述
计算机以二进制形式来表示IP地址和端口号,但是名字比数字更容易记忆,而且可以在名字不变的情况下随意改变底层的数字值
主机名是连接在网络上的一个系统的符号标识符。服务名是端口号的符号标识。
主机地址和端口的标识有如下两种方法:
- 主机地址可以标识为一个二进制值或者一个符号主机名展现格式(IPv4 是点分十进制,IPv6 是十六进制字符串)
- 端口号可以表示为一个二进制值或一个符号服务名
在二进制和人类可读的形式之间转换IPv4的地址(已过时)
inet_aton、inet_addr、inet_ntoa
函数将一个 IPv4 地址在点分十进制表示形式(127.0.0.1)和32位的网络字节序二进制值之间转换表示形式之间进行转换,仅适用于IPv4地址。现在它们已经被废弃了。请避免在程序中使用它们
在二进制和人类可读的形式之间转换 IPv4 和 IPv6 地址
inet_pton()和 inet_ntop()函数将二进制 IPv4 和 IPv6 地址转换成展现格式—即以点分十进制表示或十六进制字符串表示,或将展现格式转换成二进制 IPv4 和 IPv6 地址。
由于人类对名字的处理能力要比对数字的处理能力强,因此通常偶尔才会在程序中使用这些函数。inet_ntop()的一个用途是产生IP地址的一个可打印的表示形式以便记录日志。在有些情况下,最好使用这个函数而不是将一个IP地址解析成主机名,其原因如下:
- 将一个IP地址解析成主机名可能需要向一台服务器发送一个耗时比较长的请求
- 在一些场景中,可能并不存在一个 DNS(PTR)记录将 IP 地址映射到对应的主机名上
主机和服务名与二进制形式之间的转换(已过时)
gethostbyname()函数返回与主机名对应的二进制 IP 地址,getservbyname()函数返回与服务名对应的端口号。对应的逆向转换是由 gethostbyaddr()和 getservbyport()来完成的。现在它们已经过时了,新代码应该使用 getaddrinfo()和getnameinfo()函数来完成此类转换
主机和服务名与二进制形式之间的转换(现代的)
getaddrinfo()函数是 gethostbyname()和 getservbyname()两个函数的现代继任者。给定一个主机名和一个服务名,getaddrinfo()会返回一组包含对应的二进制IP地址和端口号的结构。与gethostbyname()不同,getaddrinfo()会透明地处理 IPv4 和 IPv6 地址。因此使用这个函数可以编写不依赖于 IP 版本的程序。所有新代码都应该使用 getaddrinfo()来将主机名和服务名转换成二进制表示。
getnameinfo()函数执行逆向转换,即将一个 IP 地址和端口号转换成对应的主机名和服务名。
使用 getaddrinfo()和 getnameinfo()还可以在二进制 IP 地址与其展现格式之间进行转换。
准备
域名系统
在DNS出现之前,主机名和IP地址之间的映射关系是在一个手工维护的本地文件/ect/hosts中定义的,该文件包含了形如下面的记录。
gethostbyname()函数(被 getaddrinfo()取代的函数)通过搜索这个文件并找出与规范主机名(即主机的官方或主要名称)或其中一个别名(可选的,以空格分隔)匹配的记录来获取一个IP 地址。
然而,/etc/hosts 模式的扩展性交叉,并且随着网络中主机数量的增长(如因特网中存在着数以亿计的主机),这种方式已经变得不太可行了。
DNS被设计用来解决这个问题。
当一个程序调用 getaddrinfo()来解析(即获取 IP 地址)一个域名时,getaddrinfo()会使用一组库函数(resolver库)来与本地的DNS服务器通信。如果这个服务器无法提供所需的信息,那么它就会与位于层级中的其他DNS服务器进行通信以便获取信息。有时候,这个解析过程可能会花费很多时间,DNS服务器采用了缓存技术来避免在查询常见域名时所发送的不必要的通信
- DSN解析请求可以分为两类:递归和等待,在一个递归请求中,请求者要求服务器处理整个解析任务,包括在必要的时候与其他DNS服务器进行通信。当位于本地主机上的一个应用程序调用getaddrinfo()时,该函数会向本地DNS服务器发起一个递归请求。如果本地DNS服务器自己并没有相关信息来进行解析,那么它就会迭代地解析这个域名。
- 如果向 gethostbyname()传递了一个不完整的域名,那么解析器在解析之前会尝试补全。域名补全的规则是在/etc/resolv.conf 中定义的(参见 resolv.conf(5)手册)。在默认情况下,解析器至少会使用本机的域名来补全。例如,如果登录机器 oghma.otago.ac.nz 并输入了命令 ssh octavo,得到的 DNS 查询将会以 octavo.otago.ac.nz 作为其名字。
/etc/services 文件
众所周知的端口号是由 IANA 集中注册的,其中每个端口都有一个对应的服务名。由于服务器是集中管理并且不会像IP地址那样频繁编号,因此没有必要采用DNS服务来管理它们。相反,端口号的服务名会记录在/ect/serivces
中。getaddrinfo()和getnameinfo()函数会使用这个文件中的信息在服务名和端口号之间进行转换
inet_pton()和 inet_ntop()函数
inet_pton、inet_ntop
函数用于:在 IPv4 和 IPv6 地址的网络字节序二进制形式和点分十进制表示法或十六进制字符串表示法之间进行转换
/*
* 参数: family: AF_INET或者AF_INET6。否则函数返回一个错误,并将error置EAFNOSUPPORT
* strptr:点分十进制指针。如果指向的字符串不是有效的表达式函数返回一个0
* addrptr: 该指针指向的空间存放转换出来的二进制结果。
* 返回值:成功1,如果输入不是有效的表达式则0,出错返回-1
*/
int inet_pton (int family, const char *__restrict strptr,
void *__restrict addrptr)
/*
* 参数: family: AF_INET或者AF_INET6。否则函数返回一个错误,并将error置EAFNOSUPPORT
* strptr: 指向函数的结果。必须不为空(为目标存储单位分配内存并指定其大小)
* __len: 目标出错单元的大小(包括结尾的空字符),以免溢出。与之有关的宏定义
* #define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
如果__len太小,该函数返回一个控制在,并将error置ENOSPC
*
* 返回值:成功返回指向结果的指针,出错为null
*/
const char *inet_ntop (int family, const void *__restrict addrptr,
char *__restrict strptr, socklen_t __len)
p表示presentation
(展现)表达式[ASCI字符串],n
表示number
数值[二进制值]
展现形式是人类可读的字符串,如:
- 204.152.189.116(IPv4 点分十进制地址);
- ::1(IPv6 冒号分隔的十六进制地址);
- ::FFFF:204.152.189.116(IPv4 映射的 IPv6 地址)。
inet_pton()函数将 src_str 中包含的展现字符串转换成网络字节序的二进制 IP 地址。domain 参数应该被指定为 AF_INET 或 AF_INET6。转换得到的地址会被放在 addrptr 指向的结构中,它应该根据在 domain 参数中指定的值指向一个 in_addr 或 in6_addr 结构。
inet_ntop()函数执行逆向转换。同样,domain 应该被指定为 AF_INET 或 AF_INET6,addrptr 应该指向一个待转换的 in_addr 或 in6_addr 结构。得到的以 null 结尾的字符串会被放置在 dst_str 指向的缓冲器中。len 参数必须被指定为这个缓冲器的大小。inet_ntop()在成功时会返回 dst_str。如果 len 的值太小了,那么 inet_ntop()会返回 NULL 并将 errno设置成 ENOSPC。
要正确计算 dst_str 指向的缓冲器的大小可以使用在<netinet/in.h>中定义的两个常量。这些常量标识出了 IPv4 和 IPv6 地址的展现字符串的最大长度(包括结尾的 null 字节)。
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
使用示例:
char str_v4[INET_ADDRSTRLEN];
struct sockaddr_in addr;
inet_ntop(AF_INET, &addr.sin_addr, str_v4, sizeof(str_v4));
char str_v6[INET6_ADDRSTRLEN];
struct sockaddr_in6 addr6;
inet_ntop(AF_INET6, &addr6.sin6_addr, str_v6, sizeof(str_v6));
#include <stdlib.h>
#include <stdio.h>
#include <getopt.h>
#include <zconf.h>
#include <sys/socket.h>
#include <rpc/types.h>
#include <arpa/inet.h>
#include <memory.h>
#define DEST_IP "127.0.0.1"
int main(int argc, char *argv[])
{
int sockfd, result;
socklen_t len;
struct sockaddr_in servaddr;
struct sockaddr_in sa;
len = sizeof(sa);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8000);
inet_pton(AF_INET, DEST_IP, &servaddr.sin_addr);
result = bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr));
if(result < 0)
{
perror("bind");
close(sockfd);
exit(1);
}
bzero(&sa, sizeof(struct sockaddr_in));
if (getsockname(sockfd, (struct sockaddr *)&sa, &len) == -1) {
perror("getsockname error");
exit(0);
}
if(sa.sin_family == AF_INET){
char str_v4[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &sa.sin_addr, str_v4, sizeof(str_v4));
printf("s_addr = %u ==> %s, sin_port = %d ==> %hu \n",
sa.sin_addr.s_addr, str_v4, sa.sin_port, ntohs(sa.sin_port));
// s_addr = 16777343 ==> 127.0.0.1, sin_port = 16415 ==> 8000
}else{
char str_v6[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &sa.sin_addr, str_v6, sizeof(str_v6));
printf("s_addr = %u ==> %s, sin_port = %d ==> %hu \n",
sa.sin_addr.s_addr, str_v6, sa.sin_port, ntohs(sa.sin_port));
}
close(sockfd);
exit(0);
}
自己实现inet_pton、inet_ntop的IPv4
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <errno.h>
int Inet_pton(int family, const char*strptr, void *addrptr){
if(family == AF_INET){
struct in_addr inAddr;
if (inet_aton(strptr, &inAddr)){
memcpy(addrptr, &inAddr, sizeof(inAddr));
return 1;
}
return 0;
}
return -1;
}
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;
}
return NULL;
}
int main(int argc, char **argv){
uint32_t inAddr;
const char *p = "127.0.0.1";
Inet_pton(AF_INET, p, &inAddr);
printf("Inet_pton(%s) = (%u)\n", p, inAddr); // 16777343
char strptr[INET_ADDRSTRLEN];
const char * n = Inet_ntop(AF_INET, &inAddr, strptr, sizeof(strptr));
printf("Inet_ntop(%u) = (%s, %s)", inAddr, strptr, n); //127.0.0.1, 127.0.0.1
}
独立于协议的主机和服务转换
getaddrinfo()函数将主机和服务名转换成 IP 地址和端口号,它作为过时的 gethostbyname()和 getservbyname()函数的(可重入的)接替者被定义在了 POSIX.1g 中。
getnameinfo()函数是 getaddrinfo()的逆函数,它将一个 socket 地址结构(IPv4 或 IPv6)转换成包含对应主机和服务名的字符串。这个函数是过时的 gethostbyaddr()和 getservbyport()函数的(可重入的)等价物。
过时的主机和服务转换 API
inet_aton()和 inet_ntoa()函数
inet_aton()和 inet_ntoa()函数将一个 IPv4 地址在点分十进制标记法和二进制形式(以网络字节序)之间进行转换。这些函数现在已经被 inet_pton()和 inet_ntop()所取代了。
/*
* 功能:将__cp所指的C字符串转换成一个32位的网络字节序二进制值,并使用指针__inp来存储
* 如果__cp指针为空,该函数仍然对输入的字符串指向有效性检查,但是不存储任何结果
* 返回值:成功1,失败0
*/
int inet_aton (const char *__cp, struct in_addr *__inp)
/*
*功能:将32位的网络字节二进制转为点位十进制。
* 这个函数是不可重入的
*/
char * inet_ntoa (struct in_addr __in)
/*
* 这个函数已经被废弃
* 功能:将__cp所指的C字符串转换成一个32位的网络字节序二进制值,并使用指针__inp来存储
* 返回值:返回32位的网络字节序二进制值。失败则返回INADDR_NONE(是一个32位均为1的值)。但是Ipv4的广播地址位255.255.255.255.
*/
in_addr_t inet_addr (const char *__cp)
使用示例:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
int main(int aargc, char* argv[])
{
struct in_addr addr1,addr2;
const char *p1 = "192.168.0.74", *p2 = "211.100.21.179";
ulong l1,l2;
l1= inet_addr(p1), l2 = inet_addr(p2);
printf("inet_addr函数已经被废弃 inet_addr(%s) = %u , inet_addr(%s) = %u\n", p1, inet_addr(p1), p2, inet_addr(p2));
memcpy(&addr1, &l1, 4); memcpy(&addr2, &l2, 4);
printf("memcpy(%lu) = %u , memcpy(%lu) = %ld\n",l1, addr1.s_addr, l2, addr2);
printf("inet_ntoa(%u) = %s , inet_ntoa(%u) = %s\n", addr1.s_addr, inet_ntoa(addr1), addr2.s_addr, inet_ntoa(addr2));
return 0;
}
gethostbyname()和 gethostbyaddr()函数
gethostbyname()和 gethostbyaddr()函数允许在主机名和 IP 地址之间进行转换。现在这些函数已经被 getaddrinfo()和 getnameinfo()所取代了。
理论
#include <netdb.h>
struct hostent
{
char *h_name; /* 主机名 */
char **h_aliases; /*主机别名列表,可能有多个 */
int h_addrtype; /* 地址类型 */
int h_length; /* 地址长度 */
char **h_addr_list; /* 根据网络字节序列出的主机IP地址列表 */
#endif
};
/*
* 功能: 根据主机名获取主机的完整信息
* 参数: __name --- 目标主机的主机名
* 返回值:非空指针——成功,空指针——出错,同时设置h_errno
* 说明: gethostbyname 先到本地的/etc/hosts配置文件中查找主机,如果没有直到,再去访问DNS服务器
* 注意: 执行对A记录的查询。所以它只能返回IPv4的地址
*/
struct hostent *gethostbyname (const char *__name)
/*
* 功能: 根据IP地址获取主机的完整信息
* 参数: addr --- 目标主机的IP地址
* len -- 直到addr的长度
* family -- 指定IP地址的类型,包括AF_INET、AF_INET6
* 返回值:非空指针——成功,空指针——出错,同时设置h_errno
*/
struct hostent * gethostbyaddr(const char *addr, size_t len , int family);
1、返回值:
从该结构体可以看出,不只返回 IP 地址,还会附带其他信息,各位读者只需关注最后一个成员 * h_addr_list。下面是对各成员的说明:
- h_name:官方域名/主机名(Official domain name)。官方域名代表某一主页,但实际上一些著名公司的域名并未用官方域名注册。
- h_aliases:别名,可以通过多个域名访问同一主机。同一 IP 地址可以绑定多个域名,因此除了当前域名还可以指定其他域名。
- h_length:IP地址长度。IPv4 的长度为 4 个字节,IPv6 的长度为 16 个字节。
- h_addr_list【按照网络字节序给出的主机IP地址族】:这是最重要的成员。通过该成员以整数形式保存域名对应的 IP 地址。对于用户较多的服务器,可能会分配多个 IP 地址给同一域名,利用多个服务器进行均衡负载。
2、出错: 当发生错误时,它不设置errno变量,设置全局整数变量h_errno
错误h_errno
在发生错误时(如无法解析一个名字),gethostbyname()和 gethostbyaddr()都会返回一个 NULL指针并设置全局变量 h_errno。
正如其名字所表达的那样,这个变量与 errno 类似(gethostbyname(3)手册描述了这个变量的可取值),herror()和 hstrerror()函数类似于 perror()和 strerror()。herror()函数(在标准错误上)显示了在 str 中给出的字符串,后面跟着一个冒号(😃,然后再显示一条与当前位于 h_errno 中的错误对应的消息。或者可以使用 hstrerror()获取一个指向与在 err 中指定的错误值对应的字符串的指针。
void herror(const char *s);
const char *hstrerror(int err);
实践
gethostbyname
获取当前主机的名字
#include <stdio.h>
#include <zconf.h>
#define HOSTNAME_MAX 1024
int main(int argc, char **argv){
char buf[HOSTNAME_MAX + 1];
if(gethostname(buf, HOSTNAME_MAX) == -1){
printf("Cannot get machine hostname.");
}
printf("%s", buf);
return 0;
}
获取域名的主机名字
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>
int main(){
int i;
struct hostent *host;
host = gethostbyname("www.baidu.com");
if(!host){
printf("Get IP address error: %s", hstrerror(h_errno));
exit(0);
}
for(i=0; host->h_aliases[i]; i++){
printf("Aliases(别名) %d: %s\n", i+1, host->h_aliases[i]);
}
printf("Address type(地址类型): %s\n", (host->h_addrtype==AF_INET) ? "AF_INET": "AF_INET6");
for( i=0; host->h_addr_list[i]; i++){
printf("IP addr(IP地址) %d: %s\n", i+1, inet_ntoa( *(struct in_addr*)host->h_addr_list[i] ) );
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>
main(int argc, char **argv){
char *ptr, **pptr;
char str[INET_ADDRSTRLEN];
struct hostent *hptr;
while (--argc){
ptr = *++argv;
if( (hptr = gethostbyname(ptr)) == NULL){
printf("gethostbyname error for host: %s: %s",
ptr, hstrerror(h_errno));
continue;
}
printf("official hostname: %s\n", hptr->h_name);
for(pptr = hptr->h_aliases; *pptr != NULL; pptr++){
printf("\talias: %s\n", *pptr);
}
switch (hptr->h_addrtype) {
case AF_INET:
pptr = hptr->h_addr_list;
for(; *pptr != NULL; pptr++){
printf("\taddress:%s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
}
break;
default:
printf("unknown address type");
break;
}
}
return 0;
}
通过主机名获取主机的完整信息
gethostbyaddr
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
int main(int argc, char **argv)
{
struct in_addr addr;
struct hostent *phost;
if (inet_pton(AF_INET, argv[1], &addr) <= 0) {
printf("inet_pton error:%s\n", strerror(errno));
return -1;
}
phost = gethostbyaddr((const char*)&addr, sizeof(addr), AF_INET);
if (phost == NULL) {
printf("gethostbyaddr error:%s\n", strerror(h_errno));
return -1;
}
printf("host name:%s\n", phost->h_name);
return 0;
}
通过IP地址获取主机的完整信息:
解决:/etc/hosts下写上 115.239.211.110 www.baidu.com
getserverbyname()和 getserverbyport()函数
getservbyname()和 getservbyport()函数从/etc/services 文件中获取记录。现在这些函数已经被 getaddrinfo()和 getnameinfo()所取代了。
类似域名映射到点位十进制标记一台主机,我们也可以将一个别名映射到一个端口标记一个服务。这个名字到端口的映射通常保存在/etc/service中。(如果端口号改变,我们所需做的所有改动就是改动文件/etc/services中的一行,而不是重新编译应用程序)
理论
#include <netdb.h>
/*
* 功能:
* 参数:name: 一个指向服务名的指针。
* proto: 指向协议名的指针(可选)。如果这个指针为空,getservbyname()返回第一个name与s_name或者某一个s_aliases匹配的服务条目。否则getservbyname()对name和proto都进行匹配。
* 返回值:成功返回非空指针,失败返回空指针
**/
struct servent * getservbyname(const char * name, const char *proto);
/*
* 功能: 给定端口号和可选协议后查找相应的服务
* 参数:name: 一个指向服务名的端口。
* proto: 指向协议名的指针(可选)。如果这个指针为空,getservbyname()返回第一个name与s_name或者某一个s_aliases匹配的服务条目。否则getservbyname()对name和proto都进行匹配。
* 返回值:成功返回非空指针,失败返回空指针
**/
struct servent *getservbyport(int port, const char *protoname);
返回值:
struct servent
{
char *s_name; //正规的服务名
char **s_aliases; // 一个以空指针结尾的可选服务名队列
int s_port;// 连接该服务时需要用到的端口号,返回的端口号是以网络字节顺序排列的
char *s_proto;// 连接该服务时用到的协议名
};
典型调用:
struct servent *sptr;
sptr = getservbyname("domain", "udp"); // DNS using UDP
sptr = getservbyname("ftp", "tcp");//FTP using TCP
sptr = getservbyname("ftp", NULL); //FTP using TCP
sptr = getservbyname("ftp", "udp");// this call will fail
由于FTP仅支持TCP,所以第二个和第三个调用 是相同的,第四个调用将失败。
struct servent *sptr;
sptr = getservbyport(htons(53), "udp"); // DNS using UDP
sptr = getservbyport(htons(21), "tcp");//FTP using TCP
sptr = getservbyport(htons(21), NULL);//FTP using TCP
sptr = getservbyport(htons(21), "udp");// this call will fai
由于UDP中没有使用21端口,所以第4个会失败
Linux平台,从/etc/services文件中读取信息,一次读取name(如smtp),port(如25),proto(如tcp),alias(如mail,部分服务有,部分没有)。
$ grep -e ^ftp -e ^domain /etc/services
ftp 21/tcp
ftp 21/udp fsp fspd
domain 53/tcp # name-domain server
domain 53/udp
ftp 21/sctp # FTP
$ grep 514 /etc/services
shell 514/tcp cmd # no passwords used
syslog 514/udp
实践
getservbyname
#include "netdb.h"
#include "stdio.h"
int main()
{
struct servent *se = NULL;
int i = 0;
se = getservbyname("domain", "udp");
if (!se)
return -1;
printf("name : %s\n", se->s_name);
printf("port : %d\n", ntohs(se->s_port));
printf("proto : %s\n", se->s_proto);
for (i = 0; se->s_aliases[i]; i++)
printf("aliases : %s\n", se->s_aliases[i]);
return 0;
}
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd, n;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
struct in_addr **pptr;
struct in_addr *inetaddrp[2];
struct in_addr inetaddr;
struct hostent *hp;
struct servent *sp;
if (argc != 3)
err_quit("usage: daytimetcpcli1 <hostname> <service>");
if ( (hp = gethostbyname(argv[1])) == NULL) {
if (inet_aton(argv[1], &inetaddr) == 0) {
err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno));
} else {
inetaddrp[0] = &inetaddr;
inetaddrp[1] = NULL;
pptr = inetaddrp;
}
} else {
pptr = (struct in_addr **) hp->h_addr_list;
}
if ( (sp = getservbyname(argv[2], "tcp")) == NULL)
err_quit("getservbyname error for %s", argv[2]);
for ( ; *pptr != NULL; pptr++) {
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = sp->s_port;
memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr));
printf("trying %s\n",
Sock_ntop((SA *) &servaddr, sizeof(servaddr)));
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) == 0)
break; /* success */
err_ret("connect error");
close(sockfd);
}
if (*pptr == NULL)
err_quit("unable to connect");
while ( (n = Read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
exit(0);
}
值-结果参数
当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式传递,也就是说传递的是指向该结构的一个指针。该结构的长度也转为一个参数来传递,不过起传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程
(1) 从进程到内核传递套接字地址结构的函数有3个:bind、connect和sendto。这些函数的一个参数是指向某个套接字地址结构的指针。比如:
struct sockaddr_in servaddr;
connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)
既然指针和指针所指的内容的大小都传递给了内核,内核就知道需要从进程复制多少数据进来
(2) 从内核到进程传递套接字地址结构的函数有4个:accept、recvfrom、getsockname、getpeername。它们其中有两个参数:一个是指向某个套接字地址结构的指针、一个指向表示该结构大小的整形变量的指针
#include <sys/socket.h>
/*
* 功能: 获取sockfd对应的本地地址,并且存储于address参数指定的内存中,地址长度由addrlen指向的变量指定:
* * 如果实际长度 大于address指定的内存区大小,那么该地址就会被阶段
* * 如果实际长度 <= address指定的内存区大小,那么该地址能够正确获取
* 返回值:成功返回0,失败-1并设置error
*/
int getsockname (int socket, struct sockaddr *__restrict address,
socklen_t *__restrict addrlen)
/*
* 功能: 获取sockfd对应的远程地址,并且存储于address参数指定的内存中,地址长度由addrlen指向的变量指定:
* * 如果实际长度 大于address指定的内存区大小,那么该地址就会被阶段
* * 如果实际长度 <= address指定的内存区大小,那么该地址能够正确获取
* 返回值:成功返回0,失败-1并设置error
*/
int getpeername (int __fd, struct sockaddr *__restrict address,
socklen_t *__restrict addrlen)
为什么要传递结构体大小的指针呢?原因在于:
- 当函数被调用是,结构大小是一个值,它告诉内核该结构的大小,这样内核在写该结构的时候不至于越界;
- 当函数返回时,结构大小又是一个结果,它告诉内核在该结构中究竟存储了多少信息。这种类型的参数称为值-结构参数
总结:套接字地址结构是每个网络程序的重要组成部分,我们分配它们,填写它们,把指向它们的指针传递给各个套接字函数。有时我们把指向这些结构之一的指针传递给一个套接字函数,并由该函数填写结构内容。我们总是以引用形式来传递这些结构,而且把该结构的大小作为另外一个参数来传递。当一个套接字函数需要填写一个函数时,该结构的长度以引用形式传递,这样它的值也可以被函数更改,我们把这样的参数称为值-结构参数