Linux网络编程之TCP/IP实现高并发网络服务器设计指南

发布时间:2023年12月25日

目录

引言:

多进程服务器

例程分享:

多线程服务器

?例程分享:

I/O多路复用服务器

select

例程分享:

poll

例程分享:

epoll

例程分享:

总结建议


引言:

????????随着互联网的迅猛发展,服务器面临着越来越多的并发请求。如何设计一个能够高效处理大量并发请求的服务器成为了一个关键问题。本文将介绍几种常见的高并发服务器设计方案,包括多进程服务器、多线程服务器、I/O多路复用服务器和epoll服务器,并分析它们的优缺点,以便读者能够选择适合自己需求的设计方案。

多进程服务器

利用fork创建子进程处理每个连接请求。

优点:充分利用多核CPU的计算能力,隔离不同连接之间的资源。

?缺点:父进程需要设置较大的文件描述符限制,进程创建和切换开销较大。

相关API函数:fork、waitpid、socket、bind、listen、accept、read、write、close

实现要点:

  • 父进程close子进程socket,避免泄漏。
  • 信号处理回收子进程。
  • 每个子进程处理一个连接请求。

例程分享:

/*
服务器监听端口,接收客户端连接。
对每个连接fork子进程处理请求。
子进程循环接收客户端数据,转换大小写后返回。
父进程关闭连接socket,信号函数回收子进程。
客户端连接后循环发送接收数据。
使用多进程处理连接请求,充分利用多核CPU。
fork创建进程,waitpid和信号处理回收子进程。
父子进程同步处理,避免混乱。
*/

/* server.c */

#include <stdio.h>   // 标准IO头文件

#include <string.h>  // 字符串处理头文件

#include <netinet/in.h> // socket编程头文件

#include <arpa/inet.h> // IP地址转换头文件

#include <signal.h> // 信号处理头文件

#include <sys/wait.h> // 等待子进程头文件

#include <sys/types.h> // 数据类型头文件

#include "wrap.h" // socket函数封装头文件

#define MAXLINE 80  // 最大读写字节数

#define SERV_PORT 800 // 服务器端口号

// 信号处理函数,回收子进程
void do_sigchild(int num) {

	while (waitpid(0, NULL, WNOHANG) > 0);
}

int main(void) {

  // socket地址结构
  struct sockaddr_in servaddr, cliaddr;

  // 客户端地址长度

  socklen_t cliaddr_len;

  // 监听和连接socket
  int listenfd, connfd;

  // 数据缓冲区
  char buf[MAXLINE];

  // 客户端IP字符串

  char str[INET_ADDRSTRLEN];

  int i, n; // 循环变量和读字节数

  pid_t pid; // 进程ID

  // 安装信号处理函数
  struct sigaction newact;
  newact.sa_handler = do_sigchild;
  sigemptyset(&newact.sa_mask);
  newact.sa_flags = 0;
  sigaction(SIGCHLD, &newact, NULL);

  // 创建监听socket
  listenfd = Socket(AF_INET, SOCK_STREAM, 0);

  // 初始化服务器地址结构
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);

  // 绑定地址和端口

  Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

  // 设置监听队列长度
  Listen(listenfd, 20);

  // 循环接收客户端连接请求
  while (1) {

  Copy code

  cliaddr_len = sizeof(cliaddr);

  // 接收一个客户端连接
  connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);

  // Fork一个子进程处理连接
  pid = fork(); 

  if (pid == 0) {

    // 子进程关闭监听socket
    Close(listenfd);

    // 处理客户端请求
    while (1) {

      // 接收客户端数据 
      n = Read(connfd, buf, MAXLINE);

      // 判断客户端是否关闭
      if (n == 0) {
        printf("the other side has been closed.\n");
        break;
      }

      // 打印客户端信息
      printf("received from %s at PORT %d\n",  
              inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
              ntohs(cliaddr.sin_port));

      // 转换为大写
      for (i = 0; i < n; i++)
        buf[i] = toupper(buf[i]);

      // 发送转换后数据
      Write(connfd, buf, n);
    }

  // 关闭连接 
  Close(connfd);

  return 0;

} else if (pid > 0) {
  
  // 父进程关闭连接socket
  Close(connfd); 

} else
  perr_exit("fork");
}

// 关闭监听socket
Close(listenfd);

return 0;
}

/* client.c */

#include <stdio.h>

#include <string.h>

#include <unistd.h>

#include <netinet/in.h>

