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

news2024/10/4 18:32:30

第三部分:网络编程

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地址,包括如下成员:
    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地址,包括如下成员:
    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位整数从主机字节序转换为网络字节序。

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

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

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

    uint16_t ntohs(uint16_t netshort);
    

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

IPv4示例代码

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

#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 地址:

#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 函数通常使用如下形式:
      int socket(int domain, int type, int protocol);
      
      • domain:协议族,比如 AF_INET 表示IPv4协议。
      • type:套接字类型,比如 SOCK_STREAM 表示流套接字。
      • protocol:一般设为 0,默认值代表TCP。
    • 示例代码:
      int sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0) {
          perror("socket creation failed");
          exit(EXIT_FAILURE);
      }
      
  • 套接字配置

    • 使用 setsockopt 函数配置套接字选项以提高通信效率、解决地址复用问题等:
      int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
      
      • sockfd:套接字描述符。

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

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

      • optval:选项对应的值。

      • optlenoptval 的长度。

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

      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)

    • 绑定套接字到特定的地址与端口上,使服务器可以接收客户端的连接请求:
      int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      
      • sockfd:套接字描述符。

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

      • addrlenaddr 的长度。

      • 示例代码:

      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 函数将套接字设为被动模式,用于接收客户端连接:
      int listen(int sockfd, int backlog);
      
      • sockfd:套接字描述符。

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

      • 示例代码:

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

    • accept 函数提取待处理连接请求,为每个连接分配一个新的套接字:
      int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
      
      • sockfd:监听套接字的描述符。

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

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

      • 示例代码:

      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 函数用于客户端尝试连接服务器:
      int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      
      • 示例代码连接服务器:
      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 函数用于将数据从客户端/服务器发送到另一端:
      ssize_t send(int sockfd, const void *buf, size_t len, int flags);
      
      • sockfd:套接字描述符,标识要发送数据的套接字。

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

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

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

      • 示例代码:

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

    • recv 函数用于从连接中接收数据:
      ssize_t recv(int sockfd, void *buf, size_t len, int flags);
      
      • sockfd:套接字描述符,从中接收数据。

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

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

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

      • 示例代码:

      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 函数用于关闭套接字及其创建的连接:
      int close(int fd);
      
      • fd:需要关闭的文件描述符,通常表示一个打开的套接字。

      • 示例代码:

      close(new_socket);
      
  • 关闭连接(shutdown)

    • shutdown 函数用于关闭部分连接,即停止进一步发送或接收数据:
      int shutdown(int sockfd, int how);
      
      • how 参数:
        • SHUT_RD:关闭读但继续写。
        • SHUT_WR:关闭写但继续读。
        • SHUT_RDWR:关闭读写。
      • 示例代码:
      shutdown(sockfd, SHUT_RDWR);
      
3.1.3 UDP 编程

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

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

套接字的创建

#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 表示默认协议。

套接字选项配置

// 省略必要的#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
// 省略必要的#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

数据发送

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 的大小,让函数正确理解地址结构的长度。

数据接收

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 函数将套接字设置为非阻塞模式。
      #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

    • 用于监视一组文件描述符(套接字),在任何一个或多个文件描述符变为可读、可写或有错误时返回。
      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, 但处理的文件描述符数量更大,且性能更好。
      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多路复用机制,适用于处理大量并发连接。
      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

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

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

    • 控制套接字关闭时的行为,避免未发送的数据丢失。
    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 多线程与多进程服务器模型(预创建线程/进程池)

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

  • 多线程模型

    • 使用线程池预创建多个线程,每个线程等待处理新的连接。
      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创建子进程处理新连接,或使用预创建的进程池。
      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

  • 设置接收超时

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188485.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Pikachu-File Inclusion-远程文件包含

