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

这篇具有很好参考价值的文章主要介绍了Linux网络编程之TCP/IP实现高并发网络服务器设计指南。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

引言:

多进程服务器

例程分享:

多线程服务器

 例程分享:

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://www.toymoban.com/news/detail-828451.html

到了这里,关于Linux网络编程之TCP/IP实现高并发网络服务器设计指南的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Socket网络编程(TCP/IP)实现服务器/客户端通信。

    一.前言 回顾之前进程间通信(无名管道,有名管道,消息队列,共享内存,信号,信号量),都是在同一主机由内核来完成的通信。 那不同主机间该怎么通信呢? 可以使用Socket编程来实现。 Socket编程可以通过网络来实现实现不同主机之间的通讯。 二.Socket编程的网络模型如

    2024年02月08日
    浏览(89)
  • Linux socket网络编程实战(tcp)实现双方聊天

    在上节已经系统介绍了大致的流程和相关的API,这节就开始写代码! 回顾上节的流程: 创建一个NET文件夹 来存放网络编程相关的代码: 这部分先实现服务器的连接部分的代码并进行验证 server1.c: 代码验证: 先编译并运行这部分代码: 可见,此时没有客户端进行连接,程

    2024年02月03日
    浏览(50)
  • 【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)

    在前一篇文章中,我们详细介绍了UDP协议和TCP协议的特点以及它们之间的异同点。 本文将延续上文内容,重点讨论简单的TCP网络程序模拟实现 。通过本文的学习,读者将能够深入了解TCP协议的实际应用,并掌握如何编写简单的TCP网络程序。让我们一起深入探讨TCP网络程序的

    2024年04月14日
    浏览(86)
  • 《TCP IP网络编程》

            2023.6.28 正式开始学习网络编程。 每一章每一节的笔记都会记录在博客中以便复习。         网络编程又叫套接字编程。所谓网络编程,就是编写程序使两台连网的计算机相互交换数据。 为什么叫套接字编程? 我们平常将插头插入插座上就能从电网中获取电力,同

    2024年02月11日
    浏览(48)
  • 《TCP/IP网络编程》阅读笔记--基于Windows实现Hello Word服务器端和客户端

    目录 1--Hello Word服务器端 2--客户端 3--编译运行 3-1--编译服务器端 3-2--编译客户端 3-3--运行 运行结果:

    2024年02月10日
    浏览(65)
  • TCP/IP网络编程(三)

    多播(Multicast)方式的数据传输是 基于 UDP 完成的 。因此 ,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据 同时传递到加入(注册)特定组的大量主机 。换言之, 采用多播方式时,可以同时向多个主机传递数据 。 14.1.1 多

    2024年02月03日
    浏览(46)
  • TCP/IP网络编程(一)

    1.1.1 构建打电话套接字 以电话机打电话的方式来理解套接字。 **调用 socket 函数(安装电话机)时进行的对话:**有了电话机才能安装电话,于是就要准备一个电话机,下面函数相当于电话机的套接字。 **调用 bind 函数(分配电话号码)时进行的对话:**套接字同样如此。就想

    2024年02月03日
    浏览(52)
  • TCP/IP网络编程(二)

    本章将讨论如何优雅地断开相互连接的套接字。之前用的方法不够优雅是因为,我们是调用 close 或 closesocket 函数单方面断开连接的。 TCP中的断开连接过程比建立连接过程更重要,因为连接过程中一般不会出现大的变数,但断开过程有可能发生预想不到的情况,因此应准确掌

    2024年02月03日
    浏览(54)
  • [Linux] 网络编程 - 初见TCP套接字编程: 实现简单的单进程、多进程、多线程、线程池tcp服务器

    网络的上一篇文章, 我们介绍了网络变成的一些重要的概念, 以及 UDP套接字的编程演示. 还实现了一个简单更简陋的UDP公共聊天室. [Linux] 网络编程 - 初见UDP套接字编程: 网络编程部分相关概念、TCP、UDP协议基本特点、网络字节序、socket接口使用、简单的UDP网络及聊天室实现…

    2024年02月16日
    浏览(66)
  • TCP/IP网络编程(一) 理解网络编程和套接字

    网络编程和套接字概要 网络编程就是编写程序使两台联网的计算机相互交换数据 为了与远程计算机进行数据传输,需要连接因特网,而编程种的套接字就是用来连接该网络的工具。 构建套接字 1.调用soecket函数创建套接字 2.调用bind函数给套接字分配地址 3.调用listen函数将套

    2024年02月11日
    浏览(174)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包