#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[]) {

  // 服务器地址结构
  struct sockaddr_in servaddr;

  // 数据缓冲区
  char buf[MAXLINE];

  // socket和读字节数
  int sockfd, n;

  // 创建socket
  sockfd = Socket(AF_INET, SOCK_STREAM, 0);

  // 初始化服务器地址结构
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
  servaddr.sin_port = htons(SERV_PORT);

  // 连接服务器
  Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

  // 循环发送接收数据
  while (fgets(buf, MAXLINE, stdin) != NULL) {

  Copy code

  // 发送数据到服务器
  Write(sockfd, buf, strlen(buf));  

  // 从服务器读取数据
  n = Read(sockfd, buf, MAXLINE);

  // 判断服务器是否关闭
  if (n == 0) {
    printf("the other side has been closed.\n");
    break;
  } else
    // 输出服务器返回数据
    Write(STDOUT_FILENO, buf, n);
  }

  // 关闭连接
  Close(sockfd);

  return 0;
}

多线程服务器

一个进程内创建线程处理每个连接请求。

?优点:高效利用多核CPU,创建和销毁线程开销较小。

?缺点:需要调整进程的文件描述符限制,需要进行线程同步,线程退出时需要进行资源清理。

?相关API函数:pthread_create、pthread_detach、pthread_join、pthread_mutex_init、pthread_mutex_lock、pthread_mutex_unlock、socket、bind、listen、accept、read、write、close

要点:

  • 调整进程文件描述符限制
  • 共享数据同步
  • 线程退出处理,防止资源泄漏
  • 过多线程会降低性能

?例程分享:

/*
服务器端创建监听套接字,绑定地址并监听。
主循环调用accept接收客户端连接,为每个客户端创建线程do_work处理请求。
do_work通过read接收客户端数据,转换为大写后返回。
客户端创建连接后,循环发送数据和读取服务器返回数据。
服务器使用线程处理每个连接请求,可以处理大量连接。
通过传递参数,线程可以获取客户端信息。
设置线程分离态,自动回收资源,实现高效的多线程服务器。
*/

/* server.c */

#include <stdio.h> // 标准输入输出头文件

#include <string.h> // 字符串处理头文件

#include <netinet/in.h> // socket编程头文件

#include <arpa/inet.h> // inet地址转换头文件

#include <pthread.h> // 线程编程头文件

#include "wrap.h" // 封装的socket函数头文件

#define MAXLINE 80 //最大读取字符数

#define SERV_PORT 6666 //服务器端口

// 用于传递给线程的客户信息
struct s_info {
struct sockaddr_in cliaddr; // 客户socket地址
int connfd; // 客户端连接套接字
};

// 线程处理函数
void *do_work(void *arg) {
  int n,i;
  struct s_info *ts = (struct s_info*)arg; // 获取传递的参数

  char buf[MAXLINE]; // 数据缓冲区
  char str[INET_ADDRSTRLEN]; // 存储socket地址的字符串

  // 设置线程为分离态,线程结束时自动释放资源
  pthread_detach(pthread_self());

  while (1) {
    // 获取客户端发送的数据
    n = Read(ts->connfd, buf, MAXLINE);  

    if (n == 0) { // 对端关闭连接
      printf("the other side has been closed.\n");
      break;
    }

    // 打印客户端信息
    printf("received from %s at PORT %d\n",
        inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
        ntohs((*ts).cliaddr.sin_port));

    // 将数据转换为大写
    for (i = 0; i < n; i++)
      buf[i] = toupper(buf[i]);

    // 发送转换后的数据
    Write(ts->connfd, buf, n);
  }

  // 关闭客户端连接
  Close(ts->connfd);
}

int main(void) {
  struct sockaddr_in servaddr, cliaddr; // 本地和客户端的socket地址
  socklen_t cliaddr_len; // 客户端socket地址长度
  int listenfd, connfd; // 监听和连接套接字
  int i = 0; 
  pthread_t tid; // 线程id
  struct s_info ts[256]; // 存储所有客户端信息的数组

  // 创建监听套接字
  listenfd = Socket(AF_INET, SOCK_STREAM, 0);

  // 初始化本地socket地址结构
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET; 
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);

  // 绑定监听套接字到本地地址
  Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

  // 设置监听队列长度
  Listen(listenfd, 20);

  printf("Accepting connections ...\n");

  // 循环接收客户端连接
  while (1) {  

    // 接收一个客户端连接
    cliaddr_len = sizeof(cliaddr);
    connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);

    // 保存客户信息
    ts[i].cliaddr = cliaddr;  
    ts[i].connfd = connfd;

    // 为客户端创建线程处理请求
    pthread_create(&tid, NULL, do_work, (void*)&ts[i]);

    i++;
  }

  return 0;
}

