文章目录
一、多路复用IO
IO multiplexing 这个词可能有点陌生,但是提到 select/epoll,大概就都能明白了。有些地方
也称这种 IO 方式为事件驱动 IO(event driven IO)。我们都知道,select/epoll 的好处就在于单
个 process 就可以同时处理多个网络连接的 IO。它的基本原理就是 select/epoll 这个 function
会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。它的流
程如图:
当用户进程调用了 select,那么整个进程会被 block,而同时,kernel 会“监视”所
有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这
个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。
二、Select 接口介绍
(1) fd_set 结构体
fd_set是一种数据类型,在select函数中包含了3个参数,就是这个fd_set类型,fd_set也是理解select模型的关键.
fd_set 类型可以简单的理解为按 bit 位标记句柄的队列,例如要在某 fd_set 中标记一个值为 16 的句柄,则该 fd_set 的第 16 个 bit 位被标记为 1
typedef struct{
long int fds_bits[32];
}fd_set;
实际上 fd_set 就是一个long int类型的数组。因为每一位可以代表一个文件描述符。所以 fd_set 最多表示1024个文件描述符
(2) 四个操作fd_set 的宏
(a) FD_ZERO
FD_ZERO清除文件描述符集fdset中的所有位(既把所有位都设置为0)
void FD_ZERO(fd_set *fdset);
FD_ZERO(&clientSet);
(b) FD_SET
FD_SET设置文件描述符集fdset中对应于文件描述符fd的位(设置为1)
void FD_SET(int fd, fd_set *fdset);
© FD_CLR
FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为0)
void FD_CLR(int fd, fd_set *fdset);
(d) FD_ISSET
FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置
void FD_ISSET(int fd, fd_set *fdset);
(3) select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout)
-
int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
-
fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
-
fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
-
fd_set *errorfds 用来监视文件错误异常文件。
-
struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
-
函数返回
-
(1) 当监视的相应的文件描述符集中满足条件时,读文件描述符集中有数据到来时,内核(I/O)根据状态修改文件描述符集,并返回一个大于0 的数。
-
(2) 当没有满足条件的文件描述符,且设置的timeval 监控时间超时时,select函数会返回一个为0的值。
-
(3) 当select返回负值时,发生错误。
三、Select 服务端程序实现
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAXLNE 4096
int main(int argc, char **argv){
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
//create socket
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
//bind
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//listen
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
fd_set rfds,rset,wfds,wset;
FD_ZERO(&rfds);
FD_SET(listenfd,&rfds);
FD_ZERO(&wfds);
int max_fd = listenfd;
while(1){
// Select
rset = rfds;
wset = wfds;
int nready = select(max_fd+1, &rset, &wset, NULL, NULL);
// accept
if(FD_ISSET(listenfd,&rset)){
struct sockaddr_in client;
socklen_t len = sizeof(client);
if((connfd = accept(listenfd,(struct sockaddr *)&client,&len)) == -1){
printf("accept error: %s(error: %d\n)",strerror(errno),errno);
return 0;
}
// 对connfd 操作
FD_SET(connfd,&rfds);
if(connfd > max_fd) max_fd = connfd;
if(--nready == 0) continue;
}
int i = 0;
for(i = listenfd+1; i<=max_fd; i++){
if(FD_ISSET(i,&rset)){
n = recv(i,buff,MAXLNE,0);
if(n>0){
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
FD_SET(i, &wfds);
}else if (n == 0){
FD_CLR(i, &rfds);
close(i);
}
if (--nready == 0) break;
}else if(FD_ISSET(i,&wset)){
send(i,buff,n,0);
FD_SET(i,&rfds);
}
}
}
close(listenfd);
return 0;
}