面试题答案
一键面试1. 在Linux系统中C语言搭建TCP服务器实现并发处理多个客户端连接的方式
在Linux系统中,C语言搭建的TCP服务器实现并发处理多个客户端连接,常用的方式是使用I/O多路复用技术,包括select、poll、epoll等机制。这些机制允许服务器在一个线程内同时监听多个文件描述符(如套接字)的状态变化,当有描述符就绪(可读、可写或异常)时,程序可以进行相应处理,从而实现并发处理多个客户端连接。
2. select、poll、epoll原理
- select:
- 原理:select函数通过设置一组文件描述符集合(读、写、异常),然后阻塞等待这些描述符中的任意一个就绪。内核会遍历这些描述符集合,检查每个描述符的状态,当有描述符就绪时,select函数返回,应用程序通过遍历原描述符集合来判断哪些描述符就绪。
- 缺点:
- 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,可以通过修改宏定义或编译参数提升,但有一定局限性。
- 采用轮询的方式扫描文件描述符,时间复杂度为O(n),随着描述符数量的增加,性能会下降。
- 每次调用select函数时,都需要将文件描述符集合从用户态拷贝到内核态,开销较大。
- poll:
- 原理:poll与select类似,也是通过轮询的方式检查文件描述符的状态。但poll使用一个pollfd结构体数组来存储文件描述符及其事件,没有最大文件描述符数量的限制(仅受限于系统资源)。
- 缺点:
- 虽然没有了文件描述符数量的限制,但仍然采用轮询方式,时间复杂度同样为O(n),在高并发场景下性能不佳。
- 每次调用poll函数时,同样需要将结构体数组从用户态拷贝到内核态,开销较大。
- epoll:
- 原理:epoll是Linux 2.6内核提出的I/O多路复用机制,采用事件驱动方式。应用程序通过epoll_create创建一个epoll实例,通过epoll_ctl向内核注册需要监听的文件描述符及相应事件。当有事件发生时,内核会将就绪的事件复制到用户空间,应用程序通过epoll_wait获取这些就绪事件,而不需要像select和poll那样轮询所有描述符。
- 优点:
- 支持大量文件描述符,能轻松处理上万甚至更多的并发连接。
- 采用事件驱动机制,只有当文件描述符就绪时才会通知应用程序,时间复杂度为O(1),性能高效。
- 在内核态和用户态之间传递数据时,使用共享内存方式,减少了数据拷贝开销。
3. 适用场景
- select:适用于小规模并发场景,且对可监听文件描述符数量要求不高的情况,因为其实现简单,跨平台性较好。
- poll:适用于需要处理的文件描述符数量较多,但对性能要求不是极高的场景,它在一定程度上改进了select对文件描述符数量的限制。
- epoll:适用于高并发场景,尤其是需要处理大量并发连接且对性能要求极高的服务器程序,如Web服务器、游戏服务器等。
4. 使用epoll实现并发处理示例
以下是一个简单的使用epoll实现TCP服务器并发处理多个客户端连接的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int epoll_fd;
struct epoll_event event, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];
// 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 配置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字到地址
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 5) == -1) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 创建epoll实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1 failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 将服务器套接字添加到epoll实例
event.data.fd = server_fd;
event.events = EPOLLIN;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl: server_fd");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
while (1) {
// 等待事件发生
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait failed");
break;
}
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == server_fd) {
// 有新的客户端连接
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept failed");
continue;
}
// 将新客户端套接字添加到epoll实例
event.data.fd = client_fd;
event.events = EPOLLIN;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl: client_fd");
close(client_fd);
}
} else {
// 客户端有数据可读
client_fd = events[i].data.fd;
int read_bytes = recv(client_fd, buffer, sizeof(buffer), 0);
if (read_bytes <= 0) {
// 客户端关闭连接
if (read_bytes == 0) {
printf("Client disconnected\n");
} else {
perror("recv failed");
}
// 从epoll实例中移除客户端套接字
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
close(client_fd);
} else {
buffer[read_bytes] = '\0';
printf("Received: %s\n", buffer);
// 回显数据给客户端
send(client_fd, buffer, read_bytes, 0);
}
}
}
}
// 关闭文件描述符
close(server_fd);
close(epoll_fd);
return 0;
}
上述代码实现了一个简单的TCP服务器,使用epoll机制处理多个客户端连接。服务器创建一个监听套接字,通过epoll监听新连接和客户端数据可读事件。当有新连接时,将新客户端套接字添加到epoll实例;当客户端有数据可读时,读取数据并回显给客户端。如果客户端关闭连接,将其从epoll实例中移除并关闭套接字。