UNP Chapter 15 - 非阻塞I/O
15.1 概述
缺省状态下,套接口是阻塞方式的。这意味着当一个套接口调用不能立即完成时,进程进入睡眠状态,等待操作完成。我们将可能阻塞的套接口调用分成四种。
1. 输入操作: read, readv, recv, recvfrom和recvmsg函数。
2. 输出操作: write, writev, send, sendto和sendmsg函数。
3. 接收外来连接: accept函数
4. 初始化外出的连接: 用于TCP的connect函数
15.2. 非阻塞读和写: str_cli函数(Revisited)
非阻塞并且直接使用read和write以代替标准I/O
我们维护两个缓冲区: to容纳从标准输入到服务器去的数据,fr容纳自服务器到标准输出来的数据。
图15.1展示了to缓冲区的组织和指向缓冲区中的指针
toiptr指针指向从标准输入读入的数据可以存放的下一个字节。tooptr指向下一个必须写入套接口的字节。有(toiptr-tooptr)个字节需写到套接口。可从标准输入读入的字节数是(&to[MAXLINE]-toiptr)。tooptr一旦到达toiptr,两个指针都复位到缓冲区的开始。
图15.2展示了相应的fr缓冲区的组织
#include "unp.h"
void str_cli(FILE * fp, int sockfd)
{
int maxfdp1, val, stdineof;
ssize_t n, nwritten;
fd_set rset, wset;
char to[MAXLINE], fr[MALINE];
char * toiptr, * tooptr, * friptr, * froptr;
//用fcntl把全部三个描述字设置为非阻塞:连接服务器的套接口,标准输入,标准输出
val = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);
val = Fcntl(STDIN_FILENO, F_GETFL, 0);
Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);
val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);
//初始化指向两个缓冲区的指针,并将最大的描述字加1,以用作select的第一个参数
toiptr = tooptr = to; /* initialize buffer pointers */
friptr = froptr = fr;
stdineof = 0;
maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
for( ; ; ) //主循环:准备调用select
{
FD_ZERO(&rset); // 两个描述字集清零
FD_ZERO(&wset);
if(stdineof == 0 && toiptr < &to[MAXLINE])
FD_SET(STDIN_FILENO, &rset); /* read from stdin */
if(fripter < &fr[MAXLINE])
FD_SET(sockfd, &rset); /* read from socket */
if(tooptr != toiptr)
FD_SET(sockfd, &wset); /* data to write to socket */
if(froptr != friptr)
FD_SET(STDOUT_FILENO, &wset); /* data to write to stdout */
Select(maxfdp1, &rset, &wset, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &rset))
{//如果标准输入上有数据可读,调用read
if((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0)
{//如果发生错误,而且是EWOULDBLOCK,就不做任何操作。
if(errno != EWOULDBLOCK)
err_sys("read error on stdin");
}
else if(n==0)
{//如果read返回0,标准输入处理就结束,我们设置stdineof标志
fprintf(stderr, "%s: EOF on stdin \n", gf_time());
stdineof = 1; /* all done with stdin */
if(tooptr == toiptr)//如果在to缓冲区中没有数据待发送,就shutdown向服务器发送FIN
Shutdown(sockfd, SHUT_WR); /* send FIN */
}
else
{//当read返回数据时,我们相应增加toiptr,另外还打开在写集合中与套接口对应的位,使后半部分的循环中对该位的测试能返回真,以试图在该套接口上write
fprintf(stderr, "%s: read %d bytes from stdin \n", gf_time(), n);
toiptr += n; /* # just read */
FD_SET(sockfd, &wset); /* try and write to socket below */
}
}
if(FD_ISSET(sockfd, &rset))
{
if((n = read(sockfd, friptr, &fr[MAXLINE]-friptr)) < 0)
{
if(errno != EWOULDBLOCK)
err_sys("read error on socket");
}
else if(n == 0)
{
fprintf(stderr, "%s: EOF on socket \n", gf_time());
if(stdineof)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely");
}
else
{
fprintf(stderr, "%s: read %d bytes from socket \n", gf_time(), n);
friptr += n; /* # just read */
FD_SET(STDOUT_FILENO, &wset); /* try and write below */
}
}
if(FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0))
{
if((nwritten = write(STDOUT_FILENO, froptr, n)) < 0)
{
if()
err_sys();
}
else
{
fprintf();
froptr += nwritten; /* # just written */
if( froptr == friptr)
froptr = friptr = fr; /* back to beginning of buffer */
}
}
if()
{
if()
{
if()
err_sys();
}
else
{
fprintf();
tooptr += nwritten; /* # just written */
if()
{
toiptr = tooptr = to; /* back to beginning of buffer */
if()
Shutdown(); /* send FIN */
}
}
}
}
}
str_cli调用的gf_time函数
#include "unp.h"
#include <time.h>
char * gf_time(void)
{
struct timeval tv;
static char str[30];
char * ptr;
if(gettimeofday(&tv, NULL) < 0)
err_sys();
ptr = ctime(&tv.tv_sec);
strcpy(str, &ptr[11]);
snprintf(str+8, sizeof(str)-8, ".%06ld", tv.tv_usec);
return(str);
}
str_cli的更简单版本
#include "unp.h"
void str_cli(FILE * fp, int sockfd)
{
pid_t pid;
char sendline[MAXLINE], recvline[MAXLINE];
if(( pid = Fork() ) ==0) /* child: server -> stdout */
{
while(Readline(sockfd, recvline, MAXLINE) > 0)
Fputs(recvline, stdout);
kill(getppid(), SIGTERM); /* in case parent still running */
exit(0);
}
/* parent: stdin -> server */
while(Fgets(sendline, MAXLINE, fp) != NULL)
Writen(sockfd, sendline, strlen(sendline));
Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
pause();
return;
}
这个函数一开始就调用fork分为父进程和子进程,子进程把从服务器来的行拷贝到标准输出,父进程把标准输入来的行拷贝到服务器
父进程和子进程共享同一个套接口:父进程想该套接口写,子进程从该套接口读。但这个套接口被两个描述字引用:一个在父进程,一个在子进程。
15.3. 非阻塞connect
在一个TCP套接口被设置为非阻塞后调用connect,connect会立即返回一个EINPROCESS错误,但TCP的三路握手继续进行。在这之后我们可以用select检查这个连接是否建立成功,非阻塞的connect有三种用途:
1. 我们可以在三路握手同时做一些其他的处理。connect要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网。
2. 可以用这种技术同时建立多个连接。这在Web浏览器中很普遍
3. 由于我们用select等待连接的完成,因此可以给select设置一个时间限制,从而缩短connect的超时时间。
15.4. 非阻塞connect: 时间/日期客户程序
#include "unp.h"
int connect_nonb(int sockfd, const SA * saptr, socklen_t salen, int nsec)
{//nsec参数是等待连接完成的秒数,它的值为0表示select没有超时时间,因此内核将使用通常的TCP连接建立超时时间。
int flags, n, error;
socklen_t len;
fd_set rset, wset;
struct timeval tval;
flags = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); //调用fcntl设置非阻塞套接口
error = 0;
if((n = connect(sockfd, saptr, salen)) < 0)
if(errno != EINPROGRESS) //开始非阻塞connect,我们期望的错误是EINPROGRESS,表示开始建立连接但还没完成,其他的错误将被返回给调用者
return(-1);
/* Do whatever we want while the connect is taking place */ //在等待连接建立完成时可以做任何我们想做的事情
if(n == 0) //如果非阻塞connect返回0,连接已建立,这种情况在服务器和客户在同一台主机上时可能发生
goto done; /* connect completed immediately */
FD_ZERO(&rset);
FD_SET(sockfd, &rset);
wset = rset;//这个赋值可能是一个结构的赋值,因为描述字集通常表示为结构
tval.tv_sec = nsec;//初始化timeval结构
tval.tv_usec = 0;
if( (n = Select(sockfd+1, &rset, &wset, NULL, nsec ? &tval : NULL)) == 0 )
{//调用select等待套接口变成可读或者可写,把rset清零,打开该描述字集中对应sockfd的位,然后把rset拷贝到wset。
close(sockfd);//处理超时,如果select返回0,那么时钟超时,于是返回ETIMEOUT错误给调用者,我们要关闭套接口,以防止三路握手继续下去。
errno = ETIMEOUT;
return(-1);
}
if(FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) //如果描述字可读或可写,我们调用getsockopt得到套接口上待处理的错误(SO_ERROR)。
{
len = sizeof(error);
if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
return(-1);
}
else
err_quit("select error: sockfd not set");
done:
Fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
if(error)
{
close(sockfd); /* just in case */
errno = error;
return(-1);
}
return(0);
}
15.5. 非阻塞connect: Web客户程序
//web.h头文件
#include "unp.h"
#define MAXFILES 20
#define SERV "80" /* port number or service name */
struct file
{
char * f_name; /* filename */
char * f_host; /* hostname or IPv4/IPv6 address */
int f_fd; /* descriptor */
int f_flags; /* F_xxx below */
} file[MAXFILES];
#define F_CONNECTIONG 1 /* connect() in progress */
#define F_READING 2 /* connect() complete; no reading */
#define F_DONE 4 /* all done */
#define GET_CMD "GET %s HTTP/1.0 \r\n\r\n"
/* globals */
int nconn, nfiles, nlefttoconn, nlefttoread, maxfd;
fd_set rset, wset;
/* function prototypes */
void home_page(const char *, const char *);
void start_connect(struct file *);
void write_get_cmd(struct file *);
main函数
#include "web.h"
int main(int argc, char * * argv)
{
int i, fd, n, maxnconn, flags, error;
char buf[MAXLINE];
fd_set rs, ws;
if(argc < 5)
err_quit("usage: web < # conns> < hostname> < homepage > < file1 > ...");
maxnconn = atoi(argv[1]);
nfiles = min(argc-4, MAXFILES); //用从命令行参数来的相关信息填写file结构
for( i = 0; i < nfiles; i++)
{
file[i].f_name = argv[i+4];
file[i].f_host = argv[2];
file[i].f_flags = 0;
}
printf("nfiles = %d \n", nfiles);
home_page(argv[2], argv[3]);//home_page函数创建一个TCP连接,向服务器发出一个命令,然后读主页,这是第一个连接,在开始并行建立多条连接之前自己
FD_ZERO(&rset);//初始化读描述字集
FD_ZERO(&wset);//初始化写描述字集
maxfd = -1;//maxfd是select使用的最大描述字,初始化成-1,因为描述字是非负的
nlefttoread = nlefttoconn = nfiles; //nlefttoread是剩余要读的文件数(当它到0时程序就结束), nlefttoconn是还需要一个TCP连接的文件数
nconn = 0; //当前打开的连接数(它不能超过第一个命令行参数)
while(nlefttoread > 0)
{
while(nconn < maxnconn && nlefttoconn > 0)
{ /* find a file to read */
for( i = 0; i < nfiles; i++ )
if(file[i].f_flags == 0)
break;
if(i == nfiles)
err_quit("nlefttoconn = %d but nothing found", nlefttoconn);
start_connect(&file[i]);
nconn++;
nlefttoconn--;
}
rs = rset;
ws = wset;
n = Select(maxfd+1, &rs, &ws, NULL, NULL);
for(i = 0; i < nfiles; i++)
{
flags = file[i].f_flags;
if(flags == 0 || flags & F_DONE)
continue;
fd = file[i].f_fd;
if(flags & F_CONNECTING && (FD_ISSET(fd, &rs) || FD_ISSET(fd, &ws)))
{
n = sizeof(error);
if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &n) < 0 || error != 0)
{
err_ret("nonblocking connect failed for %s", file[i].f_name);
}
/* connection established */
printf("connection established for %s \n", file[i].f_name);
FD_CLR(fd, &wset); /* no more writeability test */
write_get_cmd(&file[i]); /* write() the GET command */
}
else if(flags & F_READING && FD_ISSET(fd, &rs))
{
if((n = Read(fd, buf, sizeof(buf))) == 0)
{
printf("end-of-file on %s \n", file[i].f_name);
Close(fd);
file[i].f_flags = F_DONE; /* clears F_READING */
FD_CLR(fd, &rset);
nconn--;
nlefttoread--;
}
else
{
printf("read %d bytes from %s \n", n, file[i].f_name);
}
}
}
}
exit(0);
}
下面是在main函数开始时调用了一次的home_page函数
#include "web.h"
void home_page(const char * host, const char * fname)
{
int fd, n;
char line[MAXLINE];
fd = Tcp_connect(host, SERV); /* blocking connect() */
n = snprintf(line, sizeof(line), GET_CMD, fname); /* 发出一个HTTP GET命令以获取主页 */
Writen(fd, line, n);
for( ; ; )
{
if((n = Read(fd, line, MAXLINE)) == 0)
break; /* server closed connection */
printf("read %d bytes of home page \n", n);
/* do whatever with data */
}
printf("end-of-file on home page \n");
Close(fd);
}
创建套接口,设置非阻塞方式
#include "web.h"
void start_connect(struct file * fptr)
{
int fd, flags, n;
struct addrinfo * ai;
ai = Host_serv(fptr->f_host, SERV, 0, SOCK_STREAM); /* 调用host_serv函数查找和转换主机名和服务名,返回一个指向addrinfo结构的数组的指针 */
fd = Socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); /* 创建TCP套接口 */
fptr->f_fd = fd;
printf("start_connect for %s, fd %d \n", fptr->f_name, fd);
/* Sect socket nonblocking */
flags = Fcntl(fd, F_GETFL, 0);
Fcntl(fd, F_SETFL, flags | O_NONBLOCK); //设置为非阻塞
/* initiate nonblocking connect to the server */
if((n = connect(fd, ai->ai_addr, ai->ai_addrlen)) < 0)
{/* 启动非阻塞connect,并将相应文件的标志置为F_CONNECTING */
if(errno != EINPROGRESS)
err_sys("nonblocking connect error");
fptr->f_flags = F_CONNECTING;
FD_SET(fd, &rset); /* select for reading and writing */
FD_SET(fd, &wset); /* 在读集合和写集合中都打开这个套接口描述字对应的位 */
if(fd > maxfd)
maxfd = fd;
}/* 如果connect返回成功,那么连接已经完成,write_get_cmd函数向服务器发出一个命令 */
else if(n >= 0) /* connect is already done */
write_get_cmd(fptr); /* write() the GET command */
}
我们把connect所用的套接口设置为非阻塞,但永远不恢复它缺省的阻塞模式。这样做没有问题是因为只向该套接口写了少量的数据,从而可以认为这个命令大大小于套接口的发送缓冲区。即使write因为非阻塞标志造成返回的数目小于要写的数目,writen函数也会对此进行处理。让这个套接口非阻塞对后面的read没有什么影响,因此我们总是调用select等待它变为可读。
//函数 write_get_cmd, 它向服务器发出一个HTTP的GET命令
#include "web.h"
void write_get_cmd(struct file * fptr)
{
int n;
char line[MAXLINE];
n = snprintf(line, sizeof(line), GET_CMD, fptr->f_name);
Writen(fptr->f_fd, line, n);
printf("wrote %d bytes for %s \n", n, fptr->f_name);
fptr->f_flags = F_READING; /* clears F_CONNECTING */
FD_SET(fptr->f_fd, &rset); /* will read server's reply */
if(fptr->f_fd > maxfd)
maxfd = fptr->f_fd;
}
15.6. 非阻塞accept
15.7. 小结