面试题答案
一键面试select多路复用模型工作原理
select 是一种 I/O 多路复用技术,用于在多个文件描述符(如套接字)上等待事件发生。其原理如下:
- 初始化:将需要监听的文件描述符集合(如套接字描述符)添加到
fd_set
结构体中。 - 调用
select
函数:该函数会阻塞,直到指定的文件描述符集合中有一个或多个文件描述符上有事件发生(如可读、可写或异常),或者超时。 - 检查结果:
select
函数返回后,通过检查fd_set
结构体中哪些文件描述符被标记为就绪,来确定哪些文件描述符上发生了事件。
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/time.h>
#define PORT 8080
#define MAX_CLIENTS 10
int main(int argc, char const *argv[]) {
int server_fd, new_socket, valread;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项
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);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听套接字
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
fd_set read_fds;
fd_set tmp_fds;
FD_ZERO(&read_fds);
FD_ZERO(&tmp_fds);
FD_SET(server_fd, &read_fds);
int activity, new_socket_fd;
while (1) {
tmp_fds = read_fds;
activity = select(FD_SETSIZE, &tmp_fds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
printf("select error");
} else if (activity) {
if (FD_ISSET(server_fd, &tmp_fds)) {
if ((new_socket_fd = 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 \n", new_socket_fd, inet_ntoa(address.sin_addr), ntohs(address.sin_port));
FD_SET(new_socket_fd, &read_fds);
}
for (int i = 0; i < FD_SETSIZE; i++) {
if (FD_ISSET(i, &tmp_fds)) {
if (i != server_fd) {
valread = read(i, buffer, 1024);
if (valread == 0) {
getpeername(i, (struct sockaddr *)&address, (socklen_t *)&addrlen);
printf("Host disconnected, ip %s, port %d \n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
close(i);
FD_CLR(i, &read_fds);
} else {
buffer[valread] = '\0';
printf("Message from client %d : %s \n", i, buffer);
}
}
}
}
}
}
return 0;
}
fd_set
结构体及相关函数
fd_set
结构体:fd_set
是一个用于存储文件描述符集合的数据结构。它本质上是一个位图,每一位对应一个文件描述符,通过设置或清除相应的位来表示某个文件描述符是否在集合中。FD_SET
函数:用于将指定的文件描述符添加到fd_set
集合中。- 函数原型:
void FD_SET(int fd, fd_set *set);
- 示例:
FD_SET(server_fd, &read_fds);
将server_fd
添加到read_fds
集合中。
- 函数原型:
FD_CLR
函数:用于从fd_set
集合中移除指定的文件描述符。- 函数原型:
void FD_CLR(int fd, fd_set *set);
- 示例:
FD_CLR(new_socket_fd, &read_fds);
从read_fds
集合中移除new_socket_fd
。
- 函数原型:
FD_ISSET
函数:用于检查指定的文件描述符是否在fd_set
集合中。- 函数原型:
int FD_ISSET(int fd, fd_set *set);
- 示例:
if (FD_ISSET(server_fd, &tmp_fds)) {... }
检查server_fd
是否在tmp_fds
集合中。
- 函数原型:
FD_ZERO
函数:用于清空fd_set
集合,即将所有位都设置为 0。- 函数原型:
void FD_ZERO(fd_set *set);
- 示例:
FD_ZERO(&read_fds);
清空read_fds
集合。
- 函数原型: