
域名及网络地址
8.1 域名系统
什么是域名
网络中的服务器仍然通过地址区分,但相比于一串难以记忆的数字,人们更习惯使用容易记忆和表达的名称,因此出现了域名()。
域名并不是服务器真正的网络地址,而是分配给服务器的虚拟地址。程序最终仍要通过地址建立连接,所以访问服务器前还需要把域名转换为对应的地址。负责这个转换过程的系统就是(,域名系统)。
DNS服务器
每台联网计算机一般都会配置默认服务器。我们在浏览器中输入域名后,浏览器会先向默认服务器查询对应的地址,得到结果后再连接目标服务器。
一台服务器不可能保存互联网上的全部域名信息。当默认服务器无法解析某个域名时,它会向上级服务器继续询问;请求逐级到达根服务器后,再被引导到掌握目标域名信息的服务器。解析出的地址沿原路径返回,最终交给发起查询的主机。

因此,并不是一张集中保存在某台服务器上的表,而是一个具有层次结构的分布式数据库系统。域名信息由不同层级的服务器共同管理。
8.2 IP地址和域名之间的转换
程序中使用域名的必要性
如果直接把服务器地址写入客户端,一旦服务器更换,已经安装的程序也要跟着修改。相比之下,服务器的域名通常不会轻易改变。程序可以在每次运行时根据域名查询当前地址,再连接服务器,这样就不会依赖某个固定的。
利用域名获取IP地址
中可以通过查询域名对应的地址信息:
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);//成功时返回hostent结构体地址,失败时返回NULL。返回的结构体不仅保存地址,还包含官方域名、别名和地址族等信息:
struct hostent{ char *h_name; //官方域名 char **h_aliases; //域名别名列表 int h_addrtype; //地址族 int h_length; //地址长度 char **h_addr_list; //IP地址列表};各成员含义如下:
- 保存官方域名。
- 保存指向其他域名的指针列表,同一个服务器可以拥有多个域名。
- 保存地址族,一般为。
- 保存地址长度,为字节,为字节。
- 保存域名对应的地址列表。同一域名可能对应多个服务器,所以这里是一个列表。
虽然被声明为指针数组,但对于而言,各元素实际指向结构体。输出点分十进制地址时,需要先进行类型转换,再调用:
struct hostent *host = gethostbyname(argv[1]);
for (i = 0; host->h_addr_list[i]; i++){ printf("IP addr %d: %s\n", i + 1, inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));}使用指针主要是为了兼容不同长度的地址类型。早期定义这些函数时还没有广泛使用指针,因此采用了这种形式。
利用IP地址获取域名
与相反,可以根据地址查询域名信息:
#include <netdb.h>
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);//成功时返回hostent结构体地址,失败时返回NULL。指向需要查询的地址,是地址长度,表示地址族。查询地址时,可以这样调用:
struct sockaddr_in addr;struct hostent *host;
addr.sin_addr.s_addr = inet_addr(argv[1]);host = gethostbyaddr((char *)&addr.sin_addr, 4, AF_INET);该函数同样返回结构体,所以解析官方域名、别名和地址列表的方式与相同。
8.3 基于Windows的实现
同样提供和,函数名、参数和返回类型与版本基本一致:
#include <winsock2.h>
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const char *addr, int len, int type);主要区别仍是套接字程序需要先调用初始化,结束前调用。取得后,读取其中地址信息的逻辑无需改变。
套接字的多种可选项
9.1 套接字可选项和I/O缓冲大小
创建套接字后,即使不做其他设置也能正常通信,因为操作系统已经为它配置了默认特性。但在部分场景下,我们需要查看或调整这些特性,这些可以修改的属性称为套接字可选项。
套接字可选项的协议层
套接字可选项按照所属协议分层,常见的层级如下:
| 协议层 | 主要内容 | 常见选项 |
|---|---|---|
| SOL_SOCKET | 套接字通用属性 | SO_TYPE、SO_SNDBUF、SO_RCVBUF、SO_REUSEADDR |
| IPPROTO_IP | IP协议相关属性 | IP_TOS、IP_TTL、IP_MULTICAST_TTL |
| IPPROTO_TCP | TCP协议相关属性 | TCP_NODELAY、TCP_MAXSEG |
实际可选项很多,没有必要一次全部记住。重要的是理解选项所属的协议层,以及如何读取和设置。
getsockopt和setsockopt
读取套接字可选项使用:
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);//成功时返回0,失败时返回-1。是套接字文件描述符,表示选项所属协议层,是选项名,用于保存读取结果,传入缓冲大小并返回实际长度。
设置套接字可选项使用:
#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);//成功时返回0,失败时返回-1。前个参数含义相同,指向要设置的值,表示该值占用的字节数。
例如,可以通过读取套接字类型:
int sock_type;socklen_t optlen = sizeof(sock_type);
getsockopt(sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);是只读选项。套接字类型在调用时已经确定,之后无法通过把套接字改成套接字。
SO_SNDBUF和SO_RCVBUF
创建套接字时会同时生成输入、输出缓冲。用于读取或设置输入缓冲大小,用于读取或设置输出缓冲大小。
int snd_buf = 1024 * 3;int rcv_buf = 1024 * 3;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));这里设置的数值只是向操作系统提出请求。为了保证流控制和重传机制能够正常工作,系统可能对缓冲大小进行调整,所以之后通过读到的实际值不一定与请求值完全相同。
9.2 SO_REUSEADDR
地址分配错误与Time-wait状态
服务器端正常运行后,如果由服务器首先发送断开连接,再立刻使用相同端口重启,可能会返回地址分配错误。原因是套接字经过四次握手后不会立即消失,先发送的一方会进入一段时间的状态。

