当前位置:首页 > 行业动态 > 正文

c 高并发服务器

高并发服务器指能同时处理大量请求的服务器,通过优化硬件、软件及架构实现高效响应。

在当今数字化时代,高并发服务器的需求日益增长,尤其是在处理大量用户请求的场景下,C语言作为一种高效、灵活的编程语言,常被用于构建高性能的服务器系统,下面将详细介绍如何使用C语言构建高并发服务器:

1、多进程模型

原理:通过创建多个子进程来处理客户端连接,每个子进程独立运行,拥有自己的地址空间,这样即使某个子进程出现问题,也不会影响到其他子进程和整个服务器的运行,父进程通常负责监听网络端口,接受客户端连接请求,然后将建立的连接分配给子进程进行处理。

示例代码

 #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <sys/types.h>
   #include <sys/socket.h>
   #include <netinet/in.h>
   #include <string.h>
   #define PORT 8080
   #define BACKLOG 10
   int main() {
       int server_fd, new_socket;
       struct sockaddr_in address;
       int addrlen = sizeof(address);
       // 创建套接字
       if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
           perror("socket failed");
           exit(EXIT_FAILURE);
       }
       // 绑定套接字到端口
       address.sin_family = AF_INET;
       address.sin_addr.s_addr = INADDR_ANY;
       address.sin_port = htons(PORT);
       if (bind(server_fd, (struct sockaddr )&address, sizeof(address))<0) {
           perror("bind failed");
           exit(EXIT_FAILURE);
       }
       // 开始监听是否有客户端连接
       if (listen(server_fd, BACKLOG) < 0) {
           perror("listen");
           exit(EXIT_FAILURE);
       }
       while(1) {
           // 接受客户端连接
           if ((new_socket = accept(server_fd, (struct sockaddr )&address, (socklen_t)&addrlen))<0) {
               perror("accept");
               exit(EXIT_FAILURE);
           }
           // 创建子进程来处理新的连接
           pid_t pid = fork();
           if (pid == 0) {  // 子进程
               close(server_fd);  // 子进程不需要监听套接字
               char buffer[1024] = {0};
               read(new_socket, buffer, 1024);
               printf("Message from client: %s
", buffer);
               char message = "Hello from server";
               send(new_socket, message, strlen(message), 0);
               close(new_socket);
               exit(0);
           } else if (pid < 0) {  // 出错情况
               perror("fork failed");
               exit(EXIT_FAILURE);
           } else {  // 父进程
               close(new_socket);  // 父进程不需要已连接的套接字
           }
       }
       return 0;
   }

优缺点

优点:实现相对简单,利用了操作系统对进程的管理和隔离机制,稳定性较高,在处理大量并发连接时,可以将负载分散到多个进程中,充分利用多核CPU资源。

缺点:进程间通信开销较大,创建和管理大量进程会消耗较多的系统资源,如内存和CPU时间,对于每个连接都需要创建一个新进程,当连接数非常多时,可能会受到系统资源的限制。

2、多线程模型

原理:与多进程类似,但所有线程共享同一个进程的地址空间,主线程负责监听端口,接受客户端连接请求,然后为每个连接创建一个新的线程来处理,由于线程之间共享数据空间,所以在进行数据共享和通信时更加高效。

