8UNIX网络编程(第3版)

套接字选项

高级配置

getsockoptsetsockoptSO_REUSEADDR

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第8章 套接字选项

高级配置

一、导读

8.1 本章学习目标

第8章深入讲解套接字选项(socket options)——这些选项允许你精细地控制套接字的行为,从底层的协议参数到上层的缓冲区大小。掌握套接字选项是编写高性能、健壮网络程序的关键。本章将系统介绍套接字选项的设置和查询方法,以及各类常用选项的含义和使用场景。

本章的核心学习目标包括:

  • 掌握setsockopt和getsockopt的使用方法:选项的层级、名称、值的类型。
  • 理解通用套接字选项:SO_REUSEADDR、SO_KEEPALIVE、SO_RCVBUF/SO_SNDBUF、SO_RCVTIMEO/SO_SNDTIMEO等。
  • 理解TCP专用选项:TCP_NODELAY、TCP_MAXSEG、TCP_KEEPIDLE等。
  • 理解IP专用选项:IP_TTL、IP_MULTICAST_TTL、IP_ADD_MEMBERSHIP等。
  • 学会使用套接字选项优化性能:如何调整缓冲区大小?如何控制超时行为?
  • 理解getsockname和getpeername:如何获取套接字的本地/远程地址?

8.2 为什么本章如此重要

套接字选项是网络编程的"调优旋钮"。默认值适用于大多数场景,但在特定需求下,你需要调整这些参数以获得最佳性能或满足特定行为。

常见的调优场景:

  • 高并发服务器:调整SO_RCVBUF/SO_SNDBUF优化吞吐量
  • 低延迟应用:设置TCP_NODELAY禁用Nagle算法
  • 快速重启服务器:设置SO_REUSEADDR避免"Address already in use"
  • 超时控制:设置SO_RCVTIMEO/SO_SNDTIMEO避免无限阻塞
  • 保活检测:设置SO_KEEPALIVE检测死连接
  • 多播应用:设置IP_MULTICAST_TTL控制多播范围

不理解这些选项,就无法充分利用网络的性能潜力,也无法应对生产环境中的各种边界情况。

8.3 核心问题引导

在阅读本章时,请带着以下问题思考:

setsockopt 的 level 参数是什么意思?SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP有什么区别?

SO_REUSEADDR 到底允许重用的是什么?它能解决TIME_WAIT导致的端口绑定问题吗?

增大 SO_RCVBUF 一定能提高吞吐量吗?有什么限制?

SO_RCVTIMEO 和 select 的超时有什么区别?各自适用于什么场景?

TCP_NODELAY 和 TCP_CORK 有什么关系?能否同时使用?

如何获取一个TCP连接的详细信息(RTT、拥塞窗口等)?


二、核心概念详解

8.1 setsockopt和getsockopt

8.1.1 函数原型

c
#include <sys/socket.h>

// 设置套接字选项
int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

// 获取套接字选项
int getsockopt(int sockfd, int level, int optname,
               void *optval, socklen_t *optlen);

// 返回值:成功返回0,出错返回-1

参数说明:

  • sockfd:套接字描述符
  • level:选项的协议层级

- SOL_SOCKET:通用套接字层

- IPPROTO_TCP (或 SOL_TCP):TCP协议层

- IPPROTO_IP:IP协议层

- IPPROTO_UDP:UDP协议层

  • optname:选项名称
  • optval:指向选项值的指针
  • optlen:选项值的大小
c
// 示例:设置TCP_NODELAY
int nodelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));

// 示例:获取SO_RCVBUF
int bufsize;
socklen_t len = sizeof(bufsize);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, &len);
printf("Receive buffer size: %d\n", bufsize);

8.2 通用套接字选项(SOL_SOCKET层)

8.2.1 SO_REUSEADDR

c
int optval = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

作用:允许在TIME_WAIT状态下重新绑定同一地址。

解决的问题:服务器重启时,如果之前的连接处于TIME_WAIT状态,bind会失败("Address already in use")。设置SO_REUSEADDR后,bind可以成功。

注意事项:

  • 必须在bind之前设置
  • 只对TCP有效,对UDP无意义
  • 不会消除TIME_WAIT状态,只是允许绑定
  • 不影响客户端(客户端通常不需要)
c
// 典型用法
int listenfd = socket(AF_INET, SOCK_STREAM, 0);

int optval = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

struct sockaddr_in servaddr;
// ... 设置servaddr ...
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

8.2.2 SO_KEEPALIVE

c
int keepalive = 1;
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));

作用:启用TCP保活机制。如果在一定时间内没有数据交换,TCP会自动发送探测报文。如果对端无响应,连接被认为已断开。

