setsockopt系统调用及示例

1. 函数介绍

setsockopt (Set Socket Options) 是一个 Linux 系统调用,用于在已创建的套接字上设置各种选项(options)或参数(parameters)。这些选项控制着套接字的行为和底层协议的特定方面。

你可以把 setsockopt 想象成调整收音机或电视机的设置

  • 你有一个设备(套接字)。
  • 这个设备有很多可以调整的旋钮和开关(选项),比如音量(SO_RCVBUF)、音质(SO_REUSEADDR)、电源(SO_KEEPALIVE)等。
  • setsockopt 就是让你转动这些旋钮、切换这些开关,来改变设备的工作方式。

这些选项可以影响套接字的方方面面,例如:

  • 地址重用: 允许绑定到一个处于 TIME_WAIT 状态的地址(SO_REUSEADDR)。
  • 缓冲区大小: 调整发送和接收缓冲区的大小(SO_SNDBUFSO_RCVBUF)。
  • 连接保活: 启用 TCP 的 keep-alive 机制来检测死连接(SO_KEEPALIVE)。
  • ** linger**: 控制 close 调用在有未发送数据时的行为(SO_LINGER)。
  • 广播: 允许在 UDP 套接字上发送广播数据报(SO_BROADCAST)。
  • 错误: 控制是否接收带外数据的错误指示(SO_OOBINLINE)。
  • 超时: 设置发送和接收的超时时间(SO_SNDTIMEOSO_RCVTIMEO)。
  • 以及其他许多高级选项

2. 函数原型

#include <sys/socket.h> // 必需

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

3. 功能

  • 设置选项: 为套接字 sockfd 设置由 level 和 optname 共同指定的选项。
  • 传递参数: 通过 optval 指针将选项的具体值传递给内核。这个值的类型和含义取决于具体的选项。
  • 指定长度: 通过 optlen 指定 optval 指向的数据的大小(以字节为单位)。

4. 参数

  • int sockfd: 这是一个已创建(通过 socket())的有效套接字文件描述符
  • int level: 指定选项定义的协议层(level)。
    • SOL_SOCKET套接字层本身。这是最常用的级别,用于设置与具体网络协议(如 TCP/IP)无关的通用套接字选项。
    • IPPROTO_IPIP 层。用于设置 IP 协议特有的选项(如 IP_TTL)。
    • IPPROTO_TCPTCP 层。用于设置 TCP 协议特有的选项(如 TCP_NODELAY)。
    • IPPROTO_IPV6IPv6 层。用于设置 IPv6 协议特有的选项。
  • int optname: 指定在 level 层要设置的具体选项名称
    • 例如,在 SOL_SOCKET 级别,常见的 optname 有:
      • SO_REUSEADDR: 允许重用本地地址。
      • SO_KEEPALIVE: 启用 keep-alive 机制。
      • SO_LINGER: 设置 linger 选项。
      • SO_BROADCAST: 允许发送广播数据报。
      • SO_SNDBUF: 设置发送缓冲区大小。
      • SO_RCVBUF: 设置接收缓冲区大小。
      • SO_SNDTIMEO: 设置发送超时。
      • SO_RCVTIMEO: 设置接收超时。
    • 在 IPPROTO_TCP 级别,常见的 optname 有:
      • TCP_NODELAY: 禁用 Nagle 算法(发送小包时立即发送,不等待)。
  • const void *optval: 这是一个指向选项值的指针。
    • 选项值的类型取决于 optname
    • 对于布尔型选项(如 SO_REUSEADDR),通常是一个指向 int 的指针,非 0 表示启用,0 表示禁用。
    • 对于整型选项(如 SO_SNDBUF),通常是一个指向 int 的指针,值为所需的大小。
    • 对于结构体选项(如 SO_LINGER),则是一个指向相应结构体的指针。
  • socklen_t optlen: 指定 optval 指向的数据的大小(以字节为单位)。
    • 例如,如果 optval 指向一个 int,则 optlen 应为 sizeof(int)
    • 如果 optval 指向一个 struct linger,则 optlen 应为 sizeof(struct linger)

5. 返回值

  • 成功时: 返回 0。套接字选项已成功设置。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EBADF sockfd 无效,EINVAL level 或 optname 无效或 optval/optlen 不匹配,ENOPROTOOPT 协议不支持该选项等)。

6. 相似函数,或关联函数

  • getsockopt: 用于获取套接字的当前选项值。与 setsockopt 相对应。
  • socket: 创建套接字,是 setsockopt 操作的对象。
  • bind / listen / connect: 其他套接字操作函数,通常与 setsockopt 结合使用来配置套接字。

7. 示例代码

示例 1:设置 SO_REUSEADDR 选项

这个例子演示了如何在服务器套接字上设置 SO_REUSEADDR 选项,这是服务器编程中的一个常见且重要的实践。

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

#define PORT 8095
#define BACKLOG 10

