IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
LInux下的五种IO模型
[1] blocking IO - 阻塞IO
[2] nonblocking IO - 非阻塞IO
[3] IO multiplexing - IO多路复用
[4] signal driven IO - 信号驱动IO
[5] asynchronous IO - 异步IO
其中前面4种IO都可以归类为synchronous IO - 同步IO,而select、poll、epoll本质上也都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:
#include
#include
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2 maxfdp1-1均将被测试。因为文件描述符是从0开始的。
中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
返回值:就绪描述符的数目,超时返回0,出错返回-1
server:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXBUF 1024
int main(int argc, char **argv)
{
int sockfd, new_fd;
socklen_t len;
struct sockaddr_in my_addr, their_addr;
unsigned int myport, lisnum;
char buf[MAXBUF + 1];
fd_set rfds;
struct timeval tv;
int retval, maxfd = -1;
if (argv[1])
myport = atoi(argv[1]);
else
myport = 7838;
if (argv[2])
lisnum = atoi(argv[2]);
else
lisnum = 2;
// create an new socket
if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = PF_INET;
my_addr.sin_port = htons(myport);
if (argv[3])
my_addr.sin_addr.s_addr = inet_addr(argv[3]);
else
my_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
exit(1);
}
if (listen(sockfd, lisnum) == -1)
{
perror("listen");
exit(1);
}
while (1)
{
printf("\n----等待新的连接到来开始新一轮聊天……\n");
len = sizeof(struct sockaddr);
/*接受新连接并打印*/
if ((new_fd = accept(sockfd, (struct sockaddr *) &their_addr, &len)) == -1)
{
perror("accept");
exit(errno);
}
else
{
printf("server: got connection from %s, port %d, socket %d\n", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);
}
// 开始处理每个新连接上的数据收发
printf("\n准备就绪,可以开始聊天了……直接输入消息回车即可发信息给对方\n");
while (1)
{
// 把集合清空 每一次轮询开始都要把fd_set清空
FD_ZERO(&rfds);
// 把标准输入(stdin)句柄0加入到集合中
FD_SET(0, &rfds);
// 把当前连接(socket)句柄new_fd加入到集合中
FD_SET(new_fd, &rfds);
maxfd = 0;
if (new_fd > maxfd)
{
maxfd = new_fd;
}
// 设置最大等待时间
tv.tv_sec = 5;
tv.tv_usec = 0;
// 开始等待
retval = select(maxfd + 1, &rfds, NULL, NULL, &tv);
if (retval == -1)
{
printf("将退出,select出错! %s", strerror(errno));
break;
}
else if (retval == 0)
{
printf("没有任何消息到来,用户也没有按键,继续等待……\n");
continue;
}
else
{
// 判断当前IO是否是stdin
if (FD_ISSET(0, &rfds)) // 用户按键了,则读取用户输入的内容发送出去
{
bzero(buf, MAXBUF + 1);
fgets(buf, MAXBUF, stdin);
if (!strncasecmp(buf, "quit", 4))
{
printf("自己请求终止聊天!\n");
break;
}
len = send(new_fd, buf, strlen(buf) - 1, 0);
if (len > 0)
printf("消息:%s\t发送成功,共发送了%d个字节!\n", buf, len);
else
{
printf("消息'%s'发送失败!错误代码是%d,错误信息是'%s'\n", buf, errno, strerror(errno));
break;
}
}
// 判断当前IO是否是来自socket
if (FD_ISSET(new_fd, &rfds)) // 当前连接的socket上有消息到来则接收对方发过来的消息并显示
{
bzero(buf, MAXBUF + 1);
// 接收客户端的消息
len = recv(new_fd, buf, MAXBUF, 0);
if (len > 0)
{
printf("接收消息成功:'%s',共%d个字节的数据\n", buf, len);
}
else
{
if (len < 0)
printf("消息接收失败!错误代码是%d,错误信息是'%s'\n", errno, strerror(errno));
else
printf("对方退出了,聊天终止\n");
break;
}
}
}
}
close(new_fd);
// 处理每个新连接上的数据收发结束
printf("还要和其它连接聊天吗?(no->退出)");
fflush(stdout);
bzero(buf, MAXBUF + 1);
fgets(buf, MAXBUF, stdin);
if (!strncasecmp(buf, "no", 2))
{
printf("终止聊天!\n");
break;
}
}
close(sockfd);
return 0;
}
client:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXBUF 1024
int main(int argc, char **argv)
{
int sockfd, len;
struct sockaddr_in dest;
char buffer[MAXBUF + 1];
fd_set rfds;
struct timeval tv;
int retval, maxfd = -1;
if (argc != 3)
{
printf("参数格式错误!正确用法如下:\n\t\t%s IP地址 端口\n\t比如:\t%s 127.0.0.1 80\n此程序用来从某个 IP 地址的服务器某个端口接收最多 MAXBUF 个字节的消息", argv[0], argv[0]);
exit(0);
}
// 创建一个 socket 用于 tcp 通信
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("Socket");
exit(errno);
}
// 初始化服务器端(对方)的地址和端口信息
bzero(&dest, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(atoi(argv[2]));
if (inet_aton(argv[1], (struct in_addr *) &dest.sin_addr.s_addr) == 0)
{
perror(argv[1]);
exit(errno);
}
// 连接服务器
if (connect(sockfd, (struct sockaddr *) &dest, sizeof(dest)) != 0)
{
perror("Connect ");
exit(errno);
}
printf("\n准备就绪,可以开始聊天了……直接输入消息回车即可发信息给对方\n");
while (1)
{
// 把集合清空
FD_ZERO(&rfds);
// 把标准输入句柄0加入到集合中
FD_SET(0, &rfds);
maxfd = 0;
// 把当前连接句柄sockfd加入到集合中
FD_SET(sockfd, &rfds);
if (sockfd > maxfd)
maxfd = sockfd;
// 设置最大等待时间
tv.tv_sec = 3;
tv.tv_usec = 0;
// 开始等待
retval = select(maxfd + 1, &rfds, NULL, NULL, &tv);
if (retval == -1)
{
printf("将退出,select出错! %s", strerror(errno));
break;
}
else if (retval == 0)
{
/* printf("没有任何消息到来,用户也没有按键,继续等待……\n"); */
continue;
}
else
{
if (FD_ISSET(sockfd, &rfds)) // 连接的socket上有消息到来则接收对方发过来的消息并显示
{
bzero(buffer, MAXBUF + 1);
// 接收对方发过来的消息,最多接收 MAXBUF 个字节
len = recv(sockfd, buffer, MAXBUF, 0);
if (len > 0)
{
printf("接收消息成功:'%s',共%d个字节的数据\n", buffer, len);
}
else
{
if (len < 0)
{
printf("消息接收失败!错误代码是%d,错误信息是'%s'\n", errno, strerror(errno));
}
else
{
printf("对方退出了,聊天终止!\n");
}
break;
}
}
if (FD_ISSET(0, &rfds)) // 用户按键了,则读取用户输入的内容发送出去
{
bzero(buffer, MAXBUF + 1);
fgets(buffer, MAXBUF, stdin);
if (!strncasecmp(buffer, "quit", 4))
{
printf("自己请求终止聊天!\n");
break;
}
// 发消息给服务器
len = send(sockfd, buffer, strlen(buffer) - 1, 0);
if (len < 0)
{
printf("消息'%s'发送失败!错误代码是%d,错误信息是'%s'\n", buffer, errno, strerror(errno));
break;
}
else
{
printf("消息:%s\t发送成功,共发送了%d个字节!\n", buffer, len);
}
}
}
}
// 关闭连接
close(sockfd);
return 0;
}