一、IO多路复用
IO多路复用允许进程同时检查多个文件描述符,检查其中任意一个是否可以执行IO操作。有两个功能几乎相同的系统调用来执行IO多路复用,一是select,二是poll,历史上select的使用更为广泛。这两个系统调用可以检查普通文件、终端、伪终端、管道、FIFO、套接字等,允许进程要么一直等待文件描述符成为就绪态,要么指定一个超时时间。这里简单记录下select的使用
二、select调用
#include <sys/time.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//成功返回就绪的文件描述符数, 返回0表示超时,-1错误
参数readfds, writefds, exceptfds都是指向文件描述符的集合,按如下方式使用:
- readfds是用来检测输入是否就绪的文件描述符集合
- writefds是用来检测输出是否就绪的文件描述符集合
- exceptsfds是用来检测异常情况是否发生的文件描述集合
通常,fd_set数据类型使用四个宏来操作:FD_ZERO, FD_SET, FD_CLR, FD_ISSET
#include <sys/select.h>
//将fdset指向的集合初始化为空
void FD_ZERO(fd_set *fdset);
//将文件描述符fd添加到集合fdset中
void FD_SET(int fd, fd_set *fdset);
//将文件描述符fd从集合fdset中删除
void FD_CLR(int fd, fd_set *fdset);
//判断文件描述符fd是否在集合fdset中
int FD_ISSET(int fd, fd_set *fdset);
注意事项:
- 文件描述符集合有一个最大容量限制,由常量FD_SETSIZE决定,在linux上值为1024,select实现可以处理更大的文件描述符集合,但是没有简单方法可以修改FD_SETSIZE,必须修改glibc头文件中的定义。并且由于select的性能延展性不佳,超过1024个文件描述符时,应考虑使用epoll接口。
- 参数readfds, writefds, exceptfds所指向的结构体都是保存结果值的地方。在调用select前,必须使用FD_ZERO和FD_SET初始化,以包含感兴趣的文件描述符。select会修改这些集合,返回时,它们包含的就是已处于就绪态的文件描述符了。
- 由于readfds, writefds, exceptfds所指向的结构体会被select修改,所以在循环中重复调用select时,必须保证每次都要重新初始化
- 如果对某一类型事件不感兴趣,可将对应文件描述符集合置为NULL
- 参数nfds需设为比3个文件描述符集合中包含的最大的文件描述符号还要大1,这样select不会去检查大于nfds的文件描述符,使之更有效率。
关于select的返回值:
- 返回-1时表示有错误发生,可能的错误包括EBADF和EINTR,EBADF表示参数中有非法文件描述符(例如文件描述符并没有打开)。EINTR表示被信号处理器中断
- 返回0表示在还没有文件描述符成为就绪态前就超时了
- 返回一个正整数表示有一个或多个文件描述符成为就绪态。如果一个文件描述符在三个参数中都指定了,可能会被统计多次。
三、示例程序
示例程序包含两部分代码,一个是服务器,一个是客户端。使用TCP协议。服务端使用select实现并发通信。
server
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <sys/time.h>
#include <sys/select.h>
#include <set>
using namespace std;
#define PORT 60000
#define BUF_SIZE 5
int main()
{
int listenfd = 0;
int maxfd = 0;
set<int> fd_arr;
struct sockaddr_in sockaddr;
memset(&sockaddr, 0, sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
sockaddr.sin_port = htons(PORT);
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0)
{
printf("Create socket failed.\n");
return -1;
}
if(bind(listenfd, (struct sockaddr *) &sockaddr, sizeof(sockaddr)) < 0)
{
printf("bind failed.\n");
return -1;
}
if(listen(listenfd, 5) < 0)
{
printf("listen failed.\n");
return -1;
}
fd_set fds;
FD_ZERO(&fds);
FD_SET(listenfd, &fds);
maxfd = listenfd;
fd_arr.insert(listenfd);
for(;;)
{
fd_set readfds = fds;
int ret = select(maxfd + 1, &readfds, NULL, NULL, NULL);
if(ret < 0)
{
printf("select failed.\n");
}
else if(ret == 0)
{
printf("select timeout.\n");
}
else
{
//listenfd就绪,说明有新客户端连接
if(FD_ISSET(listenfd, &readfds))
{
int connfd = accept(listenfd, NULL, 0);
if(connfd < 0)
{
printf("accept failed.\n");
continue;
}
else
{
printf("New incoming connection, fd = %d\n", connfd);
FD_SET(connfd, &fds);
if(connfd > maxfd)
{
maxfd = connfd;
}
fd_arr.insert(connfd);
}
}
else
{
maxfd = 0;
for(int fd : fd_arr)
{
//除listenfd外的其它fd就绪,说明可以进行IO操作
if(FD_ISSET(fd, &readfds))
{
//READ
char buf[BUF_SIZE + 1];
int num = recv(fd, buf, BUF_SIZE, 0);
//读取的字节数小于等于0,说明客户端断开了
if(num <= 0)
{
printf("client offline.\n");
FD_CLR(fd, &fds);
fd_arr.erase(fd);
close(fd);
continue;
}
//这里设置的BUF_SIZE较小,若一次不能读取完,下次select会直接返回,因为还有未读数据,该fd仍为就绪态
buf[num] = '\0';
printf("read from %d, content = %s\n", fd, buf);
}
if(fd > maxfd)
{
maxfd = fd;
}
}
}
}
}
close(listenfd);
return 0;
}
client
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <sstream>
#define MAXLINE 10
int main(int argc,char **argv)
{
char *servInetAddr = "127.0.0.1";
int socketfd;
struct sockaddr_in sockaddr;
char recvline[MAXLINE], sendline[MAXLINE];
int n;
socketfd = socket(AF_INET,SOCK_STREAM,0);
memset(&sockaddr,0,sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(60000);
inet_pton(AF_INET,servInetAddr,&sockaddr.sin_addr);
if((connect(socketfd,(struct sockaddr*)&sockaddr,sizeof(sockaddr))) < 0 )
{
printf("connect error %s errno: %d\n",strerror(errno),errno);
exit(0);
}
for(;;)
{
printf("send message to server\n");
std::string msg;
std::cin>>msg;
if(msg == "exit")
{
printf("Got exit, close client socket.\n");
break;
}
else{
if((send(socketfd,msg.c_str(),msg.size(),0)) < 0)
{
printf("send mes error: %s errno : %d",strerror(errno),errno);
exit(0);
}
}
}
close(socketfd);
printf("exit\n");
exit(0);
}
结果: