作者: 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 获取本地和对端地址信息

💡 本章核心价值:读完第七章,你将能够——

  • 熟练使用 getsockoptsetsockopt 获取和设置套接字选项
  • 使用 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 设置的选项作用于套接字本身。当使用 dupfork 后,多个描述符可能指向同一个套接字,文件描述符标志不会共享,但套接字选项是共享的。

三、重要套接字选项详解

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_SNDBUFSO_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_RCVLOWATSO_SNDLOWAT 选项用于设置套接字在 I/O 复用(select/poll)中被视为“就绪”所需的最小数据量。

选项 用途 默认值
SO_RCVLOWAT 读低水位标记:接收缓冲区中至少有多少字节时 select 才认为可读 1
SO_SNDLOWAT 写低水位标记:发送缓冲区中至少有这么多空闲空间时 select 才认为可写 2048

💡 理解低水位标记:低水位标记决定了 selectpoll 何时报告描述符就绪。当接收缓冲区中的数据量达到或超过 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,默认值依赖于具体实现)后,如果仍未收到响应,内核会关闭套接字。应用程序后续调用 readwrite 时会返回 ETIMEDOUT 错误。因此,应用程序需要定期执行 I/O 操作才能检测到这种错误。

习题7.3

问题:设置 SO_LINGER{1, 0} 会发生什么?它有什么风险?

答案:设置 {1, 0} 会启用立即关闭模式:close 调用立即返回,但内核会发送 RST 报文给对方,跳过正常的四次挥手流程。风险包括:

  1. 所有尚未发送的数据都会被丢弃
  2. 对方收到 RST 后会得到 ECONNRESET 错误
  3. 这种方式破坏了 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_NONBLOCKO_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 套接字编程

第八章将详细讲解:

  1. UDP 与 TCP 的核心区别:无连接、数据报边界、无流量控制、不可靠
  2. recvfrom 和 sendto 函数:UDP 的核心 API,用于发送和接收数据报
  3. UDP 回射服务器/客户端程序:完整的 UDP 编程示例,与 TCP 版本的对比
  4. UDP 的异步错误处理:UDP 的异步错误为什么不会返回给套接字,如何解决
  5. connect 在 UDP 中的作用:UDP 套接字调用 connect 的意义和限制
  6. UDP 的发送和接收缓冲区:为什么 UDP 没有真正的发送缓冲区
  7. UDP 的性能考虑:什么时候选择 UDP 而非 TCP

学习目标:学完第八章后,你将能够——

  • 理解 UDP 与 TCP 的核心区别,知道何时选择 UDP
  • 使用 recvfromsendto 编写 UDP 程序
  • 理解 UDP 异步错误的特殊性及其处理方式
  • 了解 UDP connect 的作用和限制
  • 实现简单的 UDP 客户/服务器程序

敬请期待!

参考资料

  1. W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
  2. Unix网络编程卷1:套接字联网API 读书笔记 第7章:套接字选项,CSDN,https://blog.csdn.net/qq_33981116/article/details/101425909
  3. UNIX网络编程卷一 学习笔记 第七章 套接字选项,CSDN,https://blog.csdn.net/yanxiangyfc/article/details/107433212
  4. UNIX网络编程——第七章 套接字选项,CSDN,https://blog.csdn.net/weixin_41923658/article/details/108223740
  5. UNPv1第七章:套接口选项,腾讯云开发者社区,https://cloud.tencent.com.cn/developer/article/1600755
  6. SO_REUSEADDR,百度百科,https://baike.baidu.com/item/SO_REUSEADDR
  7. SO_REUSEADDR和SO_REUSEPORT:端口复用的双刃剑,百度开发者中心,https://developer.baidu.com/article/2830098
  8. TCP的KeepAlive探测详解,腾讯云开发者社区,https://cloud.tencent.cn/developer/article/1412402
  9. linux so_linger - 腾讯云开发者社区,https://cloud.tencent.cn/developer/article/1601313
  10. Linux服务器编程实践50-TCP接收与发送缓冲区:SO_RCVBUF与SO_SNDBUF设置,CSDN,https://blog.csdn.net/chexlong/article/details/134140333
  11. 高级IO之非阻塞IO和阻塞IO,腾讯云开发者社区,https://cloud.tencent.cn/developer/article/2289390

本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