int main() {
    int server_fd;
    struct sockaddr_in address;
    int opt = 1; // 用于 SO_REUSEADDR 的值

    // 1. 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    printf("Socket created successfully (fd: %d)\n", server_fd);

    // --- 关键: 设置 SO_REUSEADDR 选项 ---
    // 这允许服务器在重启时立即绑定到同一个地址,
    // 即使旧的连接可能处于 TIME_WAIT 状态。
    printf("Setting SO_REUSEADDR option...\n");
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt SO_REUSEADDR failed");
        // 注意:即使失败,程序也可以继续,但这不是好习惯
        // 最佳实践是处理错误并决定是否继续
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("SO_REUSEADDR option set successfully.\n");

    // 2. 配置服务器地址结构
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 3. 绑定套接字
    printf("Binding socket to port %d...\n", PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Socket bound successfully.\n");

    // 4. 监听
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Server is listening.\n");

    printf("Server setup complete. Press Ctrl+C to exit.\n");
    pause(); // 挂起等待信号

    close(server_fd);
    return 0;
}

代码解释:

  1. 创建 TCP 套接字。
  2. 关键步骤: 调用 setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))
    • server_fd: 要设置选项的套接字。
    • SOL_SOCKET: 选项所在的协议层(套接字层)。
    • SO_REUSEADDR: 要设置的选项名称。
    • &opt: 指向选项值的指针。这里 opt 是一个 int 变量,值为 1,表示启用该选项。
    • sizeof(opt): 选项值的大小。
  3. 如果调用成功,SO_REUSEADDR 选项就被设置为启用了。
  4. 继续进行 bind 和 listen
  5. 设置 SO_REUSEADDR 的好处是,当服务器进程因某种原因(如崩溃或重启)终止后,它之前绑定的地址和端口可能会在内核中处于 TIME_WAIT 状态一段时间。如果没有设置 SO_REUSEADDR,立即重启服务器并尝试绑定同一个地址端口会失败(EADDRINUSE)。设置了这个选项后,就可以立即重用该地址。

示例 2:设置 SO_RCVBUF 和 SO_SNDBUF 选项

这个例子演示了如何调整套接字的接收和发送缓冲区大小。

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

#define PORT 8096
#define CUSTOM_BUFFER_SIZE 64 * 1024 // 64 KB

void print_buffer_sizes(int sock, const char* role) {
    int rcv_buf_size, snd_buf_size;
    socklen_t len = sizeof(rcv_buf_size);

    if (getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, &len) == 0) {
        printf("%s SO_RCVBUF: %d bytes\n", role, rcv_buf_size);
    } else {
        perror("getsockopt SO_RCVBUF failed");
    }

    len = sizeof(snd_buf_size); // Reset len
    if (getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &snd_buf_size, &len) == 0) {
        printf("%s SO_SNDBUF: %d bytes\n", role, snd_buf_size);
    } else {
        perror("getsockopt SO_SNDBUF failed");
    }
}

int main() {
    int server_fd, client_sock;
    struct sockaddr_in address, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int opt = 1;

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置 SO_REUSEADDR
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt SO_REUSEADDR failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // --- 设置服务器套接字的缓冲区大小 ---
    printf("--- Before setting buffer sizes ---\n");
    print_buffer_sizes(server_fd, "Server (before)");

    int rcv_buf_size = CUSTOM_BUFFER_SIZE;
    int snd_buf_size = CUSTOM_BUFFER_SIZE;

    printf("\nSetting custom buffer sizes to %d bytes...\n", CUSTOM_BUFFER_SIZE);
    if (setsockopt(server_fd, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, sizeof(rcv_buf_size))) {
        perror("setsockopt SO_RCVBUF failed");
        // 注意:内核可能会调整这个值到一个合理的范围
    }
    if (setsockopt(server_fd, SOL_SOCKET, SO_SNDBUF, &snd_buf_size, sizeof(snd_buf_size))) {
        perror("setsockopt SO_SNDBUF failed");
    }

    printf("--- After setting buffer sizes ---\n");
    print_buffer_sizes(server_fd, "Server (after)");

    // 绑定和监听
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 5) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("\nServer listening on port %d\n", PORT);

    // --- 客户端连接 ---
    client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sock < 0) {
        perror("client socket failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(client_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("client connect failed");
        close(server_fd);
        close(client_sock);
        exit(EXIT_FAILURE);
    }

    // --- 检查客户端套接字的缓冲区大小 ---
    printf("\n--- Client socket buffer sizes ---\n");
    print_buffer_sizes(client_sock, "Client");

    // --- 服务器 accept 连接并检查其套接字 ---
    int accepted_sock = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
    if (accepted_sock < 0) {
        perror("accept failed");
        close(server_fd);
        close(client_sock);
        exit(EXIT_FAILURE);
    }

    printf("\n--- Server accepted socket buffer sizes ---\n");
    print_buffer_sizes(accepted_sock, "Server-Accepted");

    // 简单通信后关闭
    close(client_sock);
    close(accepted_sock);
    close(server_fd);

    return 0;
}

