2448 字
6 分钟
TCP&IP网络编程学习札记_5
2026-07-29
浏览量 443 · 访客 7

壁纸链接

域名及网络地址#

8.1 域名系统#

什么是域名#

网络中的服务器仍然通过IPIP地址区分,但相比于一串难以记忆的数字,人们更习惯使用容易记忆和表达的名称,因此出现了域名Domain NameDomain\ Name)。

域名并不是服务器真正的网络地址,而是分配给服务器的虚拟地址。程序最终仍要通过IPIP地址建立连接,所以访问服务器前还需要把域名转换为对应的IPIP地址。负责这个转换过程的系统就是DNSDNSDomain Name SystemDomain\ Name\ System,域名系统)。

DNS服务器#

每台联网计算机一般都会配置默认DNSDNS服务器。我们在浏览器中输入域名后,浏览器会先向默认DNSDNS服务器查询对应的IPIP地址,得到结果后再连接目标服务器。

一台DNSDNS服务器不可能保存互联网上的全部域名信息。当默认DNSDNS服务器无法解析某个域名时,它会向上级DNSDNS服务器继续询问;请求逐级到达根DNSDNS服务器后,再被引导到掌握目标域名信息的服务器。解析出的IPIP地址沿原路径返回,最终交给发起查询的主机。

替换文本

因此,DNSDNS并不是一张集中保存在某台服务器上的表,而是一个具有层次结构的分布式数据库系统。域名信息由不同层级的DNSDNS服务器共同管理。

8.2 IP地址和域名之间的转换#

程序中使用域名的必要性#

如果直接把服务器IPIP地址写入客户端,一旦服务器更换IPIP,已经安装的程序也要跟着修改。相比之下,服务器的域名通常不会轻易改变。程序可以在每次运行时根据域名查询当前IPIP地址,再连接服务器,这样就不会依赖某个固定的IPIP

利用域名获取IP地址#

LinuxLinux中可以通过gethostbynamegethostbyname查询域名对应的地址信息:

#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
//成功时返回hostent结构体地址,失败时返回NULL。

返回的hostenthostent结构体不仅保存IPIP地址,还包含官方域名、别名和地址族等信息:

struct hostent
{
char *h_name; //官方域名
char **h_aliases; //域名别名列表
int h_addrtype; //地址族
int h_length; //地址长度
char **h_addr_list; //IP地址列表
};

各成员含义如下:

  • h_nameh\_name保存官方域名。
  • h_aliasesh\_aliases保存指向其他域名的指针列表,同一个服务器可以拥有多个域名。
  • h_addrtypeh\_addrtype保存地址族,IPv4IPv4一般为AF_INETAF\_INET
  • h_lengthh\_length保存地址长度,IPv4IPv444字节,IPv6IPv61616字节。
  • h_addr_listh\_addr\_list保存域名对应的地址列表。同一域名可能对应多个服务器IPIP,所以这里是一个列表。

h_addr_listh\_addr\_list虽然被声明为charchar指针数组,但对于IPv4IPv4而言,各元素实际指向in_addrin\_addr结构体。输出点分十进制地址时,需要先进行类型转换,再调用inet_ntoainet\_ntoa

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]));
}

使用charchar指针主要是为了兼容不同长度的地址类型。早期定义这些函数时还没有广泛使用voidvoid指针,因此采用了这种形式。

利用IP地址获取域名#

gethostbynamegethostbyname相反,gethostbyaddrgethostbyaddr可以根据IPIP地址查询域名信息:

#include <netdb.h>
struct hostent *gethostbyaddr(const char *addr,
socklen_t len, int family);
//成功时返回hostent结构体地址,失败时返回NULL。

addraddr指向需要查询的地址,lenlen是地址长度,familyfamily表示地址族。查询IPv4IPv4地址时,可以这样调用:

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);

该函数同样返回hostenthostent结构体,所以解析官方域名、别名和地址列表的方式与gethostbynamegethostbyname相同。

8.3 基于Windows的实现#

WindowsWindows同样提供gethostbynamegethostbynamegethostbyaddrgethostbyaddr,函数名、参数和返回类型与LinuxLinux版本基本一致:

#include <winsock2.h>
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const char *addr,
int len, int type);

主要区别仍是WindowsWindows套接字程序需要先调用WSAStartupWSAStartup初始化WinsockWinsock,结束前调用WSACleanupWSACleanup。取得hostenthostent后,读取其中地址信息的逻辑无需改变。

套接字的多种可选项#

9.1 套接字可选项和I/O缓冲大小#

创建套接字后,即使不做其他设置也能正常通信,因为操作系统已经为它配置了默认特性。但在部分场景下,我们需要查看或调整这些特性,这些可以修改的属性称为套接字可选项

套接字可选项的协议层#

套接字可选项按照所属协议分层,常见的层级如下:

协议层主要内容常见选项
SOL_SOCKET套接字通用属性SO_TYPE、SO_SNDBUF、SO_RCVBUF、SO_REUSEADDR
IPPROTO_IPIP协议相关属性IP_TOS、IP_TTL、IP_MULTICAST_TTL
IPPROTO_TCPTCP协议相关属性TCP_NODELAY、TCP_MAXSEG

实际可选项很多,没有必要一次全部记住。重要的是理解选项所属的协议层,以及如何读取和设置。

getsockopt和setsockopt#

读取套接字可选项使用getsockoptgetsockopt

#include <sys/socket.h>
int getsockopt(int sock, int level, int optname,
void *optval, socklen_t *optlen);
//成功时返回0,失败时返回-1。

