【C语言系统编程】【第三部分:网络编程】3.1 套接字编程(TCP/UDP基础)

第三部分:网络编程

3.1 套接字编程(TCP/UDP基础)
3.1.1 套接字基础
3.1.1.1 套接字概念与类型

套接字(Socket)是网络编程的基础,它提供了一种进程间通信的机制。根据传输特点,套接字主要分为以下两种类型:

  • 流套接字(Stream Socket):适用于TCP协议,提供了可靠、面向连接的字节流服务。数据传输有序、不丢失且无重复。
  • 数据报套接字(Datagram Socket):适用于UDP协议,提供了无连接、尽力而为的服务。不保证数据按顺序到达、数据可能丢失或重复。
3.1.1.2 套接字地址结构

套接字地址用于表示网络上的主机和端口,包括以下几种常用结构:

  • sockaddr:通用套接字地址结构,通常作为其他具体地址结构的基类使用。

  • sockaddr_in:用于IPv4地址,包括如下成员:

    c 复制代码
    struct sockaddr_in {
        short int sin_family;   // 地址族(AF_INET)
        unsigned short int sin_port;   // 端口号
        struct in_addr sin_addr;   // IP地址
        unsigned char sin_zero[8];   // 填充字节,保证结构大小
    };
  • sockaddr_in6:用于IPv6地址,包括如下成员:

    c 复制代码
    struct sockaddr_in6 {
        u_int16_t sin6_family;   // 地址族(AF_INET6)
        u_int16_t sin6_port;   // 端口号
        u_int32_t sin6_flowinfo;   // 流量信息
        struct in6_addr sin6_addr;   // IPv6地址
        u_int32_t sin6_scope_id;   // Scope ID
    };
3.1.1.3 网络字节序与主机字节序转换

在网络编程中,数据的字节序(即在内存中存储多字节数据的方式)是一个重要概念。网络协议通常使用"大端字节序"(Big-endian),而不同主机可能使用"小端字节序"(Little-endian)。以下函数用于在主机字节序和网络字节序之间转换:

  • htonl(Host to Network Long):将32位整数从主机字节序转换为网络字节序。

    c 复制代码
    uint32_t htonl(uint32_t hostlong);
  • ntohl(Network to Host Long):将32位整数从网络字节序转换为主机字节序。

    c 复制代码
    uint32_t ntohl(uint32_t netlong);
  • htons(Host to Network Short):将16位整数从主机字节序转换为网络字节序。

    c 复制代码
    uint16_t htons(uint16_t hostshort);
  • ntohs(Network to Host Short):将16位整数从网络字节序转换为主机字节序。

    c 复制代码
    uint16_t ntohs(uint16_t netshort);

这些转换函数在网络编程中至关重要,因为它们确保在不同架构的系统之间的通信中数据能被正确解释。

IPv4示例代码

以下是一个使用套接字连接TCP服务器的简单示例,展示了如何使用上述地址结构和字节序转换函数:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr;

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

    // 设置服务器地址 [2]
    server_addr.sin_family = AF_INET;   // [3]
    server_addr.sin_port = htons(8080); // 端口号使用 htons 转换 [4]
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 本地主机 [5]

    // 连接到服务器 [6]
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("连接到服务器成功\n");

    // 关闭套接字 [7]
    close(sockfd);
    return 0;
}

在上述示例中,我们创建了一个TCP种类的套接字,并连接到本地主机的8080端口。连接成功后,会打印一条消息并关闭套接字。

  • [1] 创建套接字socket() 函数用于创建一个套接字,AF_INET 表示使用 IPv4,SOCK_STREAM 表示使用 TCP 协议,0 表示使用默认协议。成功时返回套接字描述符,失败时返回 -1

  • [2] 设置服务器地址:我们需要指定服务器的 IP 地址和端口号以建立连接。

  • [3] 设置地址族server_addr.sin_family = AF_INET; 设置地址族为 IPv4。

  • [4] 端口号转换htons(8080) 将端口号 8080 转换为网络字节序,htons 表示 "host to network short"(主机到网络短整数)。这一操作确保了字节序的正确性,因为网络的字节序通常不同于计算机的字节序。

  • [5] 本地主机地址转换inet_addr("127.0.0.1") 将 IP 地址字符串 "127.0.0.1" 转换为网络字节序的整数。127.0.0.1 是环回地址,表示本地主机。

  • [6] 连接到服务器connect() 函数用于请求与指定服务器的连接。需要传入套接字描述符、服务器地址结构指针以及该结构的大小。若连接失败,connect 函数返回 -1

  • [7] 关闭套接字close(sockfd) 关闭套接字套接字描述符,释放系统资源。

