select()
是 Unix 和类 Unix 系统中用于实现非阻塞 I/O 多路复用的一种机制。它允许程序同时监控多个文件描述符的 I/O 活动,如套接字、管道等。当其中一个文件描述符准备好进行读取或写入时,select()
函数会返回,从而让程序可以处理这个活动的文件描述符。下面详细介绍 select()
的概念、用途、API 以及示例代码。
概念
select()
是一个系统调用,它允许程序同时监控多个文件描述符的读、写或异常条件。当其中一个或多个文件描述符变为可读、可写或发生异常时,select()
函数会返回。这样,程序就可以处理这些活动的文件描述符。
用途
select()
主要用于以下场景:
- 网络编程:监控套接字上的连接请求和数据到达。
- 多任务处理:处理多个并发连接或事件。
- I/O 多路复用:同时监控多个文件描述符的 I/O 活动,提高程序的效率和响应速度。
API
select()
的基本形式如下:
1#include <sys/select.h>
2
3int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
-
参数解释:
nfds
:监控文件描述符的最高值加一。readfds
:指向一个fd_set
结构体的指针,表示要监控的可读文件描述符集合。writefds
:指向一个fd_set
结构体的指针,表示要监控的可写文件描述符集合。exceptfds
:指向一个fd_set
结构体的指针,表示要监控的异常条件文件描述符集合。timeout
:指向一个struct timeval
结构体的指针,表示超时时间。如果为NULL
,则select()
将无限期地等待直到有一个文件描述符变得可读或可写。
-
返回值:
- 如果有一个或多个文件描述符变得可读、可写或出现异常,则返回大于零的整数。
- 如果没有文件描述符变得可读、可写或出现异常,并且设置了超时时间,则返回 0。
- 如果出现错误,则返回 -1。
示例代码
下面是一个简单的 select()
示例,演示了如何使用 select()
来监控一个套接字的连接请求和数据接收。
服务器端 (server.c)
1#include <sys/socket.h>
2#include <sys/select.h>
3#include <netinet/in.h>
4#include <arpa/inet.h>
5#include <stdio.h>
6#include <stdlib.h>
7#include <unistd.h>
8#include <string.h>
9
10#define PORT 8080
11#define BUFFER_SIZE 1024
12
13int main() {
14 int server_sock, client_sock;
15 struct sockaddr_in addr;
16 fd_set read_fds;
17 struct timeval timeout;
18
19 // 创建 TCP 套接字
20 server_sock = socket(AF_INET, SOCK_STREAM, 0);
21 if (server_sock == -1) {
22 perror("socket");
23 exit(EXIT_FAILURE);
24 }
25
26 // 设置地址重用
27 int optval = 1;
28 setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
29
30 // 清空地址结构
31 memset(&addr, 0, sizeof(addr));
32 addr.sin_family = AF_INET;
33 addr.sin_port = htons(PORT);
34 addr.sin_addr.s_addr = INADDR_ANY;
35
36 // 绑定套接字
37 if (bind(server_sock, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
38 perror("bind");
39 exit(EXIT_FAILURE);
40 }
41
42 // 监听连接
43 if (listen(server_sock, 5) == -1) {
44 perror("listen");
45 exit(EXIT_FAILURE);
46 }
47
48 FD_ZERO(&read_fds); // 初始化文件描述符集合
49 FD_SET(server_sock, &read_fds); // 添加监听套接字
50
51 while (1) {
52 fd_set tmp_fds = read_fds; // 创建一个副本,避免被修改
53 int max_fd = server_sock;
54
55 // 设置超时时间
56 timeout.tv_sec = 5;
57 timeout.tv_usec = 0;
58
59 int ret = select(max_fd + 1, &tmp_fds, NULL, NULL, &timeout);
60 if (ret == -1) {
61 perror("select");
62 continue;
63 }
64 if (ret == 0) {
65 printf("Timeout occurred\n");
66 continue;
67 }
68
69 if (FD_ISSET(server_sock, &tmp_fds)) {
70 // 接受连接
71 socklen_t len = sizeof(addr);
72 client_sock = accept(server_sock, (struct sockaddr *)&addr, &len);
73 if (client_sock == -1) {
74 perror("accept");
75 continue;
76 }
77 FD_SET(client_sock, &read_fds); // 添加客户端套接字到集合
78 if (client_sock > max_fd)
79 max_fd = client_sock;
80 }
81
82 for (int i = 0; i <= max_fd; i++) {
83 if (FD_ISSET(i, &tmp_fds)) {
84 if (i == server_sock) {
85 // 已经处理过监听套接字
86 continue;
87 }
88
89 // 接收数据
90 char buf[BUFFER_SIZE];
91 ssize_t bytes_received = recv(i, buf, BUFFER_SIZE, 0);
92 if (bytes_received == -1) {
93 perror("recv");
94 continue;
95 }
96 if (bytes_received == 0) {
97 // 客户端断开连接
98 printf("Client disconnected: %d\n", i);
99 close(i);
100 FD_CLR(i, &read_fds);
101 continue;
102 }
103
104 // 输出收到的数据
105 printf("Received: %.*s\n", (int)bytes_received, buf);
106
107 // 发送响应
108 const char *response = "Hello, Client!";
109 if (send(i, response, strlen(response), 0) == -1) {
110 perror("send");
111 continue;
112 }
113 }
114 }
115 }
116
117 // 关闭套接字
118 close(server_sock);
119
120 return 0;
121}
编译和运行
为了编译上述代码,你可以使用以下命令:
1gcc -o server server.c
然后运行服务器:
1./server
服务器将监听端口 8080 上的连接。您可以使用 telnet 或 netcat 客户端连接到此服务器。
注意事项
select()
的最大文件描述符数量限制为FD_SETSIZE
(通常是 1024)。select()
可能会因为信号而提前返回,即使没有任何文件描述符变为可读或可写。- 在实际应用中,可能需要处理更复杂的错误情况,比如处理连接失败的情况。
- 使用
select()
时,需要确保文件描述符集合在调用期间不会被其他线程或信号处理程序修改。 select()
可能不是最高效的多路复用技术,尤其是在高并发场景下,可以考虑使用epoll
或kqueue
。
select()
是一种基本的多路复用技术,它在许多场景中都非常有用,特别是对于需要处理多个并发连接的应用程序。然而,在高负载情况下,select()
的性能可能会受限于 FD_SETSIZE
的限制,这时可以考虑使用更先进的多路复用技术,如 epoll
或 kqueue
。