在完成I/O操作时,程序中完成真正I/O的时间可能只有少的一部分,而大部分时间都处于一个等的时间。比如,此时需要从一个套接字中读取数据read(socket, buf, BUFSIZE); 这个操作可能会一直阻塞,直到有数据从网络的另一端发送过来。等的时间过于长,这是I/O效率低下的真正原因。可能有人会提出让代码不要阻塞的等,可以进行非阻塞的等待,比如当read一个socket发现没有数据时,就不在等待,而去read其他的socket,进行一种轮询式的read。可是这种模式还是会进行read的这个操作,不过这时进行操作时不成功的话就去read其他socket,效率还是低下的。
为了解决上述问题,提出了I/O多路转接。它的做法是这样的,一次等多个文件描述符,当有一个或者多个文件描述符就绪,可以进行I/O操作时,便返回通知有哪些那些文件描述符可以I/O。
系统提供了select函数实现多路复用输入/输出模型,select系统调用可以监视多个文件描述符的状态变化。程序会停在select这里等待,直到被监视的文件描述符至少有一个的状态发生了变化。
函数的定义:
参数描述:
- nfds:要关心的文件描述符
- readfds:表示要监视文件描述符集中,所有文件描述符的读状态
- writefds:表示要监视文件描述符集中,所有文件描述符的写状态
- exceptfds:表示要监视文件描述符集中,所有文件描述符的异常状态
- timeout:监视多长时间,
- 当timeout被设置为0,表示以非阻塞方式等待;
- 当timeout被设置为大于0的数字,则表示其等待时间,有秒和毫秒的区分
struct timeval
{
long tv_sec;//秒数
long tv_usec;//微秒数
}
- 当timeout设置为NULL时,表示已阻塞方式等待。
返回值:
- 当监视的文件描述符集中有文件描述符就绪,则会返回一个大于0的数
- 当监视的文件描述符没有任何一个就绪时,并且指定的时间已到,返回0
- 当函数调用出错时,返回-1
fd_set:
select()机制中提供一fd_set的
数据结构,可以理解为一个集合,实际上是一个位图,每一个特定位来标志相应大小文件描述符,这个集合中存放的是文件描述符,即就是文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成。
系统提供了四个宏函数对fd_set进行操作
FD_CLR用于将fd_set中fd对应的位关闭。
FD_ISSET判断fd是否在fd_set中
FD_SET将fd添加进fd_set中
FD_ZERO将fd_set清空
基于select实现的网络服务器和客户端
server:
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<error.h>
#include<unistd.h>
#include<sys/types.h>
#define NUMS 1024
static void Usage(char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}
int startup(char* ip, int port)
{
//创建文件特性
//AF_INET ipv4,SOCK_STREAM,基于字节流服务
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror("socket");
return 2;
}
printf("sock = %d\n", sock);
struct sockaddr_in local;
//确定地址协议类型
local.sin_family = AF_INET;
//绑定端口
local.sin_port = htons(port);
//绑定ip
local.sin_addr.s_addr = inet_addr(ip);
//绑定网络特性
if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
perror("bind");
return 3;
}
//监听套接字
if (listen(sock, 10) < 0)
{
perror("listen");
return 4;
}
return sock;
}
//初始化文件描述符数组
void Init(int *fds)
{
int i = 0;
for (; i < NUMS; i++)
{
fds[i] = -1;
}
}
//将文件描述符数组所存储的文件描述符设置进文件描述符集
int Addfd(int *fds, fd_set *set)
{
int i = 0;
int maxfd = fds[0];
for (; i < NUMS; i++)
{
if (fds[i] != -1)
{
FD_SET(fds[i], set);
if (maxfd < fds[i])
{
maxfd = fds[i];
}
}
}
//返回最大的文件描述符
return maxfd;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 5;
}
//获取监听到的套接字
int listen_sock = startup(argv[1], atoi(argv[2]));
//创建一个辅助的空间(一个数组)用于保存 文件描述符信息
//fds中保存的文件描述符将会被监听读状态
int fds[NUMS];
//wfd中保存的文件描述符将会被监听写状态
int wfd[NUMS];
//创建文件描述符集
fd_set set;
fd_set wset;
//将数组初始化
Init(fds);
Init(wfd);
//监听listen_sock添加进数组
fds[0] = listen_sock;
printf("listen_sock:%d\n", fds[0]);
while (1)
{
//初始化文件描述符集
FD_ZERO(&set);
FD_ZERO(&wset);
int maxrfd = -1;
int maxwfd = -1;
//将存于数组中的文件描述符添加至文件描述符集
maxrfd = Addfd(fds, &set);
maxwfd = Addfd(wfd, &wset);
struct timeval timeread = { 2, 0 };
struct timeval timewrite = { 0, 0 };
if (maxrfd < maxwfd)
maxrfd = maxwfd;
//监视set中的读状态信息,监视wset中的写状态信息
int rres = select(maxrfd + 1, &set, &wset, NULL, &timeread);
switch (rres)
{
case -1:
{
perror("selete");
break;
}
case 0:
{
printf("Timeout...\n");
break;
}
default:
{//至少有一个文件描述符状态就绪
int i = 0;
for (; i < NUMS; i++)
{
//此条件满足,说明客户端的连接已经就绪
if (i == 0 && fds[i] != -1 && \
FD_ISSET(fds[i], &set))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
//接受监听到的套接字
int new_sock = accept(listen_sock, \
(struct sockaddr*)&client, &len);
printf("client [%s] [%d]\n", \
inet_ntoa(client.sin_addr), \
ntohs(client.sin_port));
if (new_sock < 0)
{
perror("accept");
continue;
}
//连接套接字后,将其添加到数组中,他将被监听读状态就绪
for (i; i < NUMS; i++)
{
if (fds[i] == -1)
{
fds[i] = new_sock;
break;
}
}
}
//监视到有文件描述符的读状态就绪,就可以执行读操作
else if (i != 0 && fds[i] != -1 && FD_ISSET(fds[i], &set))
{
char buf[1024];
//从套接字读取信息到buf中
ssize_t s = read(fds[i], buf, sizeof(buf)-1);
if (s > 0)
{
buf[s] = 0;
printf("client# %s\n", buf);
int j = 1;
//从这个文件描述符中读取数据后,就可以监视它的写状态
for (; j < NUMS; j++)
{
if (wfd[j] == -1)
{
wfd[j] = fds[i];
fds[i] = -1;
break;
}
}
}
else if (s == 0)
{
close(fds[i]);
printf("客户端已经退出!\n");
fds[i] = -1;
}
else
{
perror("read");
close(fds[i]);
fds[i] = -1;
}
}
//监视到有文件描述符的写状态就绪,就可以进行写操作
if (wfd[i] != -1 && FD_ISSET(wfd[i], &wset))
{
char buf[1024];
printf("Please Enter# ");
fflush(stdout);
//从键盘输入信息到buf中
ssize_t _s = read(0, buf, sizeof(buf)-1);
if (_s > 0)
{
buf[_s - 1] = 0;
//发送信息到套接字
write(wfd[i], buf, strlen(buf));
//写完之后,就可等待其回复,监视其读状态
int j = 0;
for (; j < NUMS; j++)
{
if (fds[j] == -1)
{
fds[j] = wfd[i];
wfd[i] = -1;
break;
}
}
}
else
{
perror("read");
close(wfd[i]);
wfd[i] = -1;
break;
}
}
}
}
}
}
return 0;
}
client:
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<sys/types.h>
#define NUMS 1024
static void Usage(char *proc)
{
printf("Usage %s [server_ip] [server_port]\n", proc);
}
void Init(int *fds)
{
int i = 0;
for (; i < NUMS; i++)
{
fds[i] = -1;
}
}
int Addfd(int *fds, fd_set *set)
{
int i = 0;
int maxfd = fds[0];
for (; i < NUMS; i++)
{
if (fds[i] != -1)
{
FD_SET(fds[i], set);
if (maxfd < fds[i])
{
maxfd = fds[i];
}
}
}
return maxfd;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//将创建的套接字,连入指定的网络服务中,这里连到了服务器
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{
perror("connect111");
return 3;
}
//创建一个辅助的空间(一个数组)用于保存 文件描述符信息
//wfd中保存的文件描述符将会被监听写状态
int wfd[NUMS];
//rfd中保存的文件描述符将会被监听读状态
int rfd[NUMS];
//初始化数组
Init(wfd);
Init(rfd);
//客户端先写,所以将sock添加进wfd中
wfd[0] = sock;
//创建文件描述符集
fd_set rset;
fd_set wset;
char buf[1024];
while (1)
{
//清空文件文件描述符集
FD_ZERO(&rset);
FD_ZERO(&wset);
int maxrfd = -1;
int maxwfd = -1;
//将存于数组中的文件描述符添加至文件描述符集
maxrfd = Addfd(rfd, &rset);
maxwfd = Addfd(wfd, &wset);
struct timeval timeread = { 2, 0 };
struct timeval timewrite = { 0, 0 };
if (maxrfd < maxwfd)
maxrfd = maxwfd;
//监视rset中的读状态信息,监听wset中的写状态信息
int res = select(maxrfd + 1, &rset, &wset, NULL, &timeread);
switch (res)
{
case -1:
{
perror("selete");
break;
}
case 0:
{
printf("Timeout...\n");
break;
}
default:
{
int i = 0;
for (; i < NUMS; i++)
{
//写状态就绪
if (wfd[i] != -1 && FD_ISSET(wfd[i], &wset))
{
printf("Please Enter# ");
fflush(stdout);
//从键盘写入内容到缓冲区
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s >0)
{
buf[s - 1] = 0;
printf("server# ");
fflush(stdout);
//将缓冲区内容通过套接字发送到服务器
write(wfd[i], buf, strlen(buf));
int j = 0;
//写操作完成,应该进行读操作,监视其读操作
for (; j < NUMS; j++)
{
if (rfd[j] == -1)
{
rfd[j] = wfd[i];
wfd[i] = -1;
break;
}
}
}
else
{
FD_CLR(wfd[i], &wset);
close(wfd[i]);
return 5;
}
}
if (rfd[i] != -1 && FD_ISSET(rfd[i], &rset))
{
//在从套接字中读取服务器的回应信息
ssize_t _s = read(sock, buf, sizeof(buf)-1);
if (_s > 0)
{
buf[_s] = 0;
printf("%s\n", buf);
int j = 0;
//读操作完成,应该进行写操作,监视其写操作
for (; j < NUMS; j++)
{
if (wfd[j] == -1)
{
wfd[j] = rfd[i];
rfd[i] = -1;
break;
}
}
}
if (_s < 0)
{
FD_CLR(rfd[i], &rset);
close(rfd[i]);
perror("read");
return 4;
}
}
}
}
}
}
return 0;
}