相关参数(Linux):

c
// 空闲时间(秒),默认7200(2小时)
int keepidle = 60;
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));

// 探测间隔(秒),默认75
int keepintvl = 10;
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));

// 探测次数,默认9
int keepcnt = 5;
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));

// 总超时时间 = keepidle + keepintvl * keepcnt
// 上述配置:60 + 10 * 5 = 110秒

keepalive vs 应用层心跳:

  • TCP keepalive:协议层实现,对应用透明,但间隔长(默认2小时),某些路由器/防火墙会丢弃keepalive报文
  • 应用层心跳:应用自己实现,更灵活,可以自定义间隔和超时,推荐在关键应用中使用

8.2.3 SO_RCVBUF和SO_SNDBUF

c
// 设置接收缓冲区大小
int rcvbuf = 256 * 1024;  // 256KB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

// 设置发送缓冲区大小
int sndbuf = 256 * 1024;  // 256KB
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));

// 获取实际值(内核可能调整)
int actual_rcvbuf;
socklen_t len = sizeof(actual_rcvbuf);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &actual_rcvbuf, &len);
printf("Actual receive buffer: %d\n", actual_rcvbuf);
// 注意:Linux内核会将设置值翻倍(用于内部开销)
// 设置256KB,实际可能返回512KB

调优建议:

  • 默认值通常为87380字节(可通过 /proc/sys/net/core/rmem_default 调整)
  • 高带宽长延迟连接(如跨洲际)需要更大的缓冲区
  • 缓冲区大小影响TCP窗口大小,进而影响吞吐量
  • 理论最大吞吐量 ≈ 缓冲区大小 / RTT
示例:RTT=100ms,缓冲区=256KB
最大吞吐量 = 256KB / 0.1s = 2.56MB/s = 20Mbps

如果带宽是100Mbps,需要增大缓冲区到:
缓冲区 = 100Mbps × 0.1s / 8 = 1.25MB

8.2.4 SO_RCVTIMEO和SO_SNDTIMEO

c
// 设置接收超时
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

// 设置发送超时
tv.tv_sec = 3;
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

// 之后read/recv/write/send会在超时后返回-1,errno=EAGAIN或EWOULDBLOCK
ssize_t n = read(sockfd, buf, sizeof(buf));
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    printf("Read timeout\n");
}

SO_RCVTIMEO vs select:

特性SO_RCVTIMEOselect
作用范围单个套接字多个描述符
精度受系统时钟限制微秒级
适用场景单个连接的超时多路复用的超时
实现方式内核定时器内核等待队列

8.2.5 SO_LINGER

c
struct linger lg;
lg.l_onoff = 1;   // 启用linger
lg.l_linger = 5;  // 等待5秒

setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));

作用:控制close的行为。

l_onoffl_lingerclose行为
0-默认行为:立即返回,后台发送剩余数据
10丢弃未发送数据,发送RST(不进入TIME_WAIT)
1>0等待指定时间发送剩余数据,超时后丢弃并发送RST
c
// 场景1:l_onoff=1, l_linger=0
// close立即返回,发送RST,不进入TIME_WAIT
// 适用于:不需要优雅关闭,快速释放资源
struct linger lg = {1, 0};
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
close(sockfd);

// 场景2:l_onoff=1, l_linger=5
// close最多等待5秒发送剩余数据
// 如果5秒内发送完成,正常关闭(FIN)
// 如果超时,丢弃数据并发送RST
struct linger lg = {1, 5};
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
close(sockfd);

8.3 TCP专用选项

8.3.1 TCP_NODELAY

c
int nodelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));

作用:禁用Nagle算法。默认情况下,TCP会延迟发送小数据,等待更多数据积累或收到确认。设置TCP_NODELAY后,write/send立即发送数据。

Nagle算法:

  • 如果发送缓冲区中有未确认的数据,新的写入会被延迟
  • 直到收到确认或积累到MSS大小
  • 目的:减少小数据包的数量,提高网络效率

何时禁用:

  • 交互式应用(SSH、游戏):需要低延迟
  • 实时通信:不能容忍延迟
  • 小消息频繁发送:如HTTP请求

何时不禁用:

  • 批量数据传输:文件传输、大数据下载
  • 不关心延迟的应用

8.3.2 TCP_CORK(Linux特有)

c
int cork = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));

// 写入数据(不会立即发送)
write(sockfd, header, header_len);
write(sockfd, body, body_len);

// 取消cork,立即发送所有数据
cork = 0;
setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));

作用:与TCP_NODELAY相反。设置TCP_CORK后,数据会被积累直到取消cork或超时(通常200ms)。

TCP_NODELAY vs TCP_CORK:

