《UNIX 网络编程-卷1》阅读笔记08: 套接字选项
作者: andylin02
学习章节: 第七章 套接字选项
关键词: 套接字选项, getsockopt, setsockopt, SO_REUSEADDR, SO_KEEPALIVE, SO_LINGER, SO_RCVBUF, SO_SNDBUF, fcntl, TIME_WAIT, 非阻塞I/O
一、章节概述
1.1 本章焦点
第七章在前六章的基础上,深入探讨套接字编程中的一个重要课题——套接字选项(Socket Options)。前几章学习了套接字的基本API和I/O复用,但这些都是在默认行为下使用套接字。而实际应用中,往往需要调整套接字的行为来满足特定需求。
套接字选项是用于控制和获取套接字行为的一系列参数,涵盖了从通用套接字层到协议特定层的各种属性。本章的核心是介绍如何获取和设置这些选项,以及每个重要选项的用途和注意事项。
💡 为什么需要套接字选项?
- 解决 TIME_WAIT 导致的端口占用问题(SO_REUSEADDR)
- 检测对端主机崩溃(SO_KEEPALIVE)
- 控制 close 行为(SO_LINGER)
- 调整缓冲区大小优化性能(SO_RCVBUF/SO_SNDBUF)
- 实现非阻塞 I/O(fcntl 设置 O_NONBLOCK)
- 获取套接字状态信息(getsockname、getpeername)
1.2 本章内容结构
| 节号 | 标题 | 核心内容 |
|---|---|---|
| 7.1 | 概述 | 套接字选项的重要性,选项的分类(二元开关 vs 取值选项) |
| 7.2 | getsockopt 和 setsockopt 函数 | 核心API的详细用法,level和optname参数 |
| 7.3 | 检查选项是否支持并获取缺省值 | 使用getsockopt获取默认值 |
| 7.4 | SO_REUSEADDR 和 SO_REUSEPORT | 端口重用,解决TIME_WAIT问题,多实例绑定 |
| 7.5 | SO_KEEPALIVE | TCP保活机制,检测主机崩溃 |
| 7.6 | SO_LINGER | 控制close行为,延迟关闭与立即RST |
| 7.7 | SO_RCVBUF 和 SO_SNDBUF | 发送/接收缓冲区大小,内核调整机制 |
| 7.8 | fcntl 函数 | 设置非阻塞I/O,信号驱动I/O,套接字属主 |
| 7.9 | getsockname 和 getpeername | 获取本地和对端地址信息 |
💡 本章核心价值:读完第七章,你将能够——
- 熟练使用
getsockopt和setsockopt获取和设置套接字选项- 使用
SO_REUSEADDR解决服务器重启时的端口占用问题- 理解
SO_KEEPALIVE的工作原理,知道如何检测对端主机崩溃- 掌握
SO_LINGER三种行为的区别- 调整缓冲区大小以优化网络性能
- 使用
fcntl将套接字设置为非阻塞模式
二、核心API详解
2.1 getsockopt 和 setsockopt 函数
获取和设置套接字选项主要通过这两个函数实现,它们是本章最重要的API。
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
// 返回值:成功返回0,出错返回-1
参数详解:
| 参数 | 说明 |
|---|---|
sockfd |
已打开的套接字描述符 |
level |
选项所在的协议层,指定解释选项的代码 |
optname |
选项名称,指定要操作的具体选项 |
optval |
getsockopt指向返回值的缓冲区,setsockopt指向新值的缓冲区 |
optlen |
getsockopt为值-结果参数,setsockopt直接传递大小 |
level 参数的取值:
| level 值 | 含义 | 示例选项 |
|---|---|---|
SOL_SOCKET |
通用套接字层选项(与协议无关) | SO_REUSEADDR、SO_KEEPALIVE、SO_LINGER |
IPPROTO_IP |
IPv4 协议层选项 | IP_TTL、IP_MULTICAST_IF |
IPPROTO_IPV6 |
IPv6 协议层选项 | IPV6_V6ONLY |
IPPROTO_TCP |
TCP 协议层选项 | TCP_NODELAY、TCP_KEEPIDLE |
IPPROTO_UDP |
UDP 协议层选项 | — |
IPPROTO_SCTP |
SCTP 协议层选项 | — |
💡 关于 SOL_SOCKET:当 level 参数设为
SOL_SOCKET时,表示操作的是通用套接字层选项。这些选项与底层协议无关,适用于所有类型的套接字。
2.2 getsockname 和 getpeername 函数
这两个函数用于获取套接字的本地协议地址和对端协议地址。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
// 返回值:成功返回0,出错返回-1
典型应用场景:
| 场景 | 使用的函数 |
|---|---|
| TCP 客户端调用 connect 后想知道内核分配的临时端口 | getsockname |
| TCP 服务器 accept 后想获取客户端的 IP 和端口 | getpeername |
| 服务器执行 exec 后需要知道客户身份 | getpeername |
| 获取已连接套接字的本地地址 | getsockname |
2.3 fcntl 函数——设置套接字属性的 POSIX 方法
fcntl 函数是 POSIX 标准中用于设置文件描述符属性的方法,在套接字编程中主要用于设置非阻塞 I/O、信号驱动 I/O 和套接字属主。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);
fcntl 在套接字编程中的主要用途:
| cmd 值 | 用途 | 说明 |
|---|---|---|
F_SETFL |
设置文件状态标志 | 设置 O_NONBLOCK、O_ASYNC 等标志 |
F_GETFL |
获取文件状态标志 | 获取当前标志用于修改 |
F_SETOWN |
设置套接字属主 | 指定接收 SIGIO 和 SIGURG 信号的进程/进程组 |
F_GETOWN |
获取套接字属主 | 获取当前属主的进程ID |
设置非阻塞 I/O 的典型代码:
int flags;
if ((flags = fcntl(sockfd, F_GETFL, 0)) < 0)
err_sys("F_GETFL error");
flags |= O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) < 0)
err_sys("F_SETFL error");
💡 关键理解:
fcntl设置的标志作用于文件描述符本身,而setsockopt设置的选项作用于套接字本身。当使用dup或fork后,多个描述符可能指向同一个套接字,文件描述符标志不会共享,但套接字选项是共享的。
三、重要套接字选项详解
3.1 SO_REUSEADDR——解决 TIME_WAIT 导致的端口重用问题
为什么需要 SO_REUSEADDR?
在第二章中我们学习了 TIME_WAIT 状态,它是 TCP 主动关闭方在发送最后一个 ACK 后需要等待 2MSL 的状态。这导致了一个常见问题:当服务器程序关闭后立即重启,bind 操作会失败,返回 “address already in use” 错误,因为端口仍处于 TIME_WAIT 状态。对于需要快速重启的服务,这是一个严重影响用户体验的问题。
SO_REUSEADDR 套接字选项正是为解决这个问题而设计的。
SO_REUSEADDR 的核心作用
SO_REUSEADDR 选项通知内核,即使端口繁忙(处于 TIME_WAIT 状态),也允许重用该端口。这个选项使得服务器程序在停止后能够立即重启,无需等待 TIME_WAIT 超时。
int opt = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
err_sys("setsockopt error");
⚠️ 注意:
SO_REUSEADDR仅允许重用处于 TIME_WAIT 状态的端口。如果端口被其他套接字以其他状态占用(如 ESTABLISHED 状态),bind 仍然会失败。
SO_REUSEADDR 的工作原理
SO_REUSEADDR 主要影响 TCP 服务器的监听套接字在 bind 时的检查机制。它允许新绑定的套接字与现有处于 TIME_WAIT 状态的套接字使用相同的本地地址和端口。
这意味着在开发需要频繁启动和停止的服务时,SO_REUSEADDR 可以有效避免因 TIME_WAIT 状态导致的端口占用问题。
使用示例
#include "unp.h"
int main(int argc, char **argv)
{
int listenfd;
int opt = 1;
struct sockaddr_in servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
// 关键:设置 SO_REUSEADDR 选项
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
err_sys("setsockopt error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
// ... 服务器主循环
}
💡 实际应用建议:对于绝大多数 TCP 服务器程序,建议在调用
bind之前设置SO_REUSEADDR选项,这可以避免因 TIME_WAIT 状态导致的重启延迟。但需要注意的是,这不能完全替代对 TIME_WAIT 状态的理解,TIME_WAIT 的存在仍然有其合理的原因。
3.2 SO_KEEPALIVE——检测对端主机崩溃
在第五章我们讨论了服务器主机崩溃的场景:如果服务器主机崩溃,客户端在向它发送数据之前无法检测到这一事件,且 TCP 的重传会持续约 9 分钟才会放弃,这严重影响了应用的实时响应能力。SO_KEEPALIVE 选项正是用来解决这个问题的。
SO_KEEPALIVE 的工作原理
当为一个 TCP 套接字设置 SO_KEEPALIVE 选项后,如果在 2 小时(这是系统默认值,实际取决于实现)内在两个方向上都没有数据交换,TCP 会自动向对端发送一个保活探测分节(keep-alive probe)。
对端的响应有以下三种可能:
| 响应情况 | 内核反应 | 对应用层的影响 |
|---|---|---|
| 对端正常响应 ACK | 一切正常,不通知应用程序 | 对应用程序透明 |
| 对端响应 RST | 对端已崩溃后重启,内核关闭套接字 | 后续读写操作返回 ECONNRESET 错误 |
| 连续多次无响应(默认约 9 分钟) | 对端不可达,内核关闭套接字 | 后续读写操作返回 ETIMEDOUT 错误 |
保活参数的调整
默认的 2 小时保活时间对于大多数应用程序来说太长了。现代系统允许通过 TCP 协议层选项调整这些参数:
| 选项名 | 含义 | 说明 |
|---|---|---|
TCP_KEEPIDLE |
空闲时间 | 连接空闲多久后开始发送保活探测 |
TCP_KEEPINTVL |
探测间隔 | 每次探测之间的时间间隔 |
TCP_KEEPCNT |
最大探测次数 | 发送多少次探测无响应后认定连接死亡 |
#include <netinet/tcp.h>
int keepidle = 60; // 60秒空闲后开始探测
int keepintvl = 10; // 每10秒发送一次探测
int keepcnt = 3; // 最多3次探测无响应后关闭
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
⚠️ SO_KEEPALIVE 的局限性:
- SO_KEEPALIVE 是在操作系统网络协议栈中实现的,不会发送任何“真实”应用层数据
- 它只能检测网络连接的活性,无法检测应用层是否正常
- 默认的 2 小时空闲时间对多数应用来说太长,需要调整
- 它检测不到某些断线情况(如机器断电、网线拔出等)
💡 更好的替代方案:对于对实时性要求较高的应用,更推荐在应用层实现心跳机制,这样可以更好地控制检测频率和超时处理,同时还能检测应用层是否正常工作。
使用示例
int keepalive = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)) < 0)
err_sys("setsockopt SO_KEEPALIVE error");
3.3 SO_LINGER——控制 close 的行为
SO_LINGER 选项控制 close 函数关闭 TCP 连接时的行为,特别是在套接字发送缓冲区中还有未发送数据时。
linger 结构体
#include <sys/socket.h>
struct linger {
int l_onoff; /* 是否启用 linger 选项(0:关闭,非0:开启)*/
int l_linger; /* 延迟时间(秒)*/
};
三种 close 行为模式
| 模式 | l_onoff | l_linger | close 行为 |
|---|---|---|---|
| 默认模式 | 0 | 忽略 | 立即返回,内核在后台尝试发送未发送数据 |
| 延迟关闭模式 | 非0 | >0 | 阻塞等待,直到数据发送完成或超时 |
| 立即关闭模式 | 非0 | 0 | 立即发送 RST,丢弃未发送数据 |
模式一:默认模式(l_onoff = 0)
这是系统默认行为,close 立即返回,但内核会继续在后台尝试发送套接字发送缓冲区中残留的数据,然后正常完成 TCP 连接终止序列(发送 FIN)。
模式二:延迟关闭模式(l_onoff ≠ 0, l_linger > 0)
在这种模式下,close 调用会阻塞,直到:
- 所有未发送的数据都已发送并被对方确认,或
l_linger秒超时。
如果在超时时间内完成了数据发送和确认,close 成功返回;如果超时发生,close 返回 EWOULDBLOCK 错误,且套接字发送缓冲区中的任何残留数据都被丢弃。
模式三:立即关闭模式(l_onoff ≠ 0, l_linger = 0)
这是最激进的行为,close 立即返回,但不会发送 FIN,而是发送 RST 报文给对方。这会导致 TCP 连接直接跳过四次挥手,是一种硬关闭。这种方式会丢弃所有未发送的数据,并且对方会收到 ECONNRESET 错误。
struct linger ling;
// 延迟关闭模式:等待10秒
ling.l_onoff = 1;
ling.l_linger = 10;
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
// 立即关闭模式:发送RST
ling.l_onoff = 1;
ling.l_linger = 0;
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
⚠️ 使用建议:
- 立即关闭模式(l_linger = 0)会导致数据丢失,使用需谨慎
- 延迟关闭模式可能导致应用进程阻塞,在事件驱动模型中需要特别注意
- 大多数应用使用默认模式即可
3.4 SO_RCVBUF 和 SO_SNDBUF——调整缓冲区大小
套接字的发送缓冲区和接收缓冲区大小直接影响网络性能。通过 SO_SNDBUF 和 SO_RCVBUF 选项可以获取和设置这些缓冲区的大小。
为什么需要调整缓冲区大小?
- 高带宽场景:需要更大的缓冲区来避免发送方等待确认
- 高延迟网络:带宽延迟积(BDP)较大时需要更大的缓冲区
- BDP(Bandwidth-Delay Product)计算公式:
缓冲区大小 = 带宽 × RTT
缓冲区大小的设置
int sndbuf = 65536; // 64KB
int rcvbuf = 65536;
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)) < 0)
err_sys("setsockopt SO_SNDBUF error");
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) < 0)
err_sys("setsockopt SO_RCVBUF error");
内核的调整机制
默认情况下,Linux 内核会对应用程序设置的缓冲区大小进行调整(通常翻倍),这是为了保证 TCP 协议的稳定性,避免因缓冲区过小导致频繁丢包或重传。
这意味着当你设置 64KB 时,通过 getsockopt 读取到的实际值可能是 128KB。这种行为是正常的,不需要担心。
int sndbuf = 65536;
socklen_t len = sizeof(sndbuf);
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, len);
getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &len);
printf("Actual SO_SNDBUF: %d\n", sndbuf); // 可能显示 131072
💡 不同系统的默认值:
- Linux:接收缓冲区默认约 128KB,发送缓冲区默认约 16KB
- Windows:接收和发送缓冲区默认均为 8KB
- 可以通过系统参数调整全局默认值
⚠️ 注意:设置缓冲区大小后,建议使用
getsockopt验证实际生效的大小,因为内核可能会对设置值进行调整。
3.5 SO_RCVLOWAT 和 SO_SNDLOWAT——I/O 就绪的低水位标记
SO_RCVLOWAT 和 SO_SNDLOWAT 选项用于设置套接字在 I/O 复用(select/poll)中被视为“就绪”所需的最小数据量。
| 选项 | 用途 | 默认值 |
|---|---|---|
SO_RCVLOWAT |
读低水位标记:接收缓冲区中至少有多少字节时 select 才认为可读 | 1 |
SO_SNDLOWAT |
写低水位标记:发送缓冲区中至少有这么多空闲空间时 select 才认为可写 | 2048 |
💡 理解低水位标记:低水位标记决定了
select或poll何时报告描述符就绪。当接收缓冲区中的数据量达到或超过SO_RCVLOWAT时,select会将套接字标记为可读。
3.6 TCP_NODELAY——禁用 Nagle 算法
Nagle 算法是 TCP 的一个优化机制,用于减少网络中微小数据包(tinygram)的数量,通过将多个小数据包合并为一个大包发送来提高网络效率。然而在某些延迟敏感的实时应用场景中,Nagle 算法可能会带来不必要的延迟。
TCP_NODELAY 选项用于禁用 Nagle 算法。
int on = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
Nagle 算法的工作原理:
- 当应用层数据量小于 MSS(最大报文段长度)时,TCP 会等待
- 等待已发送数据的 ACK 到达,或等待更多数据积累到 MSS
- 这可以有效减少网络中的微小数据包数量
禁用 Nagle 算法的场景:
- 交互式应用(如 Telnet、SSH):每敲一个键就发送一个数据包
- 实时游戏:每个用户操作需要立即发送
- 对延迟敏感的金融交易系统
⚠️ 注意:禁用 Nagle 算法会增加网络中的微小数据包数量,可能降低网络效率。只有在确实需要最小化延迟时才应使用。
四、完整源代码示例
4.1 检查套接字选项支持的通用程序
#include "unp.h"
#include <netinet/tcp.h>
// 选项信息结构体
struct opt_info {
int level; /* 选项所在层级 */
const char *level_str; /* 层级的字符串描述 */
int optname; /* 选项名 */
const char *optname_str;/* 选项名的字符串描述 */
const char *desc; /* 选项描述 */
};
// 定义需要检查的选项列表
struct opt_info options[] = {
{ SOL_SOCKET, "SOL_SOCKET", SO_REUSEADDR, "SO_REUSEADDR", "允许重用本地地址" },
{ SOL_SOCKET, "SOL_SOCKET", SO_KEEPALIVE, "SO_KEEPALIVE", "启用保活探测" },
{ SOL_SOCKET, "SOL_SOCKET", SO_LINGER, "SO_LINGER", "延迟关闭" },
{ SOL_SOCKET, "SOL_SOCKET", SO_RCVBUF, "SO_RCVBUF", "接收缓冲区大小" },
{ SOL_SOCKET, "SOL_SOCKET", SO_SNDBUF, "SO_SNDBUF", "发送缓冲区大小" },
{ IPPROTO_TCP, "IPPROTO_TCP", TCP_NODELAY, "TCP_NODELAY", "禁用Nagle算法" },
{ IPPROTO_TCP, "IPPROTO_TCP", TCP_KEEPIDLE, "TCP_KEEPIDLE", "保活空闲时间" },
{ IPPROTO_TCP, "IPPROTO_TCP", TCP_KEEPINTVL, "TCP_KEEPINTVL", "保活探测间隔" },
{ IPPROTO_TCP, "IPPROTO_TCP", TCP_KEEPCNT, "TCP_KEEPCNT", "保活探测次数" },
};
int main(int argc, char **argv)
{
int sockfd;
int i, optval;
socklen_t optlen;
struct linger ling;
if (argc != 2)
err_quit("usage: checkopts <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
// 检查各个选项
for (i = 0; i < sizeof(options) / sizeof(options[0]); i++) {
optlen = sizeof(optval);
if (getsockopt(sockfd, options[i].level, options[i].optname,
&optval, &optlen) < 0) {
printf("%s: 不支持(getsockopt失败)\n", options[i].optname_str);
continue;
}
printf("%s: 支持, 当前值 = %d\n", options[i].optname_str, optval);
}
// 特别处理 SO_LINGER(需要 linger 结构体)
optlen = sizeof(ling);
if (getsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, &optlen) < 0)
err_sys("getsockopt SO_LINGER error");
printf("SO_LINGER: l_onoff=%d, l_linger=%d\n", ling.l_onoff, ling.l_linger);
close(sockfd);
return 0;
}
4.2 设置多个套接字选项的服务器框架
#include "unp.h"
#include <netinet/tcp.h>
void set_socket_options(int sockfd)
{
int optval;
struct linger ling;
/* 1. SO_REUSEADDR - 允许端口重用 */
optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
err_sys("setsockopt SO_REUSEADDR error");
/* 2. SO_KEEPALIVE - 启用保活探测 */
optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) < 0)
err_sys("setsockopt SO_KEEPALIVE error");
/* 3. TCP_NODELAY - 禁用Nagle算法(减少延迟)*/
optval = 1;
if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval)) < 0)
err_sys("setsockopt TCP_NODELAY error");
/* 4. 调整缓冲区大小 */
optval = 65536; /* 64KB */
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &optval, sizeof(optval)) < 0)
err_sys("setsockopt SO_SNDBUF error");
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &optval, sizeof(optval)) < 0)
err_sys("setsockopt SO_RCVBUF error");
/* 5. 可选:调整保活参数(Linux特有)*/
#ifdef TCP_KEEPIDLE
optval = 60; /* 60秒空闲后开始探测 */
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &optval, sizeof(optval));
#endif
#ifdef TCP_KEEPINTVL
optval = 10; /* 每10秒探测一次 */
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &optval, sizeof(optval));
#endif
#ifdef TCP_KEEPCNT
optval = 3; /* 最多3次探测无响应后关闭 */
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &optval, sizeof(optval));
#endif
/* 6. 可选:设置延迟关闭(谨慎使用)*/
/*
ling.l_onoff = 1;
ling.l_linger = 10;
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
*/
}
int main(int argc, char **argv)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
/* 在 bind 之前设置套接字选项 */
set_socket_options(listenfd);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept(listenfd, (SA *) NULL, NULL);
if (Fork() == 0) {
Close(listenfd);
str_echo(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}
}
4.3 使用 fcntl 设置非阻塞 I/O
#include "unp.h"
#include <fcntl.h>
int set_nonblocking(int sockfd)
{
int flags;
/* 获取当前文件状态标志 */
if ((flags = fcntl(sockfd, F_GETFL, 0)) < 0)
return -1;
/* 设置非阻塞标志 */
flags |= O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) < 0)
return -1;
return 0;
}
int set_blocking(int sockfd)
{
int flags;
if ((flags = fcntl(sockfd, F_GETFL, 0)) < 0)
return -1;
flags &= ~O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) < 0)
return -1;
return 0;
}
// 非阻塞 I/O 的使用示例
void nonblocking_read(int sockfd)
{
char buf[4096];
int n;
set_nonblocking(sockfd);
while (1) {
n = read(sockfd, buf, sizeof(buf));
if (n < 0) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
/* 没有数据可读,稍后再试 */
// usleep(1000);
// 或者调用 select 等待可读
continue;
}
err_sys("read error");
} else if (n == 0) {
break; /* EOF */
} else {
/* 处理数据 */
writen(sockfd, buf, n);
}
}
}
五、关键图表
5.1 套接字选项层级结构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 套接字选项层级结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 应用程序 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ SOL_SOCKET (通用套接字层) │ │
│ │ SO_REUSEADDR │ SO_KEEPALIVE │ SO_LINGER │ SO_RCVBUF │ SO_SNDBUF │ │
│ │ SO_RCVLOWAT │ SO_SNDLOWAT │ SO_ERROR │ SO_TYPE │ ... │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ 传输层协议 │
│ ┌─────────────────────────┐ ┌─────────────────────────────────────┐ │ │
│ │ IPPROTO_TCP (TCP层) │ │ IPPROTO_UDP (UDP层) │ │ │
│ │ TCP_NODELAY │ TCP_MAXSEG│ │ (UDP选项较少) │ │ │
│ │ TCP_KEEPIDLE│ TCP_KEEPINTVL ... │ │ │
│ └─────────────────────────┘ └─────────────────────────────────────┘ │ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ 网络层 │
│ ┌─────────────────────────┐ ┌─────────────────────────────────────┐ │ │
│ │ IPPROTO_IP (IPv4层) │ │ IPPROTO_IPV6 (IPv6层) │ │ │
│ │ IP_TTL │ IP_MULTICAST_IF│ │ IPV6_V6ONLY │ IPV6_UNICAST_HOPS │ │ │
│ │ IP_ADD_MEMBERSHIP ... │ │ ... │ │ │
│ └─────────────────────────┘ └─────────────────────────────────────┘ │ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.2 getsockopt 与 setsockopt 调用流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ getsockopt / setsockopt 调用流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ getsockopt(sockfd, level, optname, &optval, &optlen) │
│ │
│ 用户空间 内核空间 │
│ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ sockfd │ │ 套接字结构 │ │
│ │ level │ ──── 系统调用 ────────→ │ ┌─────────────────┐ │ │
│ │ optname │ │ │ 套接字选项 │ │ │
│ │ &optval (空缓冲区)│ │ │ - SO_REUSEADDR │ │ │
│ │ &optlen (大小) │ ←──── 返回数据 ───────── │ │ - SO_KEEPALIVE │ │ │
│ └─────────────────┘ │ │ - SO_RCVBUF │ │ │
│ │ └─────────────────┘ │ │
│ setsockopt(sockfd, level, optname, &optval, optlen) │ │
│ │
│ 用户空间 内核空间 │
│ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ sockfd │ │ 套接字结构 │ │
│ │ level │ ──── 系统调用 ────────→ │ ┌─────────────────┐ │ │
│ │ optname │ │ │ 套接字选项 │ │ │
│ │ &optval (新值) │ │ │ 更新为新值 │ │ │
│ │ optlen │ │ └─────────────────┘ │ │
│ └─────────────────┘ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.3 SO_LINGER 三种模式对比图
┌─────────────────────────────────────────────────────────────────────────────┐
│ SO_LINGER 三种 close 行为对比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 模式1: 默认模式 (l_onoff = 0) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ close() → 立即返回 → 内核后台发送数据 → 发送FIN → 正常关闭 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 模式2: 延迟关闭模式 (l_onoff = 1, l_linger > 0) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ close() → 阻塞等待数据发送 → 发送FIN → 接收ACK → 返回 │ │
│ │ │ │ │
│ │ └─→ 若超时则返回EWOULDBLOCK,丢弃未发送数据 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 模式3: 立即关闭模式 (l_onoff = 1, l_linger = 0) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ close() → 立即发送RST → 丢弃所有未发送数据 → 立即返回 │ │
│ │ │ │
│ │ 对方收到ECONNRESET错误 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.4 SO_KEEPALIVE 探测机制流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ SO_KEEPALIVE 保活探测机制 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务器 │
│ │ │ │
│ │ 数据交换 │ │
│ ├─────────────────────────────→│ │
│ │ │ │
│ │ (2小时空闲后) │ │
│ │ │ │
│ │ 保活探测 (Keep-Alive Probe) │ │
│ ├─────────────────────────────→│ │
│ │ │ │
│ │ 响应 (ACK) │ 服务器正常 │
│ │←─────────────────────────────┤ → 一切正常 │
│ │ │ │
│ │ 保活探测 │ 服务器已崩溃后重启 │
│ ├─────────────────────────────→│ → 无连接记录 │
│ │ │ │
│ │ RST响应 │ │
│ │←─────────────────────────────┤ → ECONNRESET │
│ │ │ │
│ │ 保活探测 │ 服务器已崩溃 │
│ ├─────────────────────────────→│ → 无响应 │
│ │ (重试TCP_KEEPCNT次) │ │
│ │ │ → ETIMEDOUT │
│ │ │ │
└─────────────────────────────────────────────────────────────────────────────┘
六、本章练习题精解
习题7.1
问题:如果在调用 bind 之前设置了 SO_REUSEADDR 选项,那么是否允许在同一个端口上启动多个服务器实例?
答案:不一定。SO_REUSEADDR 主要允许重用处于 TIME_WAIT 状态的端口。对于多个服务器实例同时运行的情况,如果它们都设置了 SO_REUSEADDR,并且绑定了不同的 IP 地址(例如不同网络接口),则可以同时运行。但如果绑定相同的地址和端口(例如都绑定 INADDR_ANY 和相同端口),通常只有第一个实例可以成功绑定,后续实例会失败。具体的多实例绑定行为还取决于系统实现和 SO_REUSEPORT 选项的设置。
习题7.2
问题:在设置 SO_KEEPALIVE 选项后,如果对端主机崩溃但没有发送 FIN,应用程序如何检测?
答案:当对端主机崩溃时,SO_KEEPALIVE 机制会在连接空闲一段时间后开始发送探测。经过系统设定的探测次数(通常为 TCP_KEEPCNT,默认值依赖于具体实现)后,如果仍未收到响应,内核会关闭套接字。应用程序后续调用 read 或 write 时会返回 ETIMEDOUT 错误。因此,应用程序需要定期执行 I/O 操作才能检测到这种错误。
习题7.3
问题:设置 SO_LINGER 为 {1, 0} 会发生什么?它有什么风险?
答案:设置 {1, 0} 会启用立即关闭模式:close 调用立即返回,但内核会发送 RST 报文给对方,跳过正常的四次挥手流程。风险包括:
- 所有尚未发送的数据都会被丢弃
- 对方收到 RST 后会得到
ECONNRESET错误 - 这种方式破坏了 TCP 的优雅关闭机制
习题7.4
问题:为什么使用 getsockopt 获取 SO_SNDBUF 时返回的值可能比设置的值大?
答案:这是内核的优化机制。Linux 内核会对应用程序设置的缓冲区大小进行调整(通常翻倍),以确保 TCP 协议的稳定性,避免因缓冲区过小导致频繁丢包或重传。因此,实际生效的缓冲区大小往往大于应用程序请求的值。
七、核心知识点总结
7.1 常用套接字选项速查表
| 选项 | level | 数据类型 | 作用 |
|---|---|---|---|
SO_REUSEADDR |
SOL_SOCKET | int | 允许重用 TIME_WAIT 状态的端口 |
SO_KEEPALIVE |
SOL_SOCKET | int | 启用 TCP 保活探测 |
SO_LINGER |
SOL_SOCKET | struct linger | 控制 close 行为 |
SO_RCVBUF |
SOL_SOCKET | int | 获取/设置接收缓冲区大小 |
SO_SNDBUF |
SOL_SOCKET | int | 获取/设置发送缓冲区大小 |
SO_RCVLOWAT |
SOL_SOCKET | int | 设置读低水位标记 |
SO_SNDLOWAT |
SOL_SOCKET | int | 设置写低水位标记 |
SO_ERROR |
SOL_SOCKET | int | 获取并清除套接字错误状态 |
SO_TYPE |
SOL_SOCKET | int | 获取套接字类型 |
TCP_NODELAY |
IPPROTO_TCP | int | 禁用 Nagle 算法 |
TCP_KEEPIDLE |
IPPROTO_TCP | int | 保活空闲时间 |
TCP_KEEPINTVL |
IPPROTO_TCP | int | 保活探测间隔 |
TCP_KEEPCNT |
IPPROTO_TCP | int | 保活探测次数 |
7.2 fcntl 操作速查表
| cmd 值 | 用途 | 参数 | 说明 |
|---|---|---|---|
F_GETFL |
获取文件状态标志 | — | 返回当前标志 |
F_SETFL |
设置文件状态标志 | O_NONBLOCK、O_ASYNC |
设置非阻塞或信号驱动 |
F_SETOWN |
设置套接字属主 | 进程 ID | 指定接收 SIGIO/SIGURG 的进程 |
F_GETOWN |
获取套接字属主 | — | 获取当前属主的进程 ID |
7.3 本章思维导图
第七章 套接字选项
├── 核心API
│ ├── getsockopt / setsockopt (level, optname, optval, optlen)
│ ├── getsockname / getpeername
│ └── fcntl (F_GETFL, F_SETFL, F_SETOWN, F_GETOWN)
├── 通用套接字选项 (SOL_SOCKET)
│ ├── SO_REUSEADDR → 解决 TIME_WAIT 端口重用
│ ├── SO_KEEPALIVE → TCP 保活探测,检测主机崩溃
│ ├── SO_LINGER → 控制 close 行为(三种模式)
│ ├── SO_RCVBUF / SO_SNDBUF → 调整缓冲区大小
│ ├── SO_RCVLOWAT / SO_SNDLOWAT → 低水位标记
│ └── SO_ERROR / SO_TYPE → 获取套接字信息
├── TCP 协议层选项 (IPPROTO_TCP)
│ ├── TCP_NODELAY → 禁用 Nagle 算法,减少延迟
│ ├── TCP_KEEPIDLE → 保活空闲时间
│ ├── TCP_KEEPINTVL → 保活探测间隔
│ └── TCP_KEEPCNT → 保活探测次数
├── IP 协议层选项 (IPPROTO_IP / IPPROTO_IPV6)
│ ├── IP_TTL / IP_MULTICAST_TTL
│ ├── IP_MULTICAST_IF
│ └── IP_ADD_MEMBERSHIP / IP_DROP_MEMBERSHIP
└── 实际应用
├── 设置 SO_REUSEADDR 避免 TIME_WAIT 阻塞
├── 设置 SO_KEEPALIVE 检测对端崩溃
├── 调整缓冲区大小优化高带宽场景
└── 使用 fcntl 实现非阻塞 I/O
八、下一章预告
📌 下一篇:《UNIX网络编程》读书笔记(八):第八章 基本 UDP 套接字编程
第八章将详细讲解:
- UDP 与 TCP 的核心区别:无连接、数据报边界、无流量控制、不可靠
- recvfrom 和 sendto 函数:UDP 的核心 API,用于发送和接收数据报
- UDP 回射服务器/客户端程序:完整的 UDP 编程示例,与 TCP 版本的对比
- UDP 的异步错误处理:UDP 的异步错误为什么不会返回给套接字,如何解决
- connect 在 UDP 中的作用:UDP 套接字调用 connect 的意义和限制
- UDP 的发送和接收缓冲区:为什么 UDP 没有真正的发送缓冲区
- UDP 的性能考虑:什么时候选择 UDP 而非 TCP
学习目标:学完第八章后,你将能够——
- 理解 UDP 与 TCP 的核心区别,知道何时选择 UDP
- 使用
recvfrom和sendto编写 UDP 程序 - 理解 UDP 异步错误的特殊性及其处理方式
- 了解 UDP connect 的作用和限制
- 实现简单的 UDP 客户/服务器程序
敬请期待!
参考资料
- W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
- Unix网络编程卷1:套接字联网API 读书笔记 第7章:套接字选项,CSDN,https://blog.csdn.net/qq_33981116/article/details/101425909
- UNIX网络编程卷一 学习笔记 第七章 套接字选项,CSDN,https://blog.csdn.net/yanxiangyfc/article/details/107433212
- UNIX网络编程——第七章 套接字选项,CSDN,https://blog.csdn.net/weixin_41923658/article/details/108223740
- UNPv1第七章:套接口选项,腾讯云开发者社区,https://cloud.tencent.com.cn/developer/article/1600755
- SO_REUSEADDR,百度百科,https://baike.baidu.com/item/SO_REUSEADDR
- SO_REUSEADDR和SO_REUSEPORT:端口复用的双刃剑,百度开发者中心,https://developer.baidu.com/article/2830098
- TCP的KeepAlive探测详解,腾讯云开发者社区,https://cloud.tencent.cn/developer/article/1412402
- linux so_linger - 腾讯云开发者社区,https://cloud.tencent.cn/developer/article/1601313
- Linux服务器编程实践50-TCP接收与发送缓冲区:SO_RCVBUF与SO_SNDBUF设置,CSDN,https://blog.csdn.net/chexlong/article/details/134140333
- 高级IO之非阻塞IO和阻塞IO,腾讯云开发者社区,https://cloud.tencent.cn/developer/article/2289390
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)