/* client.c */

#include <stdio.h>

#include <string.h>

#include <unistd.h>

#include <netinet/in.h>

#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[])
  struct sockaddr_in servaddr; // 服务器端地址结构

  char buf[MAXLINE]; // 数据缓冲区
  int sockfd, n; // 套接字和读返回值

  // 创建流式套接字
  sockfd = Socket(AF_INET, SOCK_STREAM, 0);

  // 初始化服务器地址结构
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); 
  servaddr.sin_port = htons(SERV_PORT);

  // 连接服务器
  Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

  // 循环发送数据和读取服务器返回数据
  while (fgets(buf, MAXLINE, stdin) != NULL) {   

    // 向服务器发送数据
    Write(sockfd, buf, strlen(buf));  

    // 从服务器读取数据
    n = Read(sockfd, buf, MAXLINE);

    // 判断服务器是否关闭
    if (n == 0)
      printf("the other side has been closed.\n");
    else
      // 输出服务器返回数据
      Write(STDOUT_FILENO, buf, n);
  }

  // 关闭socket连接
  Close(sockfd);
  return 0;
}

I/O多路复用服务器

select/poll/epoll使单线程可以同时处理多个连接请求。

select

优点: 可移植,使用简单。

缺点: 连接数受限,监听效率低。

要点:

  • select监听读写事件
  • 每次循环重置监听描述符
  • 根据返回就绪数遍历处理事件
  • 根据描述符状态处理连接关闭等
例程分享:
/*
服务器端初始化socket地址,创建监听套接字。
使用select()监听套接字可读事件。
调用accept()接收客户端连接请求。
添加新的连接到select监听的文件描述符集。
循环扫描就绪文件描述符,调用read()接收客户端数据。
对数据进行处理后,调用write()返回给客户端。
客户端创建连接后,循环read()和write()实现双向通信。
服务器使用select()处理多个连接,但依然是同步阻塞模型
*/

/* server.c */

/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "wrap.h" // 这是一个自定义的头文件,封装了socket函数

#define MAXLINE 80 // 缓冲区的最大长度
#define SERV_PORT 6666 // 服务器端口号

int main(int argc, char *argv[])
{
    int i, maxi, maxfd, listenfd, connfd, sockfd;
    int nready, client[FD_SETSIZE]; // FD_SETSIZE通常为1024
    ssize_t n;
    fd_set rset, allset; // 用于select()的文件描述符集
    char buf[MAXLINE]; // 数据缓冲区
    char str[INET_ADDRSTRLEN]; // 地址字符串缓冲区
    socklen_t cliaddr_len;
    struct sockaddr_in cliaddr, servaddr; // 客户端和服务器地址结构

    listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socket

    bzero(&servaddr, sizeof(servaddr)); // 清零服务器地址结构
    servaddr.sin_family = AF_INET; // 设置地址族为IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听任何接口
    servaddr.sin_port = htons(SERV_PORT); // 设置端口号

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 将socket绑定到地址

    Listen(listenfd, 20); // 监听连接,队列长度为20

    maxfd = listenfd; // 初始化maxfd
    maxi = -1; // 初始化maxi

    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1; // 初始化client[]

    FD_ZERO(&allset); // 清空allset
    FD_SET(listenfd, &allset); // 将listenfd添加到allset

    for ( ; ; ) { // 主服务器循环
        rset = allset; // 每次循环都重置rset
        nready = select(maxfd+1, &rset, NULL, NULL, NULL); // 调用select()

        if (nready < 0)
            perr_exit("select error"); // 如果select()返回错误,退出

        if (FD_ISSET(listenfd, &rset)) { // 如果有新的客户端正在连接
            cliaddr_len = sizeof(cliaddr);
            connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); // 接受新的连接

            printf("received from %s at PORT %d\n",
                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
                    ntohs(cliaddr.sin_port)); // 打印客户端的地址和端口

            for (i = 0; i < FD_SETSIZE; i++) {
                if (client[i] < 0) {
                    client[i] = connfd; // 将接受的文件描述符保存在client[]中
                    break;
                }
            }

            if (i == FD_SETSIZE) {
                fputs("too many clients\n", stderr); // 如果连接的客户端过多,打印错误并退出
                exit(1);
            }

            FD_SET(connfd, &allset); // 将新的文件描述符添加到allset

            if (connfd > maxfd)
                maxfd = connfd; // 如果需要,更新maxfd

            if (i > maxi)
                maxi = i; // 如果需要,更新maxi

            if (--nready == 0)
                continue; // 如果没有更多的就绪文件描述符,继续下一次循环
        }

        for (i = 0; i <= maxi; i++) { // 检查哪些客户端有数据准备好读取
            if ( (sockfd = client[i]) < 0)
                continue;

            if (FD_ISSET(sockfd, &rset)) {
                if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
                    Close(sockfd); // 如果客户端已经关闭了连接,也关闭服务器端
                    FD_CLR(sockfd, &allset); // 从allset中移除文件描述符
                    client[i] = -1; // 从client[]中移除文件描述符
                } else {
                    int j;
                    for (j = 0; j < n; j++)
                        buf[j] = toupper(buf[j]); // 将数据转换为大写
                    Write(sockfd, buf, n); // 将数据写回客户端
                }

                if (--nready == 0)
                    break; // 如果没有更多的就绪文件描述符,跳出循环
            }
        }
    }

    close(listenfd); // 关闭监听的socket
    return 0;
}