IPv6示例代码

以下是一个使用套接字连接到一个支持 IPv6 的 TCP 服务器的简单示例,展示了如何使用 sockaddr_in6 结构体来处理 IPv6 地址:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd;
    struct sockaddr_in6 server_addr;

    // 创建套接字 [1]
    if ((sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址 [2]
    server_addr.sin6_family = AF_INET6;  // 设置地址族为 IPv6 [3]
    server_addr.sin6_port = htons(8080); // 端口号使用 htons 转换 [4]
    inet_pton(AF_INET6, "::1", &server_addr.sin6_addr); // 本地主机 IPv6 地址 [5]
    server_addr.sin6_flowinfo = 0; // 流量信息不设置 [6]
    server_addr.sin6_scope_id = 0; // Scope ID 不设置 [7]

    // 连接到服务器 [8]
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("连接到服务器成功\n");

    // 关闭套接字 [9]
    close(sockfd);
    return 0;
}

在上述示例中,我们创建了一个支持 IPv6 的 TCP 套接字,并连接到支持 IPv6 的本地服务器的8080端口。连接成功后,程序会打印一条消息并关闭套接字。

  • [1] 创建套接字socket() 函数用于创建 IPv6 套接字,AF_INET6 表示使用 IPv6 地址,SOCK_STREAM 表示使用 TCP 协议,0 表示使用默认协议。成功时返回套接字描述符,失败时返回 -1

  • [2] 设置服务器地址:为 IPv6,我们指定需要使用的 IPv6 地址和端口号。

  • [3] 设置地址族server_addr.sin6_family = AF_INET6; 设置为 IPv6 地址族。

  • [4] 端口号转换htons(8080) 将端口号 8080 转换到网络字节序。

  • [5] 本地主机地址转换 :使用 inet_pton 函数将字符串形式的 IPv6 地址 "::1"(也即 IPv6 的环回地址,类似于 IPv4 的 127.0.0.1)转换到网络字节序。

  • [6] 流量信息sin6_flowinfo 设置成 0,通常用于服务质量 (QoS) 和流标签。

  • [7] Scope ID :对于环回地址,这里为 0。Scope ID 用于标识链路本地 IPv6 地址的接口。

  • [8] 连接到服务器 :与 IPv4 中的 connect() 类似,用于请求与指定 IPv6 服务器的连接。

  • [9] 关闭套接字:释放系统资源,关闭套接字描述符。

3.1.2 TCP 编程

在进行网络编程时,TCP(传输控制协议)是一种常见的选择,因其提供了可靠的、有序的、基于连接的数据传输服务。以下内容将详细介绍如何使用C语言进行TCP编程。

3.1.2.1 TCP 套接字的创建与配置 (socket, setsockopt)
  • 套接字创建

    • 使用 socket 函数创建TCP套接字。socket 函数通常使用如下形式:

      c 复制代码
      int socket(int domain, int type, int protocol);
      • domain:协议族,比如 AF_INET 表示IPv4协议。
      • type:套接字类型,比如 SOCK_STREAM 表示流套接字。
      • protocol:一般设为 0,默认值代表TCP。
    • 示例代码:

      c 复制代码
      int sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0) {
          perror("socket creation failed");
          exit(EXIT_FAILURE);
      }
  • 套接字配置

    • 使用 setsockopt 函数配置套接字选项以提高通信效率、解决地址复用问题等:

      c 复制代码
      int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
      • sockfd:套接字描述符。

      • level:选项所在的协议层,比如 SOL_SOCKET 表示套接字层。

      • optname:指定需要设置的选项名,如 SO_REUSEADDR

      • optval:选项对应的值。

      • optlenoptval 的长度。

      • 示例代码设置地址复用:

      c 复制代码
      int opt = 1;
      if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
          perror("setsockopt failed");
          exit(EXIT_FAILURE);
      }
