I/O多路复用是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪,就能通知应用进程进行相应的读写操作。select函数作为一种I/O多路复用的机制,程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态变化。
#include
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:
* nfsd是需要监视的最大文件描述符+1。
* readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合。是输入输出型参数,将我们关心的文件描述符添加进去,最后将我们关心的文件描述符上的就绪了的事件返回。
* 参数timeout为结构timeval,用来设置select()等待时间
timeout的取值:
* NULL:表示select没有timeout,直到某个文件描述符上发生了事件,属于阻塞式的等。
* 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生,属于非阻塞式的等。
* 特定的时间值:表示如果在指定的时间段内没有事件发生,select将超时返回。
fd_set结构,在头文件select.h中。
事实上,fd_set是一个整型数组,更严格的说,是一个“位图”,我们用位图中对应的位来表示要监视的文件描述符。
* 比特位置对应的是文件描述符值;
* 输入时,比特位表示是否关心该文件描述符,为0不关心,为1关心。输出时,表示该文件描述符上的事件是否就绪,为0不关心,为1关心。
测试了一下,我的服务器下fd_set是128个字节,最大的文件描述符为128*8=1024,其他平台可能不相同。
系统提供了一组操作fd_set的接口,使用起来较方便。
void FD_CLR(int fd, fd_set *set); //清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); //测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); //设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); //清除描述词组set的全部位
timeval结构
用于描述一段时间长度,如果在这个时间内,需要监视的文件描述符上没有事件发生,函数就返回,返回值为0.
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
select函数返回值
* 0:表示在描述词状态改变前,已超过timeout时间,没有返回。
* -1:发生错误返回,错误原因存于errno,参数值不可预测。
* 其他:执行成功,返回的是文件描述符状态已经改变的个数。
那么,select的过程是怎么样的呢?
(1)FD_ZERO(&set),则表示的是set(0000,0000);
(2)若fd=1,FD_SET(fd,&set),则set(0000,0001);
(3)加入fd=2,fd=5,则set(0001,0011);
(4)执行select(6,&set,0,0)阻塞等待;
(5)若fd=1,fd=2上发生可读事件,但是5上的没有发生可读事件,则select返回,此时的set为(0000,0011).
socket就绪条件
1.读就绪
* socket内核中,接收缓冲区中的字节数大于等于低水位SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0.
* socket TCP通信中,对端关闭连接,此时对该socket读,则返回0.
* 监听的socket上有新的连接请求
* socket上有未处理的错误。
2.写就绪
* socket内核中,发送缓冲区中的可用字节数,大于等于低水位SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0.
* socket的写操作被关闭,对于一个写操作被关闭的socket进行写操作,会出发SIGPIPE信号.
* socket使用非阻塞connect连接成功或失败之后。
* socket上有未读取的错误。
3.异常就绪
* socket上收到带外数据。
select的特点
* 我们会借助一个数组,将有效的文件描述符记录保存起来,以致于不会每次都要在fd_set中检查哪个文件描述符是我们所关心的。
* 每次调用select,都要重新将我们关心的文件描述符重新设置在数组中。
* 再select返回后,数组作为源数据和fd_set进行FDISSET判断。
* select一次等待多个文件描述符,性能相对多进程、多线程高。
select缺点
* 每次调用select,都需要重新设置fd_set,是输入输出型参数。
* 每次调用select,都需要将文件描述符从用户态拷贝到内核态,开销大。
* 每次调用,都需要在内核中遍历传递进来的所有fd,开销大。
* select支持的文件描述符数量有限制。
select服务器的实现(检测标准输出和标准输入)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_FDS sizeof(fd_set)*8
int start_up(int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock<0)
{
perror("socket");
exit(2);
}
//设置没有TIME_WAIT
int opt=1;
setsockopt(sock, SOL_SOCKET,SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(sock, (struct sockaddr*)&local, sizeof(local));
if(ret < 0)
{
perror("bind");
exit(3);
}
if(listen(sock, 5)<0)
{
perror("listen");
exit(4);
}
return sock;
}
//初始化
void Init(int arr[], int num)
{
int i=0;
for(i = 0;i < num; ++i)
{
arr[i]= -1;
}
}
int AddSockToArr(int arr[], int fd, int num)
{
int i;
for(i=0; iif(arr[i] < 0)
{
arr[i] = fd;
return i;
}
}
return -1;//full
}
int AddArrToFdSet(int arr[], int num, fd_set* rfds)
{
int max = -1;
int i;
for(i=0; iif(arr[i]>= 0)
{
FD_SET(arr[i], rfds);
if(arr[i] > max)
max = arr[i];
}
}
return max;
}
void serviceIO(int arr[], int num, fd_set* rfds)
{
int i;
for(i=0; iif(arr[i] > -1){
if(i==0&&FD_ISSET(arr[i],rfds)){
//listen sock is ready
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(arr[i], (struct sockaddr*)&client, &len);
if(sock < 0)
{
perror("accept");
continue;
}
printf("get a connet[%s:%d]\n",\
inet_ntoa(client.sin_addr),\
ntohs(client.sin_port));
if(AddSockToArr(arr, sock, num) == -1)
close(sock);
}
else if(i!=0 && FD_ISSET(arr[i],rfds)){
//normal sock
char buf[1024];
ssize_t ret = read(arr[i], buf, sizeof(buf)-1);
if(ret > 0){
buf[ret]=0;
printf("client >:%s\n",buf);
}
else if(ret == 0){
//读到文件末尾
close(arr[i]);
arr[i] = -1;
printf("client say: goodbye!\n");
}
else{
perror("read");
close(arr[i]);
arr[i] = -1;
}
}
else
{
//do nothing
}
}
}
}
//iport
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("Usag: ./server [port]\n");
return 1;
}
int listen_fd = start_up(atoi(argv[1]));
fd_set rfds;
int fd_arr[MAX_FDS];
Init(fd_arr,MAX_FDS );
AddSockToArr(fd_arr, listen_fd, MAX_FDS);
for(;;)
{
FD_ZERO(&rfds);
int max_fd = AddArrToFdSet(fd_arr, MAX_FDS, &rfds);
switch(select(max_fd+1, &rfds, NULL, NULL, NULL))
{
case -1:
{
perror("select");
break;
}
case 0:
{
printf("timeout...\n");
break;
}
default:
{
serviceIO(fd_arr, MAX_FDS, &rfds);
break;
}
}
}
}
利用telnet远程登录访问。
服务器端读到客户端的标准输入的数据,并显示在标准输出上。
我们也可以在浏览器上输入IP地址和端口号访问服务器,但是只能在同一局域网中检测。