MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言套接字选项配置

2021-10-181.3k 阅读

套接字选项概述

在Linux环境下使用C语言进行网络编程时,套接字选项(Socket Options)起着至关重要的作用。这些选项允许开发者对套接字的行为进行精细控制,以满足不同的网络应用需求。套接字选项可以分为多个层次,包括通用套接字层(SOL_SOCKET)、传输控制协议层(TCP,即IPPROTO_TCP)和网际协议层(IP,即IPPROTO_IP)等。通过设置这些选项,我们可以实现诸如地址重用、发送接收缓冲区调整、超时控制、TCP连接优化等功能。

通用套接字选项(SOL_SOCKET)

SO_REUSEADDR

  1. 作用:该选项允许在同一地址和端口上重复绑定套接字。在网络应用开发中,当一个服务器进程关闭后,其占用的端口可能会处于TIME_WAIT状态,这时候如果不设置该选项,再次绑定相同的地址和端口会失败。通过设置SO_REUSEADDR,可以避免这种情况,使得服务器能够快速重启并绑定到相同的端口。
  2. 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define IP "127.0.0.1"

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    int opt = 1;
    // 设置SO_REUSEADDR选项
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(IP);
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址和端口
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[1024] = {0};
    socklen_t len = sizeof(cliaddr);
    int n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
    buffer[n] = '\0';
    printf("Message from client: %s\n", buffer);

    close(sockfd);
    return 0;
}

在上述代码中,我们在创建套接字后,通过setsockopt函数设置了SO_REUSEADDR选项,使得即使之前有进程使用过该端口并处于TIME_WAIT状态,我们的程序依然可以绑定到该端口。

SO_RCVBUF和SO_SNDBUF

  1. 作用SO_RCVBUF用于设置接收缓冲区的大小,SO_SNDBUF用于设置发送缓冲区的大小。合理调整这两个缓冲区的大小可以提高网络数据传输的效率。例如,对于高带宽、低延迟的网络应用,可以适当增大缓冲区大小以充分利用网络带宽;而对于资源有限的设备,可能需要减小缓冲区大小以节省内存。
  2. 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define IP "127.0.0.1"
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    int recvbuf_size = 8192;
    int sndbuf_size = 8192;

    // 设置接收缓冲区大小
    if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recvbuf_size, sizeof(recvbuf_size)) < 0) {
        perror("setsockopt SO_RCVBUF failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 设置发送缓冲区大小
    if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf_size, sizeof(sndbuf_size)) < 0) {
        perror("setsockopt SO_SNDBUF failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(IP);
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址和端口
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

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

    int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, NULL);
    if (connfd < 0) {
        perror("accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[BUFFER_SIZE] = {0};
    int n = read(connfd, buffer, BUFFER_SIZE);
    buffer[n] = '\0';
    printf("Message from client: %s\n", buffer);

    char *hello = "Hello from server";
    write(connfd, hello, strlen(hello));

    close(connfd);
    close(sockfd);
    return 0;
}

在这段代码中,我们分别设置了接收缓冲区和发送缓冲区的大小为8192字节。setsockopt函数的第三个参数指定了要设置的选项,这里分别是SO_RCVBUFSO_SNDBUF,第四个参数是要设置的值,第五个参数是值的大小。

SO_TIMEOUT

  1. 作用SO_TIMEOUT用于设置套接字I/O操作的超时时间。在网络编程中,有时需要避免套接字操作长时间阻塞,例如在接收数据时,如果长时间没有数据到达,设置了超时时间后,当超过该时间仍未接收到数据,操作将返回错误,使得程序可以继续执行其他任务,而不是一直等待。
  2. 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>

#define PORT 8080
#define IP "127.0.0.1"
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    struct timeval timeout;
    timeout.tv_sec = 5; // 5秒超时
    timeout.tv_usec = 0;

    // 设置SO_TIMEOUT选项
    if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout)) < 0) {
        perror("setsockopt SO_RCVTIMEO failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(IP);
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址和端口
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[BUFFER_SIZE] = {0};
    socklen_t len = sizeof(cliaddr);
    int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
    if (n < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("Receive timed out\n");
        } else {
            perror("recvfrom failed");
        }
    } else {
        buffer[n] = '\0';
        printf("Message from client: %s\n", buffer);
    }

    close(sockfd);
    return 0;
}

在上述代码中,我们通过struct timeval结构体设置了5秒的接收超时时间,并通过setsockopt函数将其应用到套接字上。当recvfrom操作超过5秒仍未接收到数据时,将返回错误,程序根据错误码判断是否是超时错误并进行相应处理。

TCP套接字选项(IPPROTO_TCP)