socksock是套接字文件描述符,levellevel表示选项所属协议层,optnameoptname是选项名,optvaloptval用于保存读取结果,optlenoptlen传入缓冲大小并返回实际长度。

设置套接字可选项使用setsockoptsetsockopt

#include <sys/socket.h>
int setsockopt(int sock, int level, int optname,
const void *optval, socklen_t optlen);
//成功时返回0,失败时返回-1。

33个参数含义相同,optvaloptval指向要设置的值,optlenoptlen表示该值占用的字节数。

例如,可以通过SO_TYPESO\_TYPE读取套接字类型:

int sock_type;
socklen_t optlen = sizeof(sock_type);
getsockopt(sock, SOL_SOCKET, SO_TYPE,
(void *)&sock_type, &optlen);

SO_TYPESO\_TYPE是只读选项。套接字类型在调用socketsocket时已经确定,之后无法通过setsockoptsetsockoptTCPTCP套接字改成UDPUDP套接字。

SO_SNDBUF和SO_RCVBUF#

创建套接字时会同时生成输入、输出缓冲。SO_RCVBUFSO\_RCVBUF用于读取或设置输入缓冲大小,SO_SNDBUFSO\_SNDBUF用于读取或设置输出缓冲大小。

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));

这里设置的数值只是向操作系统提出请求。为了保证TCPTCP流控制和重传机制能够正常工作,系统可能对缓冲大小进行调整,所以之后通过getsockoptgetsockopt读到的实际值不一定与请求值完全相同。

9.2 SO_REUSEADDR#

地址分配错误与Time-wait状态#

服务器端正常运行后,如果由服务器首先发送FINFIN断开连接,再立刻使用相同端口重启,bindbind可能会返回地址分配错误。原因是套接字经过四次握手后不会立即消失,先发送FINFIN的一方会进入一段时间的TimewaitTime-wait状态。

替换文本

TimewaitTime-wait并非服务器端独有。无论服务器端还是客户端,只要首先发送FINFIN,都可能进入该状态。客户端通常由系统动态分配端口,所以再次运行时可以换用其他端口;服务器端则需要监听固定端口,因此更容易受到影响。

Time-wait存在的原因#

假设主动断开方发送的最后一个ACKACK在网络中丢失,对方就会认为自己的FINFIN没有到达,并再次发送FINFIN。如果主动断开方已经立即销毁套接字,就无法重发最后的ACKACK,对方也不能正常结束连接。

TimewaitTime-wait保留套接字一段时间,使其仍能接收重复的FINFIN并重新发送ACKACK。如果网络状况较差,对方再次发送FINFINTimewaitTime-wait计时器还会重新启动。

替换文本

这提高了连接关闭过程的可靠性,但也意味着服务器意外终止后,可能无法立即重新绑定原来的端口。

地址再分配#

SO_REUSEADDRSO\_REUSEADDR用于控制能否重新使用处于TimewaitTime-wait状态的本地地址。其默认值通常为00,设置为11后,可以让服务器更快地重新绑定原端口:

int option = 1;
socklen_t optlen = sizeof(option);
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR,
(void *)&option, optlen);

这个选项应在调用bindbind前设置。它并不会取消TimewaitTime-wait机制,而是允许新套接字在符合系统规则的情况下重新使用相同地址。

9.3 TCP_NODELAY#

Nagle算法#

TCPTCP默认使用NagleNagle算法减少小数据包的数量。它的基本规则可以简化为:收到前一批数据的ACKACK后,再发送缓冲中的下一批数据。

假设程序先向输出缓冲写入字符NN,随后陆续写入agleagle。使用NagleNagle算法时,NN可以先发送,剩余字符在等待ACKACK的过程中留在缓冲区,收到ACKACK后再作为一批数据发送。若关闭该算法,每次到达输出缓冲的少量数据都可能立刻形成数据包。

替换文本

数据包除了数据本身,还包含几十字节的首部。如果为了传输少量数据生成大量数据包,会增加网络流量和处理开销。因此,在不了解通信特性时不应该随意禁用NagleNagle算法。

何时禁用Nagle算法#

NagleNagle算法并非任何时候都能提升效率。如果程序连续传输大块数据,即使关闭算法,数据通常也会填满输出缓冲后再发送,不会明显增加数据包数量;此时若等待ACKACK反而可能降低传输速度。

所以只有在确认禁用后不会明显增加网络流量,同时等待ACKACK会影响传输效率或实时性时,才应考虑关闭NagleNagle算法。

设置TCP_NODELAY#

通过把TCP_NODELAYTCP\_NODELAY设为11可以禁用NagleNagle算法:

int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY,
(void *)&opt_val, sizeof(opt_val));

也可以通过getsockoptgetsockopt读取当前设置。TCP_NODELAYTCP\_NODELAY00表示正在使用NagleNagle算法,为11表示已经禁用。

9.4 基于Windows的实现#

本章介绍的可选项与TCP/IPTCP/IP协议相关,在WindowsWindowsLinuxLinux中的含义基本一致。WindowsWindows版本的函数如下:

#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。

LinuxLinux版本相比,主要区别是套接字使用SOCKETSOCKET类型,optvaloptval使用charchar指针而非voidvoid指针,移植时需要进行相应类型转换。SO_SNDBUFSO\_SNDBUFSO_RCVBUFSO\_RCVBUFSO_REUSEADDRSO\_REUSEADDRTCP_NODELAYTCP\_NODELAY的作用及设置思路均保持不变。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

TCP&IP网络编程学习札记_5
https://mkrari.cn/posts/tcp_ip_learn_5/
作者
Mkrari
发布于
2026-07-29
许可协议
CC BY-NC-SA 4.0