什么是多路转接?什么是select?
多路转接就是一次等待多个文件描述符;
简单来说,select只做一件事,那就是等,等至少一个文件描述符的读写时间就绪。
具体来说,系统提供select来实现多路复用输入/输出模型。
select系统调用可以让程序监听多个文件描述符的状态变化。
程序会在select在这里等待,直到被监视的文件描述符至少有一个发生了状态改变。
select函数声明
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
参数解释:
- nfds:需要监视的最大文件描述符的值+1
- readfds:只关心写事件,可读文件描述符的集合
- writefds:只关心写事件,可写文件描述符的集合
- exceptfds:只关心异常事件,异常文件描述符的集合
- 参数timeout为结构timeval,用来设置select()的等待时间
fd_set的结构
fd_set就是一个整形数组,更严格的说,一个“位图”,位图中的位置代表对应的文件描述符,用0或1来控制。
但是,我们不能直接操作fd_set,而是要调用函数;
void FD_ZERO(fd_set& set); //清楚set的全部位
void FD_SET(int fd, fd_set& set) //把fd设置进set的相关位
void FD_CLR(int fd, fd_set& set) //把fd在set的相关位清楚
int FD_ISSET(int fd, fd_set& set); //判断fd是否被设置相关位
fd_set作为输入输出型参数:
- 输入时:用户想告诉内核,让OS帮用户关心那些文件描述符
- 输出时:内核告诉用户,那些文件描述符已就绪
timeout:用来设置等待时间,取值为:
NULL:表示select一直阻塞,知道某个文件描述符发生了时间;
0:非阻塞,不等待外部事件发生。
特定时间:如果在指定时间内没有时间发生,将立刻超时返回。
timeval的结构:
struct timeval
{
_time_t tv_sec;//秒数
_suseconds_t tv_usec;//毫秒
};
返回值解释:
执行成功则返回已经改变状态的文件描述符的个数;
返回0则表示在描述符状态改变前已超过timeout超时,没有返回;
返回-1表示发生了错误,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测;
错误值可能为:
- EBADF 文件描述符无效或该文件描述符已关闭
- EINTR 此调用被信号中断
- EINVAL 参数n为负值
- ENOMEM 核心内存不足
socket就绪条件
- 读就绪
sochet内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RECVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
监听的socket上有新的连接请求时
socket上有未处理的错误时
- 写就绪
socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记,SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
socket的写操作被关闭(close或者shutdown),对于一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
socket使用非阻塞connect连接成功或失败之后
socket上有未读取的错误
- 异常就绪
socket上收到带外数据(关于带外数据,和TCP紧急模式相关,在TCP报头中有一个紧急指针的字段)
简述select的执行过程
为了方便说明,假设fd_set的长度为1字节,则一字节最大可以对应8个文件描述符。
1、fd_set set; FD_ZERO(&set); 此时set为0000 0000;
2、若fd=5,执行FD_SET(&set);此时set变为0001 0000 (第5位置为1);
3、在加上 fd=1 和 fd=2 文件描述符,则set变为0001 0011;
4、执行select(6, &set, 0, 0,0) 阻塞等待;
5、若 fd=1 和 fd=2 发生了可读事件,则select返回,此时set变为0000 0011。注意:因为 fd=5 没有发生事件,所以对应位置被清空;
编写select代码
一、检测标准输入:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/time.h>
4 #include<sys/types.h>
5 int main()
6 {
7 //将0(标准输入)设置进read_fds
8 fd_set read_fds;
9 FD_ZERO(&read_fds);
10 FD_SET(0, &read_fds);
11
12 while(1){
13 printf("> ");
14 fflush(stdout);
15
16 //最大文件描述符为0,只关心读事件,阻塞等待
17 int ret = select(1, &read_fds, NULL, NULL, NULL);
18 if(ret < 0){
19 perror("select error\n");
20 continue;
21 }
22 if(FD_ISSET(0, &read_fds)){//若标准输入就绪
23 char buf[1024] = {0};
24 read(0, buf, sizeof(buf)-1);
25 printf("input:%s\n", buf);
26 }
27 else{
28 continue;
29 }
30 FD_ZERO(&read_fds);
31 FD_SET(0, &read_fds);
32 }
33 return 0;
34 }
此时的运行的效果为,找到在标准输入上输入之前,select都会一直阻塞等待。
二、模拟实现select服务器
服务器
#include<stdio.h>
2 #include<sys/socket.h>
3 #include<unistd.h>
4 #include<sys/time.h>
5 #include<sys/types.h>
6 #include<stdlib.h>
7 #include<arpa/inet.h>
8 #include<netinet/in.h>
9
10 int main(int argc, char* argv[])
11 {
12 //建立监听套接字
13 if(argc != 3){
14 printf("./server [ip] [port]\n");
15 return 1;
16 }
17 int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
18 if(listen_sock < 0){
19 perror("socker error\n");
20 return 2;
21 }
22 struct sockaddr_in local;
23 local.sin_family = AF_INET;
24 local.sin_addr.s_addr = inet_addr(argv[1]);
25 local.sin_port = htons(atoi(argv[2]));
26 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
27 perror("bind error\n");
28 return 3;
29 }
30 if(listen(listen_sock, 5) < 0){
31 perror("listen error\n");
32 return 4;
33 }
35 //数组的大小就是fd_set这个位图能够存多少个fd,因为需要用数组记录
36 int fdArray[sizeof(fd_set)*8];
37
38 //把listen_sock给数组的第一个元素
39 fdArray[0] = listen_sock;
40
41 int num = sizeof(fdArray)/sizeof(fdArray[0]);//最多存在1024个fd
42
43 //然后初始化数组为-1
44 for(int i=0; i<num; i++){
45 fdArray[i] = -1;
46 }
47
48 while(1){
49 fd_set rfds;//建立关心读位图
50 FD_ZERO(&rfds);//初始化位图
51 int maxfd = fdArray[0];//当前的最大fd为listen_sock
52
53 for(int i=0; i<num; i++){
54 if(fdArray[i] >= 0){
55 FD_SET(fdArray[i], &rfds);
56 //更新最大的fd
57 if(maxfd < fdArray[i])
58 maxfd = fdArray[i];
59 }
60 }
61 switch(select(maxfd+1, &rfds, NULL, NULL, NULL)){
62 case 0:
63 printf("超时!\n");
64 break;
65 case -1:
66 printf("出错!\n");
67 break;
68 default:
69 {
70 for(int i=0; i<num; i++){
71 if(fdArray[i] == -1)
72 continue;
73 //若fd已经就绪
74 if(FD_ISSET(fdArray[i], &rfds)){
75 //listen_sock就绪
76 if(FD_ISSET(fdArray[i], &rfds) && i==0){
77 //建立连接
78 struct sockaddr_in client;
79 socklen_t len = sizeof(client);
80 int new_sock = accept(listen_sock, (struct s
81 if(new_sock < 0){
82 perror("accept error!\n");
83 return 5;
84 }
85 //遇见-1就说明处理完了
86 for(int i=0; i<num; i++){
87 if(fdArray[i] == -1)
88 break;
89 }
90 //new_sock必须在范围之内
91 if(i < num)
92 fdArray[i] = new_sock;
93 else
94 continue;
95 }
96 }
97 //普通fd就绪
98 if(FD_ISSET(fdArray[i], &rfds)){
99 char buf[1024];
100 ssize_t s = read(fdArray[i], buf, sizeof(buf
101 if(s > 0){
102 buf[s] = 0;
103 printf("client#%s\n", buf);
104 }
105 else if(s == 0){
106 printf("client quit!\n");
107 close(fdArray[i]);
108 fdArray[i] = -1;
109 }
110 else{
111 break;
112 }
113 }
114 }
115 }
116 }
117 }
118 return 0;
119 }
客户端:
#include<sys/types.h>
5 #include<stdlib.h>
6 #include<arpa/inet.h>
7 #include<netinet/in.h>
8 #include<string.h>
9
10 int main(int argc, char* argv[])
11 {
12 if(argc != 3){
13 printf("./client [ip] [port]\n");
14 return 1;
15 }
16 struct sockaddr_in server;
17 server.sin_family = AF_INET;
18 server.sin_addr.s_addr = inet_addr(argv[1]);
19 server.sin_port = htons(atoi(argv[2]));
20
21 int fd = socket(AF_INET, SOCK_STREAM, 0);
22 if(fd < 0){
23 perror("socket error\n");
24 return 2;
25 }
26
27 int ret = connect(fd, (struct sockaddr*)&server, sizeof(server));
28 if(ret < 0){
29 perror("connect error\n");
30 return 3;
31 }
32
33 while(1){
34 printf("> ");
35 fflush(stdout);
36 char buf[1024] = {0};
37 read(0, buf, sizeof(buf)-1);
38
39 ssize_t s = write(fd, buf, strlen(buf));
40 if(s < 0){
41 perror("write error\n");
42 return 4;
43 }
44 }
45 close(fd);
46 return 0;
47 }
select的特点
- 可监控的文件描述符个数取决于sizeof(fd_set)的值,在我的服务器上sizeof(fd_set) = 128,每bit表示一个文件描述符,则我的服务器上支持的最大文件描述符个数是128*8 = 1024
- 将fd加入select监控集的同时,还需要在使用一个数据结构array保存放到select监控集中的fd
一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断
二是select返回后会把以前加入的但并没有事件发生的fd清空,则每次开始 select 前都要重新从array中取得fd逐一加入,扫描array的同时取得fd的最大值maxfd,用于select的第一个参数
select的缺点
- 每次调用select,都需要手动设置fd集合,从接口使用来说非常不方便
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符有上限