远程文件包含漏洞 是指能够包含远程服务器上的文件并执行。由于远程服务器的文件是我们可控的&#xff0c;因此漏洞一旦存在&#xff0c;危害性会很大。但远程文件包含漏洞的利用条件较为苛刻&#xff1b;因此&#xff0c;在web应用系统的功能设计上尽量不要让前端用户直接传变…

【GT240X】【04】你必须知道的 50 多个 Linux 命令

文章目录 一、介绍二、五十个linux命令一览表三、50个命令详解四、结论 你必须知道的 50 多个 Linux 命令 一、介绍 你经常使用 Linux 命令&#xff1f;今天&#xff0c;我们将介绍 50 多个你必须知道的 Linux 命令。下面列出的命令是一些最有用和最常用的 Linux 命令&#x…

jmeter学习(5)定时

Jmeter之定时器_jmeter定时器-CSDN博客 Jmeter(十三) - 从入门到精通 - JMeter定时器 - 上篇&#xff08;详解教程&#xff09;-腾讯云开发者社区-腾讯云 (tencent.com) 定时器是在每个sampler之前执行的&#xff0c;无论定时器位置在sampler之前还是子节点下面当执行一个sam…

TypeScript 算法手册 【基数排序】

文章目录 1. 基数排序简介1.1 基数排序定义1.2 基数排序特点 2. 基数排序步骤过程拆解2.1 找出数组中的最大值2.2 从最低位开始&#xff0c;对每一位进行计数排序2.3 对某一位数进行计数排序2.4 将排序结果复制回原数组 3. 基数排序的优化3.1 处理负数3.2 字符串排序案例代码和…

Go语言实现随机森林 (Random Forest)算法

在 Go 语言中实现随机森林&#xff08;Random Forest&#xff09;算法通常涉及以下几个步骤&#xff1a; 数据准备&#xff1a;将数据集分为训练集和测试集&#xff0c;确保数据格式适合算法使用。 决策树的构建&#xff1a;随机森林是由多个决策树构成的&#xff0c;首先需要…

MySQL 实验1:Windows 环境下 MySQL5.5 安装与配置

MySQL 实验1&#xff1a;Windows 环境下 MySQL5.5 安装与配置 目录 MySQL 实验1&#xff1a;Windows 环境下 MySQL5.5 安装与配置一、MySQL 软件的下载二、安装 MySQL三、配置 MySQL1、配置环境变量2、安装并启动 MySQL 服务3、设置 MySQL 字符集4、为 root 用户设置登录密码 一…

使用前端三剑客实现一个备忘录

一&#xff0c;界面介绍 这个备忘录的界面效果如下&#xff1a; 可以实现任务的增删&#xff0c;并且在任务被勾选后会被放到已完成的下面。 示例&#xff1a; &#xff08;1&#xff09;&#xff0c;增加一个任务 &#xff08;2&#xff09;&#xff0c;勾选任务 &#xff…

【知乎直答】批量多线程生成原创文章软件-AI智能搜索聚合

【知乎直答】批量多线程生成原创文章软件介绍&#xff1a; 1、知乎发布的全新AI产品“知乎直答”是其AI搜索功能的产品化成果&#xff0c;旨在提升用户的提问、搜索体验以及结果生成和归纳的质量。 2、数据基础&#xff1a;该产品基于知乎平台上的真实问答数据及全网高质量问答…

Chromium 中前端js XMLHttpRequest接口c++代码实现

在JavaScript中发出HTTP请求的主要方式包括&#xff1a;XMLHttpRequest对象、Fetch API、Axios库和各种其他的HTTP客户端库。 本人主要分析下XMLHttpRequest接口在c中对应实现 一、上前端代码 <!DOCTYPE html> <html lang"en"> <head> <meta…

Go基础学习11-测试工具gomock和monkey的使用

文章目录 基础回顾MockMock是什么安装gomockMock使用1. 创建user.go源文件2. 使用mockgen生成对应的Mock文件3. 使用mockgen命令生成后在对应包mock下可以查看生成的mock文件4. 编写测试代码5. 运行代码并查看输出 GomonkeyGomonkey优势安装使用对函数进行monkey对结构体中方法…