/* client.c */

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h" // 这是一个自定义的头文件,封装了socket函数

#define MAXLINE 80 // 缓冲区的最大长度
#define SERV_PORT 6666 // 服务器端口号

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr; // 服务器地址结构
    char buf[MAXLINE]; // 数据缓冲区
    int sockfd, n; // Socket文件描述符和读取的字节数

    sockfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socket

    bzero(&servaddr, sizeof(servaddr)); // 清零服务器地址结构
    servaddr.sin_family = AF_INET; // 设置地址族为IPv4
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); // 设置服务器的IP地址
    servaddr.sin_port = htons(SERV_PORT); // 设置端口号

    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 连接到服务器

    while (fgets(buf, MAXLINE, stdin) != NULL) { // 主客户端循环
        Write(sockfd, buf, strlen(buf)); // 将输入写入服务器
        n = Read(sockfd, buf, MAXLINE); // 读取服务器的响应

        if (n == 0)
            printf("the other side has been closed.\n"); // 如果服务器已经关闭了连接,打印一条消息
        else
            Write(STDOUT_FILENO, buf, n); // 将服务器的响应写入stdout
    }

    Close(sockfd); // 关闭socket
    return 0;
}

poll

优点: 没有连接数限制。

缺点: 依然轮询模型,效率低。

要点:

  • pollfd结构体监听事件
  • 每次循环遍历pollfd处理就绪事件
  • 根据返回事件和错误处理连接状态
例程分享:
/*
服务器端:
创建监听socket,绑定地址并监听
使用pollfd数组保存所有连接的文件描述符
调用poll监听socket上的事件,主要是POLLRDNORM读事件
当监听socket有事件时,表示有新连接,调用accept获取新连接
将新连接的文件描述符添加到pollfd数组中,继续监听读事件
当连接socket有读事件时,调用read读取客户端数据
对数据进行转换处理,调用write将数据返回给客户端
根据返回事件和错误情况判断连接是否正常
客户端:
创建socket,连接服务器地址
循环调用read读取用户输入
调用write将用户输入发送给服务器
调用read读取服务器返回数据
将服务器数据输出到标准输出
关闭连接
*/

/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h" // 这是一个自定义的头文件,封装了socket函数

#define MAXLINE 80 // 缓冲区的最大长度
#define SERV_PORT 6666 // 服务器端口号
#define OPEN_MAX 1024 // 最大的打开文件描述符数量

int main(int argc, char *argv[])
{
    int i, j, maxi, listenfd, connfd, sockfd;
    int nready;
    ssize_t n;
    char buf[MAXLINE], str[INET_ADDRSTRLEN];
    socklen_t clilen;
    struct pollfd client[OPEN_MAX]; // pollfd结构体数组,用于存储多个文件描述符
    struct sockaddr_in cliaddr, servaddr; // 客户端和服务器地址结构

    listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socket

    bzero(&servaddr, sizeof(servaddr)); // 清零服务器地址结构
    servaddr.sin_family = AF_INET; // 设置地址族为IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听任何接口
    servaddr.sin_port = htons(SERV_PORT); // 设置端口号

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 将socket绑定到地址

    Listen(listenfd, 20); // 监听连接,队列长度为20

    client[0].fd = listenfd;
    client[0].events = POLLRDNORM; // listenfd监听普通读事件

    for (i = 1; i < OPEN_MAX; i++)
        client[i].fd = -1; // 用-1初始化client[]里剩下元素
    maxi = 0; // client[]数组有效元素中最大元素下标

    for ( ; ; ) { // 主服务器循环
        nready = poll(client, maxi+1, -1); // 调用poll()函数,阻塞等待文件描述符就绪

        if (client[0].revents & POLLRDNORM) { // 有客户端链接请求
            clilen = sizeof(cliaddr);
            connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); // 接受新的连接

            printf("received from %s at PORT %d\n",
                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
                    ntohs(cliaddr.sin_port)); // 打印客户端的地址和端口

            for (i = 1; i < OPEN_MAX; i++) {
                if (client[i].fd < 0) {
                    client[i].fd = connfd; // 找到client[]中空闲的位置,存放accept返回的connfd
                    break;
                }
            }

            if (i == OPEN_MAX)
                perr_exit("too many clients"); // 如果连接的客户端过多,打印错误并退出

            client[i].events = POLLRDNORM; // 设置刚刚返回的connfd,监控读事件

            if (i > maxi)
                maxi = i; // 更新client[]中最大元素下标

            if (--nready <= 0)
                continue; // 没有更多就绪事件时,继续回到poll阻塞
        }

        for (i = 1; i <= maxi; i++) { // 检测client[]
            if ((sockfd = client[i].fd) < 0)
                continue;

            if (client[i].revents & (POLLRDNORM | POLLERR)) { // 如果有数据可读或者有错误发生
                if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) { // 当收到 RST标志时
                        /* connection reset by client */
                        printf("client[%d] aborted connection\n", i);
                        Close(sockfd); // 关闭socket
                        client[i].fd = -1; // 从client[]中移除文件描述符
                    } else {
                        perr_exit("read error"); // 如果读取错误,退出
                    }
                } else if (n == 0) {
                    /* connection closed by client */
                    printf("client[%d] closed connection\n", i);
                    Close(sockfd); // 关闭socket
                    client[i].fd = -1; // 从client[]中移除文件描述符
                } else {
                    for (j = 0; j < n; j++)
                        buf[j] = toupper(buf[j]); // 将数据转换为大写
                    Writen(sockfd, buf, n); // 将数据写回客户端
                }

                if (--nready <= 0)
                    break; // 如果没有更多的就绪文件描述符,跳出循环
            }
        }
    }
    return 0;
}

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h" // 这是一个自定义的头文件,封装了socket函数

#define MAXLINE 80 // 缓冲区的最大长度
#define SERV_PORT 6666 // 服务器端口号

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr; // 服务器地址结构
    char buf[MAXLINE]; // 数据缓冲区
    int sockfd, n; // Socket文件描述符和读取的字节数

    sockfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socket

    bzero(&servaddr, sizeof(servaddr)); // 清零服务器地址结构
    servaddr.sin_family = AF_INET; // 设置地址族为IPv4
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); // 设置服务器的IP地址
    servaddr.sin_port = htons(SERV_PORT); // 设置端口号

    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 连接到服务器

    while (fgets(buf, MAXLINE, stdin) != NULL) { // 主客户端循环
        Write(sockfd, buf, strlen(buf)); // 将输入写入服务器
        n = Read(sockfd, buf, MAXLINE); // 读取服务器的响应

        if (n == 0)
            printf("the other side has been closed.\n"); // 如果服务器已经关闭了连接,打印一条消息
        else
            Write(STDOUT_FILENO, buf, n); // 将服务器的响应写入stdout
    }

    Close(sockfd); // 关闭socket
    return 0;
}

epoll

优点:提高程序在大量并发连接中的系统CPU利用率,能够高效处理大量并发请求。

缺点:需要调整进程的文件描述符限制,需要进行连接管理。

要点:

  • epoll_create创建句柄
  • epoll_ctl注册和控制事件
  • epoll_wait等待就绪事件
  • 根据就绪事件处理请求