代码解释:

  1. 定义了一个 print_buffer_sizes 函数,它使用 getsockopt 来获取并打印套接字的当前接收和发送缓冲区大小。
  2. 创建服务器套接字。
  3. 设置 SO_REUSEADDR
  4. 关键: 在 bind 之前,调用 setsockopt 来设置服务器套接字的 SO_RCVBUF 和 SO_SNDBUF
    • 传递一个 int 变量的指针和其大小。
  5. 打印设置前后的缓冲区大小。注意,内核可能会将请求的大小调整为一个内部支持的值。
  6. 继续设置服务器监听。
  7. 创建客户端套接字并连接。
  8. 打印客户端套接字的缓冲区大小。
  9. 服务器 accept 连接,得到一个新的已连接套接字。
  10. 打印这个新套接字的缓冲区大小。通常,accept 返回的套接字会继承监听套接字的一些属性,包括缓冲区大小。

示例 3:设置 TCP_NODELAY 选项

这个例子演示了如何在 TCP 套接字上设置 TCP_NODELAY 选项,以禁用 Nagle 算法。

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

#define PORT 8097

int main() {
    int server_fd, client_sock;
    struct sockaddr_in address, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int opt = 1;

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt SO_REUSEADDR failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 5) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d. Waiting for connection...\n", PORT);

    // --- 客户端连接 ---
    client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sock < 0) {
        perror("client socket failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(client_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("client connect failed");
        close(server_fd);
        close(client_sock);
        exit(EXIT_FAILURE);
    }
    printf("Client connected.\n");

    // --- 服务器 accept 连接 ---
    int accepted_sock = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
    if (accepted_sock < 0) {
        perror("accept failed");
        close(server_fd);
        close(client_sock);
        exit(EXIT_FAILURE);
    }
    printf("Server accepted connection.\n");

    // --- 关键: 在已连接的套接字上设置 TCP_NODELAY ---
    // 这会禁用 Nagle 算法,使得小的数据包能够立即发送,而不必等待 ACK 或积累更多数据。
    // 这对于实时性要求高的应用(如游戏、交互式应用)很有用。
    printf("\nSetting TCP_NODELAY on server's accepted socket...\n");
    int flag = 1; // Enable TCP_NODELAY
    if (setsockopt(accepted_sock, IPPROTO_TCP, TCP_NODELAY, (char *) &flag, sizeof(int)) < 0) {
        perror("setsockopt TCP_NODELAY on server socket failed");
        // 不是致命错误,可以继续
    } else {
        printf("TCP_NODELAY enabled on server's accepted socket (fd: %d).\n", accepted_sock);
    }

    printf("\nSetting TCP_NODELAY on client socket...\n");
    if (setsockopt(client_sock, IPPROTO_TCP, TCP_NODELAY, (char *) &flag, sizeof(int)) < 0) {
        perror("setsockopt TCP_NODELAY on client socket failed");
    } else {
        printf("TCP_NODELAY enabled on client socket (fd: %d).\n", client_sock);
    }

    // ... 这里可以进行数据传输 ...

    close(client_sock);
    close(accepted_sock);
    close(server_fd);

    printf("Sockets closed.\n");
    return 0;
}

代码解释:

  1. 设置标准的服务器和客户端连接。
  2. 服务器 accept 连接后,得到 accepted_sock
  3. 关键: 在 accepted_sock 上调用 setsockopt(accepted_sock, IPPROTO_TCP, TCP_NODELAY, ...)
    • IPPROTO_TCP: 选项所在的协议层(TCP 层)。
    • TCP_NODELAY: 要设置的选项名称。
    • &flag (flag=1): 选项值,1 表示启用(禁用 Nagle 算法)。
    • sizeof(int): 选项值大小。
  4. 同样地,在客户端套接字 client_sock 上也设置 TCP_NODELAY
  5. 启用 TCP_NODELAY 后,TCP 连接将立即发送小的数据包,而不会等待将多个小包合并成一个更大的包(Nagle 算法的默认行为)或等待前一个包的 ACK。这可以减少延迟,但可能会增加网络上的小包数量。

重要提示与注意事项:

  1. 调用时机setsockopt 可以在 socket() 之后、bind()/connect()/listen() 之前或之后调用,具体取决于选项。有些选项(如 SO_REUSEADDR)必须在 bind 之前设置才有效。
  2. level 和 optname 的匹配: 确保 level 和 optname 正确匹配。例如,TCP_NODELAY 必须在 IPPROTO_TCP 级别设置。
  3. optval 和 optlen: 确保传递给 optval 的数据类型和大小与 optname 要求的完全一致。
  4. 内核调整: 对于某些选项(如缓冲区大小),内核可能会将你请求的值调整为一个它认为更合适的值。
  5. 错误处理: 始终检查返回值。虽然某些选项设置失败可能不会导致程序无法运行,但最好处理错误并了解原因。
  6. 常见用途SO_REUSEADDRTCP_NODELAYSO_KEEPALIVESO_LINGER 是网络编程中非常常见的选项。

总结:

setsockopt 是一个功能强大的函数,允许程序员精细地调整套接字的行为。理解其参数(特别是 level 和 optname 的组合)以及各种常用选项的作用,对于编写高效、健壮的网络应用程序至关重要。它是网络编程中不可或缺的工具之一。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注