3.1.2.2 服务器端编程(bind, listen, accept
  • 绑定(bind)

    • 绑定套接字到特定的地址与端口上,使服务器可以接收客户端的连接请求:

      c 复制代码
      int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      • sockfd:套接字描述符。

      • addr:服务器地址和端口号,采用 struct sockaddr 结构体来存储。

      • addrlenaddr 的长度。

      • 示例代码:

      c 复制代码
      struct sockaddr_in address;
      address.sin_family = AF_INET;
      address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用地址
      address.sin_port = htons(PORT); // 将端口号转换为网络字节序
      
      if (bind(sockfd, (struct sockaddr *)&address, sizeof(address)) < 0) {
          perror("bind failed");
          exit(EXIT_FAILURE);
      }
  • 监听(listen)

    • listen 函数将套接字设为被动模式,用于接收客户端连接:

      c 复制代码
      int listen(int sockfd, int backlog);
      • sockfd:套接字描述符。

      • backlog:等待连接队列的最大长度。指明了内核为此套接字排队的最大连接数。

      • 示例代码:

      c 复制代码
      if (listen(sockfd, 3) < 0) {
          perror("listen failed");
          exit(EXIT_FAILURE);
      }
  • 接受连接(accept)

    • accept 函数提取待处理连接请求,为每个连接分配一个新的套接字:

      c 复制代码
      int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
      • sockfd:监听套接字的描述符。

      • addr:指向一个用于存储已连接客户端地址信息的 struct sockaddr 结构体。

      • addrlen:指向一个值的指针,最初这个值指定客户端地址结构体 addr 的大小,函数返回时更新为实际客户端地址的大小。

      • 示例代码:

      c 复制代码
      int new_socket;
      struct sockaddr_in client_address;
      socklen_t addrlen = sizeof(client_address);
      
      new_socket = accept(sockfd, (struct sockaddr *)&client_address, &addrlen);
      if (new_socket < 0) {
          perror("accept failed");
          exit(EXIT_FAILURE);
      }
      printf("Connection accepted.\n");
3.1.2.3 客户端编程(connect
  • 连接服务器(connect)
    • connect 函数用于客户端尝试连接服务器:

      c 复制代码
      int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      • 示例代码连接服务器:
      c 复制代码
      struct sockaddr_in server_address;
      server_address.sin_family = AF_INET;
      server_address.sin_port = htons(PORT);
      if (inet_pton(AF_INET, "127.0.0.1", &server_address.sin_addr) <= 0) {
          perror("invalid address/ Address not supported");
          exit(EXIT_FAILURE);
      }
      
      if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
          perror("connection failed");
          exit(EXIT_FAILURE);
      }
      printf("Connected to server.\n");
3.1.2.4 数据传输(send, recv
  • 发送数据(send)

    • send 函数用于将数据从客户端/服务器发送到另一端:

      c 复制代码
      ssize_t send(int sockfd, const void *buf, size_t len, int flags);
      • sockfd:套接字描述符,标识要发送数据的套接字。

      • buf:指向包含要发送数据的缓冲区。

      • len:要发送的数据长度,以字节为单位。

      • flags:发送数据的标志,可以是 0 或使用位掩码组合的其他标志(如 MSG_DONTWAIT)。

      • 示例代码:

      c 复制代码
      char *message = "Hello, World!";
      send(new_socket, message, strlen(message), 0);
      printf("Message sent.\n");
  • 接收数据(recv)

    • recv 函数用于从连接中接收数据:

      c 复制代码
      ssize_t recv(int sockfd, void *buf, size_t len, int flags);
      • sockfd:套接字描述符,从中接收数据。

      • buf:指向用于存储接收数据的缓冲区。

      • len:缓冲区的大小,以字节为单位。

      • flags:接收操作的标志,操作的修改行为,如 MSG_WAITALLMSG_PEEK

      • 示例代码:

      c 复制代码
      char buffer[1024] = {0};
      int valread = recv(new_socket, buffer, 1024, 0);
      printf("Received: %s\n", buffer);
3.1.2.5 连接关闭(close, shutdown
  • 连接关闭(close)

    • close 函数用于关闭套接字及其创建的连接:

      c 复制代码
      int close(int fd);
      • fd:需要关闭的文件描述符,通常表示一个打开的套接字。

      • 示例代码:

      c 复制代码
      close(new_socket);
  • 关闭连接(shutdown)

    • shutdown 函数用于关闭部分连接,即停止进一步发送或接收数据:

      c 复制代码
      int shutdown(int sockfd, int how);
      • how 参数:
        • SHUT_RD:关闭读但继续写。
        • SHUT_WR:关闭写但继续读。
        • SHUT_RDWR:关闭读写。
      • 示例代码:
      c 复制代码
      shutdown(sockfd, SHUT_RDWR);
3.1.3 UDP 编程

UDP(User Datagram Protocol)是一种无连接的协议,与面向连接的TCP不同,UDP更轻量、无需建立连接,因此常用于对时延要求高但不需要可靠传输的场景。下面是关于UDP编程的详细讲解:

3.1.3.1 UDP 套接字的创建与配置 (socket, setsockopt)

套接字的创建

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main() {
    int sockfd;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字 [1]
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    printf("UDP socket created successfully\n");
    close(sockfd);
    return 0;
}
  • [1] 创建UDP套接字socket(AF_INET, SOCK_DGRAM, 0) 创建了一个IPv4的UDP套接字。AF_INET 表示IPv4地址族,SOCK_DGRAM表示数据报套接字,0 表示默认协议。

套接字选项配置

c 复制代码
// 省略必要的#include和main函数启动部分
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { // 配置套接字选项 [2]
    perror("setsockopt failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
printf("Socket options set successfully\n");
  • [2] 配置套接字选项setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) 设置套接字的选项。SO_REUSEADDR 允许在套接字关闭后立即重新使用该端口。
3.1.3.2 服务器端编程(bind
c 复制代码
// 省略必要的#include和main函数启动部分
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // IPv4
servaddr.sin_addr.s_addr = INADDR_ANY; // 监听所有接口
servaddr.sin_port = htons(PORT); // 端口

if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { // 绑定 [3]
    perror("bind failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
printf("Bind to port %d successful\n", PORT);
  • [3] 绑定bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) 将IP地址和端口绑定到套接字上。INADDR_ANY表示绑定到所有可用接口。
3.1.3.3 数据发送与接收(sendto, recvfrom

数据发送

c 复制代码
char *message = "Hello, UDP!";
struct sockaddr_in cliaddr;
memset(&cliaddr, 0, sizeof(cliaddr));
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(CLIENT_PORT);
cliaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

int n = sendto(sockfd, message, strlen(message), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, sizeof(cliaddr)); // 发送数据 [4]
if (n < 0) {
    perror("sendto failed");
} else {
    printf("Message sent.\n");
}
  • [4] 发送数据sendto(sockfd, message, strlen(message), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, sizeof(cliaddr)) 将数据发送到指定的地址和端口。
    • sockfd:套接字描述符,标识要使用的套接字。
    • message:指向要发送数据的缓冲区,在此例中是一个字符串 "Hello, UDP!"
    • strlen(message):要发送数据的长度,以字节为单位。这一参数确保只发送指定长度的数据。
    • MSG_CONFIRM:标志参数,用于指定发送操作的特定选项,例如 MSG_CONFIRM 表示在某些协议中需要确认(该标志在 UDP 中实际上不常用)。
    • (const struct sockaddr *) &cliaddr:指向包含目标地址和端口信息的 sockaddr 结构的指针。在此例中,cliaddr 包含了目标 IP 地址和端口。
    • sizeof(cliaddr):目标地址结构的长度。指明 cliaddr 的大小,让函数正确理解地址结构的长度。

数据接收

c 复制代码
char buffer[MAXLINE];
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);

int n = recvfrom(sockfd, buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len); // 接收数据 [5]
if (n < 0) {
    perror("recvfrom failed");
} else {
    buffer[n] = '\0'; // 添加字符串终止符
    printf("Client : %s\n", buffer);
}
  • [5] 接收数据recvfrom(sockfd, buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len) 从指定的地址和端口接收数据。
    • sockfd:套接字描述符,用于标识接收数据的套接字。
    • buffer:用于存储接收到的数据的缓冲区。
    • MAXLINE:接收缓冲区的最大字节数,buffer 的大小。
    • MSG_WAITALL:接收选项标志。此标志表示在完整接收到请求字节的数据之前,调用不会返回。可以根据需要选择其他标志,如 0
    • (struct sockaddr *) &cliaddr:指向存储来源地址信息的指针。cliaddr 是一个用于保存发送端地址信息的结构体。
    • &len:指向一个变量的指针,存储 cliaddr 结构体的大小,并在接收函数返回时,包含发送端地址的实际长度。
3.1.3.4 连接管理(无连接特性分析)

UDP是无连接的,即在发送数据前不需要与对方建立连接。每个数据包(数据报)独立发送,可能会乱序到达或丢失,因此需要应用层实现可靠性保证。

3.1.3.5 数据报的丢失与重传机制

在UDP中,由于无连接特性,数据包可能丢失、重复或乱序到达。实际应用中通常需要在应用层实现:

  • 重传机制:超时未收到ACK则重传数据。
  • 序列号机制:为每个数据包加上序列号,以便接收方按序重组。
  • 校验和检查:用来检查数据完整性。

综上所述,UDP编程的核心在于通过创建和配置套接字、实现数据的发送与接收,并结合应用层机制来应对数据丢失及乱序问题。希望以上讲解对你的项目开发有所帮助。

3.1.4 高级套接字编程技巧
3.1.4.1 非阻塞套接字与多路复用(select, poll, epoll

在高级网络编程中,非阻塞I/O与多路复用技术是处理高并发连接的关键。非阻塞I/O使套接字在I/O操作时不会阻塞进程,多路复用则允许程序同时监视多个套接字,提升效率。

  • 非阻塞套接字

    • 设置方法:使用 fcntl 函数将套接字设置为非阻塞模式。

      c 复制代码
      #include <fcntl.h>
          int set_nonblocking(int sock) { // [1]
          int flags = fcntl(sock, F_GETFL, 0); // [2][3]
          if (flags == -1) return -1;
          return fcntl(sock, F_SETFL, flags | O_NONBLOCK); // [4]
      }
      • sock:要设置为非阻塞模式的套接字描述符。
      • flags:套接字的当前标志位,通过 fcntlF_GETFL 命令获取。
      • fcntl(sock, F_GETFL, 0):获取 sock 当前的文件状态标志。
      • fcntl(sock, F_SETFL, flags | O_NONBLOCK):将套接字设为非阻塞模式,在现有标志位的基础上添加 O_NONBLOCK,更新文件状态标志。
  • select

    • 用于监视一组文件描述符(套接字),在任何一个或多个文件描述符变为可读、可写或有错误时返回。

      c 复制代码
      fd_set readfds; // [1]
      FD_ZERO(&readfds); // [2]
      FD_SET(sock, &readfds); // [3]
      int result = select(sock + 1, &readfds, NULL, NULL, &timeout); // [4]
      if (result > 0 && FD_ISSET(sock, &readfds)) { // [5]
          // sock 变为可读
      }
      • readfds:一个文件描述符集合,用于存储需要监视的文件描述符,检查它们是否可读。
      • FD_ZERO(&readfds):初始化文件描述符集合 readfds,将其清空。
      • FD_SET(sock, &readfds):将套接字 sock 添加到 readfds 集合中,用于监控其可读事件。
      • select(sock + 1, &readfds, NULL, NULL, &timeout)
        • sock + 1:第一个参数指定监视的文件描述符范围,即待监控的最大描述符加一(因数组索引从零开始)。
        • &readfds:第二个参数指定需要检查可读性的文件描述符集合。
        • NULL:第三个和第四个参数用于检查可写性和异常情况的文件描述符集合,设置为 NULL 表示不检查。
        • &timeout:第五个参数为 select 等待的超时时间。
      • resultselect 函数的返回值;大于 0 表示有文件描述符变为可读、可写或有错误,小于 0 表示出错,等于 0 表示超时无事件发生。
      • FD_ISSET(sock, &readfds):宏用于判断套接字 sock 是否在 readfds 集合中可读。
  • poll

    • 类似于 select, 但处理的文件描述符数量更大,且性能更好。

      c 复制代码
      struct pollfd fds[1];
      fds[0].fd = sock; // [1]
      fds[0].events = POLLIN; // [2]
      int result = poll(fds, 1, timeout); // [3][4][5]
      if (result > 0 && (fds[0].revents & POLLIN)) { // [6]
          // sock 变为可读
      }
    • fdspollfd 结构体数组,用于指定要监视的文件描述符和事件。

    • fds[0].fd:要检测的套接字描述符。

    • fds[0].events:待检测的事件类型,例如 POLLIN 表示等待数据可读。

    • poll(fds, 1, timeout):调用 poll 函数执行检测。

    • 1:指定 fds 数组中需要检测的文件描述符数量。

    • timeout:指定 poll 等待事件的毫秒数。负值表示无限等待。

    • resultpoll 的返回值,表示准备就绪的文件描述符数量。若大于 0,表示有文件描述符满足条件。

  • epoll

    • 专为Linux设计的更高效的I/O多路复用机制,适用于处理大量并发连接。

      c 复制代码
      int epoll_fd = epoll_create1(0); // [1]
      struct epoll_event ev = {.events = EPOLLIN, .data.fd = sock}; // [2]
      epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev); // [3]
      struct epoll_event events[MAX_EVENTS]; // [4]
      int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // [5]
      for (int i = 0; i < nfds; i++) {
          if (events[i].events & EPOLLIN) {
              // sock 变为可读
          }
      }
      • epoll_create1(0):创建一个 epoll 实例,返回一个 epoll 文件描述符,用于后续的 epoll 操作。
      • struct epoll_event ev:定义一个 epoll 事件结构体 ev,用于描述要监视的事件类型和相关数据。
        • ev.events:代表事件类型,如 EPOLLIN 表示可读事件。
        • ev.data.fd:事件关联的文件描述符,这里是套接字 sock
      • epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev):将套接字 sock 添加到 epoll 监控列表中,通过 epoll_fd 进行管理。
        • epoll_fd:epoll 文件描述符。
        • EPOLL_CTL_ADD:操作码,表示将新的描述符添加到 epoll 实例中。
        • sock:要添加的文件描述符。
        • &ev:指向要添加的事件结构的指针。
      • struct epoll_event events[MAX_EVENTS]:定义事件数组 events,用于存储被触发事件的信息,数组大小由 MAX_EVENTS 定义。
      • epoll_wait(epoll_fd, events, MAX_EVENTS, -1):等待事件发生。
        • epoll_fd:epoll 文件描述符。
        • events:指向 epoll_event 结构体数组,存储触发的事件。
        • MAX_EVENTS:可以监听的最大事件数。
        • -1:超时值,-1 代表无限期等待直到事件发生。
      • nfdsepoll_wait 返回值,表示已触发事件的数量。
3.1.4.2 套接字选项(SO_REUSEADDR, SO_KEEPALIVE, SO_LINGER等)

套接字选项用于控制套接字的行为,可以通过 setsockopt 函数设置不同选项。

  • SO_REUSEADDR

    • 允许在套接字关闭后立即重用地址。
    c 复制代码
    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  • SO_KEEPALIVE

    • 启用保持连接功能,内核会定期发送探测包以检测连接是否活跃。
    c 复制代码
    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
  • SO_LINGER

    • 控制套接字关闭时的行为,避免未发送的数据丢失。
    c 复制代码
    struct linger so_linger;
    so_linger.l_onoff = 1; /* 开启linger选项 */
    so_linger.l_linger = 30; /* 超时30秒 */
    setsockopt(sock, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
3.1.4.3 多线程与多进程服务器模型(预创建线程/进程池)

为了提升服务器的性能和并发处理能力,常采用多线程或多进程模型,每个线程或进程处理不同的连接。

  • 多线程模型

    • 使用线程池预创建多个线程,每个线程等待处理新的连接。

      c 复制代码
      void *thread_function(void *arg) { // [1]
          int sock = *(int*)arg; // [2]
          // 处理连接
          return NULL;
      }
      
      void create_thread_pool(int num_threads) { // [3]
          pthread_t threads[num_threads]; // [4]
          for (int i = 0; i < num_threads; i++) { // [5]
              pthread_create(&threads[i], NULL, thread_function, (void*)&sock); // [6]
          }
      }
      • arg:传递给线程函数的参数,一般为指向套接字描述符的指针。
      • sock:从 arg 解引用得到的套接字描述符,用于处理连接。
      • num_threads:要创建的线程数量,也就是线程池中的线程数量。
      • threads:存储线程标识符的数组,用于跟踪和管理线程。
      • i:循环变量,用于迭代创建 num_threads 个线程。
      • pthread_create(&threads[i], NULL, thread_function, (void*)&sock):用于创建线程,将每个线程绑定到 thread_function 函数,并传递套接字作为参数。
  • 多进程模型

    • 使用fork创建子进程处理新连接,或使用预创建的进程池。

      c 复制代码
      void create_process_pool(int num_processes) {
          for (int i = 0; i < num_processes; i++) { // [1]
              pid_t pid = fork(); // [2]
              if (pid == 0) { 
                  // 子进程处理连接
                  exit(0); // [3]
              }
          }
          // 父进程等待子进程结束
      }
      • num_processes:需要创建的子进程数量,用于处理并发连接。
      • i:循环变量,用于迭代创建指定数量的子进程。
      • pid:进程ID,由 fork() 函数返回,用于区分父进程和子进程。pid 为0表示当前进程是子进程,正数表示父进程获得的子进程ID。
      • exit(0):子进程完成任务后正常退出,返回0表示成功执行退出。
3.1.4.4 套接字超时设置(连接超时与操作超时)

设置超时可以防止程序在某些操作上无限期等待,常用的选项有 SO_RCVTIMEOSO_SNDTIMEO

  • 设置接收超时

    c 复制代码
    struct timeval tv;
    tv.tv_sec = 5;  // 5秒超时
    tv.tv_usec = 0;
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
  • 设置发送超时

    c 复制代码
    struct timeval tv;
    tv.tv_sec = 5;  // 5秒超时
    tv.tv_usec = 0;
    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

通过上述技术,您可以显著提高基于C语言的网络应用程序的性能和稳定性,确保其在高并发场景下的健壮性。

相关推荐
XH华2 小时前
初识C语言之二维数组(下)
c语言·算法
fantasy_arch3 小时前
CPU性能优化-磁盘空间和解析时间
网络·性能优化
是Dream呀5 小时前
Python从0到100(七十八):神经网络--从0开始搭建全连接网络和CNN网络
网络·python·神经网络
Uu_05kkq5 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
kaixin_learn_qt_ing6 小时前
了解RPC
网络·网络协议·rpc
安全小王子7 小时前
Kali操作系统简单介绍
网络·web安全
Hacker_LaoYi8 小时前
【漏洞分析】DDOS攻防分析(四)——TCP篇
网络·tcp/ip·ddos
爱吃水果蝙蝠汤8 小时前
DATACOM-IP单播路由(BGP)-复习-实验
网络·网络协议·tcp/ip
嵌入式科普8 小时前
十一、从0开始卷出一个新项目之瑞萨RA6M5串口DTC接收不定长
c语言·stm32·cubeide·e2studio·ra6m5·dma接收不定长
A懿轩A8 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列