本博客参考自《Unix网络编程:卷1》
IO复用
1.IO复用的应用场景
IO复用可以使得一个程序可以同时对多个描述符服务(输入输出和套接字描述符)。关于IO复用的原理可以参考博客:https://blog.csdn.net/qq_37981695/article/details/106290994
IO复用典型的使用场景:
(1)当客户同时处理多个描述符(通常是交互式输入和网络套接字)时,必须使用IO复用。
(2)一个客户处理多个套接字时,需要IO复用。例如:Web客户。
(3)如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般要使用IO复用。
(4)如果一个服务器既要处理TCP,又要处理UDP,一般要使用IO复用。
(5)如果一个服务器要处理多个服务或者多个协议,一般需要使用IO复用。
IO复用并不局限于网络编程,很多重要的应用也需要这项技术。
2.IO复用编程
2.1 select函数
//Ubuntu:/usr/include/x86_64-linux-gnu/sys/select.h
//Ubuntu:/usr/include/linux/time.h
#include<sys/select.h>
#include<sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
//返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
下面对该函数的参数进行说明:
(1)maxfd1参数指定待测试的描述符的个数,它的值是待测试的最大描述符加1,描述符0,1,2,…,maxfdp1-1均会被测试。头文件<sys/select.h>中定义的常量FD_SETSIZE常值规定了描述符的最大值。一般系统中此值为1024
//Ubuntu:/usr/include/x86_64-linux-gnu/sys/select.h
#define FD_SETSIZE __FD_SETSIZE
//Ubuntu:/usr/include/linux/posix_types.h
#undef __FD_SETSIZE
#define __FD_SETSIZE 1024
(2)中间的三个参数readset、writeset和exceptset指定用户要让内核测试读、写和异常条件的描述符。目前支持的异常条件只有两个:某个套接字的带外数据到达和某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。
select中使用描述符集给三个参数中的每一个参数指定一个或多个描述符值。通常该描述符集是一个整形数组,其中每个整数中的每一位对应一个描述符,select使用fd_set数据类型表示该描述符集,fd_set的源码如下所示:
typedef long int __fd_mask;
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
可以看出fd_set是一个长整型的数组。对于该数组的操作都是通过四个指定的宏进行的
void FD_ZERO(fd_set *fdset);//清零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的指定位是否是打开的
举例说明如何操作fd_set,假设打开描述符1,2,3。(注意unix中描述符0,1,2一般是标准输入、标准输出和标准错误)
fd_set rset;
FD_ZERO(&rset);
FD_SET(1,&rset);
FD_SET(2,&rset);
FD_SET(3,&rset);
三个参数readset、writeset和exceptset中如果有某个我们不感兴趣,可以将它设为空指针。注意select函数会修改这三个参数的值。函数调用结束之后,我们通过FD_ISSET宏来测试fd_set数据类型中的描述符,在编程时,我们需要在调用select之前将所有描述符集内所关心的位均打开即置为1。
(3)timeout参数告知内核等待指定描述符中的任何一个就绪可花多长时间。其timeval结构体如下所示:
struct timeval{
long tv_sec;//秒数
long tv_usec;//毫秒数
};
这个参数有三种可能:
①参数为空:永远等待下去,有一个描述符准备好时返回。
②指定timeval中的秒数和微秒数:等待一段时间,有一个描述符准备好返回,但是不超过该参数指定的时间。
③timeval中的值秒数和微秒数都为0:根本不等待。检查描述符后立即返回,这称为轮询。
前两种情形如果等等待期间捕获信号中断,可能会使得select函数返回EINTR错误。
timeout变量被const修饰,证明该参数不会被内核修改。
2.2 描述符就绪的条件
(1)一个套接字准备好读
a)**该套接字接收缓冲区中的数据字节大于等于套接字接收缓冲区低水位标记的当前大小。**对这样的套接字执行读操作不会阻塞并将返回一个大于0的值。可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,此值默认为1。
b)**该连接的读半部关闭(也就是接收了FIN的TCP连接)。**对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)。
c)**该套接字是一个监听套接字且已完成的连接数不为0。**对这样的套接字的accept通常不会阻塞。
d)**套接字出现错误。**对于这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把error设置成确切的错误条件。这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
(2)一个套接字准备好写
a)该套接字发送缓冲区中可用字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接。这就意味着如果我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值。套接字的低水位标记可以通过使用SO_SNDLOWAT套接字选项来设置。对于TCP和UDP套接字而言,其默认值为1024。对于UDP发送缓冲区的大小大于等于发送低水位标记,则该套接字总是可写的。
b)该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。
c)使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
d)套接字出现错误。对这样的套接字写操作会返回-1.
(3)如果一个套接字存在**带外数据或者仍处于带外标记。**它就有异常处理。
下面对select返回某个套接字就绪的条件进行总结
2.3 select的应用
2.3.1 改进的客户处理函数(str_cli)
函数str_cli是unp库中TCP客户端程序的套接字处理函数,可以参考博客:https://blog.csdn.net/qq_37981695/article/details/106269631
下面是改进后的str_cli函数-版本1
//select/strcliselect01.c
#include "unp.h"
//fp-文件描述符 sockfd-套接字描述符
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
for ( ; ; ) {
//每次调用select之前都需要重新设置rset的值
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if (Readline(sockfd, recvline, MAXLINE) == 0)//接收到FIN
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
//fileno将文件句柄转换为文件描述符
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if (Fgets(sendline, MAXLINE, fp) == NULL)//输入Ctrl+D
return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}
该版本的程序改进了客户阻塞于fgets(Fgets是包裹函数)产生的问题。此程序阻塞于select(Select包裹函数)调用。当标准输入可读或者套接字可读时,select返回。下图展示了select所处理的各种条件
客户套接字上的三个条件处理:
(1)如果对端TCP发送数据,该套接字变为可读,并且read返回一个大于0的值。
(2)如果对端TCP发送一个FIN,该套接字变为可读,并且read返回0(EOF)。
(3)如果对端TCP发送一个RST(对端主机崩溃并重启),该套接字变为可读,并且read返回-1,而errno中含有确切的错误码。(版本1的程序中并未处理)
2.3.2 改进str_cli的问题
基本TCP客户/服务器程序,参考博客:https://blog.csdn.net/qq_37981695/article/details/106269631
其中的程序是一种停-等的交互方式,客户发送完数据之后一直等待来自服务器的数据。可以下图的时间线示例说明该交互方式
时刻0,客户发送请求。时刻7,客户接收应答。其中忽略了服务器的处理时间和接收的应答。但是在改进的版本中用户可以一直不断地输入,对数据进行批量的处理。原理可用下面的时间线图表示:
客户在每个时刻都发送请求,在时刻7,TCP的全双工管道处于充满的状态。这是如果客户突然关闭就无法接收到还在管道中的数据。在批量处理的方式下,标准输入中的EOF并不意味着我们同时也完成了从套接字上读入;可能仍有请求在去往服务器的路上,或者仍有应答在返回客户的路上。我们需要一种关闭TCP连接其中一半的方法。**客户发送一个FIN给服务器,告诉服务器已经完成了数据发送,但是仍然保持套接字描述符打开以便读取。**这就是TCP中的shutdown函数,下面对shutdown函数进行介绍
函数原型
//Ubuntu:/usr/include/x86_64-linux-gnu/sys/socket.h
#include <sys/socket.h>
int shutdown(int sockfd,int howto);
//返回:成功返回0,出错返回-1
该函数的行为依赖于howto参数的值
(1)SHUT_RD关闭连接的读这一半-套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都将被丢弃。
(2)SHUT_WR关闭连接的写这一半(半关闭)-当前留在套接字发送缓冲区中的数据将被发送,后跟TCP的正常连接终止序列。不管套接字描述符的引用计数是否等于0,这样的写半部关闭一定会发送终止序列。
(3)SHUT_RDWR连接的读半部和写半部都关闭-这与调用SHUTDOWN两次等效。
再次改进的str_cli源码如下(版本2):
//select/strcliselect02.c
#include "unp.h"
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
//标准输入结束
if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}
本程序相对于第一次修改的str_cli程序的改动:
(1)使用read和write读取和写数据。
(2)使用stdineof标识是否是客户主动关闭的。
(3)使用shutdown对套接字半关闭。
2.3.3 改进的服务器程序
基本的TCP客户/服务器程序可以参考博客:https://blog.csdn.net/qq_37981695/article/details/106269631
//tcpcliserv/tcpservselect01.c
#include "unp.h"
int
main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
maxfd = listenfd; /* initialize */
maxi = -1; /* index into client[] array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for ( ; ; ) {
rset = allset; /* structure assignment */
nready = Select(maxfd+1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
ntohs(cliaddr.sin_port));
#endif
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* save descriptor */
break;
}
if (i == FD_SETSIZE)
err_quit("too many clients");
FD_SET(connfd, &allset); /* add new descriptor to set */
if (connfd > maxfd)
maxfd = connfd; /* for select */
if (i > maxi)
maxi = i; /* max index in client[] array */
if (--nready <= 0)
continue; /* no more readable descriptors */
}
for (i = 0; i <= maxi; i++) { /* check all clients for data */
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
/*4connection closed by client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
2.4 poll函数
//Ubuntu:/usr/include/x86_64-linux-gnu/sys/poll.h
#include <poll.h>
int poll(struct pollfd *fdarray,unsigned long nfds,int timeout);
//返回:若有就绪描述符则为器数目,若超时则为0,若出错则为-1
对参数进行说明:
Ⅰ第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件
/* Data structure describing a polling request. */
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
要测试的条件由events成员指定,函数在相应的revents成员中返回改描述符的状态。这两个成员中的每一个都由指定某个特定条件的一位或多位构成。
上图被分为三个部分:第一部分是处理输入的四个常值,第二部分是处理输出的三个常值,第三部分是处理错误的三个常值。其中第三部分不能在events中设置。
poll识别三类数据:普通(normal)、优先级带(priority band)和高优先级(high priority)。
关于revents条件的几点说明:
①所有正常的TCP和UDP数据都被认为是普通数据。
②TCP的带外数据被认为是优先级带数据。
③当TCP连接的读半部关闭时(收到对端的FIN),也被认为是普通数据,随后的读操作返回0.
④TCP连接存在错误既可以认为是普通数据,也可以认为是错误(POLLERR)。
⑤在监听套接字上有新的连接可用既可以认为是普通数据,也可以认为是优先级数据。
⑥非阻塞式connect的完成被认为是使相应的套接字可写。
Ⅱ nfds-结构数组中的元素个数
timout参数指定poll函数返回前等待的时间,单位ms
INFTIM常值被定义为一个负值。如果系统不能提供毫秒级精度的定时器,该值就向上舍入到最接近的支持值。
如果我们不再关心某个特定的描述符,那么就可以把与它对应的pollfd结构的fd成员设置成一个负值。这样poll函数就会忽略。
使用poll的服务器程序
/* include fig01 */
//Ubuntu中没有OPEN_MAX,而是通过_POSIX_OPEN_MAX代替
///usr/include/x86_64-linux-gnu/bits/posix1_lim.h
///* Number of files one process can have open at once. */
//#ifdef __USE_XOPEN2K
//# define _POSIX_OPEN_MAX 20
//#else
//# define _POSIX_OPEN_MAX 16
//#endif
#include "unp.h"
#include <limits.h> /* for OPEN_MAX */
int main(int argc, char **argv)
{
int i, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLINE];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
//listen socket
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* -1 indicates available entry */
maxi = 0; /* max index into client[] array */
/* end fig01 */
/* include fig02 */
for ( ; ; ) {
//包裹函数
nready = Poll(client, maxi+1, INFTIM);
if (client[0].revents & POLLRDNORM) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
#endif
for (i = 1; i < OPEN_MAX; i++)
if (client[i].fd < 0) {
client[i].fd = connfd; /* save descriptor */
break;
}
if (i == OPEN_MAX)
err_quit("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++) { /* check all clients for data */
if ( (sockfd = client[i].fd) < 0)
continue;
if (client[i].revents & (POLLRDNORM | POLLERR)) {
if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) {
/*4connection reset by client */
#ifdef NOTDEF
printf("client[%d] aborted connection\n", i);
#endif
Close(sockfd);
client[i].fd = -1;
} else
err_sys("read error");
} else if (n == 0) {
/*4connection closed by client */
#ifdef NOTDEF
printf("client[%d] closed connection\n", i);
#endif
Close(sockfd);
client[i].fd = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
/* end fig02 */