例程分享:
/*
服务器端:
服务器端的代码主要完成以下任务:

创建一个TCP socket并绑定到指定的IP地址和端口。
使用epoll创建一个事件监听列表,并将监听socket添加到这个列表中。
进入一个无限循环,使用epoll_wait()函数等待事件的发生。
当新的客户端连接时,接受连接并将新的socket添加到epoll的监听列表中。
当已连接的客户端发送数据时,读取数据,将数据转换为大写,然后将数据回写到客户端。
当客户端关闭连接时,从epoll的监听列表中移除这个socket,并关闭这个socket。
客户端端:
客户端的代码主要完成以下任务:

创建一个TCP socket并连接到服务器。
进入一个无限循环,从stdin读取输入,将输入写入服务器,然后读取服务器的响应并将响应写入stdout。
当服务器关闭连接时,打印一条消息并退出循环。
*/

/* server.c */
// ...省略部分代码...

// 创建一个TCP socket
listenfd = Socket(AF_INET, SOCK_STREAM, 0);

// 清零服务器地址结构
bzero(&servaddr, sizeof(servaddr));
// 设置地址族为IPv4
servaddr.sin_family = AF_INET;
// 监听任何接口
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 设置端口号
servaddr.sin_port = htons(SERV_PORT);

// 将socket绑定到地址
Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

// 监听连接,队列长度为20
Listen(listenfd, 20);

// 创建epoll实例
efd = epoll_create(OPEN_MAX);
if (efd == -1)
	perr_exit("epoll_create");

// 设置监听事件为EPOLLIN(可读事件),并将listenfd添加到epoll的监听列表中
tep.events = EPOLLIN; tep.data.fd = listenfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
if (res == -1)
	perr_exit("epoll_ctl");

// 主服务器循环
while (1) {
	// 调用epoll_wait()函数,阻塞等待文件描述符就绪
	nready = epoll_wait(efd, ep, OPEN_MAX, -1);
	if (nready == -1)
		perr_exit("epoll_wait");

	// 遍历就绪的文件描述符
	for (i = 0; i < nready; i++) {
		// 如果不是可读事件,跳过
		if (!(ep[i].events & EPOLLIN))
			continue;

		// 如果是新的客户端连接
		if (ep[i].data.fd == listenfd) {
			// 接受新的连接
			clilen = sizeof(cliaddr);
			connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
			printf("received from %s at PORT %d\n", 
					inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), 
					ntohs(cliaddr.sin_port));

			// 将新的socket添加到epoll的监听列表中
			tep.events = EPOLLIN; 
			tep.data.fd = connfd;
			res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
			if (res == -1)
				perr_exit("epoll_ctl");
		} else {
			// 如果是已连接的客户端发送的数据
			sockfd = ep[i].data.fd;
			n = Read(sockfd, buf, MAXLINE);
			if (n == 0) {
				// 如果客户端关闭了连接,从epoll的监听列表中移除这个socket,并关闭这个socket
				res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
				if (res == -1)
					perr_exit("epoll_ctl");

				Close(sockfd);
				printf("client[%d] closed connection\n", j);
			} else {
				// 如果接收到数据,将数据转换为大写,然后将数据回写到客户端
				for (j = 0; j < n; j++)
					buf[j] = toupper(buf[j]);
				Writen(sockfd, buf, n);
			}
		}
	}
}
// 关闭监听socket和epoll实例
close(listenfd);
close(efd);
return 0;

/* client.c */
// ...省略部分代码...

// 创建一个TCP socket
sockfd = Socket(AF_INET, SOCK_STREAM, 0);

// 清零服务器地址结构
bzero(&servaddr, sizeof(servaddr));
// 设置地址族为IPv4
servaddr.sin_family = AF_INET;
// 设置服务器的IP地址
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
// 设置端口号
servaddr.sin_port = htons(SERV_PORT);

// 连接到服务器
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

// 主客户端循环
while (fgets(buf, MAXLINE, stdin) != NULL) {
	// 将输入写入服务器
	Write(sockfd, buf, strlen(buf));
	// 读取服务器的响应
	n = Read(sockfd, buf, MAXLINE);
	if (n == 0)
		// 如果服务器已经关闭了连接,打印一条消息
		printf("the other side has been closed.\n");
	else
		// 将服务器的响应写入stdout
		Write(STDOUT_FILENO, buf, n);
}

// 关闭socket
Close(sockfd);
return 0;

总结建议

????????epoll服务器根据不同的需求和场景,我们可以选择不同的高并发服务器设计方案。多进程服务器、多线程服务器、I/O多路复用服务器和epoll服务器都有各自的优缺点和适用场景。通过分享的例程和相关API函数的介绍,读者可以更好地理解和选择适合自己需求的设计方案,从而高效处理大量并发请求,满足互联网快速发展的需求。

文章来源:https://blog.csdn.net/qq_58288010/article/details/135163214
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。