示例代码

 #include <stdio.h>
   #include <stdlib.h>
   #include <pthread.h>
   #include <string.h>
   #include <sys/socket.h>
   #include <arpa/inet.h>
   #include <unistd.h>
   #define PORT 8080
   void client_handler(void socket_desc) {
       int sock = (int)socket_desc;
       char client_message[2000];
       // 接收客户端发送的消息
       if(recv(sock, client_message, 2000, 0) > 0) {
           printf("Client: %s
", client_message);
       }
       // 发送消息给客户端
       char message = "Hello from server";
       send(sock, message, strlen(message), 0);
       close(sock);
       free(socket_desc);
       return 0;
   }
   int main() {
       int socket_desc, client_sock, c;
       struct sockaddr_in server, client;
       pthread_t thread_id;
       // 创建套接字
       socket_desc = socket(AF_INET, SOCK_STREAM, 0);
       if (socket_desc == -1) {
           printf("Could not create socket");
       }
       puts("Socket created");
       // 准备sockaddr_in结构体
       server.sin_family = AF_INET;
       server.sin_addr.s_addr = INADDR_ANY;
       server.sin_port = htons(PORT);
       // 绑定
       if(bind(socket_desc, (struct sockaddr )&server, sizeof(server)) < 0) {
           perror("bind failed. Error");
           return 1;
       }
       puts("bind done");
       // 监听
       listen(socket_desc, 3);
       // 等待并接受连接
       puts("Waiting for incoming connections...");
       c = sizeof(struct sockaddr_in);
       while((client_sock = accept(socket_desc, (struct sockaddr )&client, (socklen_t)&c))) {
           puts("Connection accepted");
           pthread_t sniffer_thread;
           int new_sock = malloc(1);
           new_sock = client_sock;
           if(pthread_create(&sniffer_thread, NULL, client_handler, (void) new_sock) < 0) {
               perror("could not create thread");
               return 1;
           }
           pthread_detach(sniffer_thread);
       }
       if (client_sock < 0) {
           perror("accept failed");
           return 1;
       }
       return 0;
   }

优缺点

优点:线程创建和上下文切换的开销比进程小,可以更高效地利用CPU资源,共享内存使得线程间通信更加便捷,适合处理需要频繁共享数据的并发场景。

缺点:由于所有线程共享同一个进程的地址空间,一个线程的错误可能会导致整个进程崩溃,稳定性相对较差,过多的线程可能会导致竞争条件和死锁等问题,需要进行仔细的同步和互斥处理。

3、I/O多路复用模型(select、poll、epoll)

select模型

原理select函数可以监视多个文件描述符的状态变化,包括套接字,通过将多个套接字的文件描述符集合传递给select函数,它可以同时监测这些套接符是否有可读、可写或异常事件,当有事件发生时,select函数返回,程序可以根据返回的结果来确定哪些套接字有事件发生,并进行相应的处理。

示例代码

 #include <stdio.h>
     #include <sys/socket.h>
     #include <arpa/inet.h>
     #include <unistd.h>
     #include <string.h>
     #include <sys/select.h>
     #define PORT 8080
     #define MAX_CLIENTS 30
     int main() {
         int server_fd, new_socket;
         struct sockaddr_in address;
         int opt = 1;
         int addrlen = sizeof(address);
         char buffer[1024] = {0};
         fd_set readfds;
         // 创建套接字文件描述符
         if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
             perror("socket failed");
             exit(EXIT_FAILURE);
         }
         // 强制绑定套接字到端口8080
         if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
             perror("setsockopt");
             exit(EXIT_FAILURE);
         }
         address.sin_family = AF_INET;
         address.sin_addr.s_addr = INADDR_ANY;
         address.sin_port = htons(PORT);
         // 绑定套接字到端口8080
         if (bind(server_fd, (struct sockaddr )&address, sizeof(address))<0) {
             perror("bind failed");
             exit(EXIT_FAILURE);
         }
         // 开始监听是否有客户端连接
         if (listen(server_fd, 3) < 0) {
             perror("listen");
             exit(EXIT_FAILURE);
         }
         // 清空文件描述符集
         FD_ZERO(&readfds);
         // 将套接字描述符加入到文件描述符集中
         FD_SET(server_fd, &readfds);
         while(1) {
             // 等待文件描述符就绪,没有设置超时时间则一直等待
             int activity = select(FD_SETSIZE, &readfds, NULL, NULL, NULL);
             if ((activity < 0) && (errno!=EINTR)) {
                 perror("select error");
             }
             // 如果发现文件描述符为可读状态,则进行相应处理
             if (FD_ISSET(server_fd, &readfds)) {
                 if ((new_socket = accept(server_fd, (struct sockaddr )&address, (socklen_t)&addrlen))<0) {
                     perror("accept");
                     exit(EXIT_FAILURE);
                 }
                 // 打印客户端信息并清空缓冲区
                 printf("New connection, socket fd is %d, ip is : %s, port : %d 
", new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));
                  memset(buffer, 0, sizeof(buffer));
                 // 将新的套接字描述符加入到文件描述符集中,以便后续再次调用select函数时可以监视该套接字
                 FD_SET(new_socket, &readfds);
             } else {
                 // 遍历所有的文件描述符,检查是否有可读事件发生
                 for (int i = 0; i < FD_SETSIZE; i++) {
                     if (FD_ISSET(i, &readfds)) {
                         // 如果没有错误发生,并且套接字已经准备好读取数据,则读取数据并发送响应
                         if (read(i, buffer, sizeof(buffer)) > 0) {
                             printf("Client: %s
", buffer);
                             char message = "Hello from server";
                             send(i, message, strlen(message), 0);
                             memset(buffer, 0, sizeof(buffer));
                         } else {
                             close(i);
                             FD_CLR(i, &readfds);
                         }
                     }
                 }
             }
         }
         return 0;
     }

优缺点

优点:可以在一个线程内同时处理多个套接字的I/O事件,减少了线程上下文切换的开销,提高了性能,适用于处理大量并发连接且每个连接的数据交互量不大的场景。

缺点select函数有一个文件描述符数量的限制(一般为1024),无法处理大量的并发连接。select函数在每次调用时都需要重新设置文件描述符集,效率相对较低。

poll模型

原理pollselect的一个改进版本,它使用一个pollfd结构体数组来代替select中的文件描述符集,每个pollfd结构体包含一个文件描述符、一个事件标志和一个请求的事件类型,通过调用poll函数,可以同时监测多个文件描述符的事件,并根据返回的结果进行处理,与select相比,poll没有文件描述符数量的限制,并且不需要在每次调用前重新设置文件描述符集。

示例代码(与select模型类似,只需将相关函数替换为poll即可,此处不再赘述)

epoll模型(Linux特有)

原理epoll是一种高效的I/O多路复用技术,它基于事件驱动机制。epoll通过维护一个事件列表来管理多个文件描述符的状态变化,当调用epoll_wait函数时,它会阻塞直到有事件发生或者超时。epoll只会返回那些真正有事件发生的文件描述符,避免了不必要的遍历,提高了效率。epoll还支持边缘触发模式(ET模式)和水平触发模式(LT模式),可以根据具体需求进行选择。

示例代码(与select模型类似,只需将相关函数替换为epoll即可,此处不再赘述)

优缺点epoll的优点是在处理大量并发连接时性能非常出色,它的事件通知机制更加高效,不会像selectpoll那样随着文件描述符数量的增加而线性下降。epoll还支持多种事件类型和触发模式,具有很高的灵活性。epoll是Linux特有的技术,不具有跨平台性,如果需要在非Linux系统上使用类似的功能,可能需要采用其他技术或者库来实现。

以下是关于C语言高并发服务器的两个常见问题及解答:

问题1:多进程和多线程模型在高并发服务器中如何选择?

解答:选择多进程还是多线程模型取决于具体的应用场景和需求,如果对稳定性要求较高,希望各个进程之间相互隔离,避免一个进程的崩溃影响到其他进程,那么可以选择多进程模型,但如果对性能要求极高,需要更高效的资源共享和通信,且能够处理好线程同步和互斥等问题,多线程模型可能更适合,在一些对实时性要求较高的网络应用中,多线程可以减少上下文切换的开销,提高响应速度;而在一些需要严格隔离和稳定性的场景中,多进程则更为合适。

问题2:I/O多路复用模型中的epoll相比select和poll有什么优势?

解答:epoll相比select和poll具有以下优势,epoll没有文件描述符数量的限制,可以支持更多的并发连接,这对于处理大规模高并发场景非常重要,epoll只需要在事件发生时才返回有事件的文件描述符,避免了像select和poll那样对所有文件描述符进行轮询,提高了效率,epoll还支持边缘触发模式和水平触发模式,提供了更多的灵活性,epoll在内核层面的实现更加高效,能够更好地利用系统资源,因此在处理大量并发连接时性能更优。