先从一个简单的场景来理解什么叫I/O复用。学习过UNIX TCP网络编程的同学肯定知道accept和recv是阻塞的函数,accept函数是等待客户端连接,接受连接后返回,继续执行recv函数等待读取客户端发送过来的请求。但是如果一直没有客户端连接程序就会卡在accept函数上,连接后如果客户端没有数据发送就会卡在recv函数上,没法做别的事情。在处理一个客户端连接的时候也没法监听新的连接。也就是说程序在这个时候需要具有交错地监听多个套接字描述符的能力,而不至于阻塞在监测某一个描述符上面。这个能力就可以称为I/O复用,可以由select和poll函数来实现I/O复用,当然I/O复用不仅限于对网络套接字的处理。
I/O复用典型使用在下列的网络应用场合。
- 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。
- 一个客户同时处理多个套接字是可能的,不过比较少见。
- 如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。
- 如果一罐服务器要处理多个服务或者多个协议,一般就要使用I/O复用。
select 函数
select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或者多个事件发生或者经历一段指定的时间后才唤醒它。也就是说调用select告知内核对哪些描述符(读、写或异常条件)感兴趣以及等待多长时间。这个描述符不仅局限于套接字描述符,任何描述符都可以使用select来测试。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fs_set *exceptset, const struct timeval *timeout);
参数:int maxfdp1
最大的文件描述符加1(max fd plus 1)。因为fd_set结构是数组,描述符表示数组下标,这样每个描述符都有对应的位。
参数:fd_set *set
中间的三个参数readset writeset 和 exceptset指定我们关心的要让内核测试读、写和异常条件的描述符集合。不关心可以设为NULL。
通常使用一组宏来操作描述符集合fd_set。
void FD_ZERO(fd_set *fdset); /* 清空fd_set描述符集合 */
void FD_SET(int fd, fd_set *fdset); /* 把描述符fd添加到集合中 */
void FD_CLR(int fd, fd_set *fdset); /* 从fd_set集合中删除fd */
void FD_ISSET(int fd, fd_set *fdset); /* 检测fd是否在fd_set中 */
fd_set其实是使用整数数组实现的,关心哪个描述符就把该描述符对应的位置位置1,简单起见可以理解为往集合中添加描述符。
举个例子,把描述符1/4/5添加到检测可读性的fs_set集合中。
fd_set readset;
FD_ZERO(&readset);
FD_SET(1, &readset);
FD_SET(4, &readset);
FD_SET(5, &readset);
关于读、写和异常检测的3个描述符集合参数,如果对某一个条件不感兴趣,可以把该参数设为空指针。事实上如果3个参数都为空的话,这个函数就相当于一个比sleep函数更为精确的定时器函数,sleep最小单位是秒,timeval结构最小单位是微秒,通常系统真实可分辨的为10ms的倍数。
参数:struct timeval *timeout
这个参数表示超时时间,就是内核等待任何一个指定的描述符可花费多长时间,如果超过这个时间就执行后续的代码(不阻塞)。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒μs(10^-6 s) */
}
这个参数的设置有3种可能:
1. 一直等待下去:把这个参数设为空指针NULL,变成了阻塞模式,仅在有一个描述符准备好时才返回。
2. 等待固定长度的时间:如果有准备好的描述符则返回,否则等待给定长度的时间后返回。
3. 立刻返回不等待:检查描述符后立即返回,相当于非阻塞模式,这称为轮询。此时应该设置此结构体的数值都为0。
返回值
返回值>0表示已准备就绪的描述符个数。-1表示出错,0表示没有描述符准备好但是定时器超过了指定时间。
select 函数示例
这是一个可以接受多个TCP客户端同时连接 和 接收并显示多个客户端同时发送的数据 的TCP服务端程序。select内部维护一个整数数组,添加检测描述符即把以此描述符为下标的位置置位。select函数会检测其整数数组被置位的描述符的状态。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#define BACKLOG 5
#define BUFF_SIZE 200
#define DEFAULT_PORT 6666
typedef struct _CLIENT
{
int fd; /* 客户端连接描述符 */
struct sockaddr_in addr; /* 客户端地址信息 */
}CLIENT;
int main(int argc, char *argv[])
{
int SERVER_PORT = DEFAULT_PORT;
if(argc > 2)
printf("param err:\nUsage:\n\t%s port | %s\n\n",argv[0], argv[0]);
if(argc == 2)
SERVER_PORT = atoi(argv[1]);
int i, maxi, maxfd, nready, nbytes;
int servSocket, cliSocket;
fd_set allset, rset;
socklen_t addrLen;
char buffer[BUFF_SIZE];
CLIENT client[FD_SETSIZE]; /* 可以存放多个客户端信息的数组 */
struct sockaddr_in servAddr, cliAddr;
if((servSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("socket err");
exit(1);
}
bzero(&servAddr,sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(SERVER_PORT);
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(servSocket,(struct sockaddr *)&servAddr,sizeof(servAddr)) < 0)
{
printf("bind err");
exit(1);
}
if(listen(servSocket, BACKLOG) < 0)
{
printf("listen err");
exit(1);
}
printf("Listen Port: %d\nListening ...\n", SERVER_PORT);
maxi = -1;
maxfd = servSocket;
for (i = 0; i < FD_SETSIZE; i++)
client[i].fd = -1; /* -1 indicates available entry */
FD_ZERO(&allset); /* allset相当于一个数组,清空所有的位 */
FD_SET(servSocket, &allset); /*把allset数组中servSocket对应位置1,可以理解为把servSocket添加到allset集合中*/
for( ;; )
{
rset= allset;
/*
struct timeval timeout;
timeout.tv_sec = 2; /*设置超时2s*/
timeout.tv_usec = 0;
*/
if((nready = select(maxfd+1, &rset, NULL, NULL, NULL)) < 0)
{
printf("select err\n");
break;
}
else if(nready == 0) /* when timeout is set. */
{
printf("select time out\n");
continue;
}
if(FD_ISSET(servSocket, &rset)) /* servSocket有数据可读,即有新的连接可以接受了 */
{
addrLen = sizeof(cliAddr);
if((cliSocket = accept(servSocket, (struct sockaddr*)&cliAddr, &addrLen)) < 0)
{
printf("accept err");
exit(1);
}
printf("\nNew client connections %s:%d\n", inet_ntoa(cliAddr.sin_addr), ntohs(cliAddr.sin_port));
/* CLIENT数组中选择一个可以用的元素来存放客户端信息 */
for (i = 0; i < FD_SETSIZE; i++)
if (client[i].fd < 0) /* save client info */
{
client[i].fd = cliSocket;
client[i].addr = cliAddr;
break;
}
if (i == FD_SETSIZE)
perror("too many clients");
FD_SET(cliSocket, &allset); /* 把新的客户连接描述符添加到检测集合中 */
if (cliSocket > maxfd)
maxfd = cliSocket; /* 始终保持maxfd为所有描述符中的最大值,用于select参数一 */
if (i > maxi)
maxi = i; /* client[]数组最大下标 */
if (--nready <= 0)
continue; /* no more readable descriptors */
}
for (i = 0; i <= maxi; i++) /* 检测所有客户端连接的数据 */
{
if ((cliSocket = client[i].fd) < 0)
continue;
if (FD_ISSET(cliSocket, &rset)) /*某一个客户端有数据可读*/
{
memset(buffer, 0, BUFF_SIZE);
if((nbytes = recv(cliSocket, buffer, sizeof(buffer), 0)) < 0)
{
printf("recv err");
continue;
}
else if(nbytes == 0) /*客户端关闭了连接*/
{
printf("\nDisconnect %s:%d\n",
inet_ntoa(client[i].addr.sin_addr), ntohs(client[i].addr.sin_port));
close(cliSocket);
FD_CLR(cliSocket, &allset);
client[i].fd = -1;
}
else
{
printf("\nFrom %s:%d\n",
inet_ntoa(client[i].addr.sin_addr), ntohs(client[i].addr.sin_port));
printf("Recv: %sLength: %d\n\n", buffer, nbytes);
}
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
poll 函数
poll函数提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
参数 struct pollfd *fdarray 和 nfds
这个参数是指向一个pollfd结构数组第一个元素的指针,每一个元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。nfds表示数组中元素个数,数组为struct pollfd fdarray[nfds]
。
struct pollfd
{
int fd; /* 描述符 */
short events; /* 感兴趣的事件 */
short revents; /* 返回该描述符的状态 */
};
要测试的条件由events指定,函数在相应的revents成员中返回该描述符的状态。下面列出poll事件常值:
// poll.h in MacOS
// 下面事件可以作为events输入事件,也可以作为revents输出状态事件。
#define POLLIN 0x0001 /* 存在数据可读 */
#define POLLPRI 0x0002 /* 高优先数据可读priority */
#define POLLOUT 0x0004 /* 普通数据可写 */
#define POLLRDNORM 0x0040 /* 普通数据可读 */
#define POLLWRNORM POLLOUT /* 普通数据可写 */
#define POLLRDBAND 0x0080 /* 优先级带数据可读 */
#define POLLWRBAND 0x0100 /* 优先级带数据可写 */
// 下面事件只能作为revents状态事件输出
#define POLLERR 0x0008 /* 发生错误 */
#define POLLHUP 0x0010 /* 发生挂起 */
#define POLLNVAL 0x0020 /* 请求描述符/事件无效 */
POLLIN 可以表示为 (POLLRDNORM | POLLRDBAND),POLLOUT 等同于 POLLWRNORM 。
如果不关心特定描述符,可以把对应的pollfd结构的fd成员设为一个负值,这样poll函数就会忽略这个pollfd结构的events成员,返回值会将revents置为0.
参数 int timeout
这个参数表示返回前等待多长时间,单位是毫秒。
- timeout >0 : 等待的毫秒数
- timeout =0 : 立即返回不阻塞进程。
- timeout = INFTIM : 阻塞进程一直等待,如果系统没有这个宏就设为-1。
返回值
和select类似,返回整数表示准备好的描述符个数,0表示超时,-1表示出错。
poll 函数示例
这是一个可以接受多个TCP客户端同时连接 和 接收并显示多个客户端同时发送的数据 的TCP服务端程序。poll函数会检测pollfd结构的数组所表示的所有描述符和相应的事件,
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <limits.h>
#include <poll.h>
#define BACKLOG 5
#define BUFF_SIZE 200
#define DEFAULT_PORT 6666
int main(int argc, char **argv)
{
int SERV_PORT = DEFAULT_PORT;
if(argc > 2)
printf("param err:\nUsage:\n\t%s port | %s\n\n",argv[0], argv[0]);
if(argc == 2)
SERV_PORT = atoi(argv[1]);
int i, maxi, nready;
int servSocket, cliSocket;
ssize_t nbytes;
char buf[BUFF_SIZE];
socklen_t addrLen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliAddr, servAddr;
if((servSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("socket err");
exit(1);
}
bzero(&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(SERV_PORT);
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(servSocket,(struct sockaddr *)&servAddr,sizeof(servAddr)) < 0)
{
printf("bind err");
exit(1);
}
if(listen(servSocket, BACKLOG) < 0)
{
printf("listen err");
exit(1);
}
printf("Listen Port: %d\nListening ...\n", SERV_PORT);
client[0].fd = servSocket;
client[0].events = POLLRDNORM;
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* -1 表示可以使用的数组空间 */
maxi = 0; /* max index into client[] array */
for ( ; ; )
{
nready = poll(client, maxi+1, -1); /*检测client数组中所有有效元素描述符指定的事件*/
if (nready < 0)
{
printf("poll err");
exit(1);
}
if (client[0].revents & POLLRDNORM) /* 有新的客户连接 */
{
addrLen = sizeof(cliAddr);
if((cliSocket = accept(servSocket, (struct sockaddr*)&cliAddr, &addrLen)) < 0)
{
printf("accept err");
exit(1);
}
for (i = 1; i < OPEN_MAX; i++)
if (client[i].fd < 0)
{
client[i].fd = cliSocket; /* save descriptor */
break;
}
printf("\nNew client connections client[%d] %s:%d\n", i,
inet_ntoa(cliAddr.sin_addr), ntohs(cliAddr.sin_port));
if (i == OPEN_MAX)
printf("too many clients");
client[i].events = POLLRDNORM;
if (i > maxi)
maxi = i; /* max index in client[] array */
if (--nready <= 0)
continue; /* no more readable descriptors */
}
for (i = 1; i <= maxi; i++) /* 检查所有客户连接的数据 */
{
if ( (cliSocket = client[i].fd) < 0)
continue;
if (client[i].revents & (POLLRDNORM | POLLERR)) /*此客户端连接有数据可读或者出错了*/
{
memset(buf, 0, BUFF_SIZE);
if((nbytes = recv(cliSocket, buf, BUFF_SIZE, 0)) < 0)
{
printf("recv err");
continue;
}
else if(nbytes == 0) /*客户端断开了连接*/
{
printf("client[%d] closed connection\n", i);
close(cliSocket);
client[i].fd = -1;
}
else
{
printf("\nFrom client[%d]\n", i);
printf("Recv: %sLength: %d\n\n", buf, (int) nbytes);
}
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
======================================================================
Source Code: https://github.com/lmshao/codebase Poll_TcpServer.c
Select_TcpServer.c
.
可以使用TcpClient.c
或TcpClient_InputSend.c
指定服务器端口号来进行测试。
参考: 《UNIX网络编程 卷1:套接字联网API》