特性TCP_NODELAYTCP_CORK
行为立即发送延迟发送
适用场景低延迟减少小包
典型应用交互式应用HTTP响应(头+体一起发送)

注意:TCP_NODELAY和TCP_CORK不能同时设置。设置一个会自动清除另一个。

8.3.3 TCP_MAXSEG

c
int mss;
socklen_t len = sizeof(mss);
getsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &mss, &len);
printf("TCP MSS: %d\n", mss);
// 以太网场景通常为1460(1500 - 20 IP头 - 20 TCP头)

作用:获取或设置TCP的最大段大小(MSS)。通常不需要手动设置,TCP在连接建立时通过MSS选项自动协商。

8.4 IP专用选项

8.4.1 IP_TTL

c
int ttl = 64;
setsockopt(sockfd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));

作用:设置IP数据报的TTL(Time To Live)值。每经过一个路由器TTL减1,减到0时数据报被丢弃。默认值通常为64。

用途:

  • traceroute就是通过发送不同TTL值的数据报实现的
  • 限制多播的范围(TTL=1限制在本地子网)

8.4.2 IP_MULTICAST相关选项

c
// 设置多播TTL
int mcast_ttl = 32;
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &mcast_ttl, sizeof(mcast_ttl));

// 设置多播接口(指定从哪个网卡发送多播)
struct in_addr local_if;
local_if.s_addr = inet_addr("192.168.1.100");
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &local_if, sizeof(local_if));

// 加入多播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("239.255.0.1");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

// 离开多播组
setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));

// 启用多播回环(接收自己发送的多播数据)
int loop = 1;
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop));

8.5 getsockname和getpeername

8.5.1 getsockname

c
#include <sys/socket.h>

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 返回值:成功返回0,出错返回-1

作用:获取套接字的本地地址(IP和端口)。

c
// 获取本地地址
struct sockaddr_in local_addr;
socklen_t len = sizeof(local_addr);
getsockname(sockfd, (struct sockaddr *)&local_addr, &len);

char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &local_addr.sin_addr, ip, sizeof(ip));
printf("Local address: %s:%d\n", ip, ntohs(local_addr.sin_port));

// 典型用途:
// 1. 获取系统自动分配的临时端口(bind时端口号为0)
// 2. 获取通配绑定(INADDR_ANY)后实际使用的IP地址

8.5.2 getpeername

c
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 返回值:成功返回0,出错返回-1

作用:获取套接字的远程地址(对端的IP和端口)。

c
// 获取远程地址
struct sockaddr_in peer_addr;
socklen_t len = sizeof(peer_addr);
getpeername(sockfd, (struct sockaddr *)&peer_addr, &len);

char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &peer_addr.sin_addr, ip, sizeof(ip));
printf("Peer address: %s:%d\n", ip, ntohs(peer_addr.sin_port));

// 注意:getpeername只对已连接的套接字有效
// 对于未connect的UDP套接字或监听套接字,调用会返回ENOTCONN错误

8.6 获取TCP连接信息

8.6.1 TCP_INFO(Linux)

c
#include <netinet/tcp.h>

struct tcp_info info;
socklen_t len = sizeof(info);

if (getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &info, &len) == 0) {
    printf("TCP Connection Info:\n");
    printf("  State: %u\n", info.tcpi_state);
    printf("  RTT: %u us\n", info.tcpi_rtt);
    printf("  RTT variance: %u us\n", info.tcpi_rttvar);
    printf("  Send cwnd: %u\n", info.tcpi_snd_cwnd);
    printf("  Send MSS: %u\n", info.tcpi_snd_mss);
    printf("  Unacked segments: %u\n", info.tcpi_unacked);
    printf("  Lost segments: %u\n", info.tcpi_lost);
    printf("  Retransmitted: %u\n", info.tcpi_retrans);
    printf("  Total retrans: %u\n", info.tcpi_total_retrans);
}

TCP_INFO提供的信息:

  • tcpi_state:连接状态(ESTABLISHED、CLOSE_WAIT等)
  • tcpi_rtt:估计的RTT(微秒)
  • tcpi_rttvar:RTT方差
  • tcpi_snd_cwnd:发送拥塞窗口
  • tcpi_snd_mss:发送MSS
  • tcpi_unacked:未确认的段数
  • tcpi_lost:丢失的段数
  • tcpi_retrans:当前重传的段数
  • tcpi_total_retrans:总重传次数

这些信息对于性能分析和故障排查非常有用。


三、实战案例

8.1 套接字选项查看工具

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>

