1、五种I/O模型:
(1)阻塞式I/O;(2)非阻塞式I/O;(3)I/O复用(select、poll);(4)信号驱动式I/O(SIGIO);(5)异步I/O(aio_系列函数);
一个输入操作分为两个阶段:A. 等待数据准备好;B. 从内核向进程复制数据。
一个套接字上的输入也类似,A. 等待数据从网络中到达(所等待分组到达时,会被复制到内核中的某个缓冲区);B. 把数据从内核缓冲区复制到应用进程缓冲区。
2、I/O复用
(1)I/O复用模型:阻塞在select或者poll上,而不是阻塞在真正的I/O系统调用(recvfrom)上。
(2)I/O复用典型网络应用场合:
3、select函数
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
/*
maxfd1:指定待测试的描述符的个数,它的值是待测试的最大描述符+1(描述符是从0开始的)。比如打开的描述符集合是:{1, 4, 5},那么maxfd1的值就是6。
*/
/*
readset、writeset、exceptset指定我们要让内核测试读、写、异常条件的描述符。
如果对某一个条件不感兴趣,可以把它设为空指针。
如果三个参数均为空,则变成了一个比sleep函数(以秒为最小单位)更为精确的定时器。
*/
/*
timeout:告知内核等待所指定描述符中的任何一个就绪可花多长时间。
(1)永远等待下去:设为空指针;
(2)等待固定一段时间:
timeval结构体:
struct timeval{
long tv_sec; // 秒数
long tv_usec; // 微妙数
}
(3)根本不等待:指向一个timeval结构,其中的定时器值必须为0;
*/
4、使用select函数:
(要想读懂以下优化历程,需要去读UNIX网络编程的第五章)
(1)不使用select函数会出现什么异常?
//处理客户输入,从标注输入读入一行文本,写到服务器上,读回服务器对该行的回射,并把回射数据写到标准输出上
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (Fgets(sendline, MAXLINE, fp) != NULL) { //读入
Writen(sockfd, sendline, strlen(sendline)); //发给服务器
if (Readline(sockfd, recvline, MAXLINE) == 0) //读回反射数据
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout); //写到标准输出
}
}
我们先启动服务器、客户,以验证正常。接着我们关掉服务器,这导致向客户发送一个FIN,TCP客户响应一个ACK,完成了TCP终止序列的前半部分。但是客户上并没有发生任何特殊之事,也就是说客户阻塞在fgets调用上,仍等待从终端接收一行文本。
服务器发送FIN只是表示服务器进程关闭了服务端,不再往该连接上发送任何数据。但是TCP客户FIN的接收并没有告知客户该TCP服务器进程已经终止。所以客户继续输入文本,str_cli调用write,把数据发送给服务器。
服务器收到客户的数据,因为先前打开的套接字进程已经关闭,所以响应一个RST。
但是此时客户看不到这个RST,因为客户在调用write后立即调用了read,又因为上面客户收到了FIN,所以readline返回0,所以打印出错信息。
问题发生在:客户应对两个描述符——套接字和用户输入。但是客户不应该阻塞在这两个源中的某个特定源的输入上(例子中客户阻塞于用户输入)。也就是说,当套接字上发生某些事件,而客户正阻塞于fgets调用时,客户得不到服务器终止的通知。
(2)使用select优化:
正如我们上面说的I/O复用,我们将阻塞改为阻塞于select上——或是等待标准输入,或是等待套接字可读。
#include "unp.h"
//使用select优化
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
for ( ; ; ) {
FD_SET(fileno(fp), &rset); // 对应于标准I/O的fp指针
FD_SET(sockfd, &rset); // 对应于套接字scokfd
maxfdp1 = max(fileno(fp), sockfd) + 1; //计算上面两个描述符的最大值
Select(maxfdp1, &rset, NULL, NULL, NULL); //阻塞于select,timeout为NULL,阻塞到某个描述符就绪为止
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */ //如果套接字可读
if (Readline(sockfd, recvline, MAXLINE) == 0) //直接读回回射文本
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */ //如果标准输入可读
if (Fgets(sendline, MAXLINE, fp) == NULL) //就先用fgets读入一行文本
return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}
我们可以看到优化后的版本是由select驱动的,而第一个版本是由fgets驱动的。
继续来看,如果我们把标准输入和标准输出重定向到文件来运行客户,然后以批量方式运行客户。这时,我们会发现输出文件总是小于输入文件。
假设输入文件有9行,最后一行在时刻8发出,写完这个请求后,我们虽然数据写入完了,但是并不能关闭该连接,因为管道中还有其他的请求和应答。原因:在最开始的标准输入中,EOF的键入同时意味着完成了从套接字的读入。但是在批量方式下,EOF并不意味着我们同时完成了从套接字的读入——可能仍有请求去往服务器,或者仍有可能应答在返回客户的路上。
另外一点,在上述代码中我们使用了缓冲机制,这也容易犯错!批量方式中,我们有多个文本输入行,而fgets读取输入,会使可用的文本行被读入到stdio所用的缓冲区中。但是fgets只返回其中一行,其余行仍留在stdio的缓冲区中。下一次,select再次被调用以等待新的工作,没有管stdio缓冲区中的待处理文本行数据。因为select根本不知道stdio使用了缓冲区,select只是从read系统调用的角度支出是否有数据可读,而不是从fgets调用角度考虑。readline的调用存在同样的问题。
所以,混合使用stdio缓冲区和select是极容易犯错误的!
5、poll函数
poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。
poll函数定义:
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
/*
fdarray:纸箱一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构。
struct pollfd{
int fd;
short events; //指定测试条件(输入、输出、异常)
short recents;
}
*/
/*
nfds:结构数组中元素的个数
*/
/*
timeout:制定poll函数返回前等待多长时间。
(1)INFTIM:永远等待;
(2)0:立即返回,不阻塞进程;
(3)>0:等待指定的毫秒数;
*/
6、使用poll函数:
使用poll函数的TCP回射服务器程序:
/* include fig01 */
#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]; //pollfd结构的数组,维护客户信息
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);
client[0].fd = listenfd; //把client数组的第一项固定用于监听套接字
client[0].events = POLLRDNORM; //设置POLLRDNORM,当有新的连接准备好被接受时poll将通知我们
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* -1 indicates available entry */ //值为-1表示所在项未用
maxi = 0; /* max index into client[] array */
for ( ; ; ) {
nready = Poll(client, maxi+1, INFTIM); //调用poll,等待新的连接或者现有连接上有数据可读
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++) //从下标为1开始搜索
if (client[i].fd < 0) { //值为-1表示所在项未用
client[i].fd = connfd; /* save descriptor */ //保存新连接的描述符
break;
}
if (i == OPEN_MAX)
err_quit("too many clients");
client[i].events = POLLRDNORM; //设置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)) { //检查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; //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; //fd置为-1
}
else
Writen(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}