并非服务器端独有。无论服务器端还是客户端,只要首先发送,都可能进入该状态。客户端通常由系统动态分配端口,所以再次运行时可以换用其他端口;服务器端则需要监听固定端口,因此更容易受到影响。
Time-wait存在的原因
假设主动断开方发送的最后一个在网络中丢失,对方就会认为自己的没有到达,并再次发送。如果主动断开方已经立即销毁套接字,就无法重发最后的,对方也不能正常结束连接。
保留套接字一段时间,使其仍能接收重复的并重新发送。如果网络状况较差,对方再次发送,计时器还会重新启动。

这提高了连接关闭过程的可靠性,但也意味着服务器意外终止后,可能无法立即重新绑定原来的端口。
地址再分配
用于控制能否重新使用处于状态的本地地址。其默认值通常为,设置为后,可以让服务器更快地重新绑定原端口:
int option = 1;socklen_t optlen = sizeof(option);
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen);这个选项应在调用前设置。它并不会取消机制,而是允许新套接字在符合系统规则的情况下重新使用相同地址。
9.3 TCP_NODELAY
Nagle算法
默认使用算法减少小数据包的数量。它的基本规则可以简化为:收到前一批数据的后,再发送缓冲中的下一批数据。
假设程序先向输出缓冲写入字符,随后陆续写入。使用算法时,可以先发送,剩余字符在等待的过程中留在缓冲区,收到后再作为一批数据发送。若关闭该算法,每次到达输出缓冲的少量数据都可能立刻形成数据包。

数据包除了数据本身,还包含几十字节的首部。如果为了传输少量数据生成大量数据包,会增加网络流量和处理开销。因此,在不了解通信特性时不应该随意禁用算法。
何时禁用Nagle算法
算法并非任何时候都能提升效率。如果程序连续传输大块数据,即使关闭算法,数据通常也会填满输出缓冲后再发送,不会明显增加数据包数量;此时若等待反而可能降低传输速度。
所以只有在确认禁用后不会明显增加网络流量,同时等待会影响传输效率或实时性时,才应考虑关闭算法。
设置TCP_NODELAY
通过把设为可以禁用算法:
int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));也可以通过读取当前设置。为表示正在使用算法,为表示已经禁用。
9.4 基于Windows的实现
本章介绍的可选项与协议相关,在和中的含义基本一致。版本的函数如下:
#include <winsock2.h>
int getsockopt(SOCKET sock, int level, int optname, char *optval, int *optlen);
int setsockopt(SOCKET sock, int level, int optname, const char *optval, int optlen);//成功时返回0,失败时返回SOCKET_ERROR。与版本相比,主要区别是套接字使用类型,使用指针而非指针,移植时需要进行相应类型转换。、、和的作用及设置思路均保持不变。