Marp精华总结(二)进阶篇

概述 这是Marp精华总结的第二篇&#xff0c;主要补充第一篇未提到的一些内容。 系列目录 Marp精华总结&#xff08;一&#xff09;基础篇Marp精华总结&#xff08;二&#xff09;进阶篇Marp精华总结&#xff08;三&#xff09;高级篇 自适应标题 通过在标题行中插入<!-…

历经十年/头发都快掉光/秘钥生成器终极版/机器码/到期功能限制/运行时间限制/日期防篡改/跨平台

一、项目介绍 1.0 前言说明 标题一点都不夸张&#xff0c;从第一版的秘钥生成器到今天这个版本&#xff0c;确实经历了十年的时间&#xff0c;最初的版本做的非常简陋&#xff0c;就是搞了个异或加密&#xff0c;控制运行时间&#xff0c;后面又增加设备数量的控制&#xff0…

JavaFX加载fxml文件几种方法

环境&#xff1a;idea&#xff0c;maven创建JavaFX工程 工程目录如下&#xff1a; MusicPlayer.java package cn.com;import java.io.IOException;import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.geometry.Insets; import javafx.geo…

目标检测 Deformable DETR(2021)详细解读

文章目录 前言整体网络架构可变形注意力模块backbone生成多尺度特征多尺度位置编码prediction heads两个变体 前言 为解决DETR attention的计算量大导致收敛速度慢、小目标检测效果差的问题&#xff1a;提出了Deformable Attention&#xff0c;其注意力模块只关注一个query周围…

ML 系列: (10)— ML 中的不同类型的学习

一、说明 我们之前将机器学习方法分为三类&#xff1a;监督学习、无监督学习和强化学习。机器学习方法可以分为不同的类型&#xff0c;我们将在下面讨论最重要的类型。 二、懒惰学习与急切学习 预先学习的工作原理是使用训练数据构建模型&#xff0c;然后使用此模型评估测试数据…

STM32F103C8----3-3 蜂鸣器(跟着江科大学STM32)

一&#xff0c;电路图 &#xff08;接线图&#xff09; 面包板的的使用请参考&#xff1a;《面包板的使用_面包板的详细使用方法-CSDN博客》 二&#xff0c;目的/效果 3-3 蜂鸣器 三&#xff0c;创建Keil项目 详细参考&#xff1a;《STM32F103C8----2-1 Keil5搭建STM32项目模…

MySQL 中的 EXPLAIN 命令详解

在 MySQL 数据库中&#xff0c;EXPLAIN命令是一个非常强大的工具&#xff0c;它可以提供关于 SQL 查询执行计划的关键信息。理解这些信息对于优化查询性能至关重要。本文将详细介绍 MySQL 中的EXPLAIN命令提供的关键信息。 一、什么是 EXPLAIN 命令 EXPLAIN命令用于获取 MySQ…

Java多态(向上转型、动态绑定)+结合题目理解原理

第一次尝试使用markdowm写博客哈 文章目录 1.多态的引入2.重写和重载3.避免在构造方法里面去调用重写4.向上转型和向下转型5.让你真正明白什么是多态6.通过一些习题进行理解 1.多态的引入 首先说一下&#xff0c;这个想要使用多态需要我们满足的条件&#xff0c;然后具体的进行…

进程概念(冯诺依曼体系结构、操作系统、进程)-- 详解

目录 一、冯诺依曼体系结构1、概念2、硬件层面的数据流3、关于冯诺依曼的知识点强调4、CPU 工作原理5、补充&#xff08;CPU 和寄存器、高速缓存以及主存之间的关系&#xff09; 二、操作系统&#xff08;Operating System&#xff09;1、概念2、定位3、设计 OS 的目的4、如何理…