TCP_NODELAY

  1. 作用TCP_NODELAY用于禁用Nagle算法。Nagle算法是一种为了提高网络利用率而设计的算法,它会将小的数据包合并成一个大的数据包发送,以减少网络传输中的开销。然而,在一些对实时性要求较高的应用中,如网络游戏、实时视频流等,Nagle算法可能会引入不必要的延迟。通过设置TCP_NODELAY选项,可以禁用Nagle算法,使得数据能够立即发送,提高实时性。
  2. 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define IP "127.0.0.1"

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    int opt = 1;
    // 设置TCP_NODELAY选项
    if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)) < 0) {
        perror("setsockopt TCP_NODELAY failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(IP);
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址和端口
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

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

    int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, NULL);
    if (connfd < 0) {
        perror("accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[1024] = {0};
    int n = read(connfd, buffer, 1024);
    buffer[n] = '\0';
    printf("Message from client: %s\n", buffer);

    char *hello = "Hello from server";
    write(connfd, hello, strlen(hello));

    close(connfd);
    close(sockfd);
    return 0;
}

在这段代码中,我们通过setsockopt函数设置了TCP_NODELAY选项,将IPPROTO_TCP作为选项级别,TCP_NODELAY作为选项名,将opt设置为1表示启用该选项,即禁用Nagle算法。

TCP_KEEPALIVE

  1. 作用TCP_KEEPALIVE用于启用TCP连接的保活机制。在长时间没有数据传输的TCP连接中,网络故障或对端主机崩溃等情况可能导致连接处于半打开状态而未被察觉。启用保活机制后,TCP会定期向对端发送探测报文,如果连续多次未收到对端响应,则认为连接已断开,从而释放相关资源。
  2. 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define IP "127.0.0.1"

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    int opt = 1;
    // 设置TCP_KEEPALIVE选项
    if (setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPALIVE, &opt, sizeof(opt)) < 0) {
        perror("setsockopt TCP_KEEPALIVE failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(IP);
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址和端口
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

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

    int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, NULL);
    if (connfd < 0) {
        perror("accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[1024] = {0};
    int n = read(connfd, buffer, 1024);
    buffer[n] = '\0';
    printf("Message from client: %s\n", buffer);

    char *hello = "Hello from server";
    write(connfd, hello, strlen(hello));

    close(connfd);
    close(sockfd);
    return 0;
}

在上述代码中,我们通过setsockopt函数启用了TCP_KEEPALIVE选项。需要注意的是,不同系统对保活机制的具体实现细节可能有所不同,例如探测间隔时间、探测次数等,这些通常可以通过系统参数进行调整。

IP套接字选项(IPPROTO_IP)

IP_TOS

  1. 作用IP_TOS用于设置IP数据包的服务类型(Type of Service)字段。该字段可以用来指示网络设备对数据包的处理优先级,例如可以设置为低延迟、高吞吐量、高可靠性等不同的服务类型,从而影响网络路由和数据包调度策略,以满足不同应用的需求。
  2. 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define IP "127.0.0.1"

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    int tos = 0x10; // 设置为低延迟服务类型
    // 设置IP_TOS选项
    if (setsockopt(sockfd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)) < 0) {
        perror("setsockopt IP_TOS failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(IP);
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址和端口
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[1024] = {0};
    socklen_t len = sizeof(cliaddr);
    int n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
    buffer[n] = '\0';
    printf("Message from client: %s\n", buffer);

    close(sockfd);
    return 0;
}

在这段代码中,我们将IP_TOS设置为0x10,表示低延迟服务类型。通过setsockopt函数将该选项应用到套接字上,使得发送的IP数据包带有相应的服务类型标识,网络设备可以根据这个标识进行适当的处理。

IP_MULTICAST_TTL

  1. 作用IP_MULTICAST_TTL用于设置组播数据包的生存时间(Time to Live)。在组播通信中,组播数据包需要在网络中经过多个路由器转发,IP_MULTICAST_TTL决定了数据包可以经过的路由器跳数。合理设置该值可以控制组播数据包的传播范围,避免数据包在网络中无限循环转发,同时也可以根据实际需求限制组播的覆盖范围。
  2. 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define GROUP_IP "224.0.0.1"

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    int ttl = 2; // 设置组播TTL为2
    // 设置IP_MULTICAST_TTL选项
    if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0) {
        perror("setsockopt IP_MULTICAST_TTL failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(GROUP_IP);
    servaddr.sin_port = htons(PORT);

    char *message = "Hello, multicast group!";
    sendto(sockfd, (const char *)message, strlen(message), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));
    printf("Message sent to multicast group\n");

    close(sockfd);
    return 0;
}

在上述代码中,我们设置了组播数据包的IP_MULTICAST_TTL为2,意味着该组播数据包最多可以经过2个路由器转发。通过setsockopt函数将该设置应用到套接字上,然后向组播地址发送消息,此消息的传播范围将受到设置的TTL值限制。

通过合理配置这些Linux C语言套接字选项,开发者可以根据不同网络应用的需求,灵活调整套接字的行为,优化网络性能,提高应用的稳定性和可靠性。无论是开发高性能的服务器应用,还是实时性要求高的网络程序,深入理解和熟练运用套接字选项都是非常关键的。