void print_socket_options(int sockfd) {
    int val;
    socklen_t len = sizeof(val);
    
    printf("=== Socket Options ===\n");
    
    // SOL_SOCKET options
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &val, &len);
    printf("SO_RCVBUF: %d\n", val);
    
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &val, &len);
    printf("SO_SNDBUF: %d\n", val);
    
    getsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &val, &len);
    printf("SO_KEEPALIVE: %d\n", val);
    
    getsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, &len);
    printf("SO_REUSEADDR: %d\n", val);
    
    // TCP options
    getsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &val, &len);
    printf("TCP_NODELAY: %d\n", val);
    
    getsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &val, &len);
    printf("TCP_MAXSEG: %d\n", val);
    
    // TCP_INFO
    struct tcp_info info;
    len = sizeof(info);
    if (getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &info, &len) == 0) {
        printf("\n=== TCP Info ===\n");
        printf("RTT: %u us\n", info.tcpi_rtt);
        printf("RTT var: %u us\n", info.tcpi_rttvar);
        printf("Send cwnd: %u\n", info.tcpi_snd_cwnd);
        printf("Send MSS: %u\n", info.tcpi_snd_mss);
        printf("Total retrans: %u\n", info.tcpi_total_retrans);
    }
}

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <host> <port>\n", argv[0]);
        exit(1);
    }
    
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[2]));
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect");
        exit(1);
    }
    
    // 打印本地和远程地址
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    char ip[INET_ADDRSTRLEN];
    
    getsockname(sockfd, (struct sockaddr *)&addr, &len);
    inet_ntop(AF_INET, &addr.sin_addr, ip, sizeof(ip));
    printf("Local: %s:%d\n", ip, ntohs(addr.sin_port));
    
    getpeername(sockfd, (struct sockaddr *)&addr, &len);
    inet_ntop(AF_INET, &addr.sin_addr, ip, sizeof(ip));
    printf("Peer: %s:%d\n", ip, ntohs(addr.sin_port));
    
    printf("\n");
    print_socket_options(sockfd);
    
    close(sockfd);
    return 0;
}

8.2 性能调优示例

c
// 高吞吐量服务器的套接字配置
int configure_high_throughput(int sockfd) {
    // 增大缓冲区
    int bufsize = 4 * 1024 * 1024;  // 4MB
    setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
    setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
    
    // 启用keepalive
    int keepalive = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
    
    int keepidle = 300;  // 5分钟空闲后开始探测
    setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
    
    return 0;
}

// 低延迟应用的套接字配置
int configure_low_latency(int sockfd) {
    // 禁用Nagle算法
    int nodelay = 1;
    setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
    
    // 设置接收超时
    struct timeval tv = {5, 0};  // 5秒
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    
    return 0;
}

四、本章小结

本章系统讲解了套接字选项的使用,核心要点回顾:

setsockopt/getsockopt:通过level(SOL_SOCKET/IPPROTO_TCP/IPPROTO_IP)和optname设置/查询选项。

通用选项:SO_REUSEADDR允许TIME_WAIT状态下的端口重用;SO_KEEPALIVE检测死连接;SO_RCVBUF/SO_SNDBUF调整缓冲区大小影响吞吐量;SO_RCVTIMEO/SO_SNDTIMEO设置超时;SO_LINGER控制close行为。

TCP选项:TCP_NODELAY禁用Nagle算法减少延迟;TCP_CORK延迟发送以合并小包;TCP_MAXSEG获取MSS。

IP选项:IP_TTL设置生存时间;IP_MULTICAST_*系列选项控制多播行为。

地址查询:getsockname获取本地地址,getpeername获取远程地址。TCP_INFO获取连接详细信息(RTT、拥塞窗口等)。


五、思考与练习

思考题

SO_REUSEADDR为什么不能解决客户端的端口冲突?它只对服务器有意义吗?

增大SO_RCVBUF后,TCP窗口大小一定会增大吗?还有哪些因素限制窗口大小?

TCP_NODELAY和TCP_CORK能否同时设置?如果先设置TCP_NODELAY再设置TCP_CORK会怎样?

SO_LINGER设置l_linger=0时,close发送RST而不是FIN,这对应用层有什么影响?

为什么TCP_INFO中的RTT值可能与实际测量的RTT不同?

编程练习

套接字选项管理器:编写一个库,提供简洁的API设置常用的套接字选项(如set_nodelay、set_keepalive、set_recv_timeout等),封装底层的setsockopt调用。

连接质量监控器:使用TCP_INFO定期获取连接的RTT、重传统计等信息,当RTT突然增大或重传增多时发出告警。

缓冲区调优工具:编写一个程序,测试不同SO_RCVBUF/SO_SNDBUF值下的吞吐量,找到最优的缓冲区配置。